上個月寫了一篇《Android開發(fā)之仿微博貼紙效果實現(xiàn)——基礎(chǔ)篇》,文章中提到還有一篇進(jìn)階篇要寫,很早就想動筆了,因中途去維護(hù)了開源庫《高仿微信圖片選擇器2.0版本》,導(dǎo)致耽擱到了現(xiàn)在,剛好趁這幾天過年在家休息,把這篇文章給完成了。
我們來回顧下上篇文章所寫的內(nèi)容,我們通過canvas.drawBitmap(mBitmap, mMatrix, null);將Bitmap圖像繪制到Canvas上,然后用一個矩陣Matrix配合不同手勢來維護(hù)圖像的變換狀態(tài)(移動、縮放、旋轉(zhuǎn))。
這篇文章主要來講一下多圖貼紙的實現(xiàn)思路和代碼的設(shè)計重構(gòu),在上篇文章中,我們實現(xiàn)單個貼紙效果是一個自定義View,那么多個貼紙要如何實現(xiàn)呢?可能有的朋友會想,既然已經(jīng)實現(xiàn)了單個貼紙,那么多個貼紙只要依葫蘆畫瓢,讓一個ViewGroup去裝載多個貼紙不就可以了?確實沒毛病,這樣做也是可以的,不過中間可能會遇到一些麻煩,比如狀態(tài)的管理(當(dāng)多個貼紙重疊在一起時,觸摸焦點的判斷等)而且之前的實現(xiàn)代碼是面向過程的,一整坨的代碼維護(hù)起來也不是那么的舒服,這里我想介紹一種新思路,用更優(yōu)雅的方式去實現(xiàn)多圖貼紙。
國際慣例,先看一下實現(xiàn)的效果圖:

關(guān)于貼紙的核心代碼其實并沒有改變(矩陣的變換,坐標(biāo)點的映射,角度、縮放值的計算等),有了這個前提后,我們拋開代碼層面來捋一下思路,想想如何更優(yōu)雅的實現(xiàn)這個多圖貼紙效果。
開源庫的使用:
為了方便大家使用,這里我把貼紙庫做了封裝,目前已經(jīng)開源了,有什么建議可以隨時反饋給我,我會更新維護(hù)上去,傳送門:StickerView
1、如何在項目中引入該貼紙庫:
//gradle引入此行
compile 'com.lcw.library:StickeView:1.0.1'
2、布局設(shè)置:
<com.lcw.library.stickerview.StickerLayout
android:id="@+id/sl_sticker_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
3、添加貼紙:
Sticker sticker = new Sticker(mContext,mBitmap);
mStickerLayout.addSticker(sticker);
好了,開始我們今天的講解。
實現(xiàn)思路:
首先我們可以把貼紙看成一個獨立個體,在個體里包含了圖片,矩陣,初始坐標(biāo),焦點等屬性,在構(gòu)建完這些屬性后,需要通過Canvas繪制出來 ,還有手勢操作,我們通過手勢去識別出當(dāng)前的操作類型和計算出變化的數(shù)值,動態(tài)去改變矩陣,再通過Canvas繪制出來,其實這些和View并沒有直接的關(guān)系,所以我們可以把貼紙抽象成一個對象,根據(jù)面向?qū)ο蟮木幊趟枷?,我們可以大致得到下面這張圖:

