Android 刮刮卡案例學(xué)習(xí)筆記

筆記來源

張鴻洋大神在慕課網(wǎng)的視頻.
代碼位置:
https://github.com/tt88050643/GuaGuaKa
圖片:

屏幕快照 2018-03-15 下午4.57.00.png

學(xué)到的知識點(diǎn)
  1. Paint.setXfermode(Xfermode xfermode) API的使用, 以及對各種mode值的理解.
    可以看這篇文章, Android學(xué)習(xí)筆記(四):android畫圖之paint之setXfermode
    https://www.cnblogs.com/sank615/archive/2013/03/12/2955675.html
    注: 圓形頭像這樣的案例也是通過Paint.setXfermode(Xfermode xfermode) API 來實(shí)現(xiàn)的.

  2. 在閱讀別人寫的自定義View的代碼中, 經(jīng)常能看到下面這段代碼.

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        //根據(jù)寬高值, 創(chuàng)建一個(gè)空白的bitmap.
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mBitmap);

然后調(diào)用mCanvas.draw***() 一系列的API, 要明白對這些API的調(diào)用, 并不是在view上繪制內(nèi)容. 而是把各種圖形繪制到它所關(guān)聯(lián)的bitmap(初始化時(shí), 是個(gè)空白的bitmap)上.
要想在view上繪制內(nèi)容, 唯一的方法是通過 onDraw(Canvas canvas), 這個(gè)由framework傳進(jìn)來的canvas對象.

  1. 在自定義view中, 使用監(jiān)聽器interface這種設(shè)計(jì)模式, 讓view的使用者可以知道view的各種狀態(tài)的改變.
  2. 要想獲得bitmap上, 各個(gè)像素點(diǎn)的數(shù)據(jù)信息, 可以調(diào)用Bitmap.getPixels(int[] pixels)來完成.
            int[] mPixels = new int[width * height];
            //獲得bitmap的所有像素信息保存在mPixels中
            mBitmap.getPixels(mPixels, 0, width, 0, 0, width, height);
  1. 當(dāng)然對于自定義view中, 使用自定義屬性的模板代碼也可以在這里找到.

attrs.xml

    <attr name="text" format="string" />
    <attr name="textColor" format="color" />
    <attr name="textSize" format="dimension" />

    <declare-styleable name="GuaGuaKa">
        <attr name="text" />
        <attr name="textColor" />
        <attr name="textSize" />
    </declare-styleable>
  1. 如果多個(gè)線程都會(huì)去讀寫一個(gè)成員變量的話, 記得把成員變量聲明為"volatile", 保證一個(gè)線程對變量修改后, 另一個(gè)線程在讀它的時(shí)候可以得到它最新的值.
核心代碼

GuaGuaKa.java

package com.hola.game.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;

import com.hola.game.R;

//總體思路: 實(shí)際上, 就是在自定義view上畫2層數(shù)據(jù)上去.
// 最下面一層canvas.drawText()把獲獎(jiǎng)文本畫上去. 第二層, canvas.drawBitmap(mBitmap),
// 當(dāng)然這個(gè)bitmap是處理后的bitmap, 對這個(gè)要畫上去的bitmap的處理也就是本案例的重點(diǎn).


//一定要理清思路, 最終把哪些數(shù)據(jù)畫到view上, 只關(guān)注 onDraw(Canvas canvas), 對這個(gè)framework傳進(jìn)來的canvas對象都調(diào)用了哪些draw***() API.
//在這個(gè)案例中, 只調(diào)用了drawText()和drawBitmap()這兩個(gè)API.
//至于對mCanvas的所用draw***() API的操作, 只影響到它所關(guān)聯(lián)的mBitmap對象, 這些API的調(diào)用并不會(huì)在view上有繪制作用.

// PorterDuff.Mode, 處理的問題場景是, 在已有的bitmap上, 再繪制新的圖形數(shù)據(jù)上去時(shí), 當(dāng)像素點(diǎn)之間有相交部分時(shí), 如何讓bitmap保存它所有的像素點(diǎn)信息.

public class GuaGuaKa extends View {

    private Paint mPathPaint;
    private Path mPath;//手指劃屏幕的路徑
    private Canvas mCanvas;
    private Bitmap mBitmap;
    private int mLastX;
    private int mLastY;
    private Bitmap mCoverBitmap;
    private String mText;
    private int mTextSize;
    private int mTextColor;
    private Paint mTextPaint;//繪制“謝謝參與”的畫筆
    private Rect mTextBound;//“謝謝參與”的矩形范圍


