(1) 深度解析微信編輯圖片功能 - 細(xì)節(jié)分析

學(xué)習(xí)微信編輯功能可以讓我們更加扎實(shí)Android自定義的基礎(chǔ),學(xué)習(xí)最好的方法的就是
死命啃源碼两芳,當(dāng)然了摔寨,我們弄不到微信的源碼,但是我們可以找到別人寫好的源碼來(lái)一個(gè)一個(gè)學(xué)習(xí)相關(guān)功能怖辆,找到一個(gè)代碼寫的很好的一個(gè)源碼是复,而且本身也是仿照微信功能的,我們看下源碼地址:
minetsh/Imaging: Android Image Edit Lib. Android 圖片編輯庫(kù)竖螃,微信圖片編輯庫(kù) (github.com)
作者針對(duì)該源碼寫了一份博客淑廊,有興趣的同學(xué)可以去看下
Android 圖片編輯的原理與實(shí)現(xiàn)——涂鴉與馬賽克 (qq.com)

如果同學(xué)們覺得還了解不夠多的話,那么我們一起全面解析該源碼吧特咆!
我們簡(jiǎn)稱 minetsh/Imaging: Android Image Edit Lib. Android 圖片編輯庫(kù)季惩,微信圖片編輯庫(kù) (github.com) 為 圖片編輯庫(kù)

我為了更加徹底學(xué)習(xí)源碼,將一個(gè)一個(gè)功能全部拆解出來(lái)一個(gè)例子坚弱,這樣理解會(huì)容易很多蜀备!
在最下面會(huì)放出學(xué)習(xí)的例子源碼。
但是在我們講解例子前荒叶,先講圖片編輯庫(kù)主要由兩個(gè)類來(lái)構(gòu)成碾阁,一個(gè)是自定義類IMGView,繼承于FrameLayout控件,然后在繪制圖片的時(shí)候些楣,會(huì)通過自定義一個(gè)類IMGImage來(lái)進(jìn)行相關(guān)繪制脂凶。所有關(guān)于觸摸操作(單點(diǎn)移動(dòng)宪睹、多點(diǎn)縮放、涂鴉蚕钦、馬賽克等)都是在IMGView的onTouchEvent下解析亭病,針對(duì)當(dāng)前模式解析觸摸操作,進(jìn)行對(duì)應(yīng)的繪制嘶居,這就是編輯圖片的主要思路了罪帖。

那么我們看下圖片編輯庫(kù)有以下幾大技術(shù)點(diǎn):

  • Matrix矩陣
  • 移動(dòng)圖片
  • 縮放圖片
  • 涂鴉圖片
  • 馬賽克圖片
  • 裁剪圖片

Matrix矩陣

所有有關(guān)變換圖片的操作都會(huì)涉及到Matrix,所以我們這邊單獨(dú)針對(duì)Matrix矩陣進(jìn)行詳細(xì)講解,我單獨(dú)抽出一個(gè)文章講解了Matrix
Android Matrix的set\pre\post方法的區(qū)別和使用

移動(dòng)圖片

  • 通過GestureDetector手勢(shì)監(jiān)聽類邮屁,傳遞MotionEvent來(lái)執(zhí)行相關(guān)onScroll方法
  • 通過GestureDetector傳遞的xy整袁,在當(dāng)前view的xy基礎(chǔ)上疊加
  • 最后得到的值,通過當(dāng)前view的scrollTo方法調(diào)用
  • 圖像的平移不會(huì)影響圖片的畫布位置佑吝,當(dāng)前控件的視圖窗口會(huì)發(fā)生變化坐昙,也就是scrollX、scrollY 的值發(fā)生變化芋忿。
    private void initialize(Context context) {
        // 手勢(shì)監(jiān)聽類
        mGDetector = new GestureDetector(context, new MoveAdapter());
    }

    /**
     * 處理觸屏事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return onTouch(event);
    }

    /**
     * 處理觸屏事件.詳情
     */
    boolean onTouch(MotionEvent event) {
        Log.d(TAG, "onTouch");
        return onTouchNONE(event);
    }

    private boolean onTouchNONE(MotionEvent event) {
        Log.d(TAG, "onTouchNONE");
        return mGDetector.onTouchEvent(event);
    }

    private boolean onScroll(float dx, float dy) {
        return onScrollTo(getScrollX() + Math.round(dx), getScrollY() + Math.round(dy));
    }

    private boolean onScrollTo(int x, int y) {
        if (getScrollX() != x || getScrollY() != y) {
            scrollTo(x, y);
            return true;
        }
        return false;
    }

    private class MoveAdapter extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return ScrollyFrameLayout.this.onScroll(distanceX, distanceY);
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            // TODO
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    }

