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

相关推荐
码农派大星。几秒前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野7 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航10 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
GIS程序媛—椰子22 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
confiself26 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
DogEgg_00128 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
Wlq041530 分钟前
J2EE平台
java·java-ee
ZL不懂前端31 分钟前
Content Security Policy (CSP)
前端·javascript·面试
木舟100935 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
XiaoLeisj37 分钟前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee