『Android自定義View實戰(zhàn)』自定義完美的刮刮樂效果

前言

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


YScratchView.gif

?

實現(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之后财岔,效果如下:

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ù)手勢路徑和混合模式唠梨,將手指劃過的地方都變成了透明:


涂抹灰色蒙層.gif

?

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去移動路徑徙垫,效果如下:


優(yōu)化涂抹路徑.gif

?

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)』自定義帶入場動畫的弧形百分比進度條

GitHubGitHub-ZJYWidget
CSDN博客IT_ZJYANG
簡 書Android小Y
GitHub 上建了一個集合炫酷自定義View的項目顿苇,里面有很多實用的自定義View源碼及demo,會長期維護税弃,歡迎Star~ 如有不足之處或建議還望指正纪岁,相互學習,相互進步钙皮,如果覺得不錯動動小手點個喜歡蜂科, 謝謝~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市短条,隨后出現(xiàn)的幾起案子导匣,更是在濱河造成了極大的恐慌,老刑警劉巖茸时,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贡定,死亡現(xiàn)場離奇詭異,居然都是意外死亡可都,警方通過查閱死者的電腦和手機缓待,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門蚓耽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人旋炒,你說我怎么就攤上這事步悠。” “怎么了瘫镇?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵鼎兽,是天一觀的道長。 經(jīng)常有香客問我铣除,道長谚咬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任尚粘,我火速辦了婚禮择卦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘郎嫁。我一直安慰自己秉继,他們只是感情好,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布行剂。 她就那樣靜靜地躺著秕噪,像睡著了一般钳降。 火紅的嫁衣襯著肌膚如雪厚宰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天遂填,我揣著相機與錄音铲觉,去河邊找鬼。 笑死吓坚,一個胖子當著我的面吹牛撵幽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播礁击,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼盐杂,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了哆窿?” 一聲冷哼從身側(cè)響起链烈,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挚躯,沒想到半個月后强衡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡码荔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年漩勤,在試婚紗的時候發(fā)現(xiàn)自己被綠了感挥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡越败,死狀恐怖触幼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情究飞,我是刑警寧澤域蜗,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站噪猾,受9級特大地震影響霉祸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜袱蜡,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一丝蹭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坪蚁,春花似錦奔穿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嘴脾,卻和暖如春男摧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背译打。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工耗拓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奏司。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓乔询,卻偏偏與公主長得像,于是被迫代替她去往敵國和親韵洋。 傳聞我的和親對象是個殘疾皇子竿刁,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容