簡單的解釋下這張圖的設(shè)計(只體現(xiàn)出關(guān)鍵元素):
ISupportOperation:貼紙的操作接口,這里涉及到貼紙的平移,縮放,旋轉(zhuǎn),繪制,觸摸等操作。
BaseSticker:貼紙基類(抽象類),這里實現(xiàn)了ISupportOperation的部分方法,主要會對根據(jù)變換矩陣對貼紙自身的繪制(矩陣)。
Sticker:貼紙類,這里我們會對用戶的手勢進(jìn)行分析處理,得到相關(guān)的動作和數(shù)據(jù),比如平移的距離,縮放的大小,旋轉(zhuǎn)的角度等,然后交給貼紙基類做具體的繪制。
StickerManager:貼紙的管理類,這里維護(hù)著所有的貼紙,可以對貼紙進(jìn)行狀態(tài)的管理(比如增刪改查,焦點狀態(tài)等)。
StickerLayout:自定義貼紙布局(自定義View),這里會根據(jù)觸摸坐標(biāo)從貼紙的管理類中獲取當(dāng)前被操作的貼紙,然后再對觸摸事件進(jìn)行分發(fā),交給具體貼紙類處理。
整理一下流程:
用戶觸摸貼紙布局(StickerLayout)可以得到觸摸坐標(biāo),根據(jù)坐標(biāo)我們可以在貼紙管理類中(StickerManager)得到對應(yīng)的貼紙,然后我們把觸摸事件分發(fā)給貼紙(Sticker)進(jìn)行觸摸事件的分解,得到相關(guān)動作和計算數(shù)值,然后交給貼紙基類(BaseSticker)完成自身狀態(tài)變換和相關(guān)繪制,最后通知貼紙布局進(jìn)行界面刷新。
流程圖如下:

