手把手教你寫一個自定義 ViewGroup
關鍵字:kotlin、custom甘邀、ViewGroup
custom view group 書寫流程
1. override onLayout
首先琅攘,最直覺的做法,就是寫下 public class CustomViewGroup extends ViewGroup {}
松邪。對于 kotlin 呢坞琴,就是 class CustomViewGroup : ViewGroup {}
。然后逗抑,根據(jù)編譯器的提示剧辐,我們?nèi)崿F(xiàn) onLayout
方法。那么這個 onLayout
的作用是什么呢邮府?
簡單翻閱源碼荧关,發(fā)現(xiàn)在 View 的源碼下找到了答案:
public void layout(int l, int t, int r, int b) {
// ...
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// ...
onLayout(changed, l, t, r, b);
// ...
}
// ...
這段代碼告訴我們,onLayout
是在 layout
方法中調(diào)用的褂傀。從命名而角度來看忍啤,on
表示在什么什么時候。類似的紊服,我們寫自定義 view 的時候經(jīng)常寫的 onMeasure
也是在 measure
方法中調(diào)用的檀轨。那么聰明的讀者胸竞,請你告訴我,我們寫 activity 的時候?qū)懙?onCreate
方法参萄,是不是也是在 Activity 的 create
中調(diào)用的呢卫枝?
哈哈,當然不是了(請放下你們手中的西瓜刀)讹挎。onCreate
是在 Activity 中的 performCreate
方法中調(diào)用的校赤。不過,原理是類似的筒溃,onXxx
都是在實際做 xxx
的時候被調(diào)用马篮。而這些個 xxx
方法基本都是 final 的。
書歸正傳怜奖。這個 onLayout
的用途是什么呢浑测?onLayout 函數(shù)是在 layout
函數(shù)中調(diào)用的,自然就是負責處理布局相關的邏輯歪玲∏ㄑ耄看一下函數(shù)簽名:
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int)
四個參數(shù) left、top滥崩、right岖圈、bottom 相當于給我們畫了一個邊界,我們就在這個畫布上作畫钙皮,不能超出蜂科。之后我們在 onLayout
中的邏輯就是,如何排布我們的 CustomViewGroup 的 children 了短条。這個需要根據(jù)業(yè)務來导匣。如果僅僅為了測試的話,我們可以簡單的讓他們一個接一個排列不留空茸时。
在我們寫完了 onLayout
之后逐抑,運行項目。不出意外的話屹蚊,我們是看不到任何東西的厕氨。為什么?因為我們還少了非常關鍵的一個環(huán)節(jié):測量汹粤。
2. override onMeasure
編譯器只告訴了我們命斧,需要 override onLayout,但是沒有告訴我們需要 overrid onMeasure嘱兼。仔細想想国葬,在顯示生活中,如果我們想有一個 2d 的紙張來畫子 view 的話,那么汇四,我們只知道我的子 view 的排列方式還不夠接奈,必須要先告訴別人,我們的紙張的大小通孽,這樣我們才能作畫序宦。onMeasure 就做了向 CustomViewGroup 的 parent 描述我們需要的大小的工作。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = getSize(defaultSize = 300, measureSpec = widthMeasureSpec)
val height = getSize(defaultSize = 300, measureSpec = heightMeasureSpec)
measureChildren(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(width, height)
}
其中背苦,measureChildren(widthMeasureSpec, heightMeasureSpec)
完成了遞歸地 measure 子 view 的工作互捌。最后一句 setMeasuredDimension(width, height)
非常重要,每一個 onMeasure 方法都要去顯式地調(diào)用它行剂。
3. 上代碼
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = getSize(defaultSize = 300, measureSpec = widthMeasureSpec)
val height = getSize(defaultSize = 300, measureSpec = heightMeasureSpec)
measureChildren(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(width, height)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var currentTop = 0
var currentLeft = 0
var currentRowHeight = 0
for (i in 0..childCount) {
val child = getChildAt(i) ?: continue
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
// child 的 width or height 超過了 parent 的 width 和 height秕噪,直接丟棄
if (childWidth > measuredWidth
|| childHeight > measuredHeight) {
continue
}
if (currentTop + childHeight > measuredHeight) {
continue
}
if (currentLeft + childWidth > measuredWidth) {
currentLeft = 0
currentTop += currentRowHeight
currentRowHeight = 0
}
if (currentLeft + childWidth <= measuredWidth
&& currentTop + childHeight <= measuredHeight) {
child.layout(currentLeft, currentTop, currentLeft + childWidth, currentTop + childHeight)
currentLeft += childWidth
if (childHeight > currentRowHeight) {
currentRowHeight = childHeight
}
}
}
}
private fun getSize(defaultSize: Int, measureSpec: Int): Int {
val size = MeasureSpec.getSize(measureSpec)
val mode = MeasureSpec.getMode(measureSpec)
when (mode) {
MeasureSpec.EXACTLY -> {
return size
}
MeasureSpec.AT_MOST -> {
return Math.min(size, defaultSize)
}
MeasureSpec.UNSPECIFIED -> {
return defaultSize
}
else -> {
return defaultSize
}
}
}
代碼中,layout child 的關鍵的句子就是:
child.layout(currentLeft, currentTop, currentLeft + childWidth, currentTop + childHeight)
4. 小結(jié)
我們寫 CustomViewGroup 的時候厚宰,需要一次做這幾個事情:
- override onMeasure
- measureChildren(widthMeasureSpec, heightMeasureSpec)
- setMeasuredDimension(width, height)
- override onLayout
- 理清你需要的業(yè)務邏輯
child.layout(currentLeft, currentTop, currentLeft + childWidth, currentTop + childHeight)