Android 使用Xfermode合成TabBarView

一、前言

PorterDuffXfermode 作为Android重要的合成组件,可以通过区域叠加方式进行裁剪和合成,起作用和Path.Op 类似,对于音视频开发中使用蒙版抠图和裁剪的需求,这种情况一般生成不了Path时,因此只能使用PorterDuffXfermode进行合成,当然Paint设置Shader也具备一定的能力,但是还是无法做到很多效果。

二、案例

这个案例使用了Bitmap合成,在边缘区域对色彩裁剪,从而实现了圆觉裁剪。

模版

//裁剪区域

技术上没有太多难点,但要注意的是Xfermode是2个Bitmap之间只使用,不像Shader那样可以单独使用。

ini 复制代码
Canvas resultCanvas = new Canvas(resultBitmap);
resultCanvas.drawBitmap(dstBitmap, paddingLeft + point.x, paddingTop, mSolidPaint);
mSolidPaint.setXfermode(mPorterDuffXfermode);
resultCanvas.drawBitmap(srcRoundBitmap, 0, 0, mSolidPaint);
canvas.drawBitmap(resultBitmap, 0, 0, null);

另外一点就是速度计算,利用了没有时间的逼近减速公式,当然你可以使用动画去实现

arduino 复制代码
 float speed = Math.abs((mTargetZone - 1) * (contentWidth / mDivideNumber) - point.x) / mSpeed;

下面是速度控制逻辑

ini 复制代码
    @Override
    public void run() {
        //计算速度,先按照最大速度5变化,如果小于5,则表示该减速停靠
        float speed = Math.abs((mTargetZone - 1) * (contentWidth / mDivideNumber) - point.x) / mSpeed;
        speed = (float) Math.max(1f, speed);
        float vPos = (mTargetZone - 1) * (contentWidth / mDivideNumber);
        if (point.x < vPos) {
            point.x += speed;
            if (point.x > vPos) {
                point.x = vPos;
            }
        } else {
            point.x -= speed;
            if (point.x < vPos) {
                point.x = vPos;
            }
        }
        if (point.x == vPos) {
            isSliding = false;
        } else {
            isSliding = true;
            postDelayed(this, 20);
        }
        postInvalidate();
    }

全部逻辑

ini 复制代码
public class TabBarView extends View implements Runnable {
    //画笔
    private Paint mSolidPaint;
    //中间竖线与边框间隙
    private int gapPadding = 0;
    //平分量
    private int mDivideNumber = 1;
    //边框大小
    private final float mBorderSize = 1.5f;
    //避免重复绘制Bitmap,短暂保存底色bitmap
    private Bitmap srcRoundBitmap;
    //图片混合模式
    private PorterDuffXfermode mPorterDuffXfermode;
    private PointF point;
    //内容区域大小
    private float contentWidth;
    private float contentHeight;
    //滑动到的目标区域
    private int mTargetZone;
    //滑动速度
    private float mSpeed;
    //主调颜色
    private int primaryColor;
    //默认字体颜色
    private int textColor;
    //焦点字体颜色
    private int selectedTextColor;
    //item
    private CharSequence[] mStringItems;
    //字体大小
    private float textSize;
    //是否处于滑动
    private boolean isSliding;

    Bitmap dstBitmap;
    Bitmap resultBitmap;

    private RectF rectBound = new RectF();

    public TabBarView(Context context) {
        super(context);
        init(null, 0);
    }

    public TabBarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public TabBarView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
        final TypedArray a = getContext().obtainStyledAttributes(
                attrs, R.styleable.TabBarView, defStyle, 0);

        //参数值越大,速度越大,速度指数越小
        mSpeed = Math.max(10 - Math.max(a.getInt(R.styleable.TabBarView_speed, 6), 6), 1);

        mStringItems = a.getTextArray(R.styleable.TabBarView_tabEntries);
        primaryColor = a.getColor(R.styleable.TabBarView_primaryColor, 0xFF4081);
        textColor = a.getColor(R.styleable.TabBarView_textColor, primaryColor);
        selectedTextColor = a.getColor(R.styleable.TabBarView_selectedTextColor, 0xffffff);
        textSize = a.getDimensionPixelSize(R.styleable.TabBarView_textSize, 30);

