Android 自定義 View 實戰之 PuzzleView

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

在閱讀本篇文章以前,但願你們有基本的自定義View知識和Matrix的知識,固然最好閱讀了前一篇,由於不少東西是相通的,本文的重點在於前期的思考,至於具體實現細節能夠不看,選擇看源碼git

起步

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

多種佈局canvas

具體佈局編輯ide

項目地址:github.com/wuapnjie/Pu…佈局

肯定思路

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

再仔細看咱們的效果,在一個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();
}複製代碼

能夠看到,這樣是能夠達到咱們想要的圖片排列方式的,只須要對圖片進行矩陣操做,讓其適應給定的矩形區域就行了。編碼

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

咱們知道Android內置了Rect類,用上下左右四個座標肯定一個矩形,一個大的矩形能夠很容易的分爲許多小的矩形,相似這樣

rect

一個大的矩形被分爲三個小矩形。可是這個內置的Rect類真的能幫助咱們完成效果嗎?

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

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

  • 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;
      ……
}複製代碼

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

因而咱們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;
}複製代碼

固然只有這麼一個方法佈局仍是不怎麼方便的哈,因此我還添加了許多方法方便佈局,好比一個十字能夠把一個矩形分割成四個矩形,一個螺旋能夠把一個矩形分割成五個矩形。提供的方法大體就以下圖所示

舉個例子:

@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);
}複製代碼

以後咱們看一下這種佈局分割的效果

圖片位置的確立與放置

到這裏,咱們已經能夠自定義各類各樣的佈局了,一個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;
}複製代碼

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

圖片移動旋轉縮放翻轉

這個功能和上一篇所講的方法一致,在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類,同上一篇同樣,我仍是要強調思考與建模的重要性,萬事開頭難,前期的思考無疑是最難的,也佔據了整個項目大部分的時間(我花了兩週思考,嗚嗚,可能我太笨了)。

但願閱讀完這篇文章後,能夠對你有一些幫助,有什麼問題或不懂能夠隨時聯繫我,歡迎騷擾。

最近閒下來了,寫點文章記錄以前的學習並鞏固個人基礎知識,但願同你們一塊兒進步!

相關文章
相關標籤/搜索