前言
在很多電商或者金融類App中,經(jīng)常會有各種線上抽獎活動,為了提高用戶的交互性,讓用戶對中獎的體驗度更為真實,許多場景都會采用在線刮獎的UI設(shè)計,其中就有模仿真實刮刮樂的特效,例如支付寶支付成功之后的刮獎,本文將仿照這種交互定制成一個控件,最終效果如下:

?
實現(xiàn)
思路
可以看到,主要由兩個層次疊加而成,一個是底部真實要展示的刮獎結(jié)果,一個是蓋上上面的灰色蒙層,當(dāng)用戶手指滑動的時候需要涂抹掉手指劃過的區(qū)域,可以監(jiān)聽記錄手指滑動的路徑,然后結(jié)合混合模式將其路徑區(qū)域設(shè)為透明,露出底部真實內(nèi)容,從而得到刮獎的效果。另外還要注意監(jiān)聽用戶什么時候刮出結(jié)果,以及路徑曲線的優(yōu)化。主要步驟和實現(xiàn)方式如下:
1.繪制底部真實內(nèi)容和灰色蒙層
2.監(jiān)聽手指劃過的路徑,利用PorterDuffXfermode混合模式繪制路徑
3.優(yōu)化手指繪制路徑
4.監(jiān)聽刮出結(jié)果的時機

?
1.繪制底部真實內(nèi)容和灰色蒙層
底部真實內(nèi)容可能是一張圖片或者是一個布局,這里先以圖片為例,將資源Id加載成對應(yīng)的Bitmap繪制在我們自定義的控件的畫布上:
public class YScratchView extends View {
//真實結(jié)果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;
//...構(gòu)造方法同上,不重復(fù)貼了
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);
//繪制獎品結(jié)果圖
canvas.drawBitmap(mBgBm, 0, 0, null);
//繪制灰色蒙層
canvas.drawBitmap(mGrayBm, 0, 0, mBgPaint);
}
}
首先獲得控件的寬高,然后再用這個寬高值去生成一張灰色的Bitmap,并獲取其畫布(后面會用到),然后將其繪制在控件上,效果如下:

?
2.監(jiān)聽手指劃過的路徑,利用PorterDuffXfermode混合模式繪制路徑
每次手指觸摸屏幕時,可以onTouchEvent監(jiān)聽觸摸的坐標(biāo),再通過坐標(biāo)去記錄和追加路徑的位置:
@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中搞事情了~,可以看到在追加路徑的同時,調(diào)用invalidate不斷去刷新畫布,我們要的效果是涂抹的地方去除灰色層,露出底部背景圖,那么可以利用混合模式中的PorterDuff.Mode.XOR模式來繪制這個路徑,PorterDuff.Mode.XOR就是在兩個圖像相交的地方不進(jìn)行繪制,我們先舉個例子理解下這種模式的作用:
@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);
}
這里繪制了一個矩形和一個圓形,并故意讓其位置有交集部分,為畫筆設(shè)置PorterDuff.Mode.XOR之后,效果如下:

可以看到兩者交集部分變成了透明,也就是如果都有色彩的話,相交的地方完全不繪制?;氐轿覀儎偛诺淖远xView,灰色蒙層與手勢路徑,其實就相當(dāng)于這兩個角色,將它們交集的部分(也就是手勢劃過的地方)采用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);
}
可以看到,在灰色蒙層的畫布上,先繪制一個矩形,然后再根據(jù)手勢路徑和混合模式,將手指劃過的地方都變成了透明:

?
3.優(yōu)化手指繪制路徑
上面已經(jīng)實現(xiàn)了大體的效果,但是仔細(xì)看會發(fā)現(xiàn),畫筆的路徑繪制有些許生硬,特別是在畫筆寬度比較小的時候更為明顯,這是由于我們是通過Path的lineTo去移動路徑的,所以其實放大了看是一段段很小的直線連接而成,我們可以通過貝塞爾曲線,讓路徑的過度不至于那么生硬,并且調(diào)整畫筆的寬度:
@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);
}
可以看到在移動手指的時候,將貝塞爾曲線的錨點設(shè)置在曲線的中間,通過quadTo代替lineTo去移動路徑,效果如下:

?
4.監(jiān)聽刮出結(jié)果的時機
上面已經(jīng)完成了顯示部分,還有一個重要的點就是要捕獲刮出結(jié)果的時機,比如客戶端要監(jiān)聽這個時機做一些其他的操作等等,那么要如何捕獲這個時機呢?Bitmap對象有一個getPixel(x, y)方法,它可以獲得對應(yīng)坐標(biāo)位置的顏色值,如果該位置是透明,那么getPixel就會返回0,那么以此可以計算出Bitmap被繪制成透明的區(qū)域是多少,然后與我們自定義View的總面積進(jìn)行對比,當(dāng)超過一定比例之后就判定為涂抹完成。(這個比例自己決定,當(dāng)然越高就越精準(zhǔn),但也需要用戶劃得更久)
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;
}
}
開啟一個線程,每隔一小段時間就去檢測灰色蒙層位圖的每個像素的顏色值,將透明的像素點累加起來,即為當(dāng)前透明的區(qū)域,然后與整體面積做對比,這里我定為超過80%就表示涂抹成功(用戶刮到這個程度都能大概看清楚抽獎結(jié)果是什么了),回調(diào)出去,并且記得回調(diào)的地方要切換回主線程。
?
結(jié)語
整體效果比較簡單,主要是巧用混合模式去涂抹蒙層,貝塞爾曲線的優(yōu)化,以及像素顏色的判斷,另外還有可能是獎品結(jié)果圖并不是一張圖片,而是一個布局的情況,這種場景也做了觸摸事件的兼容和支持,完整代碼已上傳到 一個集合酷炫效果的自定義組件庫,歡迎Issue。
?
歡迎關(guān)注 Android小Y 的簡書,更多Android精選自定義View
『Android自定義View實戰(zhàn)』實現(xiàn)一個小清新的彈出式圓環(huán)菜單
『Android自定義View實戰(zhàn)』玩轉(zhuǎn)PathMeasure之自定義支付結(jié)果動畫
『Android自定義View實戰(zhàn)』自定義弧形旋轉(zhuǎn)菜單欄——衛(wèi)星菜單
『Android自定義View實戰(zhàn)』自定義帶入場動畫的弧形百分比進(jìn)度條
GitHub:GitHub-ZJYWidget
CSDN博客:IT_ZJYANG
簡 書:Android小Y
在 GitHub 上建了一個集合炫酷自定義View的項目,里面有很多實用的自定義View源碼及demo,會長期維護,歡迎Star~ 如有不足之處或建議還望指正,相互學(xué)習(xí),相互進(jìn)步,如果覺得不錯動動小手點個喜歡, 謝謝~