縮放圖片

跟移動(dòng)類似炸客,通過ScaleGestureDetector類觸發(fā)onScale,在本身view的基礎(chǔ)上添加getFocusX

    private void initialize(Context context) {
        // 用于處理縮放的工具類
        mSGDetector = new ScaleGestureDetector(context, this);
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        if (mPointerCount > 1) {
            mImage.onScale(detector.getScaleFactor(),
                    getScrollX() + detector.getFocusX(),
                    getScrollY() + detector.getFocusY());
            invalidate();
            return true;
        }
        return false;
    }

可以看到mImage.onScale()的具體代碼如下

    public void onScale(float factor, float focusX, float focusY) {
        if (factor == 1f) return;

        // 針對(duì)這個(gè)有圖片的邊框進(jìn)行比例縮放
        M.setScale(factor, factor, focusX, focusY);
        M.mapRect(mFrame);
    }

M是Matrix矩陣戈钢,關(guān)于這個(gè)Matrix.setScale就有點(diǎn)意思了痹仙,具體詳解如下:
public boolean postScale(float sx, float sy, float px, float py)
這個(gè)api的第一個(gè)參數(shù)是X軸的縮放大小,第二個(gè)參數(shù)是Y軸的縮放大小殉了,第三四個(gè)參數(shù)是縮放中心點(diǎn)蝶溶。
一般這個(gè)縮放中心點(diǎn)比較不好理解。這個(gè)中心點(diǎn)并不一定在圖片的中心位置宣渗。有可能在圖片的外面。我們可以這樣理解梨州。以這個(gè)中心點(diǎn)為坐標(biāo)原點(diǎn)畫X軸跟Y軸痕囱。圖片可能會(huì)跟X軸或者Y軸相交,也可能完全在一個(gè)區(qū)間內(nèi)暴匠。
畫圖理解如下:

image.png

那么用到這個(gè)方法就能實(shí)現(xiàn)到 客戶根據(jù)當(dāng)前觸摸焦點(diǎn)而進(jìn)行縮放鞍恢。

涂鴉圖片

涂鴉圖片也是在onTouchEvent分發(fā)事件下執(zhí)行相關(guān)涂鴉操作

    /**
     * 處理觸屏事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return onTouch(event);
    }

    /**
     * 處理觸屏事件.詳情
     */
    boolean onTouch(MotionEvent event) {
        return onTouchPath(event);
    }

    /**
     * 畫筆線
     */
    private boolean onTouchPath(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                // 鋼筆初始化
                return onPathBegin(event);
            case MotionEvent.ACTION_MOVE:
                // 畫線
                return onPathMove(event);
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // 畫線完成,繪制路徑加入到繪制列表
                return mPen.isIdentity(event.getPointerId(0)) && onPathDone();
        }
        return false;
    }

關(guān)于繪制整體思路是:通過上面代碼分發(fā)繪制事件,onPathMove執(zhí)行畫線每窖,每次畫線都會(huì)invalidate view帮掉,當(dāng)畫線完成后,繪制路徑加入到繪制列表窒典,在onDraw事件里面蟆炊,都會(huì)重新把繪制路徑全部渲染出來(lái)。
比較有點(diǎn)意思的一個(gè)技術(shù)點(diǎn)是瀑志,在縮放view的情況下涩搓,如何精確到繪制路徑呢污秆,那么就必須仔細(xì)看看IMGImage的addPath方法了,具體看代碼注釋和圖解

    /**
     * addPath方法詳解:
     * M.setTranslate(sx, sy);
     * 矩陣平移到跟view的xy軸一樣,注意,是getScrollX()和getScrolly()
     *
     * M.postTranslate(-mFrame.left, -mFrame.top);
     * 如果按照getScrollX()直接繪制進(jìn)手機(jī)屏幕上是會(huì)出格的昧甘,因?yàn)関iew能縮放到比手機(jī)屏幕還要大良拼,那么就需要減掉mFrame的x和y,剩下的就是手機(jī)繪制的正確的點(diǎn)
     */
    public void addPath(IMGPath path, float sx, float sy) {
        if (path == null) return;

        float scale = 1f / getScale();
        M.setTranslate(sx, sy);
        M.postTranslate(-mFrame.left, -mFrame.top);
        M.postScale(scale, scale);
        // 矩陣變換
        path.transform(M);

        mDoodles.add(path);
    }

    /**
     * 1 * view縮放后的寬度 / 圖片固定寬度 = 縮放比例
     */
    public float getScale() {
        return 1f * mFrame.width() / mImage.getWidth();
    }
