筆記來源
張鴻洋大神在慕課網(wǎng)的視頻.
代碼位置:
https://github.com/tt88050643/GuaGuaKa
圖片:
學(xué)到的知識點(diǎn)
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)的.在閱讀別人寫的自定義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對象.
- 在自定義view中, 使用監(jiān)聽器interface這種設(shè)計(jì)模式, 讓view的使用者可以知道view的各種狀態(tài)的改變.
- 要想獲得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);
- 當(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>
- 如果多個(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.------