        if (mStringItems != null && mStringItems.length > 0) {
            mDivideNumber = mStringItems.length;
        }

        a.recycle();

        mSolidPaint = new Paint();
        mSolidPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        point = new PointF(0, 0);
        mTargetZone = 1;

        invalidateTextPaintAndMeasurements();

    }

    private void invalidateTextPaintAndMeasurements() {
        mSolidPaint.setColor(primaryColor);
        mSolidPaint.setStrokeWidth(mBorderSize);
        mSolidPaint.setTextSize(textSize);
        mSolidPaint.setStyle(Paint.Style.STROKE);
        mSolidPaint.setXfermode(null);
    }



    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        recycleBitmap();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        contentWidth = getWidth() - paddingLeft - paddingRight;
        contentHeight = getHeight() - paddingTop - paddingBottom;
        float minContentSize = Math.min(contentWidth, contentHeight);

        rectBound.set(paddingLeft, paddingTop, paddingLeft + contentWidth, paddingTop + contentHeight);
        canvas.drawRoundRect(rectBound, minContentSize / 2F, minContentSize / 2F, mSolidPaint);
        for (int i = 1; i < mDivideNumber; i++) {
            canvas.drawLine(paddingLeft + 1F * contentWidth * i / mDivideNumber, paddingTop + gapPadding, paddingLeft + contentWidth * i / mDivideNumber, paddingTop + contentHeight - gapPadding, mSolidPaint);

        }

        if (srcRoundBitmap == null) {
            srcRoundBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
            Canvas srcCanvas = new Canvas(srcRoundBitmap);
            mSolidPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            srcCanvas.drawRoundRect(rectBound, minContentSize / 2F, minContentSize / 2F, mSolidPaint);
        }

        if(dstBitmap == null) {
            dstBitmap = Bitmap.createBitmap((int) (contentWidth / mDivideNumber), (int) contentHeight, Bitmap.Config.ARGB_8888);
        }
        dstBitmap.eraseColor(Color.TRANSPARENT);
        Canvas dstCanvas = new Canvas(dstBitmap);
        dstCanvas.drawColor(Color.YELLOW);

        if(resultBitmap == null) {
            resultBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        }
        resultBitmap.eraseColor(Color.TRANSPARENT);
        Canvas resultCanvas = new Canvas(resultBitmap);
        resultCanvas.drawBitmap(dstBitmap, paddingLeft + point.x, paddingTop, mSolidPaint);
        mSolidPaint.setXfermode(mPorterDuffXfermode);
        resultCanvas.drawBitmap(srcRoundBitmap, 0, 0, mSolidPaint);
        canvas.drawBitmap(resultBitmap, 0, 0, null);

        invalidateTextPaintAndMeasurements();

        if (mStringItems != null) {

            for (int i = 0; i < mStringItems.length; i++) {
                String itemChar = mStringItems[i].toString();
                float textX = (contentWidth / mDivideNumber) * i / 2 + paddingLeft + (contentWidth * (i + 1) / mDivideNumber - mSolidPaint.measureText(itemChar)) / 2;
                float textY = paddingTop + (contentHeight - mSolidPaint.getFontMetrics().bottom - mSolidPaint.getFontMetrics().ascent) / 2;
                int color = mSolidPaint.getColor();
                mSolidPaint.setStyle(Paint.Style.FILL);
                if ((i + 1) == mTargetZone && !isSliding) {
                    mSolidPaint.setColor(selectedTextColor);
                } else {
                    mSolidPaint.setColor(textColor);
                }
                canvas.drawText(itemChar, textX, textY, mSolidPaint);
                mSolidPaint.setColor(color);
                mSolidPaint.setStyle(Paint.Style.STROKE);
            }
        }
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (checkLocationIsOk(event) && !isSliding) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                return checkLocationIsOk(event);
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_OUTSIDE:
                if (checkLocationIsOk(event) && !isSliding) {
                    float x = event.getX() - getPaddingLeft();
                    mTargetZone = (int) (x / (contentWidth / mDivideNumber)) + 1;
                    //规避区域超出范围
                    mTargetZone = Math.min(mTargetZone, mDivideNumber);
                    postToMove();
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    private void postToMove() {
        if (point.x == (mTargetZone - 1) * (contentWidth / mDivideNumber)) {
            return;
        }
        postDelayed(this, 20);
    }

    /**
     * 检测位置是否可用
     *
     * @param event
     * @return
     */
    private boolean checkLocationIsOk(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        if (x - getPaddingLeft() > 0 && (getPaddingLeft() + contentWidth - x) > 0 && y - getPaddingTop() > 0 && (getPaddingTop() + contentHeight - y) > 0) {
            return true;
        }
        return false;
    }

    private void recycleBitmap(Bitmap bmp) {
        if (bmp != null && !bmp.isRecycled()) {
            bmp.recycle();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getHandler().removeCallbacksAndMessages(null);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = getResources().getDisplayMetrics().widthPixels / 2;
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    public void run() {
        //计算速度,先按照最大速度5变化,如果小于5,则表示该减速停靠
        float speed = Math.abs((mTargetZone - 1) * (contentWidth / mDivideNumber) - point.x) / mSpeed;
        speed = (float) Math.max(1f, speed);
        float vPos = (mTargetZone - 1) * (contentWidth / mDivideNumber);
        if (point.x < vPos) {
            point.x += speed;
            if (point.x > vPos) {
                point.x = vPos;
            }
        } else {
            point.x -= speed;
            if (point.x < vPos) {
                point.x = vPos;
            }
        }
        if (point.x == vPos) {
            isSliding = false;
        } else {
            isSliding = true;
            postDelayed(this, 20);
        }
        postInvalidate();
    }

    public void setSelectedTab(int tabIndex) {
        mTargetZone = Math.max(Math.min(mDivideNumber, tabIndex + 1), 1);
        recycleBitmap();
        postToMove();
    }

    public void setTabItems(CharSequence[] mStringItems) {
        this.mStringItems = mStringItems;
        recycleBitmap();
        invalidate();
    }

    private void recycleBitmap() {
        if(dstBitmap != null && !dstBitmap.isRecycled()){
            dstBitmap.recycle();
        }
        if(resultBitmap != null && !resultBitmap.isRecycled()){
            resultBitmap.recycle();
        }
        resultBitmap = null;
        dstBitmap = null;
    }
}

我们需要自定义一些属性

ini 复制代码
<declare-styleable name="TabBarView">

    <attr name="speed" format="integer" />
    <attr name="tabEntries" format="reference"/>
    <attr name="primaryColor" format="color|reference"/>
    <attr name="textSize" format="dimension"/>
    <attr name="textColor" format="color|reference"/>
    <attr name="selectedTextColor" format="color|reference"/>

</declare-styleable>

还有部分需要引用的 string-array

typescript 复制代码
<string-array name="tabEntries_array">
    <item>A</item>
    <item>B</item>
    <item>C</item>
    <item>D</item>
</string-array>

然后是布局文件(片段)

ini 复制代码
<com.android.jym.widgets.TabBarView
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:background="@android:color/transparent"
    android:padding="10dp"
    app:speed="4"
    app:tabEntries="@array/tabEntries_array"
    app:primaryColor="@color/colorAccent"
    app:textColor="@color/colorPrimaryDark"
    app:selectedTextColor="@android:color/white"
    />

三、总结

使用Xfermode + 蒙版进行抠图,是Android中重要的工具,本篇作为技术储备,后续会通过这种方式实现一些新的功能。

相关推荐
怕浪猫5 分钟前
哪些软件对 Chrome DevTools Protocol 频繁使用
人工智能·架构·前端框架
用户1563068103512 小时前
Day01 | Java 基础(Java SE)
java
Pedantic2 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘2 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师3 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
行者全栈架构师3 小时前
Maven dependency:tree 的 8 个高级用法
java·后端
雨季mo浅忆3 小时前
VSCode自动格式化三要素
前端
爱勇宝4 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员