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
![]() | ![]() | ![]() |
---|
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();
}
}