學(xué)習(xí)微信編輯功能可以讓我們更加扎實(shí)Android自定義的基礎(chǔ),學(xué)習(xí)最好的方法的就是
死命啃源碼两芳,當(dāng)然了摔寨,我們弄不到微信的源碼,但是我們可以找到別人寫好的源碼來(lái)一個(gè)一個(gè)學(xué)習(xí)相關(guān)功能怖辆,找到一個(gè)代碼寫的很好的一個(gè)源碼是复,而且本身也是仿照微信功能的,我們看下源碼地址:
minetsh/Imaging: Android Image Edit Lib. Android 圖片編輯庫(kù)竖螃,微信圖片編輯庫(kù) (github.com)
作者針對(duì)該源碼寫了一份博客淑廊,有興趣的同學(xué)可以去看下
Android 圖片編輯的原理與實(shí)現(xiàn)——涂鴉與馬賽克 (qq.com)
如果同學(xué)們覺得還了解不夠多的話,那么我們一起全面解析該源碼吧特咆!
我們簡(jiǎn)稱 minetsh/Imaging: Android Image Edit Lib. Android 圖片編輯庫(kù)季惩,微信圖片編輯庫(kù) (github.com) 為 圖片編輯庫(kù)
我為了更加徹底學(xué)習(xí)源碼,將一個(gè)一個(gè)功能全部拆解出來(lái)一個(gè)例子坚弱,這樣理解會(huì)容易很多蜀备!
在最下面會(huì)放出學(xué)習(xí)的例子源碼。
但是在我們講解例子前荒叶,先講圖片編輯庫(kù)主要由兩個(gè)類來(lái)構(gòu)成碾阁,一個(gè)是自定義類IMGView
,繼承于FrameLayout控件,然后在繪制圖片的時(shí)候些楣,會(huì)通過自定義一個(gè)類IMGImage
來(lái)進(jìn)行相關(guān)繪制脂凶。所有關(guān)于觸摸操作(單點(diǎn)移動(dòng)宪睹、多點(diǎn)縮放、涂鴉蚕钦、馬賽克等)都是在IMGView
的onTouchEvent下解析亭病,針對(duì)當(dāng)前模式解析觸摸操作,進(jìn)行對(duì)應(yīng)的繪制嘶居,這就是編輯圖片的主要思路了罪帖。
那么我們看下圖片編輯庫(kù)有以下幾大技術(shù)點(diǎn):
- Matrix矩陣
- 移動(dòng)圖片
- 縮放圖片
- 涂鴉圖片
- 馬賽克圖片
- 裁剪圖片
Matrix矩陣
所有有關(guān)變換圖片的操作都會(huì)涉及到Matrix,所以我們這邊單獨(dú)針對(duì)Matrix矩陣進(jìn)行詳細(xì)講解,我單獨(dú)抽出一個(gè)文章講解了Matrix
Android Matrix的set\pre\post方法的區(qū)別和使用
移動(dòng)圖片
- 通過GestureDetector手勢(shì)監(jiān)聽類邮屁,傳遞MotionEvent來(lái)執(zhí)行相關(guān)onScroll方法
- 通過GestureDetector傳遞的xy整袁,在當(dāng)前view的xy基礎(chǔ)上疊加
- 最后得到的值,通過當(dāng)前view的scrollTo方法調(diào)用
- 圖像的平移不會(huì)影響圖片的畫布位置佑吝,當(dāng)前控件的視圖窗口會(huì)發(fā)生變化坐昙,也就是scrollX、scrollY 的值發(fā)生變化芋忿。
private void initialize(Context context) {
// 手勢(shì)監(jiān)聽類
mGDetector = new GestureDetector(context, new MoveAdapter());
}
/**
* 處理觸屏事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
return onTouch(event);
}
/**
* 處理觸屏事件.詳情
*/
boolean onTouch(MotionEvent event) {
Log.d(TAG, "onTouch");
return onTouchNONE(event);
}
private boolean onTouchNONE(MotionEvent event) {
Log.d(TAG, "onTouchNONE");
return mGDetector.onTouchEvent(event);
}
private boolean onScroll(float dx, float dy) {
return onScrollTo(getScrollX() + Math.round(dx), getScrollY() + Math.round(dy));
}
private boolean onScrollTo(int x, int y) {
if (getScrollX() != x || getScrollY() != y) {
scrollTo(x, y);
return true;
}
return false;
}
private class MoveAdapter extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return ScrollyFrameLayout.this.onScroll(distanceX, distanceY);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// TODO
return super.onFling(e1, e2, velocityX, velocityY);
}
}
縮放圖片
跟移動(dòng)類似炸客,通過ScaleGestureDetector類觸發(fā)onScale,在本身view的基礎(chǔ)上添加getFocusX
private void initialize(Context context) {
// 用于處理縮放的工具類
mSGDetector = new ScaleGestureDetector(context, this);
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (mPointerCount > 1) {
mImage.onScale(detector.getScaleFactor(),
getScrollX() + detector.getFocusX(),
getScrollY() + detector.getFocusY());
invalidate();
return true;
}
return false;
}
可以看到mImage.onScale()的具體代碼如下
public void onScale(float factor, float focusX, float focusY) {
if (factor == 1f) return;
// 針對(duì)這個(gè)有圖片的邊框進(jìn)行比例縮放
M.setScale(factor, factor, focusX, focusY);
M.mapRect(mFrame);
}
M是Matrix矩陣戈钢,關(guān)于這個(gè)Matrix.setScale就有點(diǎn)意思了痹仙,具體詳解如下:
public boolean postScale(float sx, float sy, float px, float py)
這個(gè)api的第一個(gè)參數(shù)是X軸的縮放大小,第二個(gè)參數(shù)是Y軸的縮放大小殉了,第三四個(gè)參數(shù)是縮放中心點(diǎn)蝶溶。
一般這個(gè)縮放中心點(diǎn)比較不好理解。這個(gè)中心點(diǎn)并不一定在圖片的中心位置宣渗。有可能在圖片的外面。我們可以這樣理解梨州。以這個(gè)中心點(diǎn)為坐標(biāo)原點(diǎn)畫X軸跟Y軸痕囱。圖片可能會(huì)跟X軸或者Y軸相交,也可能完全在一個(gè)區(qū)間內(nèi)暴匠。
畫圖理解如下:
那么用到這個(gè)方法就能實(shí)現(xiàn)到 客戶根據(jù)當(dāng)前觸摸焦點(diǎn)而進(jìn)行縮放
鞍恢。
涂鴉圖片
涂鴉圖片也是在onTouchEvent分發(fā)事件下執(zhí)行相關(guān)涂鴉操作
/**
* 處理觸屏事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
return onTouch(event);
}
/**
* 處理觸屏事件.詳情
*/
boolean onTouch(MotionEvent event) {
return onTouchPath(event);
}
/**
* 畫筆線
*/
private boolean onTouchPath(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 鋼筆初始化
return onPathBegin(event);
case MotionEvent.ACTION_MOVE:
// 畫線
return onPathMove(event);
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 畫線完成,繪制路徑加入到繪制列表
return mPen.isIdentity(event.getPointerId(0)) && onPathDone();
}
return false;
}
關(guān)于繪制整體思路是:通過上面代碼分發(fā)繪制事件,onPathMove執(zhí)行畫線每窖,每次畫線都會(huì)invalidate view帮掉,當(dāng)畫線完成后,繪制路徑加入到繪制列表窒典,在onDraw事件里面蟆炊,都會(huì)重新把繪制路徑全部渲染出來(lái)。
比較有點(diǎn)意思的一個(gè)技術(shù)點(diǎn)是瀑志,在縮放view的情況下涩搓,如何精確到繪制路徑呢污秆,那么就必須仔細(xì)看看IMGImage的addPath
方法了,具體看代碼注釋和圖解
/**
* addPath方法詳解:
* M.setTranslate(sx, sy);
* 矩陣平移到跟view的xy軸一樣,注意,是getScrollX()和getScrolly()
*
* M.postTranslate(-mFrame.left, -mFrame.top);
* 如果按照getScrollX()直接繪制進(jìn)手機(jī)屏幕上是會(huì)出格的昧甘,因?yàn)関iew能縮放到比手機(jī)屏幕還要大良拼,那么就需要減掉mFrame的x和y,剩下的就是手機(jī)繪制的正確的點(diǎn)
*/
public void addPath(IMGPath path, float sx, float sy) {
if (path == null) return;
float scale = 1f / getScale();
M.setTranslate(sx, sy);
M.postTranslate(-mFrame.left, -mFrame.top);
M.postScale(scale, scale);
// 矩陣變換
path.transform(M);
mDoodles.add(path);
}
/**
* 1 * view縮放后的寬度 / 圖片固定寬度 = 縮放比例
*/
public float getScale() {
return 1f * mFrame.width() / mImage.getWidth();
}
如果覺得還不夠理解充边,具體還請(qǐng)到源碼作者博客看看Android 圖片編輯的原理與實(shí)現(xiàn)——涂鴉與馬賽克 (qq.com)
馬賽克
在馬賽克這里頻繁用到一個(gè)很有意思的東西庸推,canvas.save() 和 canvas.restore(),restoreToCount跟他們一樣意思浇冰,只是restoreToCount細(xì)化到某個(gè)id贬媒。
如果不是很了解他們的意思,可以直接看
Android canvas.save()與canvas.restore()的使用總結(jié)_Nothing-CSDN博客
馬賽克跟涂鴉一樣湖饱,都是通過onTouchEvent
分發(fā)進(jìn)行繪畫動(dòng)作掖蛤,但是跟涂鴉不一樣的是,馬賽克會(huì)先畫一個(gè)馬賽克的圖片井厌,按照源碼作者的原話是:
其實(shí)很簡(jiǎn)單蚓庭,馬賽克就是將整個(gè)區(qū)域的顏色變成一個(gè)顏色值,如將 10x10 區(qū)域內(nèi)的顏色變成其中的一個(gè)顏色值仅仆,所以我們將一張圖片縮放到一個(gè)較小的尺寸器赞,然后再放大到原始尺寸去顯示,這個(gè)圖片就很模糊了墓拜,然后關(guān)閉 Paint 的濾波功能:paint.setFilterBitmap(false)港柜,這樣就得到了一個(gè)圖片的馬賽克效果,如下:
繪制馬賽克圖片如下:
/**
* 創(chuàng)建同樣的馬賽克圖和馬賽克畫筆
*/
private void makeMosaicBitmap() {
Log.d(TAG, "makeMosaicBitmap");
if (mMosaicImage != null || mImage == null) {
return;
}
// 原圖的寬高相除64
int w = Math.round(mImage.getWidth() / 64f);
int h = Math.round(mImage.getHeight() / 64f);
// 取最大值咳榜,即不能小于8
w = Math.max(w, 8);
h = Math.max(h, 8);
// 馬賽克畫刷夏醉,注意是SRC_IN,刷子刷后就顯示相應(yīng)的馬賽克層了
if (mMosaicPaint == null) {
mMosaicPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMosaicPaint.setFilterBitmap(false);
mMosaicPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
}
// 創(chuàng)建馬賽克圖
mMosaicImage = Bitmap.createScaledBitmap(mImage, w, h, false);
}
所以為什么要將一整張圖變成馬賽克呢涌韩?可以用一張圖簡(jiǎn)單表示如下:
將馬賽克路徑圖層與馬賽克圖層合并顯示即可畔柔。也就是 Paint 的如下功能:
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
關(guān)鍵代碼如下:
private void onDrawImages(Canvas canvas) {
// 圖片
mImage.onDrawImage(canvas);
// 馬賽克
if (!mImage.isMosaicEmpty() || (!mPen.isEmpty())) {
int count = mImage.onDrawMosaicsPath(canvas);
mDoodlePaint.setStrokeWidth(IMGPath.BASE_MOSAIC_WIDTH);
canvas.save();
canvas.translate(getScrollX(), getScrollY());
canvas.drawPath(mPen.getPath(), mDoodlePaint);
canvas.restore();
mImage.onDrawMosaic(canvas, count);
canvas.save();
canvas.restore();
}
}
/**
* 繪制馬賽克路徑
*/
public int onDrawMosaicsPath(Canvas canvas) {
Log.d(TAG, "onDrawMosaicsPath");
// 所有狀態(tài)都保存
int layerCount = canvas.saveLayer(mFrame, null, Canvas.ALL_SAVE_FLAG);
if (!isMosaicEmpty()) {
canvas.save();
float scale = getScale();
canvas.translate(mFrame.left, mFrame.top);
canvas.scale(scale, scale);
for (IMGPath path : mMosaics) {
path.onDrawMosaic(canvas, mPaint);
}
canvas.restore();
}
return layerCount;
}
/**
* 繪制馬賽克
*/
public void onDrawMosaic(Canvas canvas, int layerCount) {
Log.d(TAG, "onDrawMosaic");
canvas.drawBitmap(mMosaicImage, null, mFrame, mMosaicPaint);
canvas.restoreToCount(layerCount);
}