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中重要的工具,本篇作为技术储备,后续会通过这种方式实现一些新的功能。

相关推荐
寻星探路3 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
曹牧5 小时前
Spring Boot:如何测试Java Controller中的POST请求?
java·开发语言
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法6 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
kfyty7256 小时前
集成 spring-ai 2.x 实践中遇到的一些问题及解决方案
java·人工智能·spring-ai