Android 自定義 View 進階 - Xfermode

在 Android 自定義控件中,Xfermode 知識點占有很重要的地位,它能幫助我們實現(xiàn)很多炫酷的效果。例如,實現(xiàn)各種形狀的圖片控件;結合屬性動畫實現(xiàn)漸變效果。

highlight.gif

Xfermode 介紹

Xfermode 主要是通過 paint.setXfermode(Xfermode xfermode) 方法進行設置的,其中 在 API 28 中, Xfermode 類只有一個子類 PorterDuffXfermode

PorterDuffXfermode 構造函數(shù):

public PorterDuffXfermode(PorterDuff.Mode mode)

參數(shù) mode 設置不同的混合模式,取值有以下這幾種:

Xfermode 使用方法

通常情況下,需要關閉硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null); 然后在自定義控件的 onDraw() 方法中,保存至新的圖層中,先繪制 dest 圖像,然后再設置 paint.setXfermode(new PorterDuffXfermode(getMode(mode))); 接著繪制 src 圖像,這樣畫筆就應用上了指定的模式了。主要流程如下:

@Override
protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //將繪制操作保存到新的圖層,因為圖像合成是很昂貴的操作,將用到硬件加速,這里將圖像合成的處理放到離屏緩存中進行
        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);
        // 繪制 dest
        canvas.drawBitmap(destBitmap, 0, 0, paint);
        // 設置 xfermode
        if (mode != 0) {
            paint.setXfermode(new PorterDuffXfermode(getMode(mode)));
        }
        // 繪制 src
        canvas.drawBitmap(srcBitmap, 0, 0, paint);
        paint.setXfermode(null);
        canvas.restoreToCount(saveCount);
}

官方 Sample 測試

先準備兩種圖片素材,dest 圖像為 紅色方塊 圖片,src 圖像為 藍色方塊 圖片(純?yōu)橥傅讏D)


dest.png
src.png

新建自定義控件類 XfermodeBitmapView ,繼承 View, 在 onDraw() 方法先繪制 dest 圖像,然后將 paint xfermode 設置為 指定的模式,再繪制 src 圖像。

public class XfermodeBitmapView extends View {

    private Paint textPaint;
    private Paint paint;
    private int mode;
    private Bitmap destBitmap;
    private Bitmap srcBitmap;

    public XfermodeBitmapView(Context context) {
        this(context, null);
    }

    public XfermodeBitmapView(Context context, AttributeSet attrs) {
        super(context, attrs);
        readAttrs(context, attrs);
        init();
    }

    private void readAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.XfermodeView);
        mode = typedArray.getInt(R.styleable.XfermodeView_mode, 0);
        typedArray.recycle();
    }

    private void init() {
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(Color.BLACK);
        textPaint.setTextSize(60);

        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.FILL);
        destBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.red);
        srcBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.blue);

        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 設置背景
        canvas.drawColor(Color.DKGRAY);

        //將繪制操作保存到新的圖層,因為圖像合成是很昂貴的操作,將用到硬件加速,這里將圖像合成的處理放到離屏緩存中進行
        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);
        // 繪制 dest
        canvas.drawBitmap(destBitmap, 0, 0, paint);
        // 設置 xfermode
        if (mode != 0) {
            paint.setXfermode(new PorterDuffXfermode(getMode(mode)));
        }
        // 繪制 src
        canvas.drawBitmap(srcBitmap, 0, 0, paint);
        paint.setXfermode(null);
        canvas.restoreToCount(saveCount);
        canvas.drawText(getMode(mode).toString(), getWidth() - 300, getHeight() / 2f, textPaint);
    }

    private PorterDuff.Mode getMode(int value) {
        PorterDuff.Mode mode = null;
        switch (value) {
            case 1:
                mode = PorterDuff.Mode.CLEAR;
                break;
            case 2:
                mode = PorterDuff.Mode.SRC;
                break;
            case 3:
                mode = PorterDuff.Mode.DST;
                break;
            case 4:
                mode = PorterDuff.Mode.SRC_OVER;
                break;
            case 5:
                mode = PorterDuff.Mode.DST_OVER;
                break;
            case 6:
                mode = PorterDuff.Mode.SRC_IN;
                break;
            case 7:
                mode = PorterDuff.Mode.DST_IN;
                break;
            case 8:
                mode = PorterDuff.Mode.SRC_OUT;
                break;
            case 9:
                mode = PorterDuff.Mode.DST_OUT;
                break;
            case 10:
                mode = PorterDuff.Mode.SRC_ATOP;
                break;
            case 11:
                mode = PorterDuff.Mode.DST_ATOP;
                break;
            case 12:
                mode = PorterDuff.Mode.XOR;
                break;
            case 13:
                mode = PorterDuff.Mode.DARKEN;
                break;
            case 14:
                mode = PorterDuff.Mode.LIGHTEN;
                break;
            case 15:
                mode = PorterDuff.Mode.MULTIPLY;
                break;
            case 16:
                mode = PorterDuff.Mode.SCREEN;
                break;
        }
        return mode;
    }
}