圖解

如果覺得還不夠理解充边,具體還請(qǐng)到源碼作者博客看看Android 圖片編輯的原理與實(shí)現(xiàn)——涂鴉與馬賽克 (qq.com)

馬賽克

在馬賽克這里頻繁用到一個(gè)很有意思的東西庸推,canvas.save() 和 canvas.restore(),restoreToCount跟他們一樣意思浇冰,只是restoreToCount細(xì)化到某個(gè)id贬媒。
如果不是很了解他們的意思,可以直接看
Android canvas.save()與canvas.restore()的使用總結(jié)_Nothing-CSDN博客

馬賽克跟涂鴉一樣湖饱,都是通過onTouchEvent分發(fā)進(jìn)行繪畫動(dòng)作掖蛤,但是跟涂鴉不一樣的是,馬賽克會(huì)先畫一個(gè)馬賽克的圖片井厌,按照源碼作者的原話是:

其實(shí)很簡(jiǎn)單蚓庭,馬賽克就是將整個(gè)區(qū)域的顏色變成一個(gè)顏色值,如將 10x10 區(qū)域內(nèi)的顏色變成其中的一個(gè)顏色值仅仆,所以我們將一張圖片縮放到一個(gè)較小的尺寸器赞,然后再放大到原始尺寸去顯示,這個(gè)圖片就很模糊了墓拜,然后關(guān)閉 Paint 的濾波功能:paint.setFilterBitmap(false)港柜,這樣就得到了一個(gè)圖片的馬賽克效果,如下:
繪制馬賽克圖片
繪制馬賽克圖片如下:
    /**
     * 創(chuàng)建同樣的馬賽克圖和馬賽克畫筆
     */
    private void makeMosaicBitmap() {
        Log.d(TAG, "makeMosaicBitmap");
        if (mMosaicImage != null || mImage == null) {
            return;
        }

        // 原圖的寬高相除64
        int w = Math.round(mImage.getWidth() / 64f);
        int h = Math.round(mImage.getHeight() / 64f);

        // 取最大值咳榜,即不能小于8
        w = Math.max(w, 8);
        h = Math.max(h, 8);

        // 馬賽克畫刷夏醉,注意是SRC_IN,刷子刷后就顯示相應(yīng)的馬賽克層了
        if (mMosaicPaint == null) {
            mMosaicPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mMosaicPaint.setFilterBitmap(false);
            mMosaicPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        }

        // 創(chuàng)建馬賽克圖
        mMosaicImage = Bitmap.createScaledBitmap(mImage, w, h, false);
    }

所以為什么要將一整張圖變成馬賽克呢涌韩?可以用一張圖簡(jiǎn)單表示如下:

這樣就能繪制馬賽克

