Android自定義View實戰(zhàn)之PuzzleView

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

在閱讀本篇文章之前县习,希望大家有基本的自定義View知識和Matrix的知識,當然最好閱讀了前一篇庶香,因為很多東西是相通的蝌焚,本文的重點在于前期的思考,至于具體實現(xiàn)細節(jié)可以不看妈候,選擇看源碼

起步

在圖片的處理軟件中挂滓,拼圖是很常見的一種處理方法苦银,我最喜歡Layout for Instagram的拼圖效果,簡單卻又足夠強大赶站,拼圖方式多種多樣可以對圖片進行水平垂直翻轉幔虏,移位,移動贝椿,縮放想括,改變大小之類的操作,看到這樣的操作烙博。本文制作的View正是為了實現(xiàn)這個功能瑟蜈。先看最終我們實現(xiàn)的效果。

多種布局

具體布局編輯

項目地址:https://github.com/wuapnjie/PuzzleView

確定思路

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

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

@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();
}

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

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

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

rect

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

答案是不能的衅檀,雖然內置的Rect類可以成功幫助我們確定每張圖片的位置,令圖片被畫在正確的位置上霎俩,但是有一點致命的是它浅,它內部是由上下左右四個坐標確定的脆栋,仔細看我們要實現(xiàn)的效果双肤,在隨著我們手指對矩形邊線的移動弦聂,大矩形內的小矩形大小邊界是在改變的,而且收到影響的矩形肯定大于等于2個柳击,那么我們要改變坐標的矩形也就會大于等于2個猿推,編碼上會復雜且容易出錯,所以我們不能單單只用Rect類來確定邊界捌肴。我們必須在抽象出一種新的模型來確定圖片的矩形區(qū)域并方便數(shù)據(jù)更新變化蹬叭。

在反復把玩Layout for Instagram后(因為當時我還沒做出這個View,一直拿Layout研究状知,希望你也可以去多玩一下)秽五,并把它的所有布局都在紙上畫了一遍,我發(fā)現(xiàn)了很關鍵的一點试幽,也是這個自定義View最關鍵的一部筝蚕。它的線很重要(當我們點擊其中一張圖片后,它會成為選中狀態(tài)铺坞,那個線是高亮的起宽,引人注意哦),我們每次移動的時那一根線济榨,而一個矩形可以被一根直線或橫線劃分成兩個矩形坯沪,而四根線可以確定一個矩形范圍,兩個矩形可以共享一根線擒滑,線的位置改變腐晾,共享這根線的所有矩形的大小范圍都會改變。類似這樣

  • line1,line2,line4,line5組成了Rect1
  • line2,line3,line4,line5組成了Rect2
  • Rect1和Rect2共享line2丐一,line4藻糖,line5
  • 移動了line2后,Rect1和Rect2均收到影響

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

那么整理一下大致思路,我們要用線將View的邊界分成許多個小矩形柠衍,并讓圖片畫在這些小矩形上洋满,之后同上一篇文章一致,根據(jù)我們的手勢控制對應圖片的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;
    ……
}

但是這么幾個屬性真的夠用嗎诫咱?在我試驗了之后發(fā)現(xiàn)是不夠的,我們還需要另外四個屬性洪灯,是四根其他的線坎缭,兩根確定其移動范圍的線,兩根頂點依附的線,當依附的線移動了后掏呼,可以快速更新自身的長度坏快,相應地延長或縮短。

于是我們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組成亏镰。

具體實現(xiàn)

由于許多東西的關鍵都是思路和建模,大家理解了這個思路并建立了正確方便的模型后拯爽,實現(xiàn)起來就異常容易了索抓,只是在預定的軌道上開車到終點就好了,其實后面的內容已經不重要了毯炮。

布局方式的確定

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

大家都知道,一根直線可以把一個矩形分成左右兩個矩形为迈,一根橫線可以把一個矩形分成上下兩個矩形缺菌,所以我們可以提供一個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已經被我們分割成了許多小的矩形區(qū)域公荧,接下來我們就要把圖片給畫上去,但不是隨便畫窟社,我們需要讓圖片在對應的矩形以centerCrop的方式顯示,不然我們看到的就不是圖片的重要區(qū)域灿里。那么怎么樣才可以做到呢程腹?由于每個矩形的位置我們都是知道的,所以我們只需要將圖片的中心移動到對應矩陣的中心缀去,按centerCrop的縮放規(guī)則讓圖片中心縮放就好了甸祭。這些就是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()中監(jiān)聽不同的手勢,對對應圖片的Matrix做出相關操作即可掸哑,這里就不重復說明了零远,比較基礎。