效果:

1.png
2.png
3.png

Xfermode 實現(xiàn)高亮進度 ImageView

實現(xiàn)思路:

(1) 顯示圖片,繼承 ImageView 類 ,更方便。

(2) 圓角矩形圖片,通過 canvas.clipPath() 裁剪 canvas 畫布(在 super.onDraw() 之前調(diào)用),繪制的圖片就會顯示成為圓角矩形。

(3) 圖片上的灰色蒙層和圓形鏤空,通過 Xfermode 模式,先繪制 dst 鏤空圓,在繪制 src 灰色蒙層,并將 paint 設置為 srcOut , 這樣 dst 鏤空圓和灰色蒙層重疊的部分就會變成透明了,顯示出了底層的圖片

/**
 * 高亮進度 ImageView
 */
public class HighlightProgressImageView extends AppCompatImageView {

    private Paint backgroundPaint;
    private Paint circlePaint;
    private int radius;
    private int width;
    private int height;
    private int roundCorner;
    private Path clipPath;
    private RectF pathRectF;
    private RectF circleRectF;
    private RectF backgroundRectF;
    private PorterDuffXfermode porterDuffXfermode;
    private AnimatorSet animatorSet;
    private ValueAnimator angleAnimator;
    private ValueAnimator scaleAnimator;
    // 扇形角度
    private int angle;
    // 縮放半徑
    private float scaleRadius = radius;
    private boolean needDrawArc = true;


    public HighlightProgressImageView(Context context) {
        this(context, null);
    }

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

    private void init() {
        backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        backgroundPaint.setColor(getResources().getColor(R.color.translucentGray));
        backgroundPaint.setStyle(Paint.Style.FILL);

        circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circlePaint.setColor(getResources().getColor(android.R.color.white));
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setStrokeWidth(DensityUtil.dp2Px(getContext(), 8));

        radius = DensityUtil.dp2Px(getContext(), 40);
        roundCorner = DensityUtil.dp2Px(getContext(), 10);

        clipPath = new Path();
        porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        pathRectF = new RectF(0, 0, width, height);
        clipPath.addRoundRect(pathRectF, roundCorner, roundCorner, Path.Direction.CCW);
        circleRectF = new RectF(-radius, -radius, radius, radius);
        backgroundRectF = new RectF(-width / 2, -height / 2, width / 2f, height / 2f);
    }

    /**
     * 繪制步驟: 先繪制 圓, 再在圓上繪制灰色背景,繪制灰色背景時,將 Xfermode 設置為 PorterDuff.Mode.SRC_OUT, 這樣重疊的部分就會變?yōu)橥该?,顯示出正常的圖片
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        // 通過 path, 裁剪 canvas 畫布
        canvas.clipPath(clipPath);
        // 繪制圖片
        super.onDraw(canvas);
        //將繪制操作保存到新的圖層,因為圖像合成是很昂貴的操作,將用到硬件加速,這里將圖像合成的處理放到離屏緩存中進行
        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), backgroundPaint, Canvas.ALL_SAVE_FLAG);
        canvas.translate(width / 2f, height / 2f);
//        if (needDrawArc) {
        // 繪制 dst 圓環(huán)
        circlePaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(0, 0, radius, circlePaint);
        // 繪制 dst 扇形
        circlePaint.setStyle(Paint.Style.FILL);
        canvas.drawArc(circleRectF, -90, angle, true, circlePaint);
//        }
        circlePaint.setStyle(Paint.Style.FILL);
        // 繪制 dst 圓
        canvas.drawCircle(0, 0, scaleRadius, circlePaint);
        // 設置 Xfermode 為 SRC_OUT
        backgroundPaint.setXfermode(porterDuffXfermode);
        // 繪制 src 圖片上層的灰色蒙層
        canvas.drawRoundRect(backgroundRectF, roundCorner, roundCorner, backgroundPaint);
        backgroundPaint.setXfermode(null);
        canvas.restoreToCount(saveCount);
    }

    /**
     * 開啟動畫
     */
    public void start() {
        startAnimator();
    }

