本篇文章為利用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
可以先進行一系列變換后再進行繪制泻帮,繪制完成后恢復精置,這次利用的就是canvas
的clipRect()
方法將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
類可以成功幫助我們確定每張圖片的位置,令圖片被畫在正確的位置上霎俩,但是有一點致命的是它浅,它內部是由上下左右四個坐標確定的脆栋,仔細看我們要實現(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
蒋得,這個類要有個抽象方法支持我們自定義布局级及,并提供一些簡單的方法幫助我們快速布局,并且應該保有所有的邊界Border
和Line
對象额衙,方便進行管理和更新信息创千。
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()
方法提供分割布局搜锰,將增加的Line
和Border
添加至集合。
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來說她紫,無疑是復雜了很多,我們需要建立更復雜的模型犁苏,但是所運用的核心類是一樣的,Canvas
和Matrix
類围详,同上一篇一樣,我還是要強調思考與建模的重要性买羞,萬事開頭難雹食,前期的思考無疑是最難的,也占據(jù)了整個項目大部分的時間(我花了兩周思考群叶,嗚嗚,可能我太笨了)舶衬。
希望閱讀完這篇文章后赎离,可以對你有一些幫助,有什么問題或不懂可以隨時聯(lián)系我梁剔,歡迎騷擾。
最近閑下來了码撰,寫點文章記錄之前的學習并鞏固我的基礎知識,希望同大家一起進步灸拍!