本文记录一下自定义View实现刮刮卡效果的过程。
刮刮卡的实现主要是通过图像的混合来实现,但是在实现时有诸多细节需要注意。 通过分析生活中的刮刮卡效果,可以大概知道,灰色的蒙层是一层图像,而手势摸过的地方又是一层图像。 通过合适的混合模式就可以实现刮刮卡的效果。
我们知道手势可以通过path绘制到canvas上,那么如何将手势绘制的图像输出到一张图像上呢?
可以通过传入空的Bitmap到Canvas的构造函数中,然后将手势轨迹画到这个canvas上,那么该bitmap上就会有相关的轨迹了。
具体代码如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mForeground = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
// 在 mPathCanvas 上绘制的元素均会输出到 mForeground 上。
mPathCanvas = new Canvas(mForeground);
// mPathCanvas.drawColor(Color.GRAY);
BitmapFactory.Options options = new BitmapFactory.Options();
// 图像太大了,设置一下采样
options.inSampleSize = 4;
mBackground = BitmapFactory.decodeResource(getResources(), R.drawable.test, options);
}
在捕捉用户手势轨迹时,我们使用path及其二阶贝塞尔曲线来实现圆润的曲线。 (使用一阶时会有明显的转折痕迹,给人一种卡顿的现象)
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 将path移动到触摸点
mPath.moveTo(event.getX(), event.getY());
lastX = event.getX();
lastY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
// 计算控制点,为当前点与前一个点的中点
float tx = (lastX + event.getX()) / 2;
float ty = (lastY + event.getY()) / 2;
// 实现二阶贝塞尔曲线
mPath.quadTo(lastX, lastY, tx, ty);
lastX = event.getX();
lastY = event.getY();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// new Thread(mCalcPixel).start();
break;
}
invalidate();
return true;
}
但是此时,还没有将手势绘制到bitmap上。由于这是一个动态的绘制过程,我们在onDraw中实现。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 文字绘制的坐标,如何居中
canvas.drawText("一等奖", 50, 50, mTextPaint);
// 是否全部显示底层内容。
if (mIsComplete) {
return;
}
// save与savelayer的区别,此处用save就不行。 save只保存了MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG 这两个信息
// 而图像的混合用到了alpha通道。
int layerId = canvas.saveLayer(new RectF(0, 0, getWidth(), getHeight()), mPaint, Canvas.ALL_SAVE_FLAG);
// 将手势绘制到图片上,得到bitmap
mPathCanvas.drawPath(mPath, mPaint);
// 绘制手势图像
canvas.drawBitmap(mForeground, 0, 0, mPaint);
// 可以简记为 设置xfermode前的是目标图像, 之后绘制的是源图像
mPaint.setXfermode(xfermode);
// 绘制遮罩图
canvas.drawBitmap(mBackground, 0, 0, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
想像一下,由于手势绘制的地方要漏出底层,故手触摸的地方与背景图相交时不显示,不相交时显示遮罩图。 按照上面的代码顺序,我们得用的混合模式为src_out。 定义如下:
private PorterDuffXfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);
如上便可基本实现一个刮刮卡的效果了。 但是我经常在支付宝中看到,他们可以做到在刮开一定面积后就会自动的显示刮将结果 。他们是如何实现的呢? 在网上查看了一些博客(参考此篇),原来是通过像素计算刮开的面积来实现的。
基本实现如下:
// 标识是否全部显示
private boolean mIsComplete;
private Runnable mCalcPixel = new Runnable() {
@Override
public void run() {
// 注意,此处计算的是手势图片(即在手势图片上画了多少像素)
Bitmap bitmap = mForeground; // 计算图片被抹去了多少
int w = mForeground.getWidth();
int h = mForeground.getHeight();
int wipeArea = 0;
int totalArea = w * h;
int[] mPixelArr = new int[w * h];
bitmap.getPixels(mPixelArr, 0, w, 0, 0, w, h);
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
int index = i + j * w;
// 此处只能以 0 和 非0来计算。
if (mPixelArr[index] != 0) {
wipeArea++;
}
}
}
if (wipeArea > 0 && totalArea > 0) {
// 这里注意,先用浮点数计算,然后再转整型。 否则,int计算出来的一直是0
int percent = (int) (wipeArea * 1.0f / totalArea * 100);
if (percent > 50) {
mIsComplete = true;
postInvalidate();
}
}
}
};
由于计算量比较大,故一般放入子线程中处理。 上篇参考文章中,该计算是放在move事件中计算中的。感觉计算量有点大,此处放在up或cancel事件中效果也还可以。
此自定义view需要注意的事项:
1、混合是针对图片的, 需要先将手势轨迹输出到图像上,从而与特定的图片混合实现刮的效果。
2、手势的输出使用path及二阶贝塞尔曲线,并注意move时控制点的计算。
3、混合时不能使用canvas.save()方法,而应该用saveLayer方法。因为save方法只保存了matrix及clip信息,而没alpha通道相关信息。
4、刮开面积的比例是通过获取bitmap的像素计算的,注意如果某个像素点为0则为未绘制,非0为绘制。
5、在调试时可以通过Bitmap.compressed方法将bitmap输出到文件查看。