線的移動

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

首先攒磨,我們找到我們是否觸摸在線上汤徽,因為內部的線對象必然會被2個以上的邊界引用,當這條線的信息改變時拼坎,對應的邊界也會馬上得知浮毯,并改變其邊界區(qū)域债蓝,這樣我們就可以很方便的重新畫出邊界盛龄,我們就只要更新受影響區(qū)域圖片的Matrix即可。

moveLine(event); //移動線
mPuzzleLayout.update(); //更新PuzzleLayout內Border信息
updatePieceInBorder(event); //更新圖片Matrix信息以適應變化

圖片位置交換

圖片之間的相對位置是可以改變的啊鸭,按照正常的邏輯也是當我們長按一張圖片時欧芽,那張圖片會懸浮葛圃,然后移動到要交換位置的圖片,釋放手指就交換成功了库正。那么問題就是這個懸浮起來的效果,這里用全圖顯示加個半透明來表示龙誊,利用Canvas的相關方法實現(xiàn)及其容易。

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

圖片翻轉

這個同樣利用Matrix可以輕松實現(xiàn)趟大,不贅述铣焊。

matrix.postScale(-1, 1, px, py); //水平翻轉
matrix.postScale(1, -1, px, py); //垂直翻轉

尾聲

到這里,我們所要實現(xiàn)的功能已經基本全部實現(xiàn)叽讳,剩下的就是完善細節(jié)坟募,應該提供怎么樣的接口供外部操作,只需要慢慢調試即可懈糯,感興趣的同學可以去看一下源碼

總結

這次自定義的View相對于上一次的StickerView來說她紫,無疑是復雜了很多,我們需要建立更復雜的模型犁苏,但是所運用的核心類是一樣的,CanvasMatrix類围详,同上一篇一樣,我還是要強調思考與建模的重要性买羞,萬事開頭難雹食,前期的思考無疑是最難的,也占據(jù)了整個項目大部分的時間(我花了兩周思考群叶,嗚嗚,可能我太笨了)舶衬。

希望閱讀完這篇文章后赎离,可以對你有一些幫助,有什么問題或不懂可以隨時聯(lián)系我梁剔,歡迎騷擾。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末鸡岗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子轩性,更是在濱河造成了極大的恐慌狠鸳,老刑警劉巖悯嗓,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脯厨,死亡現(xiàn)場離奇詭異坑质,居然都是意外死亡,警方通過查閱死者的電腦和手機涡扼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門吃沪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人票彪,你說我怎么就攤上這事★鼻” “怎么了荤傲?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長终佛。 經常有香客問我雾家,道長,這世上最難降的妖魔是什么芯咧? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮邪铲,結果婚禮上无拗,老公的妹妹穿的比我還像新娘。我一直安慰自己英染,他們只是感情好被饿,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布狭握。 她就那樣靜靜地躺著疯溺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喝检。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天澡谭,我揣著相機與錄音,去河邊找鬼蛙奖。 笑死杆兵,一個胖子當著我的面吹牛,可吹牛的內容都是我干的琐脏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼吹艇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了受神?” 一聲冷哼從身側響起格侯,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎撑碴,沒想到半個月后碎连,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡廉嚼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了恐似。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡矫夷,死狀恐怖憋槐,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情忧陪,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布嘶摊,位于F島的核電站评矩,受9級特大地震影響,放射性物質發(fā)生泄漏虱颗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一上枕、第九天 我趴在偏房一處隱蔽的房頂上張望弱恒。 院中可真熱鬧返弹,春花似錦、人聲如沸义起。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽齐蔽。三九已至,卻和暖如春含滴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谈况。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赡茸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓祝闻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親屉栓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,071評論 25 707
  • 手勢圖片控件 PinchImageView 點擊圖片框架 photoView packagecom.example...
    Ztufu閱讀 721評論 0 1
  • 01 在這個故事里,他們沒有名字堤框。 他與她,不過是人群里最最普通的男女蜈抓,像你、像我一般沟使,像我們生活里最常見的男孩委可、...
    Guo小鍋閱讀 607評論 0 2
  • “牧塵腊嗡,我來阻止你的腳步了⊙嗌伲”當柳青云說出這句話時,他的眼中反射著漠然的光客们,臉龐上有著淡漠的笑容材诽。 而對于柳青云的...
    混沌天書閱讀 500評論 0 0
  • 無冕何成王脸侥,東風思鳳陽。下雨知八月湿痢,卸甲獨還鄉(xiāng)。
    徐達開閱讀 338評論 0 0