Android 自定义 View 进阶 - Xfermode

在 Android 自定义控件中,Xfermode 知识点占有很重要的地位,它能帮助我们实现很多炫酷的效果。例如,实现各种形状的图片控件;结合属性动画实现渐变效果。

highlight.gif

Xfermode 介绍

Xfermode 主要是通过 paint.setXfermode(Xfermode xfermode) 方法进行设置的,其中 在 API 28 中, Xfermode 类只有一个子类 PorterDuffXfermode

PorterDuffXfermode 构造函数:

public PorterDuffXfermode(PorterDuff.Mode mode)

参数 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 图像为 蓝色方块 图片(纯为透底图)


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 实现高亮进度 ImageView

实现思路:

(1) 显示图片,继承 ImageView 类 ,更方便。

(2) 圆角矩形图片,通过 canvas.clipPath() 裁剪 canvas 画布(在 super.onDraw() 之前调用),绘制的图片就会显示成为圆角矩形。

(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, 这样重叠的部分就会变为透明,显示出正常的图片
     *
     * @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 圆环
        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 实现心状图片

实现思路:

(1) 继承自 ImageView 类,先绘制图片作为 dest 图像,此时的画笔 paint 需要是 ImageView 图片的画笔,不能是 重新创建的新画笔;

(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);
    }
}

同样的实现方式可以实现各种形状的图片控件。

love
源码地址:https://github.com/xing16/ProgressAndroid
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容