Android 自定義 View 實戰之 PuzzleView

FlyingSnowBean發表於2019-02-27

本篇文章為利用Matrix自定義View的第二篇,第一篇見Android自定義View實戰之StickerView

在閱讀本篇文章之前,希望大家有基本的自定義View知識和Matrix的知識,當然最好閱讀了前一篇,因為很多東西是相通的,本文的重點在於前期的思考,至於具體實現細節可以不看,選擇看原始碼

起步

在圖片的處理軟體中,拼圖是很常見的一種處理方法,我最喜歡Layout for Instagram的拼圖效果,簡單卻又足夠強大,拼圖方式多種多樣可以對圖片進行水平垂直翻轉,移位,移動,縮放,改變大小之類的操作,看到這樣的操作。本文製作的View正是為了實現這個功能。先看最終我們實現的效果。

多種佈局

Android 自定義 View 實戰之 PuzzleView

具體佈局編輯

Android 自定義 View 實戰之 PuzzleView

專案地址:github.com/wuapnjie/Pu…

確定思路

在前面介紹中,我們知道這一次我們還是對圖片的一系列變換操作,那麼這次我們的實現思路也是在onTouchEvent()中根據手勢控制對應的Matrix來對所畫在View上的圖片進行操作。

再仔細看我們的效果,在一個View中我們可能要畫上許多張圖片,但是位置都不同,且互相不會覆蓋,那麼可以看出我們對View進行了分割,分成不同的矩形,瞭解canvas的同學知道,canvas可以先進行一系列變換後再進行繪製,繪製完成後恢復,這次利用的就是canvasclipRect()方法將canvas分成不同的矩形區域進行繪製,先來看看大致效果可不可以達到我們的預期。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    canvas.clipRect(0, 0, getWidth() / 2, getHeight());
    canvas.drawBitmap(mBitmapOne, 0, 0, mBitmapPaint);
    canvas.restore();

    canvas.save();
    canvas.clipRect(getWidth() / 2, 0, getWidth(), getHeight());
    canvas.drawBitmap(mBitmapTwo, 0, 0, mBitmapPaint);
    canvas.restore();
}複製程式碼

Android 自定義 View 實戰之 PuzzleView

可以看到,這樣是可以達到我們想要的圖片排列方式的,只需要對圖片進行矩陣操作,讓其適應給定的矩形區域就好了。

那麼第一步的思路超不多就想好了,我們做到了如何在一個View中排列多張圖片,接下來要思考如何分割外圍的矩形(View的邊界矩形)。

我們知道Android內建了Rect類,用上下左右四個座標確定一個矩形,一個大的矩形可以很容易的分為許多小的矩形,類似這樣

Android 自定義 View 實戰之 PuzzleView
rect

一個大的矩形被分為三個小矩形。但是這個內建的Rect類真的能幫助我們完成效果嗎?

答案是不能的,雖然內建的Rect類可以成功幫助我們確定每張圖片的位置,令圖片被畫在正確的位置上,但是有一點致命的是,它內部是由上下左右四個座標確定的,仔細看我們要實現的效果,在隨著我們手指對矩形邊線的移動,大矩形內的小矩形大小邊界是在改變的,而且收到影響的矩形肯定大於等於2個,那麼我們要改變座標的矩形也就會大於等於2個,編碼上會複雜且容易出錯,所以我們不能單單隻用Rect類來確定邊界。我們必須在抽象出一種新的模型來確定圖片的矩形區域並方便資料更新變化。

在反覆把玩Layout for Instagram後(因為當時我還沒做出這個View,一直拿Layout研究,希望你也可以去多玩一下),並把它的所有佈局都在紙上畫了一遍,我發現了很關鍵的一點,也是這個自定義View最關鍵的一部。它的線很重要(當我們點選其中一張圖片後,它會成為選中狀態,那個線是高亮的,引人注意哦),我們每次移動的時那一根線,而一個矩形可以被一根直線或橫線劃分成兩個矩形,而四根線可以確定一個矩形範圍,兩個矩形可以共享一根線,線的位置改變,共享這根線的所有矩形的大小範圍都會改變。類似這樣

Android 自定義 View 實戰之 PuzzleView

  • line1,line2,line4,line5組成了Rect1
  • line2,line3,line4,line5組成了Rect2
  • Rect1和Rect2共享line2,line4,line5
  • 移動了line2後,Rect1和Rect2均收到影響

希望大家理解這幅圖,這是本次自定義View的關鍵。

那麼整理一下大致思路,我們要用線將View的邊界分成許多個小矩形,並讓圖片畫在這些小矩形上,之後同上一篇文章一致,根據我們的手勢控制對應圖片的Matrix來控制圖片的相應動作。

