關(guān)于View的工作原理忙灼、繪制流程等罚攀,在第4章 View的工作原理這篇文章已經(jīng)寫(xiě)了鲜棠。本文詳細(xì)說(shuō)一下自定義ViewGroup亭敢。
ViewGroup 繼承自View,所以 ViewGroup 是一種包含子View的特殊View墨状。
自定義 ViewGroup 有兩個(gè)主要步驟卫漫,重寫(xiě) onMeasure
與 onLayout
。
要注意到的是肾砂,ViewGroup 默認(rèn)是不走 onDraw
回調(diào)的列赎。如果想要 ViewGroup 走 onDraw 回調(diào),需要在 ViewGroup 的構(gòu)造方法中調(diào)用setWillNotDraw(false)
镐确。
一包吝、重寫(xiě)onMeasure
onMeasure
的目的是測(cè)量該 ViewGroup 和其所有子 View 的寬和高。
雖然通常情況下辫塌,ViewGroup 都會(huì)重寫(xiě) onMeasure 方法漏策,但這并不是必須的。如果 ViewGroup 不重寫(xiě) onMeasure 的話(huà)臼氨,默認(rèn)使用 View 的 onMeasure 方法掺喻,其表現(xiàn)為除非設(shè)置其寬(高)為固定的大小,否則其寬(高)與父容器相同储矩。
onMeasure 方法有兩個(gè)參數(shù)感耙,widthMeasureSpec
和heightMeasureSpec
。關(guān)于 MeasureSpec 持隧,在文章《【Android】MeasureSpec簡(jiǎn)述》中有詳細(xì)說(shuō)明即硼,這里就不贅述了。
一般來(lái)講屡拨,ViewGroup 需要先遍歷測(cè)量所有的子 View只酥,然后再根據(jù)子 View 的測(cè)量結(jié)果來(lái)計(jì)算自身的尺寸。
一種方式是調(diào)用measureChildren
方法呀狼,可以一次性測(cè)量所有的子 View裂允。然后,遍歷所有子 View哥艇,根據(jù)其measuredWidth
和mesuredHeight
計(jì)算 ViewGroup 的尺寸绝编。
另一種方式是,遍歷所有子 View貌踏,使用measureChild
或measureChildWithMargin
(二者的區(qū)別是十饥,這個(gè) ViewGroup 的 LayoutParams 是否允許 margin,這點(diǎn)將在第三節(jié)中說(shuō)明)來(lái)測(cè)量子 View祖乳,并在這個(gè)過(guò)程中計(jì)算 ViewGroup 的大小逗堵。
在計(jì)算完畢后,調(diào)用setMeasuredDimension(w, h)
來(lái)設(shè)置最終的測(cè)量結(jié)果凡资。這跟自定義 View 是相同的砸捏。
上面說(shuō)的正常情況下谬运,先測(cè)量子 View 再測(cè)量 ViewGroup ,那么非正常情況呢垦藏?比如不測(cè)量子 View 或者瞎測(cè)量梆暖,會(huì)有什么后果?這個(gè)放在第四章掂骏。
例
目標(biāo)是實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的FrameLayout
轰驳。以下代碼通過(guò)重寫(xiě)onMeasure
,實(shí)現(xiàn)了簡(jiǎn)易版FrameLayout
的測(cè)量:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
// 測(cè)量所有子View
measureChildren(widthMeasureSpec, heightMeasureSpec)
// 如果是寬高都是固定值弟灼,那么就直接返回
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(widthSize, heightSize)
return
}
var maxChildWidth = 0 // 子View的最大寬度
var maxChildHeight = 0 // 子View的最大高度
for (i in 0 until childCount) {
val child = getChildAt(i) // 子View
maxChildWidth = max(maxChildHeight, child.measuredWidth)
maxChildHeight = max(maxChildHeight, child.measuredHeight)
}
setMeasuredDimension(
resolveSize(maxChildWidth, widthMeasureSpec),
resolveSize(maxChildHeight, heightMeasureSpec)
)
}
其中级解,resolveSize(size, measureSpec)
在測(cè)量模式為AT_MOST
,并且size < specSize
時(shí)田绑,返回size
勤哗;其他情況下,將返回specSize
掩驱。
這很容易理解芒划,(FrameLayout的測(cè)量邏輯)簡(jiǎn)單的來(lái)說(shuō)就是:
- 當(dāng) ViewGroup 設(shè)置的寬度為固定值時(shí),最終寬度為這個(gè)固定值欧穴;
- 當(dāng) ViewGroup 設(shè)置的寬度為
match_parent
時(shí)民逼,最終寬度為其父容器的寬度; - 當(dāng) ViewGroup 設(shè)置的寬度為
wrap_content
并且子 View 的最大寬度小于 ViewGroup 的父容器時(shí)涮帘,ViewGroup 的最終寬度等于最大子 View 的寬度拼苍; - 當(dāng) ViewGroup 設(shè)置的寬度為
wrap_content
并且子 View 的最大寬度大于等于 ViewGroup 的父容器時(shí),ViewGroup 的最終寬度等于其父容器的寬度调缨。
二疮鲫、重寫(xiě)onLayout
onLayout
是自定義 ViewGroup 必須要實(shí)現(xiàn)的抽象方法,它的主要作用是確定 ViewGroup 各個(gè)子 View 的排列弦叶。
View 的位置由左棚点、上、右湾蔓、下四個(gè)邊界來(lái)描述。在計(jì)算出子 View 的四個(gè)邊界l
砌梆、t
默责、r
、b
后咸包,調(diào)用child.layout(l, t, r, b)
來(lái)進(jìn)行布局桃序。
例
對(duì)于簡(jiǎn)易的FrameLayout
來(lái)講,很容易得到:
- 子 View 的左烂瘫、上兩個(gè)邊界是與 ViewGroup 貼合的媒熊,所以這兩個(gè)邊界與 ViewGroup 相同奇适;
- 右邊界
r
則等于左邊界l + 子View的寬度
; - 下邊界
b
等于上邊界t + 子View的寬度
芦鳍。
由此嚷往,簡(jiǎn)易版FrameLayout
的onLayout
重寫(xiě)如下:
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in 0..childCount) {
val child = getChildAt(i)
child.layout(l, t, l + child.measuredWidth, t + child.measuredHeight)
}
}
三、自定義LayoutParams
在第一節(jié)中提到了measureChild
與measureChildWithMargin
柠衅。在使用默認(rèn) LayoutParams 的時(shí)候皮仁,如果調(diào)用measureChildWithMargin
,程序會(huì)報(bào)錯(cuò)菲宴,因?yàn)?code>ViewGroup.LayoutParams是不支持 margin 屬性的贷祈。
而 Android 自帶的那些 Layout 之所以支持 margin,是因?yàn)樗鼈兌加凶远x的 LayoutParams喝峦。
要實(shí)現(xiàn)自定義的 LayoutParams势誊,首先創(chuàng)建一個(gè)自定義 LayoutParams 類(lèi),然后實(shí)現(xiàn) generateDefaultLayoutParams
和generateLayoutParams
方法谣蠢。
-
generateDefaultLayoutParams
方法在通過(guò)addView
方法將子 View 添加到這個(gè) ViewGroup 中的時(shí)候調(diào)用粟耻,會(huì)給子 View 賦一個(gè)默認(rèn)的 LayoutParams。 -
generateLayoutParams
方法有兩個(gè)重載漩怎,其中:
generateLayoutParams(AttributeSet)
將根據(jù)布局中填寫(xiě)的屬性來(lái)生成自定義的 LayoutParams 并返回勋颖;
generateLayoutParams(LayouParams)
一般和checkLayoutParams
同時(shí)重寫(xiě)。在addView
的過(guò)程中勋锤,當(dāng)待添加的子 View 的 LayoutParams 不滿(mǎn)足checkLayoutParams
的條件時(shí)饭玲,則調(diào)用generateLayoutParams(LayouParams)
生成一個(gè)新的 LayoutParams。
例
上例中的簡(jiǎn)易FrameLayout
想要支持 margin 屬性叁执,首先創(chuàng)建一個(gè)繼承自MarginLayoutParams
的類(lèi)FrameLayoutParams
:
class FrameLayoutParams: MarginLayoutParams {
constructor(c: Context?, attrs: AttributeSet?) : super(c, attrs)
constructor(width: Int, height: Int) : super(width, height)
}
這里沒(méi)有添加任何屬性茄厘。如果想增加額外的自定義屬性,可以在 xml 中定義一個(gè) declare-styleable
標(biāo)簽谈宛,這與 View 的自定義屬性相同次哈,在此不再贅述。
然后再重寫(xiě)generateDefaultLayoutParams
與generateLayoutParams
方法吆录。
override fun generateDefaultLayoutParams() = FrameLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
override fun generateLayoutParams(attrs: AttributeSet?) = FrameLayoutParams(context, attrs)
override fun generateLayoutParams(p: LayoutParams?) = FrameLayoutParams(p)
override fun checkLayoutParams(p: LayoutParams?) = p is FrameLayoutParams
因?yàn)檫@個(gè)自定義的 FrameLayoutParams 里面沒(méi)有任何的屬性窑滞,所以其實(shí)可以不創(chuàng)建新類(lèi),直接用 MarginLayoutParams 替代恢筝。
override fun generateDefaultLayoutParams() = MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
override fun generateLayoutParams(attrs: AttributeSet?) = MarginLayoutParams(context, attrs)
override fun generateLayoutParams(p: LayoutParams?) = MarginLayoutParams(p)
override fun checkLayoutParams(p: LayoutParams?) = p is MarginLayoutParams
由于添加了 margin 屬性哀卫,所以測(cè)量與布局的過(guò)程都要考慮 margin。最終SimpleFrameLayout
的代碼如下:
class SimpleFrameLayout(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ViewGroup(context, attrs, defStyleAttr) {
constructor(context: Context) : this(context, null, 0)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
// 測(cè)量所有子View
measureChildren(widthMeasureSpec, heightMeasureSpec)
// 如果是寬高都是固定值撬槽,那么就直接返回
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(widthSize, heightSize)
return
}
// 通過(guò)子View的寬(高)此改,計(jì)算出 ViewGroup 的寬(高)
var maxChildWidth = 0 // 子View的最大寬度
var maxChildHeight = 0 // 子View的最大高度
for (i in 0 until childCount) {
val child = getChildAt(i) // 子View
maxChildWidth = max(maxChildHeight, child.measuredWidth + child.marginLeft + child.marginRight)
maxChildHeight = max(maxChildHeight, child.measuredHeight + child.marginTop + child.marginBottom)
}
setMeasuredDimension(
resolveSize(maxChildWidth, widthMeasureSpec),
resolveSize(maxChildHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in 0..childCount) {
val child = getChildAt(i)
val left = l + child.marginLeft
val top = t + child.marginTop
child.layout(left, top, left + child.measuredWidth, top + child.measuredHeight)
}
}
override fun generateDefaultLayoutParams() = MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
override fun generateLayoutParams(attrs: AttributeSet?) = MarginLayoutParams(context, attrs)
override fun generateLayoutParams(p: LayoutParams?) = MarginLayoutParams(p)
override fun checkLayoutParams(p: LayoutParams?) = p is MarginLayoutParams
}
四、不正常
上面說(shuō)了正常情況下的操作侄柔,下面看看不正常的情況下會(huì)發(fā)生什么共啃。
創(chuàng)建繼承自 ViewGroup 的類(lèi)MyLayout
占调,代碼如下:
class MyLayout : ViewGroup {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
for (i in 0 until childCount) {
val child = getChildAt(i)
val size = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)
child.measure(size, size)
}
setMeasuredDimension(600, 1200)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var top = t
for (i in 0 until childCount) {
val child = getChildAt(i)
val w = child.measuredWidth
val h = child.measuredHeight
child.layout(l, top, l + w, top + h)
top += h
}
}
}
在這個(gè)類(lèi)中,強(qiáng)制把所有子 View 的寬高都設(shè)為了100像素移剪,并且在豎直方向按照順序依次排列究珊。并且把 ViewGroup 的尺寸設(shè)置為固定的 600x1200。那么挂滓,實(shí)際的顯示效果如何呢苦银?
創(chuàng)建一個(gè) test.xml,如下:
<top.littlefogcat.ui.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f00">
<View
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="#00F" />
<View
android:layout_width="300dp"
android:layout_height="100dp"
android:background="#000" />
</top.littlefogcat.ui.MyLayout>
可以看到赶站,根布局是一個(gè)寬高都為match_parent
幔虏、背景為紅色的的MyLayout
。
它有兩個(gè)子 View:第一個(gè)子 View 寬為wrap_content
贝椿,高為match_parent
想括,藍(lán)色背景;第二個(gè)子 View 寬高為 300dp x 100dp烙博,黑色背景瑟蜈。
實(shí)際的顯示效果如下:
可以看到,的確和上面描述的相同渣窜,父布局大小為600x1200铺根,子 View 為 100x100的正方形;不管這三個(gè)控件寬高如何設(shè)置乔宿,都不影響最終的顯示效果位迂。
甚至如下段代碼所示,都不用在onMeasure
中測(cè)量子View详瑞,只用重寫(xiě)onLayout
就能達(dá)到相同的效果:
class MyLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : ViewGroup(context, attrs, defStyleAttr) {
constructor(context: Context?) : this(context, null, 0)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
override fun onMeasure(w: Int, h: Int) = setMeasuredDimension(600, 1200)
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var top = t
for (i in 0 until childCount) {
val child = getChildAt(i)
val bottom = top + 100
child.layout(l, top, l + 100, bottom)
top = bottom
}
}
}
然后掂林,我們可以在代碼中打印這些控件的大小:
root.post {
Log.d(TAG, "onCreate: root measured size = (${root.measuredWidth}, ${root.measuredHeight}), " +
"root size = (${root.width}, ${root.height})")
val count = root.childCount
for (i in 0 until count) {
val child = root.getChildAt(i)
Log.d(TAG, "onCreate: child$i/ measure=(${child.measuredWidth},${child.measuredHeight}), " +
"size=(${child.width},${child.height})")
}
}
得到以下結(jié)果:
root measured size = (600, 1200), root size = (600, 1200)
child0/ measure=(0,0), size=(100,100)
child1/ measure=(0,0), size=(100,100)
可見(jiàn)坝橡,子 View 的測(cè)量尺寸為0泻帮,因?yàn)楦揪蜎](méi)有測(cè)量過(guò)。
回過(guò)頭再來(lái)看 View 的 getMeasuredWidth
和 getWidth
方法:
public final int getWidth() { return mRight - mLeft; }
public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; }
可以看到计寇,getMeasuredWidth
返回的是 View 的 mMeasuredWidth
屬性锣杂,而這個(gè)屬性是在 ViewGroup 測(cè)量子 View 的時(shí)候通過(guò)child.measure(w, h)
傳遞過(guò)去的;因?yàn)?code>MyLayout沒(méi)有測(cè)量子 View番宁,所以它的孩子的mMeasuredWidth
都是0蹲堂。
而getWidth
返回的是mRight - mLeft
,這兩個(gè)是在 onLayout 方法里面 ViewGroup 通過(guò)child.layout
方法傳遞過(guò)去的贝淤。
所以從這里可以看出來(lái)getMeasuredWidth
和 getWidth
方法的區(qū)別了,一個(gè)是測(cè)量的寬度政供,一個(gè)是實(shí)際布局的寬度播聪。