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

你可能感兴趣的:(Android 自定义 View 进阶 - Xfermode)