想要实现一个刮刮卡的效果,其中的一种方法就是使用图层混合模式Xfermode,想要很好的理解Xfermode,我们需要先结合着谷歌的例子来。
官方的源码我们可以在这里获得 https://github.com/THEONE10211024/ApiDemos/blob/master/app/src/main/java/com/example/android/apis/graphics/Xfermodes.java 将这里面的源码拿到自己的项目中运行我们就可以看到下面这张图片。
先绘制Dst在绘制Src,我们需要知道一点,使用Xfermode混合模式绘制后,受影响的区域永远是我们的src原图像区域。下面我们来看一下这16中混合模式的具体意思1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34private static final Xfermode[] sModes = {
//所绘制的不会提交到画布上
new PorterDuffXfermode(PorterDuff.Mode.CLEAR),
//显示上层绘制的图片
new PorterDuffXfermode(PorterDuff.Mode.SRC),
//显示下层绘制的图片
new PorterDuffXfermode(PorterDuff.Mode.DST),
//上下层都显示,上层层居上
new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER),
//上下层都显示,下层居上
new PorterDuffXfermode(PorterDuff.Mode.DST_OVER),
//取两层绘制的交集,显示上层
new PorterDuffXfermode(PorterDuff.Mode.SRC_IN),
//取两层绘制的交集,显示下层
new PorterDuffXfermode(PorterDuff.Mode.DST_IN),
//取上层绘制的非交集部分,其余部分变成透明
new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT),
//取下层绘制的非交集部分,其余部分变成透明
new PorterDuffXfermode(PorterDuff.Mode.DST_OUT),
//取上层的交集部分和下层的非交集部分
new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP),
//取下层交集部分和上层非交集部分
new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP),
//除去两层的交集部分
new PorterDuffXfermode(PorterDuff.Mode.XOR),
//取全部区域,交集部分颜色加深
new PorterDuffXfermode(PorterDuff.Mode.DARKEN),
//取两图层全部区域,交集部分颜色点亮
new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN),
//取两层交集部分,颜色加深
new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY),
//取两图层全部区域,交集部分变为透明色
new PorterDuffXfermode(PorterDuff.Mode.SCREEN),
};
知道了上面的属性的意思,我们来想一下我们刮刮卡的思路一个图片上面蒙上一层,手指划过的区域让上层变成透明,就显示出下层的图片了。
我们去上面属性中找,就可以找到DST_OUT这个属性,它的意思是取下层绘制的非交集部分,交集部分变成透明
所以我们可以先绘制一个刮奖层,为dst层,手指一动的路径为src层。设置为DST_OUT属性,那么两者交集的地方就会变成透明了。
当然也可以是用SRC_OUT,使用SRC_OUT就是遮罩层绘制在path上面。使用DST_OUT就是遮罩层绘制在path下面
1 | private void init() { |
首先禁止硬件加速,有些手机不支持,然后初始化结果图片,遮罩层图片和一个可绘制path的bitmap
下面分别使用DST_OUT和SRC_OUT两种方式来实现绘制1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制结果图片
canvas.drawBitmap(mBitmapRes,0,0,null);
// //第一种 先绘制遮罩层在绘制path,path的画笔使用DST_OUT模式
// mCanvas.drawBitmap(mSrcBitmap,0,0,null);
// mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
// mCanvas.drawPath(mPath,mPaint);
// //绘制目标图像
// canvas.drawBitmap(mDstBitmap,0,0,null);
//第二种//遮罩层绘制在path上面,遮罩层的画笔使用SRC_OUT模式
//新建一个图层,不然会把原始图层也当成dst层了。上面的方法之所以不用起一个新图层,因为遮罩层和path都是绘制在新new的canvas中了。
int layerId = canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint,Canvas.ALL_SAVE_FLAG);
//绘制路径
mCanvas.drawPath(mPath,mPaint);
//绘制目标图像
canvas.drawBitmap(mDstBitmap,0,0,mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
canvas.drawBitmap(mSrcBitmap,0,0,mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
第一种先绘制遮罩层在绘制path,path的画笔使用DST_OUT模式,第二种遮罩层绘制在path上面,遮罩层的画笔使用SRC_OUT模式。
第二种方法使用canvas.saveLayer新建了一个图层,是因为如果不新建一个图层,就会把原始图层当成dst层了。第一种方式,因为我们先new了一个自己的Canvas,然后把遮罩层绘制在了new出来的画布上了,path也是绘制在这个画布上。
最后就是path的路径了,很简单通过onTouchEvent获得1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mEventX = event.getX();
mEventY = event.getY();
mPath.moveTo(mEventX,mEventY);
break;
case MotionEvent.ACTION_MOVE:
float endX = (mEventX+event.getX())/2;
float endY = (mEventY+event.getY())/2;
//可以用lineTo也可以用quadTo一个是直线一个是以阶贝塞尔曲线
mPath.quadTo(mEventX,mEventY,endX,endY);
// mPath.lineTo(event.getX(),event.getY());
mEventX = event.getX();
mEventY = event.getY();
invalidate();
break;
}
return true;
}
如果有需求,当擦除一半的遮罩层后,抬起手自动全部消除,参考鸿洋的博客我们可以统计mDstBitmap的像素数据,被清除的像素变成0。统计为0的像素点跟总的像素点相除,大于某个值之后比如0.5就不绘制遮罩层了。
由于图片可能会很大,所以在子线程中处理统计的操作。在MotionEvent.ACTION_UP中启动线程计算。
1 | private Runnable mRunnable = new Runnable() |
最后我们可以在OnMeasure方法中,将我们整个view的大小设置为我们背景图片的大小,就可以在布局文件中愉快的玩耍啦。不然在布局文件中总是已match_parent的形式存在。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 宽的测量规格
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
// 宽的测量尺寸
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
// 高度的测量规格
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
// 高度的测量尺寸
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//根据View的逻辑得到,比如TextView根据设置的文字计算wrap_content时的大小。
//这两个数据根据实现需求计算。
int wrapWidth = mBitmapRes.getWidth();
int wrapHeight = mBitmapRes.getHeight();
// 如果是是AT_MOST则对哪个进行特殊处理
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(wrapWidth, wrapHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(wrapWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, wrapHeight);
}
}
最终效果图和源码: