Android自定義View實戰之StickerView

frxq8979 8年前發布 | 7K 次閱讀 安卓開發 Android開發 移動開發

雖然Android內置了許多View供開發者組合和使用,但其多樣性還是不足,在很多場景或功能需求下,Android原生自帶的控件并不足以實現需求,這時我們就需要自定義滿足我們需求的View。

本文會講解一個自定義View的設計和開發過程,在閱讀之前希望大家有最基礎的自定義View的知識,以及 Matrix 類的基本使用。

起步

在很多圖片社交的應用,例如Lofter、Play、In等應用中,都會有添加各種可愛的貼圖到圖片上的功能,然后我們可以對圖片進行移動、旋轉、縮放、翻轉之類的操作,本文制作的View正是為了實現這個功能。最終我們將要實現的效果如下圖:

簡單思考(確定大致思路)

要實現這樣的效果,我們肯定需要對圖片進行操作,在自定義的View中,我們可以在 onDraw() 方法將我們的圖片(通常為 Bitmap )畫到 View 上。

protected void onDraw(Canvas canvas) {
     super.onDraw(canvas);
        canvas.drawBitmap(bitmap,matrix,paint);
 }

drawBitmap() 方法有許多重載方法,但是利用Matrix來控制畫在View上的圖片是最靈活最簡單的。(不熟悉Matrix類可以先去了解下,這里就不介紹基礎的知識了)

利用 Matrix 可以方便的控制圖片的位置,旋轉角度,縮放比。

再看我們的功能,用不同的手勢來操作圖片,既然利用 Matrix 可以操作圖片,那么我們只需要在View的 onTouchEvent() 方法中監聽不同的手勢操作,再對其Matrix進行變換,重繪View即可。整個思路流程就很清楚了。

仔細思考(決定結構)

有了思路,那么我們就要來考慮我們應該怎么樣組織代碼,怎么樣設計代碼的結構。當然這個View并不復雜,設計起來也不復雜。

首先,對于貼紙功能,在沒有一張貼紙時就只顯示一張圖片,而這個功能ImageView已經為我們實現了,于是StickerView應該繼承自ImageView,并且重寫 onDraw() 和 onTouchEvent() 方法。

其次,因為一張圖片上可以添加多張貼紙,而每一張貼紙都需要一個Matrix來控制其相關變換,所以我們可以設計一個類封裝一下,方便對貼紙的操作。

public abstract class Sticker {
    protected Matrix mMatrix;
      public abstract void draw(Canvas canvas);
      ……
}

因為貼紙可能是Bitmap,也就是普通的圖片,但是我們也可以添加氣泡啊,標簽啊之類的自定義的Drawable,

當然也可能是各種圖形,為了其擴展性,這里將Sticker類抽象。

擴展的 DrawableSticker

public class DrawableSticker extends Sticker {
    private Drawable mDrawable;
    private Rect mRealBounds;
    ……
      @Override
    public void draw(Canvas canvas) {
        canvas.save();
        canvas.concat(mMatrix);
        mDrawable.setBounds(mRealBounds);
        mDrawable.draw(canvas);
        canvas.restore();
    }
      ……
}

那么大致的結構就確定了,在View的 onTouchEvent() 中,我們根據手勢改變Sticker的Matrix,并在 onDraw() 方法中將Sticker畫出。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
      ……
    sticker.draw(canvas);
      ……
}

實現

在有了思路和一個結構后,大致已經成功了一半,接下來就是一個個功能的實現,和一遍遍的調試了。

由于我們可以添加不止一個Sticker,所以我們的StickerView需要保有對所有添加的Sticker應用,這里可以用一個List集合來儲存。而對于當前正在操作的Sticker引用需要額外儲存。

因為對于不同的手勢,我們所做出的操作不同,那么我們需要在內部聲明所有存在的狀態和一個當前狀態

public class StickerView extends ImageView {
    private enum ActionMode {
        NONE,   //nothing
        DRAG,   //drag the sticker with your finger
        ZOOM_WITH_TWO_FINGER,   //zoom in or zoom out the sticker and rotate the sticker with two finger
        ZOOM_WITH_ICON,    //zoom in or zoom out the sticker and rotate the sticker with icon
        DELETE,  //delete the handling sticker
        FLIP_HORIZONTAL //horizontal flip the sticker
    }
      private ActionMode mCurrentMode = ActionMode.NONE;

