android畫板---涂鴉,縮放合瓢,旋轉(zhuǎn),貼紙實(shí)現(xiàn)

前言

最近有需求要做一個(gè)畫布透典,這個(gè)畫布以一個(gè)圖片為背景晴楔,可以實(shí)現(xiàn)縮放,涂鴉以及貼紙的功能峭咒,縮放和涂鴉要兼顧税弃,于是就想到了可以加入手勢和多點(diǎn)觸控,大致就是兩只手指頭可以拖動(dòng)或者旋轉(zhuǎn)或者放大凑队,單只手指可以涂鴉畫東西之類的则果,恩,具體的需求在這里先描述了顽决,然后看下大致的實(shí)現(xiàn)短条。

效果展示

自定義view.gif

思路

  1. 思考一
    通過繼承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)行映射處理
  2. 思 考二
    則是直接復(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)聽
  }          

  1. 涂鴉
    就是將拇指略過之處的所以坐標(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();

    }
  1. 縮放
    那么如何得到縮放的中心點(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
    }
  1. 拖動(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();

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

總結(jié)

在做圖片處理時(shí)旅赢,首要理解坐標(biāo)的轉(zhuǎn)換齿桃,矩陣有著非常重要的地位,理解好android提供的Martix煮盼,很多類似的問題都會(huì)事倍功半短纵。

參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市僵控,隨后出現(xiàn)的幾起案子香到,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悠就,死亡現(xiàn)場離奇詭異千绪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)梗脾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門荸型,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人炸茧,你說我怎么就攤上這事瑞妇。” “怎么了梭冠?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵辕狰,是天一觀的道長。 經(jīng)常有香客問我控漠,道長蔓倍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任盐捷,我火速辦了婚禮偶翅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘毙驯。我一直安慰自己倒堕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布爆价。 她就那樣靜靜地躺著,像睡著了一般媳搪。 火紅的嫁衣襯著肌膚如雪铭段。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天秦爆,我揣著相機(jī)與錄音序愚,去河邊找鬼。 笑死等限,一個(gè)胖子當(dāng)著我的面吹牛爸吮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播望门,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼形娇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了筹误?” 一聲冷哼從身側(cè)響起桐早,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后哄酝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體友存,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年陶衅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屡立。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搀军,死狀恐怖侠驯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奕巍,我是刑警寧澤吟策,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站的止,受9級(jí)特大地震影響檩坚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诅福,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一匾委、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧氓润,春花似錦赂乐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至崩溪,卻和暖如春浅役,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伶唯。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工觉既, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人乳幸。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓瞪讼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親粹断。 傳聞我的和親對(duì)象是個(gè)殘疾皇子符欠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 手勢圖片控件 PinchImageView 點(diǎn)擊圖片框架 photoView packagecom.example...
    Ztufu閱讀 717評(píng)論 0 1
  • 背景 一年多以前我在知乎上答了有關(guān)LeetCode的問題, 分享了一些自己做題目的經(jīng)驗(yàn)。 張土汪:刷leetcod...
    土汪閱讀 12,724評(píng)論 0 33
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程姿染,因...
    小菜c閱讀 6,358評(píng)論 0 17
  • 版權(quán)聲明:本文為博主原創(chuàng)文章背亥,未經(jīng)博主允許不得轉(zhuǎn)載 前言 Canvas 本意是畫布的意思,然而將它理解為繪制工具一...
    cc榮宣閱讀 41,531評(píng)論 1 47
  • 有的時(shí)候是我們把事情弄反了 現(xiàn)在這個(gè)年代秒际,本來就是90后00后 干掉80后,移動(dòng)互聯(lián)網(wǎng)+人才輩出 模式更新狡汉,摩爾定...
    紅日言知有理閱讀 137評(píng)論 0 0