建立模型

既然思路已經確定了,那麼我們就要來確定我們的程式碼結構和相應的模型類。上面講我們要用線來分割矩形,而Android原生是沒有Line這個模型類的,於是我們要自己抽象一個。那麼線是怎麼組成的呢?很簡單,在座標系中,兩點確定一根直線,所以我們要有兩個點PointF,因為我們只用橫線或直線,所以只抽象了兩個方向,斜線不考慮(本效果只需要直線和橫線)。

public class Line {

    public enum Direction {
        HORIZONTAL,
        VERTICAL
    }

    /**
     * for horizontal line, start means left, end means right
     * for vertical line, start means top, end means bottom
     */
    final PointF start;
    final PointF end;

    private Direction direction = Direction.HORIZONTAL;
      ……
}複製程式碼

但是這麼幾個屬性真的夠用嗎?在我試驗了之後發現是不夠的,我們還需要另外四個屬性,是四根其他的線,兩根確定其移動範圍的線,兩根頂點依附的線,當依附的線移動了後,可以快速更新自身的長度,相應地延長或縮短。

Android 自定義 View 實戰之 PuzzleView

於是我們Line的模型類就可以去確定了。

public class Line {

    public enum Direction {
        HORIZONTAL,
        VERTICAL
    }

    /**
     * for horizontal line, start means left, end means right
     * for vertical line, start means top, end means bottom
     */
    final PointF start;
    final PointF end;

    private Direction direction = Direction.HORIZONTAL;

    private Line attachLineStart;
    private Line attachLineEnd;

    private Line mUpperLine;
    private Line mLowerLine;
    ……
}複製程式碼

那麼我們就可以確定一個邊界Border類,它由4條Line構成,並可方便的匯出Rect物件方便我們擺放圖片。

class Border {
    Line lineLeft;
    Line lineTop;
    Line lineRight;
    Line lineBottom;
    ……
}複製程式碼

接下來就要思考如何支援多樣化佈局,當然要提供介面供使用者自定義,所以我們要抽象出一個拼圖佈局類PuzzleLayout,這個類要有個抽象方法支援我們自定義佈局,並提供一些簡單的方法幫助我們快速佈局,並且應該保有所有的邊界BorderLine物件,方便進行管理和更新資訊。

public abstract class PuzzleLayout {
    ……
    private Border mOuterBorder;

    private List<Border> mBorders = new ArrayList<>();
    private List<Line> mLines = new ArrayList<>();
    private List<Line> mOuterLines = new ArrayList<>(4);
      ……
    public abstract void layout();
      ……
}複製程式碼

至於圖片物件,同上一篇文章一樣,每張圖片需要一個Matrix物件進行控制,只是在這之上還要保有一個邊界Border的引用。這裡就不貼了。

這樣,我們所有的模型就已經確定了。大致關係就是,每個PuzzleView的佈局方式由PuzzleLayout決定,PuzzleLayout可自定義佈局,由一系列的邊界Border組成,而Border則由一系列的Line組成。

具體實現

由於許多東西的關鍵都是思路和建模,大家理解了這個思路並建立了正確方便的模型後,實現起來就異常容易了,只是在預定的軌道上開車到終點就好了,其實後面的內容已經不重要了。

佈局方式的確定

起初,我們要先把佈局方式確定才可以決定畫多少張圖片上去,所以佈局方式是最先要被解決的功能。

大家都知道,一根直線可以把一個矩形分成左右兩個矩形,一根橫線可以把一個矩形分成上下兩個矩形,所以我們可以提供一個addLine()方法提供分割佈局,將增加的LineBorder新增至集合。

protected List<Border> addLine(Border border, Line.Direction direction, float ratio) {
    mBorders.remove(border);
    Line line = BorderUtil.createLine(border, direction, ratio);
    mLines.add(line);

    List<Border> borders = BorderUtil.cutBorder(border, line);
    mBorders.addAll(borders);

    updateLineLimit();
    Collections.sort(mBorders, mBorderComparator);

    return borders;
}複製程式碼

當然只有這麼一個方法佈局還是不怎麼方便的哈,所以我還新增了許多方法方便佈局,比如一個十字可以把一個矩形分割成四個矩形,一個螺旋可以把一個矩形分割成五個矩形。提供的方法大致就如下圖所示

Android 自定義 View 實戰之 PuzzleView

舉個例子:

