前言
最近有需求要做一個(gè)畫布透典,這個(gè)畫布以一個(gè)圖片為背景晴楔,可以實(shí)現(xiàn)縮放,涂鴉以及貼紙的功能峭咒,縮放和涂鴉要兼顧税弃,于是就想到了可以加入手勢和多點(diǎn)觸控,大致就是兩只手指頭可以拖動(dòng)或者旋轉(zhuǎn)或者放大凑队,單只手指可以涂鴉畫東西之類的则果,恩,具體的需求在這里先描述了顽决,然后看下大致的實(shí)現(xiàn)短条。
效果展示
思路
- 思考一
通過繼承ImageView,類似PhotoView 的實(shí)現(xiàn)才菠,因?yàn)閜hotoivew 已經(jīng)實(shí)現(xiàn)了旋轉(zhuǎn)和縮放的功能茸时,在其基礎(chǔ)上繼承拓展,只需要復(fù)寫onDraw方法赋访,將觸摸的軌跡轉(zhuǎn)化為Path 直接draw到canvas上即可可都。可以實(shí)現(xiàn)的蚓耽,但是要注意一點(diǎn)渠牲,那就是坐標(biāo)轉(zhuǎn)化:你的單個(gè)手指移動(dòng)的軌跡坐標(biāo)點(diǎn)們是相對(duì)于這個(gè)view的位置的,當(dāng)你旋轉(zhuǎn)或者縮放這個(gè)view 的時(shí)候步悠,結(jié)果是先前保存的坐標(biāo)軌跡是無法匹配到當(dāng)前旋轉(zhuǎn)或縮放處理后的view签杈,這個(gè)時(shí)候就需要你將坐標(biāo)軌跡進(jìn)行映射處理
。 - 思 考二
則是直接復(fù)寫View控件,通過將圖片直接轉(zhuǎn)換為bitmap后答姥,draw到view 的畫布上铣除。整個(gè)過程就是先在bitmap上新建一個(gè)畫布,然后將軌跡坐標(biāo)draw到這個(gè)bitmap的canvas上鹦付,也就是這個(gè)bitmap上尚粘,最后在onDraw的回調(diào)里面,將這個(gè)bitmap 畫到整個(gè)View 的canvas上敲长,當(dāng)然郎嫁,最后要自行實(shí)現(xiàn)bitmap的縮放,旋轉(zhuǎn)等坐標(biāo)轉(zhuǎn)換功能祈噪,好處是先前的涂鴉會(huì)一直保持泽铛。
預(yù)先準(zhǔn)備
這個(gè)時(shí)候就必須要提一下Martix,Andorid 貼心的給我們提供了這樣一個(gè)工具類,我們完全可以擺脫坐標(biāo)點(diǎn)計(jì)算之苦啦钳降。
在這里強(qiáng)烈推薦大家看下 android matrix 最全方法詳解與進(jìn)階(完整篇)厚宰,原理以及api介紹的相當(dāng)詳細(xì)腌巾。
實(shí)現(xiàn)
考慮到要有貼圖遂填,并且貼圖支持大小縮放的功能,拖動(dòng)功能澈蝙,采用了第二種方式吓坚,其實(shí)感覺采用第一種方式應(yīng)該會(huì)更簡單點(diǎn)(微笑臉),好了灯荧,下面介紹下具體實(shí)現(xiàn)
首先要處理這個(gè)view 的touch事件:
if (actionMode == ACTION_DRAG) {
onDragAction(curX - preX, curY - preY, event);//拖動(dòng)監(jiān)聽
} else if (actionMode == ACTION_ROTATE) {
onRotateAction(curPhotoRecord);//旋轉(zhuǎn)監(jiān)聽
} else if (actionMode == ACTION_SCALE) {
mScaleGestureDetector.onTouchEvent(event); //縮放監(jiān)聽
}
- 涂鴉
就是將拇指略過之處的所以坐標(biāo)連接起來礁击,而這個(gè)坐標(biāo)id呢,不是絕對(duì)坐標(biāo)逗载,而是對(duì)于這個(gè)view 的相對(duì)坐標(biāo)(畢竟還要支持縮放和撤銷操作的)某弦,單是縮放則不用過多約束洲赵,只要將path畫到bitmap Canvas上,顯示出來即可,但是需要支持撤銷抗果,這就要求必須要保持每一個(gè)筆畫的坐標(biāo)點(diǎn)組啦,縮放或者旋轉(zhuǎn)時(shí)切黔,相對(duì)于這個(gè)view 的坐標(biāo)肯定會(huì)發(fā)生變化暮的,大致給下代碼:
//縮放處理描點(diǎn)位置
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方向縮放參數(shù),scaleY為y軸縮放參數(shù)感挥,(x,y)為縮放中心點(diǎn)坐標(biāo)
for( Object object :curSketchData.drawPathList){//drawPathList為存放坐標(biāo)的數(shù)組
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();
}
- 縮放
那么如何得到縮放的中心點(diǎn)呢缩搅?實(shí)現(xiàn)ScaleGestureDetector 實(shí)例,調(diào)用onTouchEvent,此時(shí)會(huì)回調(diào)onScale(ScaleGestureDetector detector)
触幼,我們來看下使用這個(gè)detector 的具體邏輯
private void onScaleAction(ScaleGestureDetector detector) {
Log.e("shang", "onscale :" + detector.getScaleFactor());
float[] photoCorners = calculateBgCorners(backgroundSrcRect);//獲取現(xiàn)階段底圖的標(biāo)志坐標(biāo)點(diǎn)
//目前圖片對(duì)角線長度
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();
//設(shè)置Matrix縮放參數(shù)
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]);//涂鴉點(diǎn)坐標(biāo)轉(zhuǎn)換
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代表左上角點(diǎn)XY硼瓣,2,3代表右上角點(diǎn)XY,4,5代表右下角點(diǎn)XY置谦,6,7代表左下角點(diǎn)XY堂鲤,8,9代表中心點(diǎn)XY
float[] photoCorners = new float[10];//0,1代表左上角點(diǎn)XY噪猾,2,3代表右上角點(diǎn)XY,4,5代表右下角點(diǎn)XY筑累,6,7代表左下角點(diǎn)XY袱蜡,8,9代表中心點(diǎn)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);//現(xiàn)階段的底圖的矩陣
return photoCorners;
}
其中
private void apply2DrawedCanvas() {
Matrix matrix = new Matrix();
currentDrawedBgM.invert(matrix);
mBGCanvas.setMatrix(matrix);//mBGCanvas為底圖bitmap所在的canvas
}
- 拖動(dòng)
拖動(dòng)和縮放類似,都是對(duì)當(dāng)前涂鴉坐標(biāo)做轉(zhuǎn)換慢宗,另對(duì)底圖矩陣做變換
private void onDragAction(float distanceX, float distanceY, MotionEvent event) {
//底圖變化
currentDrawedBgM.postTranslate((int) distanceX, (int) distanceY);
apply2DrawedCanvas();
//涂鴉坐標(biāo)轉(zhuǎn)換
convertDrawedPointPosition(distanceX,distanceY);
drawDrawedPosition();
}
- 旋轉(zhuǎn)
private void onRotateAction(PhotoRecord record) {
float[] corners = calculateCorners(record);
//放大
//目前觸摸點(diǎn)與圖片顯示中心距離
float a = (float) Math.sqrt(Math.pow(curX - corners[8], 2) + Math.pow(curY - corners[9], 2));
//目前上次旋轉(zhuǎn)圖標(biāo)與圖片顯示中心距離
float b = (float) Math.sqrt(Math.pow(corners[4] - corners[0], 2) + Math.pow(corners[5] - corners[1], 2)) / 2;
//旋轉(zhuǎn)
//根據(jù)移動(dòng)坐標(biāo)的變化構(gòu)建兩個(gè)向量坪蚁,以便計(jì)算兩個(gè)向量角度.
PointF preVector = new PointF();
PointF curVector = new PointF();
preVector.set(preX - corners[8], preY - corners[9]);//旋轉(zhuǎn)后向量
curVector.set(curX - corners[8], curY - corners[9]);//旋轉(zhuǎn)前向量
//計(jì)算向量長度
double preVectorLen = getVectorLength(preVector);
double curVectorLen = getVectorLength(curVector);
//計(jì)算兩個(gè)向量的夾角.
double cosAlpha = (preVector.x * curVector.x + preVector.y * curVector.y)
/ (preVectorLen * curVectorLen);
//由于計(jì)算誤差,可能會(huì)帶來略大于1的cos镜沽,例如
if (cosAlpha > 1.0f) {
cosAlpha = 1.0f;
}
//本次的角度已經(jīng)計(jì)算出來敏晤。
double dAngle = Math.acos(cosAlpha) * 180.0 / Math.PI;
// 判斷順時(shí)針和逆時(shí)針.
//判斷方法其實(shí)很簡單,這里的v1v2其實(shí)相差角度很小的缅茉。
//先轉(zhuǎn)換成單位向量
preVector.x /= preVectorLen;
preVector.y /= preVectorLen;
curVector.x /= curVectorLen;
curVector.y /= curVectorLen;
//作curVector的逆時(shí)針垂直向量嘴脾。
PointF verticalVec = new PointF(curVector.y, -curVector.x);
//判斷這個(gè)垂直向量和v1的點(diǎn)積,點(diǎn)積>0表示倆向量夾角銳角蔬墩。=0表示垂直译打,<0表示鈍角
float vDot = preVector.x * verticalVec.x + preVector.y * verticalVec.y;
if (vDot > 0) {
//v2的逆時(shí)針垂直向量和v1是銳角關(guān)系,說明v1在v2的逆時(shí)針方向拇颅。
} else {
dAngle = -dAngle;
}
currentDrawedBgM.postRotate((float) dAngle, corners[8], corners[9]);
}
- 撤銷
撤銷就是你首先保存了涂鴉的坐標(biāo)組奏司,和原始的底圖,將坐標(biāo)組坐標(biāo)減一樟插,重新畫到原始底圖上韵洋。
恢復(fù)類似,代碼我就不貼出來了黄锤。
mBGCanvas.drawBitmap(curSketchData.backgroundBMOrigin, currentDrawedBgM, null);
mBGCanvas.drawPath(mPath)搪缨;
- 貼紙
其實(shí)貼紙的邏輯,和增加第一個(gè)底圖的邏輯是一直的鸵熟,只不過要加一個(gè)flag來標(biāo)志操作的是貼紙 還是 底圖副编。這里推薦大家看下這篇文章Android貼紙。
總結(jié)
在做圖片處理時(shí)旅赢,首要理解坐標(biāo)的轉(zhuǎn)換齿桃,矩陣有著非常重要的地位,理解好android提供的Martix煮盼,很多類似的問題都會(huì)事倍功半短纵。