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

相关推荐
赵_叶紫2 分钟前
聊聊 Agent Skills 这个东西
前端
心在飞扬2 小时前
ReRank重排序提升RAG系统效果
前端·后端
开源之眼2 小时前
《github star 加星 Taimili.com 艾米莉 》为什么Java里面,Service 层不直接返回 Result 对象?
java·后端·github
心在飞扬2 小时前
RAPTOR 递归文档树优化策略
前端·后端
前端Hardy2 小时前
别再无脑用 `JSON.parse()` 了!这个安全漏洞你可能每天都在触发
前端·javascript·vue.js
前端Hardy2 小时前
别再让 `console.log` 上线了!它正在悄悄拖垮你的生产系统
前端·javascript·vue.js
青青家的小灰灰2 小时前
从入门到精通:Vue3 ref vs reactive 最佳实践与底层原理
前端·vue.js·面试
OpenTiny社区2 小时前
我的新同事是个AI:支持skill后,它用TinyVue搭项目还挺溜!
前端·vue.js·ai编程
心在飞扬3 小时前
MultiVector 多向量检索
前端·后端
用户39051332192883 小时前
async 函数返回的 Promise 状态何时变为 resolved
前端