Android開發(fā)之仿微博貼紙效果實現(xiàn)——進(jìn)階篇

上個月寫了一篇《Android開發(fā)之仿微博貼紙效果實現(xiàn)——基礎(chǔ)篇》,文章中提到還有一篇進(jìn)階篇要寫,很早就想動筆了,因中途去維護(hù)了開源庫《高仿微信圖片選擇器2.0版本》,導(dǎo)致耽擱到了現(xiàn)在,剛好趁這幾天過年在家休息,把這篇文章給完成了。

我們來回顧下上篇文章所寫的內(nèi)容,我們通過canvas.drawBitmap(mBitmap, mMatrix, null);將Bitmap圖像繪制到Canvas上,然后用一個矩陣Matrix配合不同手勢來維護(hù)圖像的變換狀態(tài)(移動、縮放、旋轉(zhuǎn))。

這篇文章主要來講一下多圖貼紙的實現(xiàn)思路和代碼的設(shè)計重構(gòu),在上篇文章中,我們實現(xiàn)單個貼紙效果是一個自定義View,那么多個貼紙要如何實現(xiàn)呢?可能有的朋友會想,既然已經(jīng)實現(xiàn)了單個貼紙,那么多個貼紙只要依葫蘆畫瓢,讓一個ViewGroup去裝載多個貼紙不就可以了?確實沒毛病,這樣做也是可以的,不過中間可能會遇到一些麻煩,比如狀態(tài)的管理(當(dāng)多個貼紙重疊在一起時,觸摸焦點的判斷等)而且之前的實現(xiàn)代碼是面向過程的,一整坨的代碼維護(hù)起來也不是那么的舒服,這里我想介紹一種新思路,用更優(yōu)雅的方式去實現(xiàn)多圖貼紙。

國際慣例,先看一下實現(xiàn)的效果圖:


stickerview.gif

關(guān)于貼紙的核心代碼其實并沒有改變(矩陣的變換,坐標(biāo)點的映射,角度、縮放值的計算等),有了這個前提后,我們拋開代碼層面來捋一下思路,想想如何更優(yōu)雅的實現(xiàn)這個多圖貼紙效果。

開源庫的使用:

為了方便大家使用,這里我把貼紙庫做了封裝,目前已經(jīng)開源了,有什么建議可以隨時反饋給我,我會更新維護(hù)上去,傳送門:StickerView

1、如何在項目中引入該貼紙庫:

//gradle引入此行
compile 'com.lcw.library:StickeView:1.0.1'

2、布局設(shè)置:

 <com.lcw.library.stickerview.StickerLayout
        android:id="@+id/sl_sticker_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

3、添加貼紙:

                Sticker sticker = new Sticker(mContext,mBitmap);
                mStickerLayout.addSticker(sticker);

好了,開始我們今天的講解。

實現(xiàn)思路:

首先我們可以把貼紙看成一個獨立個體,在個體里包含了圖片,矩陣,初始坐標(biāo),焦點等屬性,在構(gòu)建完這些屬性后,需要通過Canvas繪制出來 ,還有手勢操作,我們通過手勢去識別出當(dāng)前的操作類型和計算出變化的數(shù)值,動態(tài)去改變矩陣,再通過Canvas繪制出來,其實這些和View并沒有直接的關(guān)系,所以我們可以把貼紙抽象成一個對象,根據(jù)面向?qū)ο蟮木幊趟枷?,我們可以大致得到下面這張圖:

Sticker貼紙類圖(主枝干)

簡單的解釋下這張圖的設(shè)計(只體現(xiàn)出關(guān)鍵元素):
ISupportOperation:貼紙的操作接口,這里涉及到貼紙的平移,縮放,旋轉(zhuǎn),繪制,觸摸等操作。
BaseSticker:貼紙基類(抽象類),這里實現(xiàn)了ISupportOperation的部分方法,主要會對根據(jù)變換矩陣對貼紙自身的繪制(矩陣)。
Sticker:貼紙類,這里我們會對用戶的手勢進(jìn)行分析處理,得到相關(guān)的動作和數(shù)據(jù),比如平移的距離,縮放的大小,旋轉(zhuǎn)的角度等,然后交給貼紙基類做具體的繪制。
StickerManager:貼紙的管理類,這里維護(hù)著所有的貼紙,可以對貼紙進(jìn)行狀態(tài)的管理(比如增刪改查,焦點狀態(tài)等)。
StickerLayout:自定義貼紙布局(自定義View),這里會根據(jù)觸摸坐標(biāo)從貼紙的管理類中獲取當(dāng)前被操作的貼紙,然后再對觸摸事件進(jìn)行分發(fā),交給具體貼紙類處理。

