旋轉控制元件(一):矩形的平移旋轉縮放
之前做了這個東西,一個內部素材加外操作邊框,包含基本的移動、縮放、旋轉,拉伸,快速定位,十字對齊等操作。常見使用場景如新增馬賽克,新增畫中畫等。感覺比較有意思而且中間也遇到了一些問題就記錄一下
先上圖:

scaleRotae.gif
如圖,這次就先講一下平移、旋轉、縮放
如果只是view做平移,有很多種實現方式比如通過layout、動畫等。
基於我們的使用場景:知道一個圖片的位置資訊和旋轉角度(中心旋轉),就可以將它畫出來。所以選擇了維護一個rect以及繞中心旋轉的角度rotation,這樣一些第三方如需要c層操作的地方,直接把這些資訊給c層也可以馬上明確。
一、如何畫
可以看出這個控制元件其實是一個圖片+外面一個框,不過要考慮到旋轉的情況,這裡我們就藉助matrix來做中心旋轉。其實相對的就是先把canvas繞圖片的中心做一次旋轉
@Override protected void dispatchDraw(Canvas canvas) { canvas.save(); canvas.concat(mRotateMatrix); mainDrawable.setBounds((int) mRect.left, (int) mRect.top, (int) mRect.right, (int) mRect.bottom); mainDrawable.draw(canvas); canvas.drawRect(mRect, mPaint); drawAnchors(canvas, mRect); canvas.restore(); super.dispatchDraw(canvas); }
虛線就用paint設定setPathEffect就可以了,虛線框角上的幾個角標也是藉助rect的位置資訊來畫出來
比如畫右上角的旋轉按鈕:
if (rotateDrawable != null) { //右上角 rotateDrawable.setBounds(right - drawableWidth, top - drawableHeight, right + drawableWidth, top + drawableHeight); rotateDrawable.draw(canvas); }
重點就是去計算上面程式碼的mRotateMatrix
private void invalidateMatrix() { mRotateMatrix.reset(); mRotateMatrix.postTranslate(-mRect.centerX(), -mRect.centerY()); mRotateMatrix.postRotate(mRotation); mRotateMatrix.postTranslate(mRect.centerX(), mRect.centerY()); }
這裡簡單提一下matrix。學過線性代數的都知道矩陣吧,matrix其實就是個3x3的矩陣,裡面的元素控制著旋轉、縮放、平移、錯切。直接new出來的矩陣是一個單位矩陣,描述的就是原來的圖形資訊,沒有做變換。
然後就是矩陣計算不滿足交換律,換句話說矩陣的前乘和後乘結果不一樣,反應在matrix裡面就是pre和post介面效果不一樣。
簡單點理解就是pre是放在操作佇列頭,post放在隊尾,還有個set會清空整個佇列再把它放進去。如果覺得擔心記混,推薦就用post介面,符合先進先出的原則,先post的先執行。
二、如何判斷什麼時候該縮放、旋轉或者平移
這裡肯定是事件處理相關的了。處於方便,我們在ontouchEvent的時候把操作委託給GestureDetector,在onDown回撥時判斷點到了哪裡,比如旋轉、縮放、平移。然後在onScroll回撥的時候通過每次的增量dx,dy計算縮放的比例、旋轉角度以及移動距離。
在手指頭按下時,由於我們知道圖片rec的位置資訊和角標位置資訊,所以通過x和y可以判斷是否點到了角標。
但是有個問題是,畫的矩形是在旋轉過的畫布上面,我們手指頭的xy是螢幕上的位置,這裡對應不起來,會出現旋轉後就點不到角標了
為了解決這個問題,手指頭按下的point需要利用matix做一次對映。在這裡的場景其實就相當於把旋轉的資訊考慮進去,其實也完全可以自己用三角函式算,但是matrix已經提供這種介面了,而且是呼叫的c層計算,效率應該更高些。
final Matrix rotateMatrix = new Matrix(); //反向旋轉回去 抵消canvas的旋轉 rotateMatrix.postTranslate(-mRect.centerX(), -mRect.centerY()); rotateMatrix.postRotate(-mRotation); rotateMatrix.postTranslate(mRect.centerX(), mRect.centerY()); rotateMatrix.mapPoints(point); eventX = point[0]; eventY = point[1]; RectF rectF = mRect; //hit rotate右上 if (Math.abs(rectF.right - eventX) < drawableWidth * 2 && Math.abs(rectF.top - eventY) < drawableHeight * 2) { return HitModes.ROTATE; }
需要注意的是這裡matix的旋轉角度和canvas的是相反的,其實就相當於轉了n角度,然後又轉回去n角度,相當於沒有轉。然後就可以按照沒有旋轉的情況判斷有沒有點到角標。
三、如何進行旋轉、縮放、平移
第二步已經判斷到使用者想要進行什麼操作了,接下來就是執行對應的操作了。
平移
先說簡單的平移吧,上面說了,我們的場景是一個矩形位置資訊+繞中心旋轉角度。所以平移其實就是改矩形的位置罷了,直接調rec的offset,然後一定要記得在重新繪製前更新canvas的旋轉矩陣
private void onMove(float dx, float dy) { mRect.offset(-dx, -dy); invalidateMatrix(); invalidate(); }
縮放
首先我們要明確一個東西,就是縮放時旋轉中心一定是不變的。所以可以算出中心和右下角的距離以及scroll後的中心和右下角的距離算出兩個距離的變化當成x的變化。我們這裡是等比例縮放,根據比例算出y的變化。
這裡做了一個縮放最小的限制,縮放到1.5個角度寬高後就不嫩再縮小了,這樣可以保證角標不會擠在一起。
private void onScale(float dx, float dy) { // TODO: 2019/4/8 這裡的dx,dy計算需要改進 float[] pt1 = new float[] { mRect.centerX(), mRect.centerY() }; float[] pt2 = new float[] { mRect.right, mRect.bottom }; float[] pt3 = new float[] { mRect.right + dx, mRect.bottom + dy }; float distance1 = getPointDistance(pt1, pt2); float distance2 = getPointDistance(pt1, pt3); float distance = distance1 - distance2; if (!checkCanScale(distance)) { return; } mRect.inset(-distance, -distance / mRatio); invalidateMatrix(); invalidate(); }
旋轉
至於旋轉其實也簡單,因為旋轉時中心也是不變的,類似縮放的操作。根據右上角的旋轉角標和中心的角度,以及scroll後右上角和中心的角度,這兩個的角度差就是旋轉角度。
已知兩個點的位置,通過math的atan2函式可以算出角度
private void onRotate(float triggerX, float triggerY) { // TODO: 2019/4/8 這裡的dx,dy計算需要改進 float[] pt1 = new float[] { mRect.centerX(), mRect.centerY() }; float[] pt2 = new float[] { mRect.right, mRect.top }; float[] pt3 = new float[] { triggerX, triggerY }; double angel1 = PointUtil.calculateAngleBetweenPoints(pt2, pt1); double angel2 = PointUtil.calculateAngleBetweenPoints(pt3, pt1); mRotation = (float) (angel1 - angel2); invalidateMatrix(); invalidate(); }
到這裡基本的平移旋轉縮放都介紹完了,詳細的一些位置計算可以參考demo
https://github.com/dynamicBai/ScaleRotateView 給個star鼓勵下唄
後面會逐步介紹:拉伸四邊的操作(旋轉中心會變化)、圖片在螢幕上的快速定位和微調、移動時十字輔助線對齊等效果。