Android 刮刮卡案例学习笔记

笔记来源

张鸿洋大神在慕课网的视频.
代码位置:
https://github.com/tt88050643/GuaGuaKa
图片:

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

学到的知识点
  1. Paint.setXfermode(Xfermode xfermode) API的使用, 以及对各种mode值的理解.
    可以看这篇文章, Android学习笔记(四):android画图之paint之setXfermode
    https://www.cnblogs.com/sank615/archive/2013/03/12/2955675.html
    注: 圆形头像这样的案例也是通过Paint.setXfermode(Xfermode xfermode) API 来实现的.

  2. 在阅读别人写的自定义View的代码中, 经常能看到下面这段代码.

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        //根据宽高值, 创建一个空白的bitmap.
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mBitmap);

然后调用mCanvas.draw***() 一系列的API, 要明白对这些API的调用, 并不是在view上绘制内容. 而是把各种图形绘制到它所关联的bitmap(初始化时, 是个空白的bitmap)上.
要想在view上绘制内容, 唯一的方法是通过 onDraw(Canvas canvas), 这个由framework传进来的canvas对象.

  1. 在自定义view中, 使用监听器interface这种设计模式, 让view的使用者可以知道view的各种状态的改变.
  2. 要想获得bitmap上, 各个像素点的数据信息, 可以调用Bitmap.getPixels(int[] pixels)来完成.
            int[] mPixels = new int[width * height];
            //获得bitmap的所有像素信息保存在mPixels中
            mBitmap.getPixels(mPixels, 0, width, 0, 0, width, height);
  1. 当然对于自定义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. 如果多个线程都会去读写一个成员变量的话, 记得把成员变量声明为"volatile", 保证一个线程对变量修改后, 另一个线程在读它的时候可以得到它最新的值.
核心代码

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;

//总体思路: 实际上, 就是在自定义view上画2层数据上去.
// 最下面一层canvas.drawText()把获奖文本画上去. 第二层, canvas.drawBitmap(mBitmap),
// 当然这个bitmap是处理后的bitmap, 对这个要画上去的bitmap的处理也就是本案例的重点.


//一定要理清思路, 最终把哪些数据画到view上, 只关注 onDraw(Canvas canvas), 对这个framework传进来的canvas对象都调用了哪些draw***() API.
//在这个案例中, 只调用了drawText()和drawBitmap()这两个API.
//至于对mCanvas的所用draw***() API的操作, 只影响到它所关联的mBitmap对象, 这些API的调用并不会在view上有绘制作用.