整理一下流程:
用戶觸摸貼紙布局(StickerLayout)可以得到觸摸坐標(biāo),根據(jù)坐標(biāo)我們可以在貼紙管理類中(StickerManager)得到對應(yīng)的貼紙,然后我們把觸摸事件分發(fā)給貼紙(Sticker)進(jìn)行觸摸事件的分解,得到相關(guān)動作和計算數(shù)值,然后交給貼紙基類(BaseSticker)完成自身狀態(tài)變換和相關(guān)繪制,最后通知貼紙布局進(jìn)行界面刷新。

流程圖如下:

Sticker貼紙流程圖(主枝干)

編碼實現(xiàn):

了解了實現(xiàn)思路,就完成一大半工作了,剩下的只需要我們用代碼將它闡述出來即可,下面給出核心代碼:

首先當(dāng)用戶觸摸貼紙布局(StickerLayout)的時候,需要根據(jù)坐標(biāo)去獲取對一個的貼紙(由于添加貼紙的時候是加入一個List集合,所以再取出貼紙的時候,我們應(yīng)該倒序去遍歷這個集合,才可以拿到最近時間添加的那個貼紙)

   /**
     * 根據(jù)觸摸坐標(biāo)返回當(dāng)前觸摸的貼紙
     *
     * @param x
     * @param y
     * @return
     */
    public Sticker getSticker(float x, float y) {

        float[] dstPoints = new float[2];
        float[] srcPoints = new float[]{x, y};

        for (int i = mStickerList.size() - 1; i >= 0; i--) {
            Sticker sticker = mStickerList.get(i);
            Matrix matrix = new Matrix();
            sticker.getMatrix().invert(matrix);
            matrix.mapPoints(dstPoints, srcPoints);
            if (sticker.getStickerBitmapBound().contains(dstPoints[0], dstPoints[1])) {
                return sticker;
            }
        }
        return null;
    }

然后把貼紙交給對應(yīng)的Sticker類去做處理:

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                //單指是否觸摸到貼紙
                mStick = StickerManager.getInstance().getSticker(event.getX(), event.getY());
                if (mStick == null) {
                    if (event.getPointerCount() == 2) {
                        //處理雙指觸摸屏幕,第一指沒有觸摸到貼紙,第二指觸摸到貼紙情況
                        mStick = StickerManager.getInstance().getSticker(event.getX(1), event.getY(1));
                    }
                }
                break;
            default:
                break;
        }
        if (mStick != null) {
            mStick.onTouch(event);
             invalidate();
        }
        return true;
    }

在貼紙類(Sticker)中處理手勢的識別、數(shù)據(jù)計算和動作下發(fā)給貼紙基類(BaseSticker)執(zhí)行:

    /**
     * 處理觸摸事件
     *
     * @param event
     */
    public void onTouch(MotionEvent event) {
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                //有觸摸到貼紙
                mMode = Sticker.MODE_SINGLE;
                //記錄按下的位置
                mLastSinglePoint.set(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                if (event.getPointerCount() == 2) {
                    mMode = Sticker.MODE_MULTIPLE;
                    //記錄雙指的點位置
                    mFirstPoint.set(event.getX(0), event.getY(0));
                    mSecondPoint.set(event.getX(1), event.getY(1));
                    //計算雙指之間向量
                    mLastDistanceVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);
                    //計算雙指之間距離
                    mLastDistance = calculateDistance(mFirstPoint, mSecondPoint);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (mMode == MODE_SINGLE) {
                    translate(event.getX() - mLastSinglePoint.x, event.getY() - mLastSinglePoint.y);
                    mLastSinglePoint.set(event.getX(), event.getY());
                }
                if (mMode == MODE_MULTIPLE && event.getPointerCount() == 2) {
                    //記錄雙指的點位置
                    mFirstPoint.set(event.getX(0), event.getY(0));
                    mSecondPoint.set(event.getX(1), event.getY(1));
                    //操作自由縮放
                    float distance = calculateDistance(mFirstPoint, mSecondPoint);
                    //根據(jù)雙指移動的距離獲取縮放因子
                    float scale = distance / mLastDistance;
                    scale(scale, scale);
                    mLastDistance = distance;
                    //操作自由旋轉(zhuǎn)
                    mDistanceVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);
                    rotate(calculateDegrees(mLastDistanceVector, mDistanceVector));
                    mLastDistanceVector.set(mDistanceVector.x, mDistanceVector.y);
                }
                break;
            case MotionEvent.ACTION_UP:
                reset();
                break;
        }
    }