    /**
     * 停止動畫
     */
    public void stop() {
        if (animatorSet != null) {
            animatorSet.cancel();
            animatorSet = null;
        }
    }

    private void startAnimator() {
        // 扇形進度動畫
        if (angleAnimator == null) {
            angleAnimator = ValueAnimator.ofInt(0, 360);
//            angleAnimator.setDuration(2000);
            angleAnimator.setInterpolator(new LinearInterpolator());
            angleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    angle = (int) animation.getAnimatedValue();
                    invalidate();
                }
            });
            angleAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    needDrawArc = false;
                }
            });
        }

        if (scaleAnimator == null) {
            scaleAnimator = ValueAnimator.ofFloat(radius, width > height ? width : height);
//            scaleAnimator.setDuration(2000);
            scaleAnimator.setInterpolator(new LinearInterpolator());
            scaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    scaleRadius = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
        }
        if (animatorSet == null) {
            animatorSet = new AnimatorSet();
            animatorSet.setDuration(2000);
            animatorSet.setInterpolator(new LinearInterpolator());
            animatorSet.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                }
            });
            animatorSet.playSequentially(angleAnimator, scaleAnimator);
        }
        animatorSet.start();
    }
}

效果:

highlight.gif

Xfermode 實現(xiàn)心狀圖片

實現(xiàn)思路:

(1) 繼承自 ImageView 類,先繪制圖片作為 dest 圖像,此時的畫筆 paint 需要是 ImageView 圖片的畫筆,不能是 重新創(chuàng)建的新畫筆;

(2) 設置畫筆 Xfermode 模式為 SRC_IN ,并利用 path 貝塞爾曲線繪制一個心形,這樣心形和圖片重合的部分就保留顯示了心形部分的圖片。

/**
 * 心形圖片
 */
public class XfermodeHeartShapeImageView extends android.support.v7.widget.AppCompatImageView {

    private int mViewWidth;
    private int mViewHeight;
    private Paint paint;
    private PorterDuffXfermode xfermode;
    private float radius;
    private Path path;

    public XfermodeHeartShapeImageView(Context context) {
        this(context, null);
    }

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

    private void init() {
        xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        path = new Path();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
        radius = Math.min(mViewWidth, mViewHeight) / 3f;

        // 獲取繪制圖片對應的 paint
        paint = ((BitmapDrawable) getDrawable()).getPaint();

        // 二階貝塞爾曲線
        path.moveTo(mViewWidth / 2f, mViewHeight / 4f);
        path.cubicTo(mViewWidth / 10f, mViewHeight / 12f,
                mViewWidth / 9f, (mViewHeight * 3) / 5f,
                mViewWidth / 2f, (mViewHeight * 5) / 6f);
        path.cubicTo(
                mViewWidth * 8 / 9f, (mViewHeight * 3) / 5f,
                mViewWidth * 9 / 10f, mViewHeight / 12f,
                mViewWidth / 2f, mViewHeight / 4f);

        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int saveCount = canvas.saveLayer(0, 0, mViewWidth, mViewHeight, null, Canvas.ALL_SAVE_FLAG);
        // 繪制 dst 心形
        /***為什么 paint.setStyle() 放在 onDraw() 中才生效,放在 onSizeChanged() 中進行不能生效***/
        paint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, paint);
        // 將繪制圖片對應的 paint Xfermode 設置為 SRC_IN , 重疊的部分顯示為 src 圖片, dst 中不重疊的部分不變, src 中不重疊的部分顯示為透明
        paint.setXfermode(xfermode);
        // 繪制 src 圖片
        super.onDraw(canvas);
        paint.setXfermode(null);
        canvas.restoreToCount(saveCount);
    }
}

同樣的實現(xiàn)方式可以實現(xiàn)各種形狀的圖片控件。

love
源碼地址:https://github.com/xing16/ProgressAndroid
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容