『Android自定义View实战』自定义完美的刮刮乐效果

前言

在很多电商或者金融类App中,经常会有各种线上抽奖活动,为了提高用户的交互性,让用户对中奖的体验度更为真实,许多场景都会采用在线刮奖的UI设计,其中就有模仿真实刮刮乐的特效,例如支付宝支付成功之后的刮奖,本文将仿照这种交互定制成一个控件,最终效果如下:


YScratchView.gif

 

实现

思路

可以看到,主要由两个层次叠加而成,一个是底部真实要展示的刮奖结果,一个是盖上上面的灰色蒙层,当用户手指滑动的时候需要涂抹掉手指划过的区域,可以监听记录手指滑动的路径,然后结合混合模式将其路径区域设为透明,露出底部真实内容,从而得到刮奖的效果。另外还要注意监听用户什么时候刮出结果,以及路径曲线的优化。主要步骤和实现方式如下:

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之后,效果如下:

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);
}

可以看到,在灰色蒙层的画布上,先绘制一个矩形,然后再根据手势路径和混合模式,将手指划过的地方都变成了透明:


涂抹灰色蒙层.gif

 

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去移动路径,效果如下:


优化涂抹路径.gif

 

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实战』自定义带入场动画的弧形百分比进度条

GitHubGitHub-ZJYWidget
CSDN博客IT_ZJYANG
简 书Android小Y
GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容