概述
了解過(guò)自定義View的童鞋 對(duì)Canvas.drawBitmap(Bitmap, Matrix, Paint)這個(gè)函數(shù)應(yīng)該不會(huì)陌生,Bitmap的位置、大小、旋轉(zhuǎn)角度、扭曲程度等都由Matrix來(lái)管理,而實(shí)現(xiàn)貼紙效果的就需要借助這個(gè)神奇的函數(shù)。我們可以通過(guò)很多種方法獲取到貼紙的Bitmap,也可以很容易的定義繪制Bitmap所使用的Paint,那么剩下我們只需要關(guān)心怎樣可以借助Matrix來(lái)讓貼紙隨著我們的指尖翩翩起舞。

為了更好的管理每個(gè)貼紙的Bitmap和Matrix信息,我簡(jiǎn)單的將二者進(jìn)行了封裝,大家不要在意名字,知道這個(gè)類是干嘛的就好了,畢竟如何起一個(gè)優(yōu)雅準(zhǔn)確的名字是一個(gè)世界性的難題。
public static class ImageGroup {
public Bitmap bitmap;
public Matrix matrix = new Matrix();
//刪除貼紙時(shí)釋放資源時(shí)使用
public void release() {
if (bitmap != null) {
bitmap.recycle();
bitmap = null;
}
if (matrix != null) {
matrix.reset();
matrix = null;
}
}
}
說(shuō)到隨著指尖,我們就會(huì)想到Android豐富的手勢(shì)操作,因?yàn)槭亲远xView,所有對(duì)Bitmap的操作都需要用到手勢(shì)觸點(diǎn)坐標(biāo),因此我使用了View的onTouchEvent(MotionEvent event)方法直接對(duì)手勢(shì)觸點(diǎn)就行操作,onTouchEvent也是整個(gè)貼紙模塊的核心。
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
anchorX = event.getX();
anchorY = event.getY();
moveTag = decalCheck(anchorX, anchorY);
deleteTag = deleteCheck(anchorX, anchorY);
if (moveTag != -1 && deleteTag == -1) {
downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
mode = DRAG;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
moveTag = decalCheck(event.getX(0), event.getY(0));
transformTag = decalCheck(event.getX(1), event.getY(1));
if (moveTag != -1 && transformTag == moveTag && deleteTag == -1) {
downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
mode = ZOOM;
}
oldDistance = getDistance(event);
oldRotation = getRotation(event);
midPoint = midPoint(event);
break;
case MotionEvent.ACTION_MOVE:
if (mode == ZOOM) {
moveMatrix.set(downMatrix);
float newRotation = getRotation(event) - oldRotation;
float newDistance = getDistance(event);
float scale = newDistance / oldDistance;
moveMatrix.postScale(scale, scale, midPoint.x, midPoint.y);// 縮放
moveMatrix.postRotate(newRotation, midPoint.x, midPoint.y);// 旋轉(zhuǎn)
if (moveTag != -1) {
mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();
} else if (mode == DRAG) {
moveMatrix.set(downMatrix);
moveMatrix.postTranslate(event.getX() - anchorX, event.getY() - anchorY);// 平移
if (moveTag != -1) {
mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if (deleteTag != -1) {
mDecalImageGroupList.remove(deleteTag).release();
invalidate();
}
mode = NONE;
break;
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
}
return true;
}
手勢(shì)操作
onTouchEvent中我們使用了ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_MOVE、ACTION_UP、ACTION_POINTER_UP五種事件,其中ACTION_DOWN和ACTION_UP、ACTION_POINTER_DOWN和ACTION_POINTER_UP分別對(duì)應(yīng)。
- ACTION_DOWN和ACTION_UP:當(dāng)View從無(wú)到有手指觸摸,ACTION_DOWN會(huì)被觸發(fā),對(duì)應(yīng)的ACTION_UP則為從有到無(wú),也就是說(shuō)只有當(dāng)沒有任何一根手指在觸摸View時(shí)ACTION_UP才會(huì)被觸發(fā),ACTION_DOWN和ACTION_UP是整個(gè)手勢(shì)操作生命周期的起點(diǎn)和終點(diǎn)。
- ACTION_POINTER_DOWN和ACTION_POINTER_UP:當(dāng)View有多點(diǎn)觸摸時(shí)ACTION_POINTER_DOWN會(huì)被觸發(fā),而當(dāng)其中某個(gè)觸點(diǎn)消失后ACTION_POINTER_UP會(huì)被觸發(fā)。這里我們只考慮有兩根手指觸摸的情況。
- ACTION_MOVE:當(dāng)View被觸摸且該觸摸點(diǎn)在移動(dòng)時(shí)ACTION_MOVE會(huì)被觸發(fā),多點(diǎn)觸摸時(shí)無(wú)論哪個(gè)點(diǎn)移動(dòng)都會(huì)觸發(fā)。
Matrix的Translate(平移)外,Scale(縮放)、Rotate(旋轉(zhuǎn))、Skew(扭曲)四大操作除了Skew外我們都需要使用,對(duì)應(yīng)到手勢(shì)上我們通過(guò)單點(diǎn)觸摸來(lái)控制Bitmap平移,通過(guò)多點(diǎn)觸摸來(lái)控制Bitmap縮放和旋轉(zhuǎn),因此,在ACTION_MOVE階段我們需要根據(jù)兩種不同情況做區(qū)分。
int NONE = 0;//無(wú)
int DRAG = 1;//平移模式
int ZOOM = 2;//縮放、旋轉(zhuǎn)模式
我們定義三種mode,mode的初始值為NONE,當(dāng)ACTION_DOWN被觸發(fā)時(shí)mode置為DRAG,當(dāng)ACTION_POINTER_DOWN被觸發(fā)時(shí)mode置為ZOOM。當(dāng)ACTION_MOVE被觸發(fā)時(shí),我們對(duì)mode進(jìn)行判斷,針對(duì)DRAG和ZOOM兩種情況分別進(jìn)行處理。
- mode == DRAG
anchorX = event.getX();
anchorY = event.getY();
moveTag = decalCheck(anchorX, anchorY);
deleteTag = deleteCheck(anchorX, anchorY);
if (moveTag != -1 && deleteTag == -1) {
downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
mode = DRAG;
}
ACTION_DOWN被觸發(fā)時(shí)我們首先將當(dāng)前觸摸點(diǎn)的坐標(biāo)保存下來(lái)以備使用。之后要判斷當(dāng)前觸摸點(diǎn)是否在某一個(gè)貼紙的Bitmap范圍內(nèi)以及當(dāng)前觸摸點(diǎn)是否在某一個(gè)貼紙的刪除按鈕范圍內(nèi),我們分別用moveTag和deleteTag來(lái)保存結(jié)果,當(dāng)結(jié)果為-1時(shí)表示沒有在任何相關(guān)范圍內(nèi),結(jié)果為0~貼紙數(shù)量-1時(shí)表示在某個(gè)貼紙的相關(guān)范圍內(nèi)。只有當(dāng)moveTag != -1 && deleteTag == -1(觸摸點(diǎn)某一個(gè)貼紙范圍內(nèi)且不在任何刪除按鈕范圍內(nèi))時(shí),我們將當(dāng)前貼紙的Matrix保存到downMatrix中并將mode置成DRAG,若deleteTag不等于-1時(shí),我們?cè)贏CTION_UP就將對(duì)應(yīng)的貼紙從貼紙列表中移除。
moveMatrix.set(downMatrix);
moveMatrix.postTranslate(event.getX() - anchorX, event.getY() - anchorY);// 平移
if (moveTag != -1) {
mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();
進(jìn)入到ACTION_MOVE階段,我們首先將downMatrix賦值給moveMatrix,downMatrix是這次手勢(shì)操作的起始Matrix,之后的變換都是基于downMatrix進(jìn)行的,所以我們不能直接對(duì)downMatrix進(jìn)行操作,moveMatrix承擔(dān)了這個(gè)責(zé)任。DRAG模式下表示當(dāng)前要進(jìn)行的是平移操作,而平移的橫縱距離是在ACTION_DOWN階段保存下來(lái)的觸摸點(diǎn)橫縱坐標(biāo)值與當(dāng)前移動(dòng)到的觸摸點(diǎn)橫縱坐標(biāo)值的差值。最后將處理好的moveMatrix賦值回該貼紙的Matrix,并調(diào)用重繪函數(shù),完成此次貼紙平移操作。因?yàn)锳CTION_MOVE會(huì)在ACTION_UP觸發(fā)之前一直保持,所以整個(gè)平移操作會(huì)一直持續(xù),貼紙隨著手指移動(dòng)而移動(dòng)的效果就出現(xiàn)了。
- mode == ZOOM
moveTag = decalCheck(event.getX(0), event.getY(0));
transformTag = decalCheck(event.getX(1), event.getY(1));
if (moveTag != -1 && transformTag == moveTag && deleteTag == -1) {
downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
mode = ZOOM;
}
oldDistance = getDistance(event);
oldRotation = getRotation(event);
midPoint = midPoint(event);
ACTION_POINTER_DOWN被觸發(fā)時(shí),我們首先對(duì)兩個(gè)觸摸點(diǎn)是否在某個(gè)貼紙范圍內(nèi)進(jìn)行判斷,結(jié)果分別用moveTag和transformTag進(jìn)行保存。當(dāng)moveTag != -1 && transformTag == moveTag && deleteTag == -1(兩個(gè)觸摸點(diǎn)在同一個(gè)貼紙范圍內(nèi)且不在任何刪除按鈕范圍內(nèi))條件滿足時(shí),我們將當(dāng)前貼紙的Matrix保存到downMatrix中并將mode置成ZOOM。同時(shí)我們需要將當(dāng)前兩個(gè)觸摸點(diǎn)之間的距離、中點(diǎn)、角度用oldDistance、midPoint、oldRotation保存起來(lái)以備使用。
moveMatrix.set(downMatrix);
float newRotation = getRotation(event) - oldRotation;
float newDistance = getDistance(event);
float scale = newDistance / oldDistance;
moveMatrix.postScale(scale, scale, midPoint.x, midPoint.y);// 縮放
moveMatrix.postRotate(newRotation, midPoint.x, midPoint.y);// 旋轉(zhuǎn)
if (moveTag != -1) {
mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();
進(jìn)入到ACTION_MOVE階段,我們首先將downMatrix賦值給moveMatrix。用當(dāng)前的兩個(gè)觸摸點(diǎn)算出新的角度,同之前保存的值算出差值newRotation,算出新的距離newDistance并和oldDistance做商,算出縮放比例。分別對(duì)moveMatrix進(jìn)行縮放和旋轉(zhuǎn)操作,處理好后將moveMatrix賦值回該貼紙的Matrix,并調(diào)用重繪函數(shù),完成此次貼紙平移操作。因?yàn)锳CTION_MOVE會(huì)在ACTION_UP觸發(fā)之前一直保持且在ACTION_POINTER_UP觸發(fā)之前ZOOM模式會(huì)一直保持,所以縮放、旋轉(zhuǎn)操作會(huì)一直持續(xù),貼紙隨著兩根手指之間距離變化而變化,角度變化而變化的效果就出現(xiàn)了。
觸摸點(diǎn)判斷
protected float[] getBitmapPoints(Bitmap bitmap, Matrix matrix) {
float[] dst = new float[8];
float[] src = new float[]{
0, 0,
bitmap.getWidth(), 0,
0, bitmap.getHeight(),
bitmap.getWidth(), bitmap.getHeight()
};
matrix.mapPoints(dst, src);
return dst;
}
private boolean pointCheck(ImageGroup imageGroup, float x, float y) {
float[] points = getBitmapPoints(imageGroup);
float x1 = points[0];
float y1 = points[1];
float x2 = points[2];
float y2 = points[3];
float x3 = points[4];
float y3 = points[5];
float x4 = points[6];
float y4 = points[7];
float edge = (float) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
if ((2 + Math.sqrt(2)) * edge >= Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2))
+ Math.sqrt(Math.pow(x - x2, 2) + Math.pow(y - y2, 2))
+ Math.sqrt(Math.pow(x - x3, 2) + Math.pow(y - y3, 2))
+ Math.sqrt(Math.pow(x - x4, 2) + Math.pow(y - y4, 2))) {
return true;
}
return false;
}
private boolean circleCheck(ImageGroup imageGroup, float x, float y) {
float[] points = getBitmapPoints(imageGroup);
float x2 = points[2];
float y2 = points[3];
int checkDis = (int) Math.sqrt(Math.pow(x - x2, 2) + Math.pow(y - y2, 2));
if (checkDis < 40) {
return true;
}
return false;
}
在整個(gè)手勢(shì)操作流程中我們需要多次使用觸摸點(diǎn)判斷,不論是判斷是否在貼紙范圍還是刪除按鈕范圍。Matrix提供了map開頭的映射方法,其中的mapPoints(float[] dst, float[] src)可以將src坐標(biāo)數(shù)組根據(jù)Matrix映射到dst。使用Matrix來(lái)存儲(chǔ)Bitmap的繪制信息,Bitmap默認(rèn)的繪制起點(diǎn)(左上角點(diǎn))為(0,0),因此默認(rèn)情況下Bitmap四個(gè)點(diǎn)的坐標(biāo)為(0,0)、(bitmap.getWidth(), 0)、(0, bitmap.getHeight())、(bitmap.getWidth(), bitmap.getHeight()),依此我們可以映射出當(dāng)前Bitmap四個(gè)點(diǎn)的坐標(biāo)。我們的貼紙?jiān)谡麄€(gè)流程任何操作下都是正方形,因此我們可以使用已知正方形四個(gè)頂點(diǎn)來(lái)判斷第五點(diǎn)是否在正方形范圍內(nèi),算法是第五點(diǎn)到四頂點(diǎn)的距離是否小于等于2√2倍的邊長(zhǎng)。這個(gè)算法對(duì)長(zhǎng)方形適不適用我沒有驗(yàn)證,如果要添加非正方形Bitmap的話需要自行優(yōu)化此處。判斷點(diǎn)是否在一個(gè)圓的范圍內(nèi)很簡(jiǎn)單,只要將該點(diǎn)到圓心的距離和半徑進(jìn)行比較即可。
總結(jié)
整個(gè)Android貼紙的簡(jiǎn)單實(shí)現(xiàn)思路就行這樣,完整代碼鏈接如下,有需要的童鞋可以搞下來(lái)看看,有什么問(wèn)題或者好點(diǎn)子歡迎交流。
代碼地址:https://github.com/JunyiZhou/AndroidExercises/tree/master/ImageHandleDemo