美文网首页Android进阶之路高级UIAndroid开发
『Android自定义View实战』自定义完美的刮刮乐效果

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

作者: Android小Y | 来源:发表于2019-05-19 17:55 被阅读81次

    前言

    在很多电商或者金融类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~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~

    关注Android 技术小栈,更多精彩原创

    相关文章

      网友评论

        本文标题:『Android自定义View实战』自定义完美的刮刮乐效果

        本文链接:https://www.haomeiwen.com/subject/fmsgaqtx.html