將馬賽克路徑圖層與馬賽克圖層合并顯示即可畔柔。也就是 Paint 的如下功能:

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
關(guān)鍵代碼如下:

    private void onDrawImages(Canvas canvas) {
        // 圖片
        mImage.onDrawImage(canvas);

        // 馬賽克
        if (!mImage.isMosaicEmpty() || (!mPen.isEmpty())) {
            int count = mImage.onDrawMosaicsPath(canvas);

            mDoodlePaint.setStrokeWidth(IMGPath.BASE_MOSAIC_WIDTH);
            canvas.save();
            canvas.translate(getScrollX(), getScrollY());
            canvas.drawPath(mPen.getPath(), mDoodlePaint);
            canvas.restore();

            mImage.onDrawMosaic(canvas, count);
            canvas.save();
            canvas.restore();
        }
    }
    /**
     * 繪制馬賽克路徑
     */
    public int onDrawMosaicsPath(Canvas canvas) {
        Log.d(TAG, "onDrawMosaicsPath");
        // 所有狀態(tài)都保存
        int layerCount = canvas.saveLayer(mFrame, null, Canvas.ALL_SAVE_FLAG);

        if (!isMosaicEmpty()) {
            canvas.save();
            float scale = getScale();
            canvas.translate(mFrame.left, mFrame.top);
            canvas.scale(scale, scale);
            for (IMGPath path : mMosaics) {
                path.onDrawMosaic(canvas, mPaint);
            }
            canvas.restore();
        }

        return layerCount;
    }
    /**
     * 繪制馬賽克
     */
    public void onDrawMosaic(Canvas canvas, int layerCount) {
        Log.d(TAG, "onDrawMosaic");
        canvas.drawBitmap(mMosaicImage, null, mFrame, mMosaicPaint);
        canvas.restoreToCount(layerCount);
    }

最后放出源碼:zhongjhATC/ImageingStudy: 這是詳細(xì)拆分Imaging項(xiàng)目的代碼進(jìn)行一個(gè)一個(gè)的學(xué)習(xí)代碼邏輯 (github.com)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市臣樱,隨后出現(xiàn)的幾起案子靶擦,更是在濱河造成了極大的恐慌,老刑警劉巖雇毫,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玄捕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡棚放,警方通過查閱死者的電腦和手機(jī)枚粘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)席吴,“玉大人赌结,你說我怎么就攤上這事捞蛋。” “怎么了柬姚?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵拟杉,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我量承,道長(zhǎng)搬设,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任撕捍,我火速辦了婚禮拿穴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘忧风。我一直安慰自己默色,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布狮腿。 她就那樣靜靜地躺著腿宰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缘厢。 梳的紋絲不亂的頭發(fā)上吃度,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音贴硫,去河邊找鬼椿每。 笑死,一個(gè)胖子當(dāng)著我的面吹牛英遭,可吹牛的內(nèi)容都是我干的间护。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼挖诸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼兑牡!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起税灌,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎亿虽,沒想到半個(gè)月后菱涤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡洛勉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年粘秆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片收毫。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡攻走,死狀恐怖殷勘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情昔搂,我是刑警寧澤玲销,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站摘符,受9級(jí)特大地震影響贤斜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜逛裤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一瘩绒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧带族,春花似錦锁荔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至拍霜,卻和暖如春嘱丢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背祠饺。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工越驻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人道偷。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓缀旁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親勺鸦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子并巍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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

  • 先來(lái)看兩張效果圖: 哈哈懊渡,就是這樣了。效果差了一些军拟,感興趣的小伙伴們可以運(yùn)行代碼感受絲滑與彈性剃执。前段時(shí)間在競(jìng)品小紅...
    文淑閱讀 2,173評(píng)論 1 22
  • Pixelmator for mac破解版是一款任何人都可以使用的輕量級(jí)圖片編輯工具。Pixelmator for...
    希希的莫斯呱閱讀 1,658評(píng)論 0 0
  • 久違的晴天懈息,家長(zhǎng)會(huì)肾档。 家長(zhǎng)大會(huì)開好到教室時(shí),離放學(xué)已經(jīng)沒多少時(shí)間了。班主任說已經(jīng)安排了三個(gè)家長(zhǎng)分享經(jīng)驗(yàn)怒见。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,493評(píng)論 16 22
  • 今天感恩節(jié)哎俗慈,感謝一直在我身邊的親朋好友。感恩相遇遣耍!感恩不離不棄闺阱。 中午開了第一次的黨會(huì),身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,551評(píng)論 0 11
  • 可愛進(jìn)取配阵,孤獨(dú)成精馏颂。努力飛翔,天堂翱翔棋傍。戰(zhàn)爭(zhēng)美好救拉,孤獨(dú)進(jìn)取。膽大飛翔瘫拣,成就輝煌亿絮。努力進(jìn)取,遙望麸拄,和諧家園派昧。可愛游走...
    趙原野閱讀 2,716評(píng)論 1 1