    private List<Sticker> mStickers = new ArrayList<>();
    private Sticker mHandlingSticker;
      ……
}

接下來就是一個一個功能實現,但肯定的是,最先需要實現的就是將貼紙添加進來的方法。

添加貼紙

實現起來也很簡單,這里就是new一個Sticker對象,并把它加入到我們的List中并重繪,注意,我們默認將Sticker縮放至原來的一半,并放在StickerView中央。

public void addSticker(Drawable stickerDrawable) {
        Sticker drawableSticker = new DrawableSticker(stickerDrawable);

        float offsetX = (getWidth() - drawableSticker.getWidth()) / 2;
        float offsetY = (getHeight() - drawableSticker.getHeight()) / 2;
        drawableSticker.getMatrix().postTranslate(offsetX, offsetY);

        float scaleFactor;
        if (getWidth() < getHeight()) {
            scaleFactor = (float) getWidth() / stickerDrawable.getIntrinsicWidth();
        } else {
            scaleFactor = (float) getHeight() / stickerDrawable.getIntrinsicWidth();
        }
        drawableSticker.getMatrix().postScale(scaleFactor / 2, scaleFactor / 2, getWidth() / 2, getHeight() / 2);

        mHandlingSticker = drawableSticker;
        mStickers.add(drawableSticker);

        invalidate();
}

找到貼紙

在我們的貼紙對象被添加進來后我們才可以繼續接下來的操作,在我們觸摸屏幕時,要判斷是否按在貼紙區域,按在哪個貼紙上。實現比較簡單,我們的每個Sticker都有一個矩形范圍,在經過移動縮放之類的操作后也可以通過Matrix來輕松得到那個矩形區域( Rect 類),只需要判斷這個范圍是否包含我們按下的點,而這一步應該在Touch事件的 ACTION_DOWN 事件中進行。

switch (action) {
     case MotionEvent.ACTION_DOWN:
         mCurrentMode = ActionMode.DRAG;
         mDownX = event.getX();
         mDownY = event.getY();
         mHandlingSticker = findHandlingSticker();          
          ……
}

其中 findHandlingSticker() 正是做了這樣一些事情

private Sticker findHandlingSticker() {
    for (int i = mStickers.size() - 1; i >= 0; i--) {
        if (isInStickerArea(mStickers.get(i), mDownX, mDownY)) {
            return mStickers.get(i);
         }
    }
    return null;
}

移動貼紙

找到了我們要操作的Sticker后,我們就可以對其進行操作了,移動操作最為簡單,只涉及一根手指,在 ACTION_DOWN 事件中我們記錄下當前Sticker的狀態和事件起始坐標,在 ACTION_MOVE 事件中,我們利用當前點的坐標計算出實際偏移量,利用Matrix的 postTransition() 方法讓Sticker做出隨手指的移動。

mMoveMatrix.set(mDownMatrix);
mMoveMatrix.postTranslate(event.getX() - mDownX, event.getY() - mDownY);
mHandlingSticker.getMatrix().set(mMoveMatrix);

縮放與旋轉貼紙

一般的縮放與旋轉操作都是需要兩根手指,所以我們需要在 ACTION_POINT_DOWN 事件中監聽第二根手指按下。這時我們還需要計算出兩根手指之間的距離以及中心點還有角度,因為我們要讓Sticker以這個中心點為中心縮放旋轉,在 ACTION_MOVE 事件中以新的兩指尖距離/起始兩指尖距離作為縮放比縮放。以新的角度-起始角度作為旋轉角。

switch (action) {
     case MotionEvent.ACTION_POINTER_DOWN:
        mOldDistance = calculateDistance(event);
        mOldRotation = calculateRotation(event);
        mMidPoint = calculateMidPoint(event);          
          ……
}

相應的縮放與旋轉,利用Matrix的 postScale 和 postRotate 方法實現

float newDistance = calculateDistance(event);
float newRotation = calculateRotation(event);

mMoveMatrix.set(mDownMatrix);
mMoveMatrix.postScale(newDistance / mOldDistance, newDistance / mOldDistance, mMidPoint.x, mMidPoint.y);
mMoveMatrix.postRotate(newRotation - mOldRotation, mMidPoint.x, mMidPoint.y);

