Android刮刮卡的實現
做了幾年開發以前都是去看書, 看大神的部落格, 看別人的文章, 確實收穫了不少, 遇到不會的就查, 看到別人寫好的第三方控制元件就拿過來用 , 使用別人製造的輪子感覺灰常好, 還節省時間, 慢慢的發現程式碼寫的很快, 輪子用的很熟, 但是具體細節還是不懂, 光顧著埋頭寫程式碼, 使用輪子了, 對具體的知識點還是一知半解. 不是說不應該使用別人的輪子, 不重複造輪子是個好習慣, 但是我們在使用別人的輪子的同時也要對別人造的輪子理解透徹, 這樣才能更好的進行改進麼不是 , 而且我們也不能只是看書, 看部落格, 看文章, 看完之後需要動手寫一下, 而且寫的過程中我相信肯定不是一帆風順的(/ □ \)(我寫例子的時候真是各種趟坑啊, 也許是我技術不到家), 因此我萌發了寫部落格的念頭, 這樣不僅能督促我看書學習, 通過寫部落格的過程中還能讓我對知識點掌握的更加牢固
這是本人第一次寫部落格, 寫的不好勿噴哈, 建議隨便提, 我好藉此改正, 但是可以說我技術搓, 文筆差, 甚至長得醜, 只要不人身攻擊, 不涉及家人就好, 好的意見一定接受, 我寫的也沒啥高深的玩意, 大神們可以忽略了, 都是寫看過的書, 或者知識點, 寫篇部落格權當做一點筆記了, 平時工作也挺忙, 所以也只能抽空寫寫.
接下來進入正題, 刮刮卡的實現, 我相信很多同學都看過很多大神的部落格(例如我看過的大神 鴻洋的部落格
先上圖,
見圖, 灰色區域就是自定義的view, 火拳艾斯就是被遮擋的圖片, 紅色部分是自定義view的background, 這裡我對艾斯的圖片進行了等比例的縮放, 這樣可以更好的適應自定義view, 接下來就是具體實現方式了
1.用到關鍵知識點PorterDuffXfermode
這裡借別人部落格裡的一張圖做講解, 如下圖所示:一張圖做講解
這張圖形象的解釋了PorterDuffXfermode所支援的幾種mode型別, 其中Dst是先繪製的圖形, 而Src是後繪製的圖形.
本人文字表述能力著實有限, 這裡就不用文字描述各個mode的意思了, 相信大家根據上圖能理解各個mode的意思, 這裡主要用到的mode是SRC_IN, 也就是上圖第二行第二個模式
實現原理很簡單,
1.先畫出Dst的bitmap也就是準備好的艾斯的圖片, 然後再畫一個和view同等寬高的遮擋圖也就是灰色區域的bitmap
2.利用view的onTouchEvent方法, 獲取使用者手指的移動, 運用Path物件畫出手指移動的軌跡
3.利用設定了Xfermode屬性, 並且Alpha的值不要設定為0(其實不要設定為不透明即可)的畫筆, 畫出手指的移動軌跡, 因為選取的PorterDuffXfermode模式是SRC_IN, 因此就只會顯示我們的手機移動軌跡的那片區域, 有因為我們設定有透明度所以手指經過的地方就會變得透明, 就把後面的艾斯的圖片給顯示出來了, 這就完成了類似刮刮卡的效果
下面介紹主要程式碼
public ScratchImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
// 透明度 不設定為255 即可
mPaint.setAlpha(0);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
mPaint.setStyle(Paint.Style.STROKE);
// 設定結合處的形狀為圓角
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeWidth(50);
// 設定結尾處的形狀為圓角
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPath = new Path();
}
首先是構造方法, 及初始化方法, 初始化裡主要對畫筆物件做了初始化工作, 主要是設定了透明度和Xfermode屬性
接下來是對我們的遮蓋層和被遮蓋層進行初始化工作
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 獲取控制元件的寬和高
viewWidth = getWidth();
viewHeight = getHeight();
// 遮蓋層
mFgBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
// 底層也就是背景層(被遮蓋層)
mBgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ace);
// bitmap的寬高和view的寬高也許不是等比例的, 這裡計算需要縮放的比例
float scaleWidth = mFgBitmap.getWidth() * 1.0f / mBgBitmap.getWidth();
float scaleHeight = mFgBitmap.getHeight() * 1.0f / mBgBitmap.getHeight();
// 為了保證圖片能夠等比例縮放, 而不是寬/高會被拉伸, 這裡要取得相對小的那個值
float scale = Math.min(scaleWidth, scaleHeight);
// 通過矩陣進行縮放
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
// 得到新的圖片
mBgBitmap = Bitmap.createBitmap(mBgBitmap, 0, 0, mBgBitmap.getWidth(), mBgBitmap.getHeight(), matrix,
true);
mCanvas = new Canvas(mFgBitmap);
mCanvas.drawColor(Color.GRAY);
}
這裡我選擇onSizeChanged方法進行初始化的原因是, 這個方法是在檢視大小發生改變的時候回撥, 也就是說這個方法裡我們能夠方便的正確拿到view的大小, 這裡對程式碼稍微做下講解, 首先獲取view的寬高, 然後根據view的寬高得到遮蓋層的bitmap物件, 在載入了被遮蓋層的bitmap後, 我們根據view的寬高和mBgBitmap的寬高進行比較, 為了讓圖片進行等比例縮放並很好的填充view, 我們計算下寬高的縮放比, 根據縮放比對mBgBitmap進行了縮放,
接下來是最重要的onDraw方法
@Override
protected void onDraw(Canvas canvas) {
// 第一個矩形區域表示要畫的bitmap的大小
Rect bgBtimapRect = new Rect(0, 0, mBgBitmap.getWidth(), mBgBitmap.getHeight());
// 為了使得bitmap繪製在view的正中心, 這裡計算一下起始的x, y的值
int left = viewWidth / 2 - mBgBitmap.getWidth() / 2;
int top = viewHeight / 2 - mBgBitmap.getHeight() / 2;
// 第二個矩形區域表示bitmap要繪製的區域
Rect bgBitmapStartRect = new Rect(left, top,
mBgBitmap.getWidth() + left, mBgBitmap.getHeight() + top);
canvas.drawBitmap(mBgBitmap, bgBtimapRect, bgBitmapStartRect, null);
canvas.drawBitmap(mFgBitmap, 0, 0, null);
mCanvas.drawPath(mPath, mPaint);
}
這個方法裡我們首先畫出來mBgBitmap物件, 這裡用的是canvase的這個
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,@Nullable Paint paint)方法, 這裡著重說明一下第二和第三個引數, 該方法的第二個引數src是指所要繪製的bitmap的區域, 第三個引數dst是指bitmap所要繪製在那個矩形區域內, 再看前面幾行程式碼, 這裡主要就是計算第三個引數的過程, 防止圖片在不能完美填充view的情況下, 通過計算讓圖片位於view的正中央, 這樣兩幅圖畫完了, 最後一行畫使用者手指的路徑即可
最後看一下onTouchEvent,
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.reset();
mPath.moveTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(), event.getY());
break;
}
invalidate();
return true;
}
這裡沒什麼可仔細說明的, 就是action_down的時候moveto到指定位置, 在action_move的時候, 利用path物件的lineTo方法獲取路徑, 最後電泳invalidate()方法 , 呼叫onDraw進行繪製, 這樣整個刮刮卡就完成了
最後貼一下完整的程式碼
public class ScratchImageView2 extends View {
private String TAG = "ScratchImageView2";
// 被遮擋的bitmap, 和用於遮擋的bitmap
private Bitmap mBgBitmap, mFgBitmap;
// 畫筆物件, 主要設定了setXfermode 和透明度
private Paint mPaint;
private Canvas mCanvas;
private Path mPath;
// 控制元件的寬和高
private int viewWidth;
private int viewHeight;
public ScratchImageView2(Context context) {
super(context);
}
public ScratchImageView2(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ScratchImageView2(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 獲取控制元件的寬和高
viewWidth = getWidth();
viewHeight = getHeight();
// 遮蓋層
mFgBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
// 底層也就是背景層(被遮蓋層)
mBgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ace);
// bitmap的寬高和view的寬高也許不是等比例的, 這裡計算需要縮放的比例
float scaleWidth = mFgBitmap.getWidth() * 1.0f / mBgBitmap.getWidth();
float scaleHeight = mFgBitmap.getHeight() * 1.0f / mBgBitmap.getHeight();
// 為了保證圖片能夠等比例縮放, 而不是寬/高會被拉伸, 這裡要取得相對小的那個值
float scale = Math.min(scaleWidth, scaleHeight);
// 通過矩陣進行縮放
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
// 得到新的圖片
mBgBitmap = Bitmap.createBitmap(mBgBitmap, 0, 0, mBgBitmap.getWidth(), mBgBitmap.getHeight(), matrix,
true);
mCanvas = new Canvas(mFgBitmap);
mCanvas.drawColor(Color.GRAY);
}
private void init() {
mPaint = new Paint();
// 透明度 不設定為255 即可
mPaint.setAlpha(0);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
mPaint.setStyle(Paint.Style.STROKE);
// 設定結合處的形狀為圓角
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeWidth(50);
// 設定結尾處的形狀為圓角
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPath = new Path();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.reset();
mPath.moveTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(), event.getY());
break;
}
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
// 第一個矩形區域表示要畫的bitmap的大小
Rect bgBtimapRect = new Rect(0, 0, mBgBitmap.getWidth(), mBgBitmap.getHeight());
// 為了使得bitmap繪製在view的正中心, 這裡計算一下起始的x, y的值
int left = viewWidth / 2 - mBgBitmap.getWidth() / 2;
int top = viewHeight / 2 - mBgBitmap.getHeight() / 2;
// 第二個矩形區域表示bitmap要繪製的區域
Rect bgBitmapStartRect = new Rect(left, top,
mBgBitmap.getWidth() + left, mBgBitmap.getHeight() + top);
canvas.drawBitmap(mBgBitmap, bgBtimapRect, bgBitmapStartRect, null);
canvas.drawBitmap(mFgBitmap, 0, 0, null);
mCanvas.drawPath(mPath, mPaint);
}
}
完事, 第一次寫部落格, 寫了好長時間, 現在感覺大神們寫部落格分享東西 真的是很辛苦的一件事, 更別說大神們還能把複雜的只是講解的很透徹, 再次真心感謝網上那些樂於奉獻的大神們, 我希望自己也能堅持下去