@Override
public void layout() {
    addLine(getOuterBorder(), Line.Direction.VERTICAL, 1f / 2);
    cutBorderEqualPart(getBorder(1), 4, Line.Direction.HORIZONTAL);
    cutBorderEqualPart(getBorder(0), 3, Line.Direction.HORIZONTAL);
}複製程式碼

之後我們看一下這種佈局分割的效果

Android 自定義 View 實戰之 PuzzleView

圖片位置的確立與放置

到這裡,我們已經可以自定義各種各樣的佈局了,一個View已經被我們分割成了許多小的矩形區域,接下來我們就要把圖片給畫上去,但不是隨便畫,我們需要讓圖片在對應的矩形以centerCrop的方式顯示,不然我們看到的就不是圖片的重要區域。那麼怎麼樣才可以做到呢?由於每個矩形的位置我們都是知道的,所以我們只需要將圖片的中心移動到對應矩陣的中心,按centerCrop的縮放規則讓圖片中心縮放就好了。這些就是Matrix的基本應用了,這裡就不重複說明了,至於centerCrop的縮放比也很好計算,不會的話,看一下ImageView的原始碼就好了。

下面的程式碼是生成讓圖片已對應Border正確顯示的Matrix生成

static Matrix createMatrix(Border border, int width, int height, float extraSize) {
        final RectF rectF = border.getRect();

        Matrix matrix = new Matrix();

        float offsetX = rectF.centerX() - width / 2;
        float offsetY = rectF.centerY() - height / 2;

        matrix.postTranslate(offsetX, offsetY);

        float scale;

        if (width * rectF.height() > rectF.width() * height) {
            scale = (rectF.height() + extraSize) / height;
        } else {
            scale = (rectF.width() + extraSize) / width;
        }

        matrix.postScale(scale, scale, rectF.centerX(), rectF.centerY());

    return matrix;
}複製程式碼

將圖片畫上去後的效果,是不是效果很好呀?

Android 自定義 View 實戰之 PuzzleView

圖片移動旋轉縮放翻轉

這個功能和上一篇所講的方法一致,在onTouchEvent()中監聽不同的手勢,對對應圖片的Matrix做出相關操作即可,這裡就不重複說明了,比較基礎。

線的移動

看效果圖,這個佈局並不是不變的,我們可以通過對可移動線的移動,可以使一些邊界變大,另一些邊界變小,同時令圖片適應邊界的變化。這時候模型的正確建立就大大地簡化了我們的編碼效率。

首先,我們找到我們是否觸控線上上,因為內部的線物件必然會被2個以上的邊界引用,當這條線的資訊改變時,對應的邊界也會馬上得知,並改變其邊界區域,這樣我們就可以很方便的重新畫出邊界,我們就只要更新受影響區域圖片的Matrix即可。

moveLine(event); //移動線
mPuzzleLayout.update(); //更新PuzzleLayout內Border資訊
updatePieceInBorder(event); //更新圖片Matrix資訊以適應變化複製程式碼

圖片位置交換

圖片之間的相對位置是可以改變的,按照正常的邏輯也是當我們長按一張圖片時,那張圖片會懸浮,然後移動到要交換位置的圖片,釋放手指就交換成功了。那麼問題就是這個懸浮起來的效果,這裡用全圖顯示加個半透明來表示,利用Canvas的相關方法實現及其容易。

if (mHandlingPiece != null && mCurrentMode == Mode.SWAP) {
    mHandlingPiece.draw(canvas, mBitmapPaint, 128);
    if (mReplacePiece != null) {
        drawSelectedBorder(canvas, mReplacePiece);
    }
}複製程式碼

圖片翻轉

這個同樣利用Matrix可以輕鬆實現,不贅述。

matrix.postScale(-1, 1, px, py); //水平翻轉
matrix.postScale(1, -1, px, py); //垂直翻轉複製程式碼

尾聲

到這裡,我們所要實現的功能已經基本全部實現,剩下的就是完善細節,應該提供怎麼樣的介面供外部操作,只需要慢慢除錯即可,感興趣的同學可以去看一下原始碼

總結

這次自定義的View相對於上一次的StickerView來說,無疑是複雜了很多,我們需要建立更復雜的模型,但是所運用的核心類是一樣的,CanvasMatrix類,同上一篇一樣,我還是要強調思考與建模的重要性,萬事開頭難,前期的思考無疑是最難的,也佔據了整個專案大部分的時間(我花了兩週思考,嗚嗚,可能我太笨了)。

希望閱讀完這篇文章後,可以對你有一些幫助,有什麼問題或不懂可以隨時聯絡我,歡迎騷擾。

最近閒下來了,寫點文章記錄之前的學習並鞏固我的基礎知識,希望同大家一起進步!

相關文章