11_android刮刮卡自定义组件编写

11_android刮刮卡自定义组件编写

一.先上效果图

在这里插入图片描述

二.自定义组件类创建,绘制遮罩

创建一个自定义组件类,你也可以继承自FrameLayout/RelativeLayout/ConstraintLayout,甚至是ViewGroup,只不过如果继承自ViewGroup,需要自己实现measure和layout过程,这里继承LinearLayout实现

public class ScratchCardLayout extends LinearLayout {

    private Paint mPaint;
    private int maskColor = 0xffcccccc;

    public ScratchCardLayout(Context context) {
        this(context, null);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ScratchCardLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        setWillNotDraw(false);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(maskColor);
    }
}

在这里插入图片描述

这个时候可以看到,文字被盖在了遮罩上面了,二我们想要的效果是遮罩覆盖在文字上,这是因为android中的View在绘制时,首先会调用drawBackground绘制背景,然后调用onDraw绘制自身,然后调用dispatchDraw绘制子View,然后调用onDrawForeground绘制前景,上述的文字是一个TextView作为当前自定义组件的子View,因此我们可以考虑在重写dispatchDraw或者onDrawForeground来绘制遮罩,我们如果重写dispatchDraw,在dispatchDraw方法中绘制遮罩,那么当前自定义组件如果设置了foreground,我们绘制的遮罩就会被foreground盖住,因此,为了保险起见,我们重写onDrawForeground来绘制遮罩,并且为了不让当前组件设置的foreground影响到我们的遮罩,在onDrawForeground方法中,不需要调用super.onDrawForeground

public class ScratchCardLayout extends LinearLayout {

    private Paint mPaint;
    private int maskColor = 0xffcccccc;

    public ScratchCardLayout(Context context) {
        this(context, null);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ScratchCardLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        setWillNotDraw(false);
    }

    @Override
    public void onDrawForeground(Canvas canvas) {
        canvas.drawColor(maskColor);
    }
}

在这里插入图片描述

三.写字板效果实现

刮奖的过程和写字板的效果很类似,只不过在实现写字板的基础上,需要使用xfmode颜色叠加,将写字板上的轨迹变为透明色,实现写字板,首先重写onTouchEvent,将手指的移动轨迹存储到Path中,然后调用postInvalidate通知组件重绘,最后在onDrawForeground中把保存手指移动轨迹的Path绘制出来即可

public class ScratchCardLayout extends LinearLayout {

    /**
     * 遮罩颜色
     */
    private int maskColor = 0xffcccccc;
    /**
     * 笔触大小
     */
    private int touchSize = 40;

    private Paint mPaint;
    private Path mScratchPath;

    public ScratchCardLayout(Context context) {
        this(context, null);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ScratchCardLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        setWillNotDraw(false);
        mScratchPath = new Path();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        int action = event.getAction();

        float x = event.getX();
        float y = event.getY();

        if(mScratchPath == null) {
            mScratchPath = new Path();
        }

        switch (action) {
            case ACTION_DOWN:
            case ACTION_UP:
                mScratchPath.moveTo(x, y);
                mScratchPath.arcTo(x - 0.5f, y - 0.5f, x + 0.5f, y + 0.5f, 0, 360, false);
                postInvalidate();
                break;
            case ACTION_MOVE:
                mScratchPath.lineTo(x, y);
                mScratchPath.moveTo(x, y);
                postInvalidate();
                break;
        }
        return true;
    }

    @Override
    public void onDrawForeground(Canvas canvas) {
        /**
         * 绘制遮罩
         */
        canvas.drawColor(maskColor);

        /**
         * 绘制手指移动轨迹
         */
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(touchSize);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setColor(Color.RED);
        canvas.drawPath(mScratchPath, mPaint);
    }
}

在这里插入图片描述

四.使用xfmode颜色叠加,将轨迹变为透明

关于xfmode的说明可以参考google的文档,使用PorterDuff.Mode.DST_OUT,遮罩层作为DST,手指移动轨迹作为SRC

05_scratch06_scratch07_scratch
public class ScratchCardLayout extends LinearLayout {

    /**
     * 遮罩颜色
     */
    private int maskColor = 0xffcccccc;
    /**
     * 笔触大小
     */
    private int touchSize = 40;

