在 Android 自定义控件中,Xfermode 知识点占有很重要的地位,它能帮助我们实现很多炫酷的效果。例如,实现各种形状的图片控件;结合属性动画实现渐变效果。
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 图像为 蓝色方块 图片(纯为透底图)
新建自定义控件类 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;
}
}
效果:
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();
}
}
效果:
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);
}
}
同样的实现方式可以实现各种形状的图片控件。