在貼紙基類(BaseSticker)執(zhí)行貼紙類(Sticker)下發(fā)的動作命令:

    /**
     * 平移操作
     *
     * @param dx
     * @param dy
     */
    @Override
    public void translate(float dx, float dy) {
        mMatrix.postTranslate(dx, dy);
        updatePoints();
    }

    /**
     * 縮放操作
     *
     * @param sx
     * @param sy
     */
    @Override
    public void scale(float sx, float sy) {
        mMatrix.postScale(sx, sy, mMidPointF.x, mMidPointF.y);
        updatePoints();
    }

    /**
     * 旋轉(zhuǎn)操作
     *
     * @param degrees
     */
    @Override
    public void rotate(float degrees) {
        mMatrix.postRotate(degrees, mMidPointF.x, mMidPointF.y);
        updatePoints();
    }

    /**
     * 當(dāng)矩陣發(fā)生變化的時候,更新坐標(biāo)點(src坐標(biāo)點經(jīng)過matrix映射變成了dst坐標(biāo)點)
     */
    private void updatePoints() {
        //更新貼紙點坐標(biāo)
        mMatrix.mapPoints(mDstPoints, mSrcPoints);
    }
    /**
     * 繪制貼紙自身
     *
     * @param canvas
     * @param paint
     */
    @Override
    public void onDraw(Canvas canvas, Paint paint) {
        //繪制貼紙
        canvas.drawBitmap(mStickerBitmap, mMatrix, paint);
        if (isFocus) {
            //繪制貼紙邊框
            canvas.drawLine(mDstPoints[0] - PADDING, mDstPoints[1] - PADDING, mDstPoints[2] + PADDING, mDstPoints[3] - PADDING, paint);
            canvas.drawLine(mDstPoints[2] + PADDING, mDstPoints[3] - PADDING, mDstPoints[4] + PADDING, mDstPoints[5] + PADDING, paint);
            canvas.drawLine(mDstPoints[4] + PADDING, mDstPoints[5] + PADDING, mDstPoints[6] - PADDING, mDstPoints[7] + PADDING, paint);
            canvas.drawLine(mDstPoints[6] - PADDING, mDstPoints[7] + PADDING, mDstPoints[0] - PADDING, mDstPoints[1] - PADDING, paint);
            //繪制移除按鈕
            canvas.drawBitmap(mDelBitmap, mDstPoints[0] - mDelBitmap.getWidth() / 2 - PADDING, mDstPoints[1] - mDelBitmap.getHeight() / 2 - PADDING, paint);
        }
    }

最后完成貼紙布局的刷新:

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        List<Sticker> stickerList = StickerManager.getInstance().getStickerList();
        for (int i = 0; i < stickerList.size(); i++) {
            Sticker sticker = stickerList.get(i);
            sticker.onDraw(canvas, mPaint);
        }
    }

具體的貼紙實現(xiàn)代碼在上篇文章已經(jīng)提過了,所以這里就不再重復(fù)的代碼粘貼,不清楚的朋友可以看下上篇的文章,或者去github下載源碼細(xì)看。

好了,到這里文章就結(jié)束啦~

源碼下載:

這里附上源碼地址(歡迎Star,歡迎Fork):StickerView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 之前寫過一篇關(guān)于圖像變換處理的文章《Android開發(fā)之圖像處理那點事——變換》,學(xué)以致用,這次我們來實現(xiàn)仿微博的...
    李晨瑋閱讀 7,283評論 4 31
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,338評論 4 61
  • 這是預(yù)覽 #一級標(biāo)題 ##二級標(biāo)題 ###三級標(biāo)題
    aduil閱讀 220評論 0 0
  • 要約束,要拘謹(jǐn) 逃避不了的 像一幢裝滿思想的灰塵 雨不用走 那些口舌,那些緯度 異地的篝火,拔地而起 不必退熱 鐘...
    投我木瓜啊閱讀 207評論 0 0
  • 懂,你要說的,都懂 撫上樹,希望思念能通過枝蔓,通過微風(fēng) 送到你耳中,籠上你 輕輕的籠上你,告訴你,我的心情 瞧,...
    紅石兒閱讀 636評論 1 17

友情鏈接更多精彩內(nèi)容