編碼實現(xiàn):
了解了實現(xiàn)思路,就完成一大半工作了,剩下的只需要我們用代碼將它闡述出來即可,下面給出核心代碼:
首先當(dāng)用戶觸摸貼紙布局(StickerLayout)的時候,需要根據(jù)坐標(biāo)去獲取對一個的貼紙(由于添加貼紙的時候是加入一個List集合,所以再取出貼紙的時候,我們應(yīng)該倒序去遍歷這個集合,才可以拿到最近時間添加的那個貼紙)
/**
* 根據(jù)觸摸坐標(biāo)返回當(dāng)前觸摸的貼紙
*
* @param x
* @param y
* @return
*/
public Sticker getSticker(float x, float y) {
float[] dstPoints = new float[2];
float[] srcPoints = new float[]{x, y};
for (int i = mStickerList.size() - 1; i >= 0; i--) {
Sticker sticker = mStickerList.get(i);
Matrix matrix = new Matrix();
sticker.getMatrix().invert(matrix);
matrix.mapPoints(dstPoints, srcPoints);
if (sticker.getStickerBitmapBound().contains(dstPoints[0], dstPoints[1])) {
return sticker;
}
}
return null;
}
然后把貼紙交給對應(yīng)的Sticker類去做處理:
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
//單指是否觸摸到貼紙
mStick = StickerManager.getInstance().getSticker(event.getX(), event.getY());
if (mStick == null) {
if (event.getPointerCount() == 2) {
//處理雙指觸摸屏幕,第一指沒有觸摸到貼紙,第二指觸摸到貼紙情況
mStick = StickerManager.getInstance().getSticker(event.getX(1), event.getY(1));
}
}
break;
default:
break;
}
if (mStick != null) {
mStick.onTouch(event);
invalidate();
}
return true;
}
在貼紙類(Sticker)中處理手勢的識別、數(shù)據(jù)計算和動作下發(fā)給貼紙基類(BaseSticker)執(zhí)行:
/**
* 處理觸摸事件
*
* @param event
*/
public void onTouch(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//有觸摸到貼紙
mMode = Sticker.MODE_SINGLE;
//記錄按下的位置
mLastSinglePoint.set(event.getX(), event.getY());
break;
case MotionEvent.ACTION_POINTER_DOWN:
if (event.getPointerCount() == 2) {
mMode = Sticker.MODE_MULTIPLE;
//記錄雙指的點位置
mFirstPoint.set(event.getX(0), event.getY(0));
mSecondPoint.set(event.getX(1), event.getY(1));
//計算雙指之間向量
mLastDistanceVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);
//計算雙指之間距離
mLastDistance = calculateDistance(mFirstPoint, mSecondPoint);
}
break;
case MotionEvent.ACTION_MOVE:
if (mMode == MODE_SINGLE) {
translate(event.getX() - mLastSinglePoint.x, event.getY() - mLastSinglePoint.y);
mLastSinglePoint.set(event.getX(), event.getY());
}
if (mMode == MODE_MULTIPLE && event.getPointerCount() == 2) {
//記錄雙指的點位置
mFirstPoint.set(event.getX(0), event.getY(0));
mSecondPoint.set(event.getX(1), event.getY(1));
//操作自由縮放
float distance = calculateDistance(mFirstPoint, mSecondPoint);
//根據(jù)雙指移動的距離獲取縮放因子
float scale = distance / mLastDistance;
scale(scale, scale);
mLastDistance = distance;
//操作自由旋轉(zhuǎn)
mDistanceVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);
rotate(calculateDegrees(mLastDistanceVector, mDistanceVector));
mLastDistanceVector.set(mDistanceVector.x, mDistanceVector.y);
}
break;
case MotionEvent.ACTION_UP:
reset();
break;
}
}
在貼紙基類(BaseSticker)執(zhí)行貼紙類(Sticker)下發(fā)的動作命令:
/**
* 平移操作
*
* @param dx
* @param dy
*/
@Override
public void translate(float dx, float dy) {
mMatrix.postTranslate(dx, dy);
updatePoints();
}
/**
* 縮放操作
*
* @param sx
* @param sy
*/
@Override
public void scale(float sx, float sy) {
mMatrix.postScale(sx, sy, mMidPointF.x, mMidPointF.y);
updatePoints();
}
/**
* 旋轉(zhuǎn)操作
*
* @param degrees
*/
@Override
public void rotate(float degrees) {
mMatrix.postRotate(degrees, mMidPointF.x, mMidPointF.y);
updatePoints();
}
/**
* 當(dāng)矩陣發(fā)生變化的時候,更新坐標(biāo)點(src坐標(biāo)點經(jīng)過matrix映射變成了dst坐標(biāo)點)
*/
private void updatePoints() {
//更新貼紙點坐標(biāo)
mMatrix.mapPoints(mDstPoints, mSrcPoints);
}
/**
* 繪制貼紙自身
*
* @param canvas
* @param paint
*/
@Override
public void onDraw(Canvas canvas, Paint paint) {
//繪制貼紙
canvas.drawBitmap(mStickerBitmap, mMatrix, paint);
if (isFocus) {
//繪制貼紙邊框
canvas.drawLine(mDstPoints[0] - PADDING, mDstPoints[1] - PADDING, mDstPoints[2] + PADDING, mDstPoints[3] - PADDING, paint);
canvas.drawLine(mDstPoints[2] + PADDING, mDstPoints[3] - PADDING, mDstPoints[4] + PADDING, mDstPoints[5] + PADDING, paint);
canvas.drawLine(mDstPoints[4] + PADDING, mDstPoints[5] + PADDING, mDstPoints[6] - PADDING, mDstPoints[7] + PADDING, paint);
canvas.drawLine(mDstPoints[6] - PADDING, mDstPoints[7] + PADDING, mDstPoints[0] - PADDING, mDstPoints[1] - PADDING, paint);
//繪制移除按鈕
canvas.drawBitmap(mDelBitmap, mDstPoints[0] - mDelBitmap.getWidth() / 2 - PADDING, mDstPoints[1] - mDelBitmap.getHeight() / 2 - PADDING, paint);
}
}
最后完成貼紙布局的刷新:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
List<Sticker> stickerList = StickerManager.getInstance().getStickerList();
for (int i = 0; i < stickerList.size(); i++) {
Sticker sticker = stickerList.get(i);
sticker.onDraw(canvas, mPaint);
}
}
具體的貼紙實現(xiàn)代碼在上篇文章已經(jīng)提過了,所以這里就不再重復(fù)的代碼粘貼,不清楚的朋友可以看下上篇的文章,或者去github下載源碼細(xì)看。
好了,到這里文章就結(jié)束啦~
源碼下載:
這里附上源碼地址(歡迎Star,歡迎Fork):StickerView