先繪製Dst在繪製Src,我們需要知道一點,使用Xfermode混合模式繪製後,受影響的區域永遠是我們的src原圖像區域。下面我們來看一下這16中混合模式的具體意思
private static final Xfermode[] sModes = {
//所繪製的不會提交到畫布上
new PorterDuffXfermode(PorterDuff.Mode.CLEAR),
//顯示上層繪製的圖片
new PorterDuffXfermode(PorterDuff.Mode.SRC),
//顯示下層繪製的圖片
new PorterDuffXfermode(PorterDuff.Mode.DST),
//上下層都顯示,上層層居上
new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER),
//上下層都顯示,下層居上
new PorterDuffXfermode(PorterDuff.Mode.DST_OVER),
//取兩層繪製的交集,顯示上層
new PorterDuffXfermode(PorterDuff.Mode.SRC_IN),
//取兩層繪製的交集,顯示下層
new PorterDuffXfermode(PorterDuff.Mode.DST_IN),
//取上層繪製的非交集部分,其餘部分變成透明
new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT),
//取下層繪製的非交集部分,其餘部分變成透明
new PorterDuffXfermode(PorterDuff.Mode.DST_OUT),
//取上層的交集部分和下層的非交集部分
new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP),
//取下層交集部分和上層非交集部分
new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP),
//除去兩層的交集部分
new PorterDuffXfermode(PorterDuff.Mode.XOR),
//取全部區域,交集部分顏色加深
new PorterDuffXfermode(PorterDuff.Mode.DARKEN),
//取兩圖層全部區域,交集部分顏色點亮
new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN),
//取兩層交集部分,顏色加深
new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY),
//取兩圖層全部區域,交集部分變爲透明色
new PorterDuffXfermode(PorterDuff.Mode.SCREEN),
};
知道了上面的屬性的意思,我們來想一下我們刮刮卡的思路一個圖片上面蒙上一層,手指劃過的區域讓上層變成透明,就顯示出下層的圖片了。
我們去上面屬性中找,就可以找到DST_OUT這個屬性,它的意思是取下層繪製的非交集部分,交集部分變成透明
所以我們可以先繪製一個刮獎層,爲dst層,手指一動的路徑爲src層。設置爲DST_OUT屬性,那麼兩者交集的地方就會變成透明瞭。
當然也可以是用SRC_OUT,使用SRC_OUT就是遮罩層繪製在path上面。使用DST_OUT就是遮罩層繪製在path下面
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(30);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
//禁止硬件加速
setLayerType(LAYER_TYPE_SOFTWARE,null);
//結果圖片
mBitmapRes = BitmapFactory.decodeResource(getResources(), R.mipmap.guagua);
//遮罩層圖片
mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.gua);
//創建一個可繪製path的bitmap
mDstBitmap = Bitmap.createBitmap(mSrcBitmap.getWidth(),mSrcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mDstBitmap);
}
首先禁止硬件加速,有些手機不支持,然後初始化結果圖片,遮罩層圖片和一個可繪製path的bitmap
下面分別使用DST_OUT和SRC_OUT兩種方式來實現繪製
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪製結果圖片
canvas.drawBitmap(mBitmapRes,0,0,null);
// //第一種 先繪製遮罩層在繪製path,path的畫筆使用DST_OUT模式
// mCanvas.drawBitmap(mSrcBitmap,0,0,null);
// mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
// mCanvas.drawPath(mPath,mPaint);
// //繪製目標圖像
// canvas.drawBitmap(mDstBitmap,0,0,null);
//第二種//遮罩層繪製在path上面,遮罩層的畫筆使用SRC_OUT模式
//新建一個圖層,不然會把原始圖層也當成dst層了。上面的方法之所以不用起一個新圖層,因爲遮罩層和path都是繪製在新new的canvas中了。
int layerId = canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint,Canvas.ALL_SAVE_FLAG);
//繪製路徑
mCanvas.drawPath(mPath,mPaint);
//繪製目標圖像
canvas.drawBitmap(mDstBitmap,0,0,mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
canvas.drawBitmap(mSrcBitmap,0,0,mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
第一種先繪製遮罩層在繪製path,path的畫筆使用DST_OUT模式,第二種遮罩層繪製在path上面,遮罩層的畫筆使用SRC_OUT模式。
第二種方法使用canvas.saveLayer新建了一個圖層,是因爲如果不新建一個圖層,就會把原始圖層當成dst層了。第一種方式,因爲我們先new了一個自己的Canvas,然後把遮罩層繪製在了new出來的畫布上了,path也是繪製在這個畫布上。
最後就是path的路徑了,很簡單通過onTouchEvent獲得
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mEventX = event.getX();
mEventY = event.getY();
mPath.moveTo(mEventX,mEventY);
break;
case MotionEvent.ACTION_MOVE:
float endX = (mEventX+event.getX())/2;
float endY = (mEventY+event.getY())/2;
//可以用lineTo也可以用quadTo一個是直線一個是以階貝塞爾曲線
mPath.quadTo(mEventX,mEventY,endX,endY);
// mPath.lineTo(event.getX(),event.getY());
mEventX = event.getX();
mEventY = event.getY();
invalidate();
break;
}
return true;
}
如果有需求,當擦除一半的遮罩層後,擡起手自動全部消除,參考鴻洋的博客我們可以統計mDstBitmap的像素數據,被清除的像素變成0。統計爲0的像素點跟總的像素點相除,大於某個值之後比如0.5就不繪製遮罩層了。
由於圖片可能會很大,所以在子線程中處理統計的操作。在MotionEvent.ACTION_UP中啓動線程計算。
private Runnable mRunnable = new Runnable()
{
private int[] mPixels;
@Override
public void run()
{
int w = mDstBitmap.getWidth();
int h = mDstBitmap.getHeight();
float wipeArea = 0;
float totalArea = w * h;
Bitmap bitmap = mDstBitmap;
mPixels = new int[w * h];
/**
* 拿到所有的像素信息
*/
bitmap.getPixels(mPixels, 0, w, 0, 0, w, h);
/**
* 遍歷統計擦除的區域
*/
for (int i = 0; i < w; i++)
{
for (int j = 0; j < h; j++)
{
int index = i + j * w;
if (mPixels[index] == 0)
{
wipeArea++;
}
}
}
/**
* 根據所佔百分比,進行一些操作
*/
if (wipeArea > 0 && totalArea > 0)
{
int percent = (int) (wipeArea * 100 / totalArea);
if (percent > 50)
{
isComplete = true;
postInvalidate();
}
}
}
};
最後我們可以在OnMeasure方法中,將我們整個view的大小設置爲我們背景圖片的大小,就可以在佈局文件中愉快的玩耍啦。不然在佈局文件中總是已match_parent的形式存在。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 寬的測量規格
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
// 寬的測量尺寸
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
// 高度的測量規格
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
// 高度的測量尺寸
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//根據View的邏輯得到,比如TextView根據設置的文字計算wrap_content時的大小。
//這兩個數據根據實現需求計算。
int wrapWidth = mBitmapRes.getWidth();
int wrapHeight = mBitmapRes.getHeight();
// 如果是是AT_MOST則對哪個進行特殊處理
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(wrapWidth, wrapHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(wrapWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, wrapHeight);
}
}