mHandlingSticker.getMatrix().set(mMoveMatrix);

添加選中效果

在經過上面的步驟后,我們的StickerView已經可以添加貼紙,用手勢操縱貼紙移動,縮放,旋轉了,但是我們并沒有對選中的貼紙進行特殊處理,因為一般的應用對于選中的貼紙,都會用一個邊框圍住,并在相應的邊框邊角顯示一些操作按鈕。因為這個按鈕有圖標,所以我們也可以把其作為一個Sticker,只是還需要一個位置的x,y值。

public class BitmapStickerIcon extends DrawableSticker {
    private float x;
    private float y;
      ……
}

因為對于每個Sticker的邊框及其坐標是很容易獲得的,所以我們只需要在 onDraw 方法中在正在處理的Sticker周圍畫上邊框和按鈕就可以了。下面的代碼獲得了選中Sticker的邊角坐標,并將操作按鈕畫在相應位置。

if (mHandlingSticker != null && !mLooked) {

    float[] bitmapPoints = getStickerPoints(mHandlingSticker);

    float x1 = bitmapPoints[0];
    float y1 = bitmapPoints[1];
    float x2 = bitmapPoints[2];
    float y2 = bitmapPoints[3];
    float x3 = bitmapPoints[4];
    float y3 = bitmapPoints[5];
    float x4 = bitmapPoints[6];
    float y4 = bitmapPoints[7];

    canvas.drawLine(x1, y1, x2, y2, mBorderPaint);
    canvas.drawLine(x1, y1, x3, y3, mBorderPaint);
    canvas.drawLine(x2, y2, x4, y4, mBorderPaint);
    canvas.drawLine(x4, y4, x3, y3, mBorderPaint);

    float rotation = calculateRotation(x3, y3, x4, y4);
    //draw delete icon
    canvas.drawCircle(x1, y1, mIconRadius, mBorderPaint);
    mDeleteIcon.setX(x1);
    mDeleteIcon.setY(y1);
    mDeleteIcon.getMatrix().reset();

    mDeleteIcon.getMatrix().postRotate(
                    rotation, mDeleteIcon.getWidth() / 2, mDeleteIcon.getHeight() / 2);
    mDeleteIcon.getMatrix().postTranslate(
                    x1 - mDeleteIcon.getWidth() / 2, y1 - mDeleteIcon.getHeight() / 2);

    mDeleteIcon.draw(canvas);

            //draw zoom icon
    canvas.drawCircle(x4, y4, mIconRadius, mBorderPaint);
    mZoomIcon.setX(x4);
    mZoomIcon.setY(y4);

    mZoomIcon.getMatrix().reset();
    mZoomIcon.getMatrix().postRotate(
                    45f + rotation, mZoomIcon.getWidth() / 2, mZoomIcon.getHeight() / 2);

    mZoomIcon.getMatrix().postTranslate(
                    x4 - mZoomIcon.getWidth() / 2, y4 - mZoomIcon.getHeight() / 2);

    mZoomIcon.draw(canvas);

    //draw flip icon
    canvas.drawCircle(x2, y2, mIconRadius, mBorderPaint);
    mFlipIcon.setX(x2);
    mFlipIcon.setY(y2);

    mFlipIcon.getMatrix().reset();
    mFlipIcon.getMatrix().postRotate(
                    rotation, mDeleteIcon.getWidth() / 2, mDeleteIcon.getHeight() / 2);
    mFlipIcon.getMatrix().postTranslate(
                    x2 - mFlipIcon.getWidth() / 2, y2 - mFlipIcon.getHeight() / 2);

    mFlipIcon.draw(canvas);
}

總結

這樣,我們大致完成了StickerView的所有功能,當然上面并沒有太完整的代碼,只是一些代碼片段,但是已經說明了大致的思路及操作。我們在自定義View時,首先最需要的是一個思路,有了思路之后要想其代碼結構,在這兩塊都想好了以后再開發其功能,會事半功倍。

希望可以對你有幫助。如果有什么疑問,可以隨時聯系我,歡迎提issue和pr。

 

 

 本文由用戶 frxq8979 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!