前言
在很多电商或者金融类App中,经常会有各种线上抽奖活动,为了提高用户的交互性,让用户对中奖的体验度更为真实,许多场景都会采用在线刮奖的UI设计,其中就有模仿真实刮刮乐的特效,例如支付宝支付成功之后的刮奖,本文将仿照这种交互定制成一个控件,最终效果如下:
实现
思路
可以看到,主要由两个层次叠加而成,一个是底部真实要展示的刮奖结果,一个是盖上上面的灰色蒙层,当用户手指滑动的时候需要涂抹掉手指划过的区域,可以监听记录手指滑动的路径,然后结合混合模式将其路径区域设为透明,露出底部真实内容,从而得到刮奖的效果。另外还要注意监听用户什么时候刮出结果,以及路径曲线的优化。主要步骤和实现方式如下:
1.绘制底部真实内容和灰色蒙层
2.监听手指划过的路径,利用PorterDuffXfermode混合模式绘制路径
3.优化手指绘制路径
4.监听刮出结果的时机
1.绘制底部真实内容和灰色蒙层
底部真实内容可能是一张图片或者是一个布局,这里先以图片为例,将资源Id加载成对应的Bitmap绘制在我们自定义的控件的画布上:
public class YScratchView extends View {
//真实结果Bitmap
private Bitmap mBgBm;
public YScratchView(Context context) {
super(context, null);
}
public YScratchView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public YScratchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mBgBm = BitmapFactory.decodeResource(getResources(), R.drawable.xxx);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBgBm, 0, 0, null);
}
}
其实就是简单地将图片资源解析为Bitmap对象并绘制到画布上,然后接着绘制我们的灰色蒙层:
public class YScratchView extends View {
private Bitmap mBgBm, mGrayBm;
private Canvas mGrayCanvas;
private Paint mBgPaint;
//...构造方法同上,不重复贴了
private void init(){
mBgBm = BitmapFactory.decodeResource(getResources(), R.drawable.xxx);
mBgPaint = new Paint();
mBgPaint.setColor(Color.GRAY);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mWidth = right - left;
mHeight = bottom - top;
initGrayArea();
mIsInit = true;
}
private void initGrayArea() {
mGrayBm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
mGrayCanvas = new Canvas(mGrayBm);
mGrayCanvas.drawColor(Color.GRAY);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制奖品结果图
canvas.drawBitmap(mBgBm, 0, 0, null);
//绘制灰色蒙层
canvas.drawBitmap(mGrayBm, 0, 0, mBgPaint);
}
}
首先获得控件的宽高,然后再用这个宽高值去生成一张灰色的Bitmap,并获取其画布(后面会用到),然后将其绘制在控件上,效果如下:
2.监听手指划过的路径,利用PorterDuffXfermode混合模式绘制路径
每次手指触摸屏幕时,可以onTouchEvent
监听触摸的坐标,再通过坐标去记录和追加路径的位置:
@Override
public boolean onTouchEvent(MotionEvent event) {
mMoveX = event.getX();
mMoveY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchPath.moveTo(mMoveX, mMoveY);
invalidate();
return true;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float endY = event.getY();
mTouchPath.lineTo(endX, endY);
invalidate();
return true;
}
return super.onTouchEvent(event);
}
路径记录好了自然要在onDraw
中搞事情了~,可以看到在追加路径的同时,调用invalidate
不断去刷新画布,我们要的效果是涂抹的地方去除灰色层,露出底部背景图,那么可以利用混合模式中的PorterDuff.Mode.XOR
模式来绘制这个路径,PorterDuff.Mode.XOR
就是在两个图像相交的地方不进行绘制,我们先举个例子理解下这种模式的作用:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
Bitmap bm1 = Bitmap.createBitmap(600, 600, Bitmap.Config.ARGB_8888);
Canvas c1 = new Canvas(bm1);
Paint p1 = new Paint(Paint.ANTI_ALIAS_FLAG);
p1.setColor(Color.parseColor("#00b7ee"));
c1.drawOval(new RectF(0, 0, 600, 600), p1);
Bitmap bm2 = Bitmap.createBitmap(600, 600, Bitmap.Config.ARGB_8888);
Canvas c2 = new Canvas(bm2);
Paint p2 = new Paint(Paint.ANTI_ALIAS_FLAG);
p2.setColor(Color.parseColor("#ec6941"));
c2.drawRect(0, 0, 600, 600, p2);
canvas.drawBitmap(bm1,0, 0, mPaint);
canvas.drawBitmap(bm2, 300, 300, mPaint);
}
这里绘制了一个矩形和一个圆形,并故意让其位置有交集部分,为画笔设置PorterDuff.Mode.XOR
之后,效果如下:
可以看到两者交集部分变成了透明,也就是如果都有色彩的话,相交的地方完全不绘制。回到我们刚才的自定义View,灰色蒙层与手势路径,其实就相当于这两个角色,将它们交集的部分(也就是手势划过的地方)采用XOR绘制,那么就会使得灰色蒙层被擦除,从而显示出底部奖品图:
//初始化手势路径画笔
mPathPaint = new Paint();
mPathPaint.setColor(Color.GRAY);
mPathPaint.setStrokeWidth(30);
mPathPaint.setStyle(Paint.Style.STROKE);
mPathPaint.setStrokeJoin(Paint.Join.ROUND);
mDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.XOR);
mPathPaint.setXfermode(mDuffXfermode);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//...这里省略绘制底部图案和灰色蒙层的代码,详见步骤一
mGrayCanvas.drawRect(0, 0, mWidth, mHeight, mBgPaint);
mGrayCanvas.drawPath(mTouchPath, mPathPaint);
}
可以看到,在灰色蒙层的画布上,先绘制一个矩形,然后再根据手势路径和混合模式,将手指划过的地方都变成了透明:
3.优化手指绘制路径
上面已经实现了大体的效果,但是仔细看会发现,画笔的路径绘制有些许生硬,特别是在画笔宽度比较小的时候更为明显,这是由于我们是通过Path的lineTo去移动路径的,所以其实放大了看是一段段很小的直线连接而成,我们可以通过贝塞尔曲线,让路径的过度不至于那么生硬,并且调整画笔的宽度:
@Override
public boolean onTouchEvent(MotionEvent event) {
mMoveX = event.getX();
mMoveY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchPath.moveTo(mMoveX, mMoveY);
invalidate();
return true;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float endY = event.getY();
mTouchPath.quadTo((endX - mMoveX) / 2 + mMoveX, (endY - mMoveY) / 2 + mMoveY, endX, endY);
invalidate();
return true;
}
return super.onTouchEvent(event);
}
可以看到在移动手指的时候,将贝塞尔曲线的锚点设置在曲线的中间,通过quadTo代替lineTo去移动路径,效果如下:
4.监听刮出结果的时机
上面已经完成了显示部分,还有一个重要的点就是要捕获刮出结果的时机,比如客户端要监听这个时机做一些其他的操作等等,那么要如何捕获这个时机呢?Bitmap对象有一个getPixel(x, y)
方法,它可以获得对应坐标位置的颜色值,如果该位置是透明,那么getPixel
就会返回0,那么以此可以计算出Bitmap被绘制成透明的区域是多少,然后与我们自定义View的总面积进行对比,当超过一定比例之后就判定为涂抹完成。(这个比例自己决定,当然越高就越精准,但也需要用户划得更久)
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (mThread.isInterrupted()) {
return;
}
while (!mHasFinish) {
SystemClock.sleep(500);
if(mIsInit){
for (int i = 0; i < mWidth; i++) {
for (int j = 0; j < mHeight; j++) {
int pixel = mGrayBm.getPixel(i, j);
if (pixel == 0) {
mScratchSize++;
}
}
}
checkFinish();
}
mScratchSize = 0;
}
}
};
private void checkFinish(){
float totalArea = mWidth * mHeight;
if (mScratchSize / totalArea > 0.8f) {
post(new Runnable() {
@Override
public void run() {
if (mListener != null) {
mListener.finish();
}
}
});
mHasFinish = true;
}
}
开启一个线程,每隔一小段时间就去检测灰色蒙层位图的每个像素的颜色值,将透明的像素点累加起来,即为当前透明的区域,然后与整体面积做对比,这里我定为超过80%就表示涂抹成功(用户刮到这个程度都能大概看清楚抽奖结果是什么了),回调出去,并且记得回调的地方要切换回主线程。
结语
整体效果比较简单,主要是巧用混合模式去涂抹蒙层,贝塞尔曲线的优化,以及像素颜色的判断,另外还有可能是奖品结果图并不是一张图片,而是一个布局的情况,这种场景也做了触摸事件的兼容和支持,完整代码已上传到 一个集合酷炫效果的自定义组件库,欢迎Issue。
欢迎关注 Android小Y 的,更多Android精选自定义View
『Android自定义View实战』实现一个小清新的弹出式圆环菜单
『Android自定义View实战』玩转PathMeasure之自定义支付结果动画
『Android自定义View实战』自定义弧形旋转菜单栏——卫星菜单
『Android自定义View实战』自定义带入场动画的弧形百分比进度条
GitHub:GitHub-ZJYWidget
CSDN博客:IT_ZJYANG
简 书:Android小Y
在 GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~