// PorterDuff.Mode, 处理的问题场景是, 在已有的bitmap上, 再绘制新的图形数据上去时, 当像素点之间有相交部分时, 如何让bitmap保存它所有的像素点信息.

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;//“谢谢参与”的矩形范围


    // 对于两个线程都要访问(读/写)的变量, 要使用volatile关键字, 保证内存的可见性.
    private volatile boolean mComplete = false;//判断擦除的比例是否达到60%

    //编码规范:
    //一个参数的构造方法去调用2个参数的构造方法, 2个参数的调用3个参数的, 最终的初始化操作放到3个参数的方法中去完成.
    public GuaGuaKa(Context context) {
        this(context, null);
    }

    //在xml中定义view的话, 会走2个参数的构造方法.
    public GuaGuaKa(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    //只有在java代码中, new的view, 主动传入3个参数, 才可能走到3个参数的构造方法中去.
    public GuaGuaKa(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
        //获得自定义属性的各个值.
        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:
                        //默认值给22sp
            //applyDimension方法的目的是根据单位信息, 例如这里的SP单位, 把sp的单位值, 转换为像素值.
                        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);
    //在调用完父类的onMeasure()方法后, 就可以通过调用getMeasureWidth/Height()得到view的实际的宽高像素值了.
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        //根据宽高值, 创建一个空白的bitmap.
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

        //这个mCanvas是凭白创建出来的, 它和GuaGuaKa这个view是如何进行关联的, 我目前还没有想明白.
        //现在想明白了! 操作这个mCanvas产生的效果完全体现在mBitmap对象上.
        mCanvas = new Canvas(mBitmap);

        setupPathPaint();//设置“橡皮擦”画笔的属性
        setupTextPaint();//设置绘制“谢谢参与”的画笔属性
        //画圆角矩形, API: drawRoundRect().
        mCanvas.drawRoundRect(new RectF(0, 0, width, height), 30, 30, mPathPaint);
        //画“刮刮卡”这个封面的bitmap, 参数里的new Rect就是把bitmap的绘制限定在特定的区域内.
        mCanvas.drawBitmap(mCoverBitmap, null, new Rect(0, 0, width, height), null);

        //!!!
        // 上面这两条对mCanvas的API调用的作用, 并不是把圆角矩形和mOutterBitmap画到屏幕上, 而是把圆角矩形和mOutterBitmap画到和mCanvas关联的空白mBitmap上.
        //!!!, 对这点的准确理解是非常的重要.
        //在onMeasure()中的这些操作的目的就是为了给mBitmap这个之前空白的bitmap上填充数据, 也就是刮刮卡的封面图. 在onDraw(Canvas canvas)中再使用系统传入参数的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:
        //手指抬起后, 去检测擦除的区域所占的比例, 因为是耗时操作, 所以开新线程做这件事.
                new Thread(mRunnable).start();
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = Math.abs(x - mLastX);//用户滑动的距离
                int dy = Math.abs(y - mLastY);
        //距离差大于3个像素时, 才会画线, 目的是避免频繁的调用lineTo()方法.
                if (dx > 3 || dy > 3) {
                    mPath.lineTo(x, y);
                }
                mLastX = x;
                mLastY = y;
                break;
        }
        invalidate();//执行此方法会调用onDraw方法绘制
        return true;
    }

    //检测的是mBitmap上面所有像素点的数据信息.
    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            int width = getWidth();
            int height = getHeight();

            float wipeArea = 0;//已经擦除的比例
            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;
                    //这个像素点的int值为0, 表示的就是没有数据, 也就是说这个像素点不保存任何信息数据, 也就是完全透明.
                    if (mPixels[index] == 0) {
                        wipeArea++;
                    }
                }
            }
            if (wipeArea > 0 && totalArea > 0) {
                int percent = (int) (wipeArea * 100 / totalArea);
                Log.i("cool", percent + "");
                if (percent > 60) {
                    // 大于60%认为就没必要让用户继续擦除操作了. 设置完成的标志位为true.
                    mComplete = true;
                    postInvalidate();
                }
            }
        }
    };

    @Override
    protected void onDraw(Canvas canvas) {
        //第一步, 绘制最下层的获奖信息文本, eg. “谢谢参与”这样的文本.
        canvas.drawText(mText, getWidth() / 2 - mTextBound.width() / 2, getHeight() / 2 + mTextBound.height() / 2, mTextPaint);
        if (mComplete) {
    //如果已经使用者已经刮完, 并且用户设置了监听的话, 调用接口回调, 通知view的使用者.
            if (mListener != null) {
                mListener.onComplete();
            }
        }
    //如果没有刮完的情况下, 再去绘制path和图片.
        if (!mComplete) {
            //mBitmap里面保存的就是封面图数据, 在onMeasure()中对mBitmap进行的填充数据的操作.
            //第二步, 把path路径画到之前已经保存了封面图的bitmap上面去.
            drawPath();

            //第三步, 把mBitmap画到view上, 也只有通过framework传进来的canvas, 才能把数据画到view上.
            //之前对mCanvas的操作, 都是把数据画到它所关联的mBitmap上, 并不是画到了真正的view上.
            canvas.drawBitmap(mBitmap, 0, 0, null);
        }

    }

    private void drawPath() {
        // mPath对象的赋值, 是在onTouchEvent()中根据用户的操作, 进行的设置.
        mPathPaint.setStyle(Paint.Style.STROKE);

        //这里是一个技术的关键点.
        //PorterDuff.Mode, 各种模式的解释. https://www.cnblogs.com/sank615/archive/2013/03/12/2955675.html
        //1. 在之前的onMeasure()中, 对mBitmap中已经画上了封面图, 之前的封面图就叫做Dst, 在官方文档中, Dst用圆形表示.
        //2. 在已经包含了封面图的mBitmap上画path. 后画的叫做Src, 在官方文档中, Src用方形表示.
        //期望的效果是, 对于path和封面图相交的区域, 是透明的, 那么path和封面图不相交的区域呢? 就显示封面图.
        //也可以这么理解, path作为src, 是不显示出来的. 封面图作为Dst, 是保留和path的非相交区域.
        //所以要用这种模式, PorterDuff.Mode.DST_OUT 取下层图像非交集部分. 相交部分的像素信息就变为了0, 也就是完全透明.
        mPathPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));

        //mCanvas 实际上关联的是那个之前空白的mBitmap.
        //所以下面调用的drawPath()实际产生的效果是在mBitmap上画一条path上去. 实际上操作的还是之前那个空白bitmap.
        mCanvas.drawPath(mPath, mPathPaint);
    }

    /**
     * 刮完的回调接口, 可以让这个view的使用者来设置一个listener进来, 用来监听这个view的一些情况.
     * 在这个案例中, 就是当刮完60%的区域后, 如果view的使用者想知道这个情况, 就设置一个监听器进来, view在适当的时候告诉外界一声.
     */
    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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容