    // 對于兩個(gè)線程都要訪問(讀/寫)的變量, 要使用volatile關(guān)鍵字, 保證內(nèi)存的可見性.
    private volatile boolean mComplete = false;//判斷擦除的比例是否達(dá)到60%

    //編碼規(guī)范:
    //一個(gè)參數(shù)的構(gòu)造方法去調(diào)用2個(gè)參數(shù)的構(gòu)造方法, 2個(gè)參數(shù)的調(diào)用3個(gè)參數(shù)的, 最終的初始化操作放到3個(gè)參數(shù)的方法中去完成.
    public GuaGuaKa(Context context) {
        this(context, null);
    }

    //在xml中定義view的話, 會(huì)走2個(gè)參數(shù)的構(gòu)造方法.
    public GuaGuaKa(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    //只有在java代碼中, new的view, 主動(dòng)傳入3個(gè)參數(shù), 才可能走到3個(gè)參數(shù)的構(gòu)造方法中去.
    public GuaGuaKa(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
        //獲得自定義屬性的各個(gè)值.
        TypedArray a = null;
        try {
            a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.GuaGuaKa, defStyleAttr, 0);
            int n = a.getIndexCount();
            for (int i = 0; i < n; i++) {
                int attr = a.getIndex(i);
                switch (attr) {
                    case R.styleable.GuaGuaKa_text:
                        mText = a.getString(attr);
                        break;
                    case R.styleable.GuaGuaKa_textColor:
                        mTextColor = a.getColor(attr, 0x000000);
                        break;
                    case R.styleable.GuaGuaKa_textSize:
                        //默認(rèn)值給22sp
            //applyDimension方法的目的是根據(jù)單位信息, 例如這里的SP單位, 把sp的單位值, 轉(zhuǎn)換為像素值.
                        mTextSize = (int) a.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 22, getResources().getDisplayMetrics()));
                        break;
                }
            }
        } finally {
            a.recycle();
        }
        init();
    }

    private void init() {
        mPathPaint = new Paint();
        mPath = new Path();
        mTextBound = new Rect();
        mTextPaint = new Paint();
        mText = "謝謝惠顧正什!";

        mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 22, getResources().getDisplayMetrics());
        mCoverBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.fg_guaguaka);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //在調(diào)用完父類的onMeasure()方法后, 就可以通過調(diào)用getMeasureWidth/Height()得到view的實(shí)際的寬高像素值了.
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        //根據(jù)寬高值, 創(chuàng)建一個(gè)空白的bitmap.
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

        //這個(gè)mCanvas是憑白創(chuàng)建出來的, 它和GuaGuaKa這個(gè)view是如何進(jìn)行關(guān)聯(lián)的, 我目前還沒有想明白.
        //現(xiàn)在想明白了! 操作這個(gè)mCanvas產(chǎn)生的效果完全體現(xiàn)在mBitmap對象上.
        mCanvas = new Canvas(mBitmap);

        setupPathPaint();//設(shè)置“橡皮擦”畫筆的屬性
        setupTextPaint();//設(shè)置繪制“謝謝參與”的畫筆屬性
        //畫圓角矩形, API: drawRoundRect().
        mCanvas.drawRoundRect(new RectF(0, 0, width, height), 30, 30, mPathPaint);
        //畫“刮刮卡”這個(gè)封面的bitmap, 參數(shù)里的new Rect就是把bitmap的繪制限定在特定的區(qū)域內(nèi).
        mCanvas.drawBitmap(mCoverBitmap, null, new Rect(0, 0, width, height), null);

        //!!!
        // 上面這兩條對mCanvas的API調(diào)用的作用, 并不是把圓角矩形和mOutterBitmap畫到屏幕上, 而是把圓角矩形和mOutterBitmap畫到和mCanvas關(guān)聯(lián)的空白mBitmap上.
        //!!!, 對這點(diǎn)的準(zhǔn)確理解是非常的重要.
        //在onMeasure()中的這些操作的目的就是為了給mBitmap這個(gè)之前空白的bitmap上填充數(shù)據(jù), 也就是刮刮卡的封面圖. 在onDraw(Canvas canvas)中再使用系統(tǒng)傳入?yún)?shù)的canvas對象,
        // canvas.drawBitmap(mBitmap), 這才真正的把封面圖畫到了屏幕上.

    }

    private void setupTextPaint() {
        mTextPaint.setColor(mTextColor);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setTextSize(mTextSize);
        //獲得畫筆繪制文本的寬和高(矩形范圍)
        mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBound);
    }


    private void setupPathPaint() {
        mPathPaint.setColor(Color.parseColor("#c0c0c0"));
        mPathPaint.setAntiAlias(true);
        mPathPaint.setDither(true);
        mPathPaint.setStrokeJoin(Paint.Join.ROUND);
        mPathPaint.setStrokeCap(Paint.Cap.ROUND);
        mPathPaint.setStyle(Paint.Style.STROKE);
        mPathPaint.setStrokeWidth(20);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //繪制path
        int action = event.getAction();
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                mPath.moveTo(mLastX, mLastY);
                break;
            case MotionEvent.ACTION_UP:
        //手指抬起后, 去檢測擦除的區(qū)域所占的比例, 因?yàn)槭呛臅r(shí)操作, 所以開新線程做這件事.
                new Thread(mRunnable).start();
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = Math.abs(x - mLastX);//用戶滑動(dòng)的距離
                int dy = Math.abs(y - mLastY);
        //距離差大于3個(gè)像素時(shí), 才會(huì)畫線, 目的是避免頻繁的調(diào)用lineTo()方法.
                if (dx > 3 || dy > 3) {
                    mPath.lineTo(x, y);
                }
                mLastX = x;
                mLastY = y;
                break;
        }
        invalidate();//執(zhí)行此方法會(huì)調(diào)用onDraw方法繪制
        return true;
    }

    //檢測的是mBitmap上面所有像素點(diǎn)的數(shù)據(jù)信息.
    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            int width = getWidth();
            int height = getHeight();

            float wipeArea = 0;//已經(jīng)擦除的比例
            float totalArea = width * height;

            int[] mPixels = new int[width * height];
            //獲得bitmap的所有像素信息保存在mPixels中
            mBitmap.getPixels(mPixels, 0, width, 0, 0, width, height);
            for (int i = 0; i < width; i++) {
                for (int j = 0; j < height; j++) {
                    int index = width*j + i;
                    //這個(gè)像素點(diǎn)的int值為0, 表示的就是沒有數(shù)據(jù), 也就是說這個(gè)像素點(diǎn)不保存任何信息數(shù)據(jù), 也就是完全透明.
                    if (mPixels[index] == 0) {
                        wipeArea++;
                    }
                }
            }
            if (wipeArea > 0 && totalArea > 0) {
                int percent = (int) (wipeArea * 100 / totalArea);
                Log.i("cool", percent + "");
                if (percent > 60) {
                    // 大于60%認(rèn)為就沒必要讓用戶繼續(xù)擦除操作了. 設(shè)置完成的標(biāo)志位為true.
                    mComplete = true;
                    postInvalidate();
                }
            }
        }
    };

    @Override
    protected void onDraw(Canvas canvas) {
        //第一步, 繪制最下層的獲獎(jiǎng)信息文本, eg. “謝謝參與”這樣的文本.
        canvas.drawText(mText, getWidth() / 2 - mTextBound.width() / 2, getHeight() / 2 + mTextBound.height() / 2, mTextPaint);
        if (mComplete) {
    //如果已經(jīng)使用者已經(jīng)刮完, 并且用戶設(shè)置了監(jiān)聽的話, 調(diào)用接口回調(diào), 通知view的使用者.
            if (mListener != null) {
                mListener.onComplete();
            }
        }
    //如果沒有刮完的情況下, 再去繪制path和圖片.
        if (!mComplete) {
            //mBitmap里面保存的就是封面圖數(shù)據(jù), 在onMeasure()中對mBitmap進(jìn)行的填充數(shù)據(jù)的操作.
            //第二步, 把path路徑畫到之前已經(jīng)保存了封面圖的bitmap上面去.
            drawPath();

            //第三步, 把mBitmap畫到view上, 也只有通過framework傳進(jìn)來的canvas, 才能把數(shù)據(jù)畫到view上.
            //之前對mCanvas的操作, 都是把數(shù)據(jù)畫到它所關(guān)聯(lián)的mBitmap上, 并不是畫到了真正的view上.
            canvas.drawBitmap(mBitmap, 0, 0, null);
        }

    }

    private void drawPath() {
        // mPath對象的賦值, 是在onTouchEvent()中根據(jù)用戶的操作, 進(jìn)行的設(shè)置.
        mPathPaint.setStyle(Paint.Style.STROKE);

        //這里是一個(gè)技術(shù)的關(guān)鍵點(diǎn).
        //PorterDuff.Mode, 各種模式的解釋. https://www.cnblogs.com/sank615/archive/2013/03/12/2955675.html
        //1. 在之前的onMeasure()中, 對mBitmap中已經(jīng)畫上了封面圖, 之前的封面圖就叫做Dst, 在官方文檔中, Dst用圓形表示.
        //2. 在已經(jīng)包含了封面圖的mBitmap上畫path. 后畫的叫做Src, 在官方文檔中, Src用方形表示.
        //期望的效果是, 對于path和封面圖相交的區(qū)域, 是透明的, 那么path和封面圖不相交的區(qū)域呢? 就顯示封面圖.
        //也可以這么理解, path作為src, 是不顯示出來的. 封面圖作為Dst, 是保留和path的非相交區(qū)域.
        //所以要用這種模式, PorterDuff.Mode.DST_OUT 取下層圖像非交集部分. 相交部分的像素信息就變?yōu)榱?, 也就是完全透明.
        mPathPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));

        //mCanvas 實(shí)際上關(guān)聯(lián)的是那個(gè)之前空白的mBitmap.
        //所以下面調(diào)用的drawPath()實(shí)際產(chǎn)生的效果是在mBitmap上畫一條path上去. 實(shí)際上操作的還是之前那個(gè)空白bitmap.
        mCanvas.drawPath(mPath, mPathPaint);
    }

    /**
     * 刮完的回調(diào)接口, 可以讓這個(gè)view的使用者來設(shè)置一個(gè)listener進(jìn)來, 用來監(jiān)聽這個(gè)view的一些情況.
     * 在這個(gè)案例中, 就是當(dāng)刮完60%的區(qū)域后, 如果view的使用者想知道這個(gè)情況, 就設(shè)置一個(gè)監(jiān)聽器進(jìn)來, view在適當(dāng)?shù)臅r(shí)候告訴外界一聲.
     */
    public interface OnGuaGuaKaCompleteListener {
        void onComplete();
    }

    private OnGuaGuaKaCompleteListener mListener;

    public void setOnGuaGuaKaCompleteListener(OnGuaGuaKaCompleteListener mListener) {
        this.mListener = mListener;
    }

    public void setText(String text){
        this.mText = text;
        //獲得畫筆繪制文本的寬和高(矩形范圍)
        mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBound);
    }
}



