版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、简述
通过使用 Xfermode,将绘制的图形的像素和 Canvas 上对应位置的像素按照一定的规则进行混合,形成新的像素,再更新到 Canvas 中形成最终的图形。
这是谷歌 api 提供的 demo 中展示的16中混合的效果。
我们主要是要了解这16中模式的具体计算规则。查看安卓源码 android.graphics.PorterDuff 这个类(不要用反编译打开,主要要看备注)。可以看见里面枚举出各个类型以及对应类型的混合规则。
我们一个像素的颜色都是由四个分量组成,即 ARGB,A 表示的是我们 Alpha 值,RGB 表示的是颜色
S 表示的是原像素,原像素的值表示 [Sa,Sc] Sa 表示的就是源像素的 Alpha 值,Sc 表示源像素的颜色值
D 表示的是目标像素,目标像素的值表示 [Da,Dc] Da 表示的就是目标像素的 Alpha 值
在示例中,黄色圆形表示原图片,蓝色矩形是目标图片。
public enum Mode {
/** [0, 0] */
CLEAR (0),
/** [Sa, Sc] */
SRC (1),
/** [Da, Dc] */
DST (2),
/** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
SRC_OVER (3),
/** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
DST_OVER (4),
/** [Sa * Da, Sc * Da] */
SRC_IN (5),
/** [Sa * Da, Sa * Dc] */
DST_IN (6),
/** [Sa * (1 - Da), Sc * (1 - Da)] */
SRC_OUT (7),
/** [Da * (1 - Sa), Dc * (1 - Sa)] */
DST_OUT (8),
/** [Da, Sc * Da + (1 - Sa) * Dc] */
SRC_ATOP (9),
/** [Sa, Sa * Dc + Sc * (1 - Da)] */
DST_ATOP (10),
/** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
XOR (11),
/** [Sa + Da - Sa*Da,
Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
DARKEN (16),
/** [Sa + Da - Sa*Da,
Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
LIGHTEN (17),
/** [Sa * Da, Sc * Dc] */
MULTIPLY (13),
/** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
SCREEN (14),
/** Saturate(S + D) */
ADD (12),
OVERLAY (15);
Mode(int nativeInt) {
this.nativeInt = nativeInt;
}
/**
* @hide
*/
public final int nativeInt;
}
二、常见混合模式
- CLEAR [0, 0]
混合规则:透明度全选0,颜色也全选0。
处理图片相交区域时,总是透明的。
2.SRC [Sa, Sc]
混合规则:透明度选 S 的,颜色也选 S 的。
处理图片相交区域时,总是显示的是原图片。
3.SRC_IN [Sa * Da, Sc * Da]
混合规则:透明度选 S 的透明度与 D 的透明度乘积,颜色选 S 的颜色与 D 的透明度乘积。
处理图片相交区域时,受到目标图片的 Alpha 值影响,当我们的目标图片为空白像素的时候,目标图片也会变成空白
简单的来说就是用目标图片的透明度来改变源图片的透明度和饱和度,当目标图片的透明度为0时,源图片就不会显示
**圆角头像:**把头像图片设为 S,把透明的圆角图片设为 D 即可。
透明圆角图片。(要显示的范围为不透明的即可)
代码直接在上一章中的圆形头像代码进行修改。(如果圆角图片换成圆形图片,一样显示圆形头像)
public class RoundImageView_SRCIN extends View {
//加载的图片
private Bitmap bitmapDST,bitmapSRC;
//图片的宽高
private int mWidthDST;
private int mHeightDST;
//图片的宽高
private int mWidthSRC;
private int mHeightSRC;
private Paint mPaint;
//测量的宽高
float measureWidth;
float measureHeight;
//缩放比例
float scaleDST;
float scaleSRC;
public RoundImageView_SRCIN(Context context, AttributeSet attrs) {
super(context, attrs);
//关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
//加载圆角图片
bitmapDST = BitmapFactory.decodeResource(getResources(), R.drawable.shade);
mWidthDST = bitmapDST.getWidth();
mHeightDST = bitmapDST.getHeight();
//加载头像图片,并获取图片的宽高
bitmapSRC = BitmapFactory.decodeResource(getResources(), R.drawable.timg);
mWidthSRC = bitmapSRC.getWidth();
mHeightSRC = bitmapSRC.getHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureWidth = MeasureSpec.getSize(widthMeasureSpec);
measureHeight = MeasureSpec.getSize(heightMeasureSpec);
//考虑到宽高可能不一致,我们取宽高比的最小值
scaleSRC = Math.min(measureWidth/mWidthSRC, measureHeight/mHeightSRC);
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//这边用矩阵进行设置图片的缩放,这个不在这边细讲
Matrix matrix = new Matrix();
//scale = 缩放大小 / 原大小
matrix.setScale(mWidthSRC * scaleSRC / mWidthDST, mHeightSRC * scaleSRC / mHeightDST);
//新建层次,避免原有层次进行干扰
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(bitmapDST, matrix, mPaint);;
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//scale = 缩放大小 / 原大小
matrix.setScale(scaleSRC, scaleSRC);
canvas.drawBitmap(bitmapSRC, matrix, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}
**倒影图片:**显示的图片设为 S,把透明度递增的图片设置为 D 即可。
透明度递增图片单独看的话看不出来效果,就不上传在这了。
4.SRC_OUT [Sa * (1 - Da), Sc * (1 - Da)]
同SRC_IN类似 (1 - Da),用我们目标图片的透明度的补值来改变源图片的透明度和饱和度,当目标图片的透明度为不透明时,源图片就不会显示。
**橡皮擦效果:**把要擦除的图片设为 S,手势的轨迹设为 D。
public class EraserView_SRCOUT extends View {
//加载的图片
private Bitmap bitmapDST,bitmapSRC;
//图片的宽高
private int mWidthSRC;
private int mHeightSRC;
private Paint mPaint;
//测量的宽高
float measureWidth;
float measureHeight;
//缩放比例
float scaleDST;
float scaleSRC;
//橡皮擦路径
private Path mPath;
private float mPreX,mPreY;
public EraserView_SRCOUT(Context context, AttributeSet attrs) {
super(context, attrs);
//关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(45);
mPath = new Path();
//加载头像图片,并获取图片的宽高
bitmapSRC = BitmapFactory.decodeResource(getResources(), R.drawable.timg);
mWidthSRC = bitmapSRC.getWidth();
mHeightSRC = bitmapSRC.getHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureWidth = MeasureSpec.getSize(widthMeasureSpec);
measureHeight = MeasureSpec.getSize(heightMeasureSpec);
//考虑到宽高可能不一致,我们取宽高比的最小值
scaleSRC = Math.min(measureWidth/mWidthSRC, measureHeight/mHeightSRC);
//橡皮擦路径图
bitmapDST = Bitmap.createBitmap((int) (mWidthSRC * scaleSRC), (int)(mHeightSRC * scaleSRC), Bitmap.Config.ARGB_8888);
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//绘制橡皮擦路径图片
Canvas c = new Canvas(bitmapDST);
c.drawPath(mPath, mPaint);
//新建层次,避免原有层次进行干扰
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
//然后把目标图像画到画布上
canvas.drawBitmap(bitmapDST, 0, 0, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
//这边用矩阵进行设置图片的缩放,这个不在这边细讲
Matrix matrix = new Matrix();
//scale = 缩放大小 / 原大小
matrix.setScale(scaleSRC, scaleSRC);
canvas.drawBitmap(bitmapSRC, matrix, mPaint);;
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mPath.moveTo(event.getX(),event.getY());
mPreX = event.getX();
mPreY = event.getY();
return true;
case MotionEvent.ACTION_MOVE:
float endX = (mPreX+event.getX())/2;
float endY = (mPreY+event.getY())/2;
mPath.quadTo(mPreX,mPreY,endX,endY);
mPreX = event.getX();
mPreY =event.getY();
break;
case MotionEvent.ACTION_UP:
break;
}
postInvalidate();
return super.onTouchEvent(event);
}
}
注:代码中使用了 saveLayer 来保存保存层次,如果在这之前先行绘制一张底片,则变成刮刮卡效果。
5.SRC_ATOP [Da, Sc * Da + (1 - Sa) * Dc]
当 S 和 D 的透明度分别为为100%和0%时,SRC_IN 和 SRC_ATOP 是通用的。
当透明度不为上述的两个值时,SRC_ATOP 比 SRC_IN 源图像的饱和度会增加,变得更亮一些。
6.DST_IN [Sa * Da, Sa * Dc]
对比一下SRC_IN,正好和我们SRC_IN相反,在相交的时候以源图片的透明度来改变目标图片的透明度和饱和度。(SRC 开头的与对应的 DST 开头的都是这样相反的)
心电图效果,不规则水波纹效果,当然也可以做SRC_IN 的效果(注意选择谁为源图片,谁为目标图片)
心电图效果: 表层遮挡的空白图片为 S ,心电图图片为 D。
心电图的图片:
添加之后的效果:
public class HeartMap_DSTIN extends View {
private Paint mPaint;
private int mItemWaveLength = 0;
private int dx=0;
private Bitmap BmpSRC,BmpDST;
public HeartMap_DSTIN(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
BmpDST = BitmapFactory.decodeResource(getResources(),R.drawable.heartmap,null);
BmpSRC = Bitmap.createBitmap(BmpDST.getWidth(), BmpDST.getHeight(), Bitmap.Config.ARGB_8888);
mItemWaveLength = BmpDST.getWidth();
startAnim();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Canvas c = new Canvas(BmpSRC);
//清空bitmap
c.drawColor(Color.RED, PorterDuff.Mode.CLEAR);
//画上矩形
c.drawRect(BmpDST.getWidth() - dx,0,BmpDST.getWidth(),BmpDST.getHeight(),mPaint);
//模式合成
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(BmpDST,0,0,mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(BmpSRC,0,0,mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
public void startAnim(){
//慢慢减小遮挡的区域,是整个心电图图片展示出来
ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);
animator.setDuration(6000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
dx = (Integer)animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
}
注:使用 Xfermode 主要要注意两点。1.关闭硬件加速,不然很多效果异常。2.使用 canvas.saveLayer() 分层,避免背景对效果的影响