前言
在很多電商或者金融類App中辰如,經(jīng)常會有各種線上抽獎活動普监,為了提高用戶的交互性,讓用戶對中獎的體驗度更為真實琉兜,許多場景都會采用在線刮獎的UI設計凯正,其中就有模仿真實刮刮樂的特效,例如支付寶支付成功之后的刮獎豌蟋,本文將仿照這種交互定制成一個控件廊散,最終效果如下:
?
實現(xiàn)
思路
可以看到,主要由兩個層次疊加而成梧疲,一個是底部真實要展示的刮獎結(jié)果允睹,一個是蓋上上面的灰色蒙層,當用戶手指滑動的時候需要涂抹掉手指劃過的區(qū)域幌氮,可以監(jiān)聽記錄手指滑動的路徑缭受,然后結(jié)合混合模式將其路徑區(qū)域設為透明,露出底部真實內(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加載成對應的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)造方法同上太防,不重復貼了
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)聽觸摸的坐標沥潭,再通過坐標去記錄和追加路徑的位置:
@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
就是在兩個圖像相交的地方不進行繪制因谎,我們先舉個例子理解下這種模式的作用:
@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
之后财岔,效果如下:
可以看到兩者交集部分變成了透明,也就是如果都有色彩的話河爹,相交的地方完全不繪制匠璧。回到我們剛才的自定義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);
}
可以看到侨艾,在灰色蒙層的畫布上,先繪制一個矩形拓挥,然后再根據(jù)手勢路徑和混合模式唠梨,將手指劃過的地方都變成了透明:
?
3.優(yōu)化手指繪制路徑
上面已經(jīng)實現(xiàn)了大體的效果,但是仔細看會發(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);
}
可以看到在移動手指的時候醉箕,將貝塞爾曲線的錨點設置在曲線的中間,通過quadTo代替lineTo去移動路徑徙垫,效果如下:
?
4.監(jiān)聽刮出結(jié)果的時機
上面已經(jīng)完成了顯示部分讥裤,還有一個重要的點就是要捕獲刮出結(jié)果的時機,比如客戶端要監(jiān)聽這個時機做一些其他的操作等等姻报,那么要如何捕獲這個時機呢己英?Bitmap對象有一個getPixel(x, y)
方法,它可以獲得對應坐標位置的顏色值吴旋,如果該位置是透明损肛,那么getPixel
就會返回0寒亥,那么以此可以計算出Bitmap被繪制成透明的區(qū)域是多少,然后與我們自定義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;
}
}
開啟一個線程加勤,每隔一小段時間就去檢測灰色蒙層位圖的每個像素的顏色值,將透明的像素點累加起來同波,即為當前透明的區(qū)域鳄梅,然后與整體面積做對比,這里我定為超過80%就表示涂抹成功(用戶刮到這個程度都能大概看清楚抽獎結(jié)果是什么了)未檩,回調(diào)出去戴尸,并且記得回調(diào)的地方要切換回主線程。
?
結(jié)語
整體效果比較簡單冤狡,主要是巧用混合模式去涂抹蒙層孙蒙,貝塞爾曲線的優(yōu)化,以及像素顏色的判斷悲雳,另外還有可能是獎品結(jié)果圖并不是一張圖片挎峦,而是一個布局的情況,這種場景也做了觸摸事件的兼容和支持合瓢,完整代碼已上傳到 一個集合酷炫效果的自定義組件庫坦胶,歡迎Issue。
?
歡迎關注 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)』自定義帶入場動畫的弧形百分比進度條
GitHub:GitHub-ZJYWidget
CSDN博客:IT_ZJYANG
簡 書:Android小Y
在 GitHub 上建了一個集合炫酷自定義View的項目顿苇,里面有很多實用的自定義View源碼及demo,會長期維護税弃,歡迎Star~ 如有不足之處或建議還望指正纪岁,相互學習,相互進步钙皮,如果覺得不錯動動小手點個喜歡蜂科, 謝謝~