前言
最近有需求要做一個畫布,這個畫布以一個圖片為背景,可以實現縮放,涂鴉以及貼紙的功能,縮放和涂鴉要兼顧,于是就想到了可以加入手勢和多點觸控,大致就是兩只手指頭可以拖動或者旋轉或者放大,單只手指可以涂鴉畫東西之類的,恩,具體的需求在這里先描述了,然后看下大致的實現。
效果展示
自定義view.gif
思路
- 思考一
通過繼承ImageView,類似PhotoView 的實現,因為photoivew 已經實現了旋轉和縮放的功能,在其基礎上繼承拓展,只需要復寫onDraw方法,將觸摸的軌跡轉化為Path 直接draw到canvas上即可。可以實現的,但是要注意一點,那就是坐標轉化:你的單個手指移動的軌跡坐標點們是相對于這個view的位置的,當你旋轉或者縮放這個view 的時候,結果是先前保存的坐標軌跡是無法匹配到當前旋轉或縮放處理后的view,這個時候就需要你將坐標軌跡進行映射處理
。 - 思 考二
則是直接復寫View控件,通過將圖片直接轉換為bitmap后,draw到view 的畫布上。整個過程就是先在bitmap上新建一個畫布,然后將軌跡坐標draw到這個bitmap的canvas上,也就是這個bitmap上,最后在onDraw的回調里面,將這個bitmap 畫到整個View 的canvas上,當然,最后要自行實現bitmap的縮放,旋轉等坐標轉換功能,好處是先前的涂鴉會一直保持。
預先準備
這個時候就必須要提一下Martix,Andorid 貼心的給我們提供了這樣一個工具類,我們完全可以擺脫坐標點計算之苦啦。
在這里強烈推薦大家看下 android matrix 最全方法詳解與進階(完整篇),原理以及api介紹的相當詳細。
實現
考慮到要有貼圖,并且貼圖支持大小縮放的功能,拖動功能,采用了第二種方式,其實感覺采用第一種方式應該會更簡單點(微笑臉),好了,下面介紹下具體實現
首先要處理這個view 的touch事件:
if (actionMode == ACTION_DRAG) {
onDragAction(curX - preX, curY - preY, event);//拖動監聽
} else if (actionMode == ACTION_ROTATE) {
onRotateAction(curPhotoRecord);//旋轉監聽
} else if (actionMode == ACTION_SCALE) {
mScaleGestureDetector.onTouchEvent(event); //縮放監聽
}
- 涂鴉
就是將拇指略過之處的所以坐標連接起來,而這個坐標id呢,不是絕對坐標,而是對于這個view 的相對坐標(畢竟還要支持縮放和撤銷操作的),單是縮放則不用過多約束,只要將path畫到bitmap Canvas上,顯示出來即可,但是需要支持撤銷,這就要求必須要保持每一個筆畫的坐標點組啦,縮放或者旋轉時,相對于這個view 的坐標肯定會發生變化,大致給下代碼:
//縮放處理描點位置
private void convertDrawedPoiontsPosition(float scaleX, float scaleY, float x, float y) {
curTextSize = curTextSize * scaleX;
textPaint.setTextSize(curTextSize);
Matrix pointsMatrix = new Matrix();
pointsMatrix.postScale(scaleX,scaleY,x,y); //scaleX 為 x方向縮放參數,scaleY為y軸縮放參數,(x,y)為縮放中心點坐標
for( Object object :curSketchData.drawPathList){//drawPathList為存放坐標的數組
if(object instanceof SketchData.Angle){
SketchData.Angle angle = (SketchData.Angle)object;
float[] photoCornersSrc = new float[6];
float[] photoCorners = new float[6];
photoCornersSrc[0] = angle.start.x;
photoCornersSrc[1] = angle.start.y;
photoCornersSrc[2] = angle.middle.x;
photoCornersSrc[3] = angle.middle.y;
photoCornersSrc[4] = angle.end.x;
photoCornersSrc[5] = angle.end.y;
//angle.matrix.mapPoints(photoCorners, photoCornersSrc);
pointsMatrix.mapPoints(photoCorners, photoCornersSrc);
angle.start.x = photoCorners[0];
angle.start.y = photoCorners[1];
angle.middle.x = photoCorners[2];
angle.middle.y = photoCorners[3];
angle.end.x = photoCorners[4];
angle.end.y = photoCorners[5];
}else if(object instanceof SketchData.Length){
SketchData.Length length = (SketchData.Length)object;
float[] photoCornersSrc = new float[4];
float[] photoCorners = new float[4];
photoCornersSrc[0] = length.start.x;
photoCornersSrc[1] = length.start.y;
photoCornersSrc[2] = length.end.x;
photoCornersSrc[3] = length.end.y;
//angle.matrix.mapPoints(photoCorners, photoCornersSrc);
pointsMatrix.mapPoints(photoCorners, photoCornersSrc);
length.start.x = photoCorners[0];
length.start.y = photoCorners[1];
length.end.x = photoCorners[2];
length.end.y = photoCorners[3];
}
}
drawDrawedPosition();
}
- 縮放
那么如何得到縮放的中心點呢?實現ScaleGestureDetector 實例,調用onTouchEvent,此時會回調onScale(ScaleGestureDetector detector)
,我們來看下使用這個detector 的具體邏輯
private void onScaleAction(ScaleGestureDetector detector) {
Log.e("shang", "onscale :" + detector.getScaleFactor());
float[] photoCorners = calculateBgCorners(backgroundSrcRect);//獲取現階段底圖的標志坐標點
//目前圖片對角線長度
float len = (float) Math.sqrt(Math.pow(photoCorners[0] - photoCorners[4], 2) + Math.pow(photoCorners[1] - photoCorners[5], 2));
double photoLen = Math.sqrt(Math.pow(backgroundSrcRect.width(), 2) + Math.pow(backgroundSrcRect.height(), 2));
float scaleFactor = detector.getScaleFactor();
//設置Matrix縮放參數
if ((scaleFactor < 1 && len >= photoLen * SCALE_MIN && len >= SCALE_MIN_LEN) || (scaleFactor > 1 && len <= photoLen * SCALE_MAX)) {
Log.e(scaleFactor + "", scaleFactor + "");
convertDrawedPoiontsPosition(scaleFactor, scaleFactor, photoCorners[8], photoCorners[9]);//涂鴉點坐標轉換
currentDrawedBgM.postScale(scaleFactor, scaleFactor, photoCorners[8], photoCorners[9]);//底圖矩陣縮放
apply2DrawedCanvas();
mScaleValue = scaleFactor * mScaleValue;
Log.e("shang", "scale :" + mScaleValue);
drawDrawedPosition();
}
}
其中
private float[] calculateBgCorners(RectF rectF) {
float[] photoCornersSrc = new float[10];//0,1代表左上角點XY,2,3代表右上角點XY,4,5代表右下角點XY,6,7代表左下角點XY,8,9代表中心點XY
float[] photoCorners = new float[10];//0,1代表左上角點XY,2,3代表右上角點XY,4,5代表右下角點XY,6,7代表左下角點XY,8,9代表中心點XY
photoCornersSrc[0] = rectF.left;
photoCornersSrc[1] = rectF.top;
photoCornersSrc[2] = rectF.right;
photoCornersSrc[3] = rectF.top;
photoCornersSrc[4] = rectF.right;
photoCornersSrc[5] = rectF.bottom;
photoCornersSrc[6] = rectF.left;
photoCornersSrc[7] = rectF.bottom;
photoCornersSrc[8] = rectF.centerX();
photoCornersSrc[9] = rectF.centerY();
currentDrawedBgM.mapPoints(photoCorners, photoCornersSrc);//現階段的底圖的矩陣
return photoCorners;
}
其中
private void apply2DrawedCanvas() {
Matrix matrix = new Matrix();
currentDrawedBgM.invert(matrix);
mBGCanvas.setMatrix(matrix);//mBGCanvas為底圖bitmap所在的canvas
}
- 拖動
拖動和縮放類似,都是對當前涂鴉坐標做轉換,另對底圖矩陣做變換
private void onDragAction(float distanceX, float distanceY, MotionEvent event) {
//底圖變化
currentDrawedBgM.postTranslate((int) distanceX, (int) distanceY);
apply2DrawedCanvas();
//涂鴉坐標轉換
convertDrawedPointPosition(distanceX,distanceY);
drawDrawedPosition();
}
- 旋轉
private void onRotateAction(PhotoRecord record) {
float[] corners = calculateCorners(record);
//放大
//目前觸摸點與圖片顯示中心距離
float a = (float) Math.sqrt(Math.pow(curX - corners[8], 2) + Math.pow(curY - corners[9], 2));
//目前上次旋轉圖標與圖片顯示中心距離
float b = (float) Math.sqrt(Math.pow(corners[4] - corners[0], 2) + Math.pow(corners[5] - corners[1], 2)) / 2;
//旋轉
//根據移動坐標的變化構建兩個向量,以便計算兩個向量角度.
PointF preVector = new PointF();
PointF curVector = new PointF();
preVector.set(preX - corners[8], preY - corners[9]);//旋轉后向量
curVector.set(curX - corners[8], curY - corners[9]);//旋轉前向量
//計算向量長度
double preVectorLen = getVectorLength(preVector);
double curVectorLen = getVectorLength(curVector);
//計算兩個向量的夾角.
double cosAlpha = (preVector.x * curVector.x + preVector.y * curVector.y)
/ (preVectorLen * curVectorLen);
//由于計算誤差,可能會帶來略大于1的cos,例如
if (cosAlpha > 1.0f) {
cosAlpha = 1.0f;
}
//本次的角度已經計算出來。
double dAngle = Math.acos(cosAlpha) * 180.0 / Math.PI;
// 判斷順時針和逆時針.
//判斷方法其實很簡單,這里的v1v2其實相差角度很小的。
//先轉換成單位向量
preVector.x /= preVectorLen;
preVector.y /= preVectorLen;
curVector.x /= curVectorLen;
curVector.y /= curVectorLen;
//作curVector的逆時針垂直向量。
PointF verticalVec = new PointF(curVector.y, -curVector.x);
//判斷這個垂直向量和v1的點積,點積>0表示倆向量夾角銳角。=0表示垂直,<0表示鈍角
float vDot = preVector.x * verticalVec.x + preVector.y * verticalVec.y;
if (vDot > 0) {
//v2的逆時針垂直向量和v1是銳角關系,說明v1在v2的逆時針方向。
} else {
dAngle = -dAngle;
}
currentDrawedBgM.postRotate((float) dAngle, corners[8], corners[9]);
}
- 撤銷
撤銷就是你首先保存了涂鴉的坐標組,和原始的底圖,將坐標組坐標減一,重新畫到原始底圖上。
恢復類似,代碼我就不貼出來了。
mBGCanvas.drawBitmap(curSketchData.backgroundBMOrigin, currentDrawedBgM, null);
mBGCanvas.drawPath(mPath);
- 貼紙
其實貼紙的邏輯,和增加第一個底圖的邏輯是一直的,只不過要加一個flag來標志操作的是貼紙 還是 底圖。這里推薦大家看下這篇文章Android貼紙。
總結
在做圖片處理時,首要理解坐標的轉換,矩陣有著非常重要的地位,理解好android提供的Martix,很多類似的問題都會事倍功半。