android畫板---涂鴉,縮放,旋轉,貼紙實現

前言

最近有需求要做一個畫布,這個畫布以一個圖片為背景,可以實現縮放,涂鴉以及貼紙的功能,縮放和涂鴉要兼顧,于是就想到了可以加入手勢和多點觸控,大致就是兩只手指頭可以拖動或者旋轉或者放大,單只手指可以涂鴉畫東西之類的,恩,具體的需求在這里先描述了,然后看下大致的實現。

效果展示

自定義view.gif

思路

  1. 思考一
    通過繼承ImageView,類似PhotoView 的實現,因為photoivew 已經實現了旋轉和縮放的功能,在其基礎上繼承拓展,只需要復寫onDraw方法,將觸摸的軌跡轉化為Path 直接draw到canvas上即可。可以實現的,但是要注意一點,那就是坐標轉化:你的單個手指移動的軌跡坐標點們是相對于這個view的位置的,當你旋轉或者縮放這個view 的時候,結果是先前保存的坐標軌跡是無法匹配到當前旋轉或縮放處理后的view,這個時候就需要你將坐標軌跡進行映射處理
  2. 思 考二
    則是直接復寫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); //縮放監聽
  }          

  1. 涂鴉
    就是將拇指略過之處的所以坐標連接起來,而這個坐標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();

    }
  1. 縮放
    那么如何得到縮放的中心點呢?實現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
    }
  1. 拖動
    拖動和縮放類似,都是對當前涂鴉坐標做轉換,另對底圖矩陣做變換
private void onDragAction(float distanceX, float distanceY, MotionEvent event) {
        //底圖變化
         currentDrawedBgM.postTranslate((int) distanceX, (int) distanceY);
         apply2DrawedCanvas();
        //涂鴉坐標轉換
         convertDrawedPointPosition(distanceX,distanceY);
         drawDrawedPosition();

    }
  1. 旋轉
    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]);
    }
  1. 撤銷
    撤銷就是你首先保存了涂鴉的坐標組,和原始的底圖,將坐標組坐標減一,重新畫到原始底圖上。
    恢復類似,代碼我就不貼出來了。
  mBGCanvas.drawBitmap(curSketchData.backgroundBMOrigin, currentDrawedBgM, null);
   mBGCanvas.drawPath(mPath);
  1. 貼紙
    其實貼紙的邏輯,和增加第一個底圖的邏輯是一直的,只不過要加一個flag來標志操作的是貼紙 還是 底圖。這里推薦大家看下這篇文章Android貼紙

總結

在做圖片處理時,首要理解坐標的轉換,矩陣有著非常重要的地位,理解好android提供的Martix,很多類似的問題都會事倍功半。

參考鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容

  • 手勢圖片控件 PinchImageView 點擊圖片框架 photoView packagecom.example...
    Ztufu閱讀 735評論 0 1
  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,762評論 0 33
  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,483評論 0 17
  • 版權聲明:本文為博主原創文章,未經博主允許不得轉載 前言 Canvas 本意是畫布的意思,然而將它理解為繪制工具一...
    cc榮宣閱讀 41,600評論 1 47
  • 有的時候是我們把事情弄反了 現在這個年代,本來就是90后00后 干掉80后,移動互聯網+人才輩出 模式更新,摩爾定...
    紅日言知有理閱讀 146評論 0 0