    private static final PorterDuffXfermode DST_OUT = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);

    private Paint mPaint;
    private Path mScratchPath;

    public ScratchCardLayout(Context context) {
        this(context, null);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ScratchCardLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        setWillNotDraw(false);
        mScratchPath = new Path();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        int action = event.getAction();

        float x = event.getX();
        float y = event.getY();

        if(mScratchPath == null) {
            mScratchPath = new Path();
        }

        switch (action) {
            case ACTION_DOWN:
            case ACTION_UP:
                mScratchPath.moveTo(x, y);
                mScratchPath.arcTo(x - 0.5f, y - 0.5f, x + 0.5f, y + 0.5f, 0, 360, false);
                postInvalidate();
                break;
            case ACTION_MOVE:
                mScratchPath.lineTo(x, y);
                mScratchPath.moveTo(x, y);
                postInvalidate();
                break;
        }
        return true;
    }

    @Override
    public void onDrawForeground(Canvas canvas) {
        /**
         * 绘制遮罩
         */
        canvas.drawColor(maskColor);

        /**
         * 绘制手指移动轨迹
         */
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(touchSize);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setColor(Color.RED);
        mPaint.setXfermode(DST_OUT);
        canvas.drawPath(mScratchPath, mPaint);
    }
}

在这里插入图片描述

如上图所示,说好的轨迹变透明,按照官方文档,轨迹应该变成透明才对啊,为什么呢,仔细看表格中的第一张图和第二张图,它们的背景都是透明的,而我们的自定义组件之上还有其他的View,他们的背景不一定是透明的,因此这里需要开启离屏缓存,也就是使用离屏画布

@Override
public void onDrawForeground(Canvas canvas) {
  /**
   * 开启离屏缓存
   */
  int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
  /**
   * 绘制遮罩
   */
  canvas.drawColor(maskColor);

  /**
   * 绘制手指移动轨迹
   */
  mPaint.setStyle(Paint.Style.STROKE);
  mPaint.setStrokeWidth(touchSize);
  mPaint.setStrokeJoin(Paint.Join.ROUND);
  mPaint.setStrokeCap(Paint.Cap.ROUND);
  mPaint.setColor(Color.RED);
  mPaint.setXfermode(DST_OUT);
  canvas.drawPath(mScratchPath, mPaint);
  /**
   * 还原画布
   */
  canvas.restoreToCount(saveCount);
}

在这里插入图片描述

搞定

五.怎么计算刮了多少? 什么时候算刮完?

把画布上绘制的内容保存到Bitmap中,遍历Bitmap中四个通道的颜色信息,如果四个通道的值都是0,说明是已经被刮了的其中一个像素点,最中可以计算出一共刮了多少个像素点,而总的像素点是等于画布的宽度(组件的宽度)*画布的高度(组件的高度),那么一共刮了多少,就可以计算出来了,接下来,在ACTION_DOWN时,计算刮了多少,判断计算出的刮了多少的结果是否大于某个阈值,如果大于,则说明刮完了

public class ScratchCardLayout extends LinearLayout {

    /**
     * 遮罩颜色
     */
    private int maskColor = 0xffcccccc;
    /**
     * 笔触大小
     */
    private int touchSize = 40;

    private static final PorterDuffXfermode DST_OUT = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
    private static final double WIPE_THRESHOLD = 50;

    private Paint mPaint;
    private Path mScratchPath;

    private Bitmap mScratchCacheBitmap;
    private Canvas mScratchCacheCanvas;
    private double wipePercent;

    public ScratchCardLayout(Context context) {
        this(context, null);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ScratchCardLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        setWillNotDraw(false);
        mScratchPath = new Path();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        int action = event.getAction();

        float x = event.getX();
        float y = event.getY();

        if(mScratchPath == null) {
            mScratchPath = new Path();
        }

        switch (action) {
            case ACTION_DOWN:
                mScratchPath.moveTo(x, y);
                mScratchPath.arcTo(x - 0.5f, y - 0.5f, x + 0.5f, y + 0.5f, 0, 360, false);
                postInvalidate();
            case ACTION_MOVE:
                mScratchPath.lineTo(x, y);
                mScratchPath.moveTo(x, y);
                postInvalidate();
                break;
            case ACTION_UP:
                mScratchPath.moveTo(x, y);
                mScratchPath.arcTo(x - 0.5f, y - 0.5f, x + 0.5f, y + 0.5f, 0, 360, false);
                postInvalidate();
                calcWipePercent();
                break;
        }
        return true;
    }

