Xfermode原理与案例
目录:
- Xfermode的基本原理
- Xfermode的多种模式
- Xfermode的使用案例
Xfermode的基本原理
-
Xfermode是什么?
在Android绘制中,通过使用Xfermode将绘制的图形的像素和Canvas上对应位置的像素按照一定的规则进行混合,形成新的像素,再更新到Canvas中形成最终的图形。 -
像素组成的4元素:ARGB
我们一个像素的颜色都是由四个分量组成,即ARGB,A表示的是我们Alpha值,RGB表示的是颜色
S表示的是源像素,源像素的值表示[Sa,Sc] Sa表示的就是源像素的Alpha值,Sc表示源像素的颜色值
D表示的是目标像素,目标像素的值表示[Da,Dc] Da表示的就是目标像素的Alpha值
PorterDuffXfermode
Xfermode有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode。其中AvoidXfermode, PixelXorXfermode已经过时不推荐使用。那么PorterDuffXfermode则是需要了解的东西。
- 以下的一张图和一段伪代码可以理解PorterDuffXfermode的基本概念
2. Xfermode理解起来并不是很难,根据上面的图可以理解为,两个不同的像素点。通过Xfermode的不同的混合模式混合之后展示出来的新的像素点效果。(注意这里是针对每一个像素的混合效果。而且这两个像素点需要是在画布上的同一位置,可以理解为重叠)
- 伪代码可以这样表示:
// 初始化PorterDuffXfermode
private PorterDuffXfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
// 在ondraw中使用PorterDuffXfermode
protected void onDraw(Canvas canvas) {
// DstBitmap SRCBitmap 为两个不同的bitmap
canvas.drawBitmap(SrcBitmap,0,0,mPaint);
// PorterDuffXfermode和paint联用
mPaint.setXfermode(xfermode);
canvas.drawBitmap(DstBitmap,0,0,mPaint);
// 将xfermode制空
mPaint.setXfermode(null);
}
以上的代码也比较简单理解: 先draw一个bitmap,然后设置paint的xfermode,然后在画第二个bitmap。这样他们重叠的部分就会出现不通过的UI效果了。
Xfermode的多种混合模式
官方的贴图非常形象的展示出各种混合模式使用后展示的效果。
接下来挑出一个常用的例子SRC_IN来解释下这些算法的基本应用。
圆形头像实现的方式可能有很多。比如用bitmapshader等等。使用xfermode同样能实现。
SRC_IN (5),
/** [Sa * Da, Sa * Dc] */
SRC_IN的算法是这样的:
(a)Sa * Da:源图(S)像素透明度和目标图片(D)像素的透明的决定混合后像素的透明度
(b)Sa * Dc:源图(S)像素透明度和目标图片(D)像素的颜色决定混合后像素的颜色
那么混合的图解:
从(a)(b)可以看出,源图片只采用了透明度的变化。混合后图像像素的透明度和颜色都和源图的像素的透明度的有关。如果源图的像素是透明的,那么混合后的像素为透明。反之不透明。所以源图为:
从(b)可以看出,决定混合后图像素颜色是由目标图片(D)决定的。所以目标图片是:
这里主要是理解算法:[Sa * Da, Sa * Dc]
示例代码:
package com.hdp.testvie.xformode;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
/**
* 使用xfermode中的src_in模式叠加做头像。
*/
public class CustomHeadView extends View {
private Bitmap DBitmap;
private Bitmap SBitmap;
private Paint mPaint;
private PorterDuffXfermode xfermode;
public CustomHeadView(Context context) {
this(context, null);
}
public CustomHeadView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomHeadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
public void drawHead(int src, int dst) {
SBitmap = BitmapFactory.decodeResource(getResources(), src, null);
DBitmap = BitmapFactory.decodeResource(getResources(), dst, null);
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(SBitmap.getWidth(), SBitmap.getHeight());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(SBitmap, 0, 0, mPaint);
mPaint.setXfermode(xfermode);
canvas.drawBitmap(DBitmap, 0, 0, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}
上述例子已经非常清晰的说明了xfermode的算法:
源图(S)和目标图(D)像素的透明度和颜色,通过特定的算法来算出混合后新图的透明度和颜色。(注意这里是对每个像素进行操作)
一些常用PorterDuffXfermode的例子
各种形状的图形
使用xfermode来完成圆形头像只是其中之一。如果有特殊要求,想弄成其他的形状都是可以的。 如果我上面写的圆形图片的例子能够理解,那么其他的各种形状的例子使用的方法是一样的。
刮刮卡效果
实际上实现一个效果并不是说只能采用一种叠加模式。用不同的模式也能做到相同的效果。 这里展示的刮刮卡效果,采用DST_OUT模式。
实现源码如下:
package com.hdp.testvie.xformode;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.hdp.testvie.R;
public class GuaGuaCard extends View implements View.OnTouchListener {
/**
* 原图
*/
private Bitmap SBitmap;
/**
* 目标图片
*/
private Bitmap DBitmap;
private Bitmap bitmap;
private Paint mPaint;
private PorterDuffXfermode xfermode;
/**
* 记录手指划过的路劲
*/
private Path mPath = new Path();
private float startX;
private float startY;
public GuaGuaCard(Context context) {
this(context, null);
}
public GuaGuaCard(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public GuaGuaCard(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 去掉硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
init(context);
setOnTouchListener(this);
}
private void init(Context context) {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(45);
SBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.one);
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.two);
DBitmap = Bitmap.createBitmap(SBitmap.getWidth(), SBitmap.getHeight(), Bitmap.Config.ARGB_8888);
bitCanvas = new Canvas(DBitmap);
xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
}
private Canvas bitCanvas;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(bitmap.getWidth(), bitmap.getHeight());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap, 0, 0, mPaint);
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
bitCanvas.drawPath(mPath, mPaint);
canvas.drawBitmap(SBitmap, 0, 0, mPaint);
// 叠加模式
mPaint.setXfermode(xfermode);
canvas.drawBitmap(DBitmap, 0, 0, mPaint);
// 记得重置xfermode模式
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
// 移动的时候记录移动的路线
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = motionEvent.getX();
startY = motionEvent.getY();
mPath.moveTo(startX, startY);
break;
case MotionEvent.ACTION_MOVE:
float endx = motionEvent.getX();
float endY = motionEvent.getY();
mPath.quadTo(startX, startY, endx, endY);
startX = motionEvent.getX();
startY = motionEvent.getY();
break;
case MotionEvent.ACTION_UP:
break;
}
postInvalidate();
return true;
}
}
draw方法分析:
- 首先绘制底部刮开后显示的图片(这个实际和混合没有关系)
- 保存现场,这里是混合的开始
- bitCanvas.drawPath(mPath, mPaint);通过canvas,将手指移动的path绘制到目标Bitmap(一段透明的path)
- 绘制遮罩,就是需要擦掉的源Bitmap
- 设置叠加模式DST_OUT
- 绘制目标Bitmap(),这里由于是DST_OUT,相交的部分变成透明了,故实现了擦除效果
- 重制Xformode,恢复现场
给美女脱衣效果
这里使用SRC_OUT的混合模式,和上面刮刮乐相反,我们使用手指移动path生成的Bitmap作为源图,使用遮罩层作为目标图,这样混合后,源图和目标图相交的部分会变成透明,露出了底图,也就是实现了我们的效果
效果图如下:
带圆角和阴影的布局
圆角和阴影的实现方式:
- 通过定义XML文件实现
- 通过自定义View实现,给自己添加圆角和阴影
这里,我们另辟蹊径,通过统一的一个父布局,通过控制子布局的位置,来进行子布局圆角和阴影的控制
圆角的实现方式:
- 通过Canvas.clipPath来实现,这个可以实现效果,但是有一个问题,就是当圆角比较大,布局较大的时候,会有明显的锯齿效果,这是clip方式裁剪的硬伤,无法避免
- 第二种就是我们要用到的通过paint的Xformode来实现,给paint添加抗锯齿即可
圆角阴影父布局实现
注:这里不是给父布局添加阴影,是通过父布局的drawChild来控制子布局的阴影,这样实现的好处就是:父布局里面的任意子布局都可以很方便的添加阴影和圆角效果
1、定义HLayoutParamsData类
作用:
- 解析自定义的属性
- 保存单个子View的布局信息,生成对应的path
源代码如下:
package com.hdp.testvie.roundview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import com.hdp.testvie.R;
public class HLayoutParamsData {
int radius;
int shadowColor;
int shadowDx;
int shadowDy;
int shadowEvaluation;
RectF widgetRect;
Path widgetPath;
Path clipPath;
boolean needClip;
boolean hasShadow;
public HLayoutParamsData(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HLayout);
radius = a.getDimensionPixelOffset(R.styleable.HLayout_layout_radius, 0);
shadowDx = a.getDimensionPixelOffset(R.styleable.HLayout_layout_shadowDx, 0);
shadowDy = a.getDimensionPixelOffset(R.styleable.HLayout_layout_shadowDy, 0);
shadowColor = a.getColor(R.styleable.HLayout_layout_shadowColor, 0x99999999);
shadowEvaluation = a.getDimensionPixelOffset(R.styleable.HLayout_layout_shadowEvaluation, 0);
a.recycle();
needClip = radius > 0;
hasShadow = shadowEvaluation > 0;
}
public void initPaths(View v) {
widgetRect = new RectF(v.getLeft(),
v.getTop(),
v.getRight(),
v.getBottom());
widgetPath = new Path();
clipPath = new Path();
clipPath.addRect(widgetRect, Path.Direction.CCW);
clipPath.addRoundRect(
widgetRect,
radius,
radius,
Path.Direction.CW
);
widgetPath.addRoundRect(
widgetRect,
radius,
radius,
Path.Direction.CW
);
}
}
2、定义HLayoutParams类
方便我们自定义的父布局对子类参数的解析,自定义LayoutParam实现该接口
package com.hdp.testvie.roundview;
public interface HLayoutParams {
HLayoutParamsData getData();
}
3、定义HConstraintLayout类
现在最常用的外层布局就是约束布局,实现给子布局添加圆角和阴影
- 自定义LayoutParams,继承ConstraintLayout.LayoutParams,并实现接口HLayoutParams
- 在构造函数中,初始化圆角和阴影画笔
- 重写generateLayoutParams返回自定义的LayoutParams
- 在onLayout方法中初始化每个子view相关的额path,为混合成圆角和阴影做准备
- 重写drawChild方法,实现圆角和阴影,使用Xformode实现
源代码如下:
package com.hdp.testvie.roundview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.constraintlayout.widget.ConstraintLayout;
public class HConstraintLayout extends ConstraintLayout {
private Paint shadowPaint;
private Paint clipPaint;
public HConstraintLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化沪指阴影的paint
shadowPaint = new Paint();
shadowPaint.setAntiAlias(true);
shadowPaint.setDither(true);
shadowPaint.setFilterBitmap(true);
shadowPaint.setStyle(Paint.Style.FILL);
//初始化绘制圆角的paint
clipPaint = new Paint();
clipPaint.setAntiAlias(true);
clipPaint.setDither(true);
clipPaint.setFilterBitmap(true);
clipPaint.setStyle(Paint.Style.FILL);
//关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
/**
* 重写generateLayoutParams
* @param attrs
* @return
*/
@Override
public ConstraintLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
/**
* 布局时候,便利所有子view,初始化每个子view相关的额path,为混合成圆角和阴影做准备
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
for (int i = 0, size = getChildCount(); i < size; i++) {
View v = getChildAt(i);
ViewGroup.LayoutParams lp = v.getLayoutParams();
if (lp instanceof HLayoutParams) {
HLayoutParams Hlp = (HLayoutParams) lp;
Hlp.getData().initPaths(v);
}
}
}
/**
* 重写drawChild方法,实现圆角和阴影
* @param canvas
* @param child
* @param drawingTime
* @return
*/
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
boolean ret = false;
if (lp instanceof HLayoutParams) {
HLayoutParams elp = (HLayoutParams) lp;
HLayoutParamsData data = elp.getData();
if (isInEditMode()) {//预览模式采用裁剪
Log.d("TAG", "isInEditMode");
canvas.save();
canvas.clipPath(data.widgetPath);
ret = super.drawChild(canvas, child, drawingTime);
canvas.restore();
return ret;
}
if (!data.hasShadow && !data.needClip)
return super.drawChild(canvas, child, drawingTime);
//为解决锯齿问题,正式环境采用xfermode
if (data.hasShadow) {
int count = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
shadowPaint.setShadowLayer(data.shadowEvaluation, data.shadowDx, data.shadowDy, data.shadowColor);
shadowPaint.setColor(data.shadowColor);
canvas.drawPath(data.widgetPath, shadowPaint);
shadowPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
shadowPaint.setColor(Color.WHITE);
canvas.drawPath(data.widgetPath, shadowPaint);
shadowPaint.setXfermode(null);
canvas.restoreToCount(count);
}
if (data.needClip) {
Log.d("TAG", "PorterDuffXfermode CLEAR");
int count = canvas.saveLayer(child.getLeft(), child.getTop(), child.getRight(), child.getBottom(), null, Canvas.ALL_SAVE_FLAG);
ret = super.drawChild(canvas, child, drawingTime);
clipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
clipPaint.setColor(Color.WHITE);
canvas.drawPath(data.clipPath, clipPaint);
clipPaint.setXfermode(null);
canvas.restoreToCount(count);
}
}
return ret;
}
/**
* 自定义LayoutParams
*/
static class LayoutParams extends ConstraintLayout.LayoutParams implements HLayoutParams {
private HLayoutParamsData data;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
data = new HLayoutParamsData(c, attrs);
}
@Override
public HLayoutParamsData getData() {
return data;
}
}
}
style文件:
<!--为了方便扩展其他layout,定义在外层,命名以layout_开头,否则lint会报红警告-->
<!--为了方便扩展其他layout,定义在外层,命名以layout_开头,否则lint会报红警告-->
<attr name="layout_radius" format="dimension" />
<attr name="layout_shadowColor" format="color" />
<attr name="layout_shadowEvaluation" format="dimension" />
<attr name="layout_shadowDx" format="dimension" />
<attr name="layout_shadowDy" format="dimension" />
<!--用统一一个EasyLayout,用于封装读取自定义属性-->
<declare-styleable name="HLayout">
<attr name="layout_radius" />
<attr name="layout_shadowColor" />
<attr name="layout_shadowEvaluation" />
<attr name="layout_shadowDx" />
<attr name="layout_shadowDy" />
</declare-styleable>
<!--和EasyLayout属性列表一样,但是命名要以XXX_Layout格式,这样开发工具会提示自定义属性-->
<declare-styleable name="HConstraintLayout_Layout">
<attr name="layout_radius" />
<attr name="layout_shadowColor" />
<attr name="layout_shadowEvaluation" />
<attr name="layout_shadowDx" />
<attr name="layout_shadowDy" />
</declare-styleable>
注意这里,为了在所以子View中使用自定义属性,我们需要将所有的属性定义成layout开头
使用效果如下:最外层使用HConstraintLayout