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

前言

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


YScratchView.gif

?

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

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


涂抹灰色蒙層.gif

?

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


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

?

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)度條

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容