    private void calcWipePercent() {
        int width = mScratchCacheBitmap.getWidth();
        int height = mScratchCacheBitmap.getHeight();

        //1.开辟像素缓冲区(int数组),其长度为(bitmap的宽度 * bitmap的高度)
        int[] pixels = new int[width * height];
        mScratchCacheBitmap.getPixels(pixels, 0, width, 0, 0, width, height);

        //3.通过pixel遍历每一个像素,并对两个图像进行混合
        int pixelA = 0, pixelR = 0, pixelG = 0, pixelB = 0, wipeCount = 0;
        for(int i=0; i<pixels.length; i++) {
            int pixel = pixels[i];
            pixelA = (pixel >> 24) & 0xff;
            pixelR = (pixel >> 16) & 0xff;
            pixelG = (pixel >> 8) & 0xff;
            pixelB = pixel & 0xff;

            if(pixelA == 0 && pixelR == 0 && pixelG == 0 && pixelB == 0) {
                wipeCount ++;
            }
        }
        wipePercent = (wipeCount * 1.0/(width * height)) * 100;
        if(wipePercent > WIPE_THRESHOLD) {
            Toast.makeText(getContext(), "刮完了: " + wipePercent, Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void onDrawForeground(Canvas canvas) {
        if (mScratchCacheBitmap == null) {
            mScratchCacheBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        }
        if (mScratchCacheCanvas == null) {
            mScratchCacheCanvas = new Canvas(mScratchCacheBitmap);
        }

        /**
         * 开启离屏缓存
         */
        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        /**
         * 绘制遮罩
         */
        canvas.drawColor(maskColor);
        mScratchCacheCanvas.drawColor(maskColor);

        /**
         * 绘制手指移动轨迹
         */
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(touchSize);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setColor(Color.RED);
        mPaint.setXfermode(DST_OUT);
        canvas.drawPath(mScratchPath, mPaint);
        mScratchCacheCanvas.drawPath(mScratchPath, mPaint);
        /**
         * 还原画布
         */
        canvas.restoreToCount(saveCount);
    }
}

在这里插入图片描述

五.刮完之后,整个画布设置为透明

比较简单,只需要在onDrawForeground时,判断是否已经刮完了,如果刮完了,就只绘制一个透明颜色,然后在ACTION_UP时,计算出刮了多少的值,如果大于阈值,通知组件重绘即可

private void calcWipePercent() {
  int width = mScratchCacheBitmap.getWidth();
  int height = mScratchCacheBitmap.getHeight();

  //1.开辟像素缓冲区(int数组),其长度为(bitmap的宽度 * bitmap的高度)
  int[] pixels = new int[width * height];
  mScratchCacheBitmap.getPixels(pixels, 0, width, 0, 0, width, height);

  //3.通过pixel遍历每一个像素,并对两个图像进行混合
  int pixelA = 0, pixelR = 0, pixelG = 0, pixelB = 0, wipeCount = 0;
  for(int i=0; i<pixels.length; i++) {
    int pixel = pixels[i];
    pixelA = (pixel >> 24) & 0xff;
    pixelR = (pixel >> 16) & 0xff;
    pixelG = (pixel >> 8) & 0xff;
    pixelB = pixel & 0xff;

    if(pixelA == 0 && pixelR == 0 && pixelG == 0 && pixelB == 0) {
      wipeCount ++;
    }
  }
  wipePercent = (wipeCount * 1.0/(width * height)) * 100;
  if(wipePercent > WIPE_THRESHOLD) {
    postInvalidate();
  }
}

@Override
public void onDrawForeground(Canvas canvas) {
  if(wipePercent > WIPE_THRESHOLD) {
    //刮完了
    mPaint.setXfermode(null);
    mScratchPath.reset();
    canvas.drawColor(Color.TRANSPARENT);
    return;
  }

  if (mScratchCacheBitmap == null) {
    mScratchCacheBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
  }
  if (mScratchCacheCanvas == null) {
    mScratchCacheCanvas = new Canvas(mScratchCacheBitmap);
  }

  /**
   * 开启离屏缓存
   */
  int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
  /**
   * 绘制遮罩
   */
  canvas.drawColor(maskColor);
  mScratchCacheCanvas.drawColor(maskColor);

  /**
   * 绘制手指移动轨迹
   */
  mPaint.setStyle(Paint.Style.STROKE);
  mPaint.setStrokeWidth(touchSize);
  mPaint.setStrokeJoin(Paint.Join.ROUND);
  mPaint.setStrokeCap(Paint.Cap.ROUND);
  mPaint.setColor(Color.RED);
  mPaint.setXfermode(DST_OUT);
  canvas.drawPath(mScratchPath, mPaint);
  mScratchCacheCanvas.drawPath(mScratchPath, mPaint);
  /**
   * 还原画布
   */
  canvas.restoreToCount(saveCount);
}

在这里插入图片描述

六.定义刮奖完成回调,暴露方法重置组件
public class ScratchCardLayout extends LinearLayout {

    /**
     * 遮罩颜色
     */
    private int maskColor = 0xffcccccc;
    /**
     * 笔触大小
     */
    private int touchSize = 40;

    private static final PorterDuffXfermode DST_OUT = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
    private static final double WIPE_THRESHOLD = 50;

    private Paint mPaint;
    private Path mScratchPath;

    private Bitmap mScratchCacheBitmap;
    private Canvas mScratchCacheCanvas;
    private double wipePercent;

    private ScratchListener scratchListener;

    public ScratchCardLayout(Context context) {
        this(context, null);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScratchCardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ScratchCardLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        setWillNotDraw(false);
        mScratchPath = new Path();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        int action = event.getAction();

        float x = event.getX();
        float y = event.getY();

        if(mScratchPath == null) {
            mScratchPath = new Path();
        }

        switch (action) {
            case ACTION_DOWN:
                mScratchPath.moveTo(x, y);
                mScratchPath.arcTo(x - 0.5f, y - 0.5f, x + 0.5f, y + 0.5f, 0, 360, false);
                postInvalidate();
            case ACTION_MOVE:
                mScratchPath.lineTo(x, y);
                mScratchPath.moveTo(x, y);
                postInvalidate();
                break;
            case ACTION_UP:
                mScratchPath.moveTo(x, y);
                mScratchPath.arcTo(x - 0.5f, y - 0.5f, x + 0.5f, y + 0.5f, 0, 360, false);
                postInvalidate();
                calcWipePercent();
                break;
        }
        return true;
    }

    private void calcWipePercent() {
        int width = mScratchCacheBitmap.getWidth();
        int height = mScratchCacheBitmap.getHeight();

        //1.开辟像素缓冲区(int数组),其长度为(bitmap的宽度 * bitmap的高度)
        int[] pixels = new int[width * height];
        mScratchCacheBitmap.getPixels(pixels, 0, width, 0, 0, width, height);

        //3.通过pixel遍历每一个像素,并对两个图像进行混合
        int pixelA = 0, pixelR = 0, pixelG = 0, pixelB = 0, wipeCount = 0;
        for(int i=0; i<pixels.length; i++) {
            int pixel = pixels[i];
            pixelA = (pixel >> 24) & 0xff;
            pixelR = (pixel >> 16) & 0xff;
            pixelG = (pixel >> 8) & 0xff;
            pixelB = pixel & 0xff;

            if(pixelA == 0 && pixelR == 0 && pixelG == 0 && pixelB == 0) {
                wipeCount ++;
            }
        }
        wipePercent = (wipeCount * 1.0/(width * height)) * 100;
        if(wipePercent > WIPE_THRESHOLD) {
            postInvalidate();
            if(scratchListener != null) {
                scratchListener.onScratchFinish();
            }
        }
    }

    @Override
    public void onDrawForeground(Canvas canvas) {
        if(wipePercent > WIPE_THRESHOLD) {
            mPaint.setXfermode(null);
            mScratchPath.reset();
            canvas.drawColor(Color.TRANSPARENT);
            return;
        }

        if (mScratchCacheBitmap == null) {
            mScratchCacheBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        }
        if (mScratchCacheCanvas == null) {
            mScratchCacheCanvas = new Canvas(mScratchCacheBitmap);
        }

        /**
         * 开启离屏缓存
         */
        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        /**
         * 绘制遮罩
         */
        canvas.drawColor(maskColor);
        mScratchCacheCanvas.drawColor(maskColor);

        /**
         * 绘制手指移动轨迹
         */
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(touchSize);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setColor(Color.RED);
        mPaint.setXfermode(DST_OUT);
        canvas.drawPath(mScratchPath, mPaint);
        mScratchCacheCanvas.drawPath(mScratchPath, mPaint);
        /**
         * 还原画布
         */
        canvas.restoreToCount(saveCount);
    }

    public void reset() {
        wipePercent = 0;
        mScratchPath.reset();
        postInvalidate();
    }

    public void setScratchListener(ScratchListener scratchListener) {
        this.scratchListener = scratchListener;
    }

    public interface ScratchListener {
        void onScratchFinish();
    }
}
七.遮罩颜色,笔触大小自定义属性抽取(略)
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值