GuaGuaKaActivity.java



public class GuaGuaKaActivity extends AppCompatActivity {

    private GuaGuaKa mGuaGuaKa;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mGuaGuaKa = (GuaGuaKa) findViewById(R.id.id_guaguaka);
        mGuaGuaKa.setOnGuaGuaKaCompleteListener(new GuaGuaKa.OnGuaGuaKaCompleteListener() {
            @Override
            public void onComplete() {
                Toast.makeText(GuaGuaKaActivity.this, "刮到60%了!", Toast.LENGTH_SHORT).show();
            }
        });
        mGuaGuaKa.setText("掛掛卡效果!");
    }
}

代碼存檔

/Users/zy/develop/src/wangxin/github/guaguaka

refer to

http://www.reibang.com/p/2eca76145aae // setXPermode
https://github.com/appium/android-apidemos/blob/master/src/io/appium/android/apis/graphics/Xfermodes.java

------DONE.------

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末杆逗,一起剝皮案震驚了整個(gè)濱河市嚎莉,隨后出現(xiàn)的幾起案子影斑,更是在濱河造成了極大的恐慌后裸,老刑警劉巖洼裤,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件序厉,死亡現(xiàn)場離奇詭異锐膜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)弛房,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門道盏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人文捶,你說我怎么就攤上這事荷逞。” “怎么了粹排?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵种远,是天一觀的道長。 經(jīng)常有香客問我顽耳,道長坠敷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任斧抱,我火速辦了婚禮常拓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辉浦。我一直安慰自己弄抬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布宪郊。 她就那樣靜靜地躺著掂恕,像睡著了一般拖陆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上懊亡,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天依啰,我揣著相機(jī)與錄音,去河邊找鬼店枣。 笑死速警,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鸯两。 我是一名探鬼主播闷旧,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼钧唐!你這毒婦竟也來了忙灼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤钝侠,失蹤者是張志新(化名)和其女友劉穎该园,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帅韧,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡里初,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弱匪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片青瀑。...
    茶點(diǎn)故事閱讀 40,561評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖萧诫,靈堂內(nèi)的尸體忽然破棺而出斥难,到底是詐尸還是另有隱情,我是刑警寧澤帘饶,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布哑诊,位于F島的核電站,受9級特大地震影響及刻,放射性物質(zhì)發(fā)生泄漏镀裤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一缴饭、第九天 我趴在偏房一處隱蔽的房頂上張望暑劝。 院中可真熱鬧,春花似錦颗搂、人聲如沸担猛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽傅联。三九已至先改,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蒸走,已是汗流浹背仇奶。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留比驻,地道東北人该溯。 一個(gè)月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像别惦,于是被迫代替她去往敵國和親朗伶。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評論 2 359

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