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

9字数 1668阅读 805

前言

在很多电商或者金融类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 技术小栈,更多精彩原创

推荐阅读更多精彩内容