終于可以寫寫技術文了~ 最近忙著各種總結,想必大家也是一樣的吧株旷?今年年初的規(guī)劃再登,現(xiàn)在完成的怎么樣了呢?是不是也像我一樣“虎頭蛇尾”晾剖?哈哈锉矢!至少竹子比去年進步了不少,這是今年的最后一篇啦齿尽!希望2022年大家一起加油沽损!一起進步!
這一篇是為了填上一篇學習筆記三中提到的 Compose 也可多次測量的“坑”循头,那就是固有特性測量绵估。
Google 起的這名字個人感覺太不直觀了,第一次看到這個官方的翻譯真的讓我一頭霧水卡骂,這是個啥国裳?其實,這個東西主要作用就是全跨,調節(jié)需要展示的 Composable 組件的寬高大小缝左。
固有特性測量的基本用法
前面文章中也提到了,Compose 有一項規(guī)則螟蒸,即子元素只能測量一次盒使,測量兩次就會引發(fā)運行時異常。但是七嫌,有時又需要先收集一些關于子組件的信息,然后再測量父組件苞慢。那么诵原,借助固有特性,就可以先查詢子組件挽放,然后再進行實際測量绍赛。下面是一個栗子。
假如需要像下面展示的那樣:
根據(jù)之前講的布局內容辑畦,我們很容易就可以寫出如下代碼:
// code 1
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
Row(modifier = modifier) {
Text(
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.CenterHorizontally),
text = text1
)
Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
Text(
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.CenterHorizontally),
text = text2
)
}
}
text1 和 text2 設置為 “Hello”吗蚌、“World”。實際展示居然是這樣的:
嗯纯出?蚯妇?怎么中間的分割線“放飛自我”了敷燎?這是因為 Row 沒有對它的子組件的測量做任何限制,而 Divider 的高度設置的是 fillMaxHeight
箩言,它會盡可能撐大父布局硬贯。那么要達到我們想要的效果,就需要使用固有特性 IntrinsicSize
先規(guī)定一下測量的方式陨收,這里需要將 Row 的 height 設置為 IntrinsicSize.Min
饭豹,即把 Row 的高度調整為盡可能小的固有高度。具體代碼如下:
// code 2
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
Row(modifier = modifier.height(IntrinsicSize.Min)) {
...
}
}
具體是如何做到的呢务漩?實際上拄衰,是因為 Row 父組件通過 IntrinsicSize
預先獲取到了它左右兩邊的 Text 組件的高度信息了,然后計算出了兩個 Text 組件的高度最大值作為它自己的高度值饵骨,最后將分割線的高度鋪滿整個父組件肾砂。
為了實現(xiàn)父組件能預先獲得子組件寬高信息從而確定自身寬高信息,Compose 為開發(fā)者提供了固有特性測量機制宏悦,允許開發(fā)者在每個子組件正式測量前能獲得各個子組件的寬高等信息镐确。
那么,這玩意兒是怎么實現(xiàn)的呢饼煞?
很遺憾竹子沒有翻到源碼源葫,哪位大神如果找到的話,歡迎一起交流~
雖然沒有找到源碼砖瞧,但是也知道了一些關鍵點息堂。下面是找源碼未遂的過程,不感興趣的同學可以跳過块促。荣堰。
固有特性測量實現(xiàn)的關鍵點
從使用的地方開始,從 code 2 的 height(IntrinsicSize.Min)
進入竭翠,到了 Intrinsic.kt 中的一個靜態(tài)內部類中:
// code 3
private object MinIntrinsicHeightModifier : IntrinsicSizeModifier {
override fun MeasureScope.calculateContentConstraints(
measurable: Measurable,
constraints: Constraints
): Constraints {
val height = measurable.minIntrinsicHeight(constraints.maxWidth)
return Constraints.fixedHeight(height)
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
) = measurable.minIntrinsicHeight(width)
}
這個靜態(tài)內部類又是實現(xiàn)了 IntrinsicSizeModifier
這個接口振坚,而這個 IntrinsicSizeModifier
接口實際上又是實現(xiàn)了 LayoutModifier
接口:
// code 4
private interface IntrinsicSizeModifier : LayoutModifier {
val enforceIncoming: Boolean get() = true
fun MeasureScope.calculateContentConstraints(
measurable: Measurable,
constraints: Constraints
): Constraints
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val contentConstraints = calculateContentConstraints(measurable, constraints)
val placeable = measurable.measure(
if (enforceIncoming) constraints.constrain(contentConstraints) else contentConstraints
)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(IntOffset.Zero)
}
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
) = measurable.minIntrinsicWidth(height)
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
) = measurable.minIntrinsicHeight(width)
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
) = measurable.maxIntrinsicWidth(height)
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
) = measurable.maxIntrinsicHeight(width)
}
綜合 code 3 和 code 4 可以看出,核心的方法就是 measurable.minIntrinsicHeight()
這一類的方法斋扰。比如在 code 3 中渡八,重寫的 MeasureScope.calculateContentConstraints
方法和 IntrinsicMeasureScope.maxIntrinsicHeight
方法,最重要的部分都是調用了 measurable.minIntrinsicHeight()
方法传货。這個 minIntrinsicHeight
都會傳一個 width 參數(shù)進去屎鳍,點進去,發(fā)現(xiàn)是一個 IntrinsicMeasurable
接口问裕,接口方法的說明如下所示:
// code 5 IntrinsicMeasurable.kt
/**
* Calculates the minimum height that the layout can be such that
* the content of the layout will be painted correctly.
*/
fun minIntrinsicHeight(width: Int): Int
方法的注釋寫的很清楚:計算能正確繪制 layout 內容時的 layout 的最小高度逮壁。OK,因為每個 Composable 組件擺放子組件的方式不同粮宛,所以每個組件的 IntrinsicMeasurable
接口的實現(xiàn)方式就不同了窥淆,但是沒有找到 Row 組件具體實現(xiàn)的源碼卖宠。。此路不通祖乳,看有沒有其他的路逗堵,在上篇筆記三中,我們知道 Composable 組件計算自身寬高是在 Layout 方法中進行的眷昆,那么從 Layout 處入手看會怎樣呢惕澎?
從 Row 的 Layout 方法進入到 Layout.kt失受,測量的部分肯定是在 MeasurePolicy 類中:
// code 6 Layout.kt
@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
······
}
其實 MeasurePolicy 不是一個類虑啤,而是一個接口拂募,在這個接口中,可以看到實現(xiàn)的 IntrinsicMeasureScope.minIntrinsicHeight()
方法:
// code 7 MeasurePolicy.kt
/**
* The function used to calculate [IntrinsicMeasurable.minIntrinsicHeight]. It represents
* defines the minimum height this layout can take, given a specific width, such
* that the content of the layout will be painted correctly.
*/
fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int {
val mapped = measurables.fastMap {
DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Min, IntrinsicWidthHeight.Height)
}
val constraints = Constraints(maxWidth = width)
val layoutReceiver = IntrinsicsMeasureScope(this, layoutDirection)
val layoutResult = layoutReceiver.measure(mapped, constraints)
return layoutResult.height
}
注釋寫的比較清楚:這個方法是用來計算 IntrinsicMeasurable.minIntrinsicHeight
帅刊,它定義了在給定寬度的情況下纸泡,該布局在正確繪制布局內容的情況下,可以獲得的最小高度赖瞒。
怎么獲得的呢女揭?看代碼是先通過 DefaultIntrinsicMeasurable 類求出每個子組件的最小高度,最小高度的計算還是調用的 measurable.minIntrinsicHeight(constraints.maxWidth)
方法栏饮。吧兔。。呃袍嬉。境蔼。又回到了之前的 IntrinsicMeasurable 接口中的 fun minIntrinsicHeight(width: Int): Int
方法(笑Cry.jpg)。雖然沒有找到真正實現(xiàn)這個接口的代碼伺通,但是通過上面的源碼跟蹤箍土,竹子也得知了兩個關鍵點。
關鍵點一就是 IntrinsicMeasurable 這個接口罐监,不光是 minIntrinsicHeight
方法吴藻,同樣的還有 maxIntrinsicHeight
、minIntrinsicWidth
笑诅、maxIntrinsicWidth
這一類的方法都是在 IntrinsicMeasurable 接口调缨,真正實現(xiàn)了這四個方法的地方就是真正實現(xiàn)了固有特性測量的地方。
再來看這些方法的參數(shù)吆你,都是對應的另一個尺寸的極值,這些都在 DefaultIntrinsicMeasurable 類中所有體現(xiàn):
// code 8 LayoutModifier.kt DefaultIntrinsicMeasurable class
override fun measure(constraints: Constraints): Placeable {
if (widthHeight == IntrinsicWidthHeight.Width) {
val width = if (minMax == IntrinsicMinMax.Max) {
measurable.maxIntrinsicWidth(constraints.maxHeight)
} else {
measurable.minIntrinsicWidth(constraints.maxHeight)
}
return EmptyPlaceable(width, constraints.maxHeight)
}
val height = if (minMax == IntrinsicMinMax.Max) {
measurable.maxIntrinsicHeight(constraints.maxWidth)
} else {
measurable.minIntrinsicHeight(constraints.maxWidth)
}
return EmptyPlaceable(constraints.maxWidth, height)
}
比如之前是傳入的 widthHeight = IntrinsicWidthHeight.Height
俊犯,minMax = IntrinsicMinMax.Min
妇多,所以就是調用的 measurable.minIntrinsicHeight(constraints.maxWidth)
,也就是將 約束條件中 width 最大值傳給了方法燕侠。
那之前我們僅使用 Modifier.height(IntrinsicSize.Min)
為 Row 的高度設置了固有特性測量并沒有設置寬度罢咦妗立莉?那是因為會將約束條件的 width 最大值作為默認值傳進去,如 code 3 中的代碼七问,這里的最大值其實就是不限制寬度的大小蜓耻,所以 Modifier.height(IntrinsicSize.Min)
所表達的意思就是,當寬度不限時通過子組件預先測量的寬高信息所能計算的 Row 最小高度是多少械巡。當然也可以自己設置一個寬度刹淌,那么子組件就可以根據(jù)你設置的 Row 寬度以及預先測量的寬高信息得出 Row 的最小高度是多少。這就是關鍵點二讥耗。
寬度受限會影響高度的例子很常見的就是 TextView 中顯示長文本的情況有勾。顯示內容不變時,寬度越小高度自然會越大古程,可看參考文獻2 中的例子蔼卡。
上面說的都是在 Compose 官方提供的 Composable 組件中的情況,那么在自定義 Layout 中呢挣磨?很遺憾雇逞,如果我們要在自定義 Layout 中使用固有特性測量,則必須自己實現(xiàn)茁裙,否則會有問題塘砸。
實現(xiàn)自定義layout中的固有特性測量
由之前的 學習筆記三 可知,自定義 Layout 主要還是重寫了 Layout()
方法呜达,如果我們要適配自己寫的自定義 Layout 的固有特性測量谣蠢,就需要對 Layout()
方法中的 MeasurePolicy 接口進行重寫了。
之前的自定義 Layout 主要是重寫了 MeasurePolicy 接口的 measure
方法查近,如果要實現(xiàn)固有特性測量眉踱,則還需要重寫相應的 Intrinsic 方法,具體來說一共有四個:
override fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int)
override fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int)
override fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int)
override fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int)
不一定全部都要實現(xiàn)霜威,根據(jù)具體的需求谈喳,需要用到哪種固有特性測量,實現(xiàn)哪種方法即可戈泼。例如竹子這里實現(xiàn)了一個自定義的 Column 組件婿禽,實現(xiàn)了 minIntrinsicWidth
方法:
// code 9
// 自定義 Column 組件
@Composable
fun MyColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content,
measurePolicy = object: MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
// 自定義 Layout 實現(xiàn)測量與擺放的邏輯
var height = 0 // 自定義 Layout 高度
var width = 0 // 自定義 Layout 寬度
val placeables = measurables.map {
val placeable = it.measure(constraints)
height += placeable.height
width = max(width, placeable.width)
placeable
}
return layout(width, height) {
var lastHeight = 0
placeables.map {
it.placeRelative(0, lastHeight)
lastHeight += it.height
}
}
}
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int {
TODO("Not yet implemented")
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int {
TODO("Not yet implemented")
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int {
var maxWidth = 0
measurables.forEach {
maxWidth = max(maxWidth, it.minIntrinsicWidth(height))
}
return maxWidth
}
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int {
TODO("Not yet implemented")
}
})
}
這樣就可以在 MyColumn 組件的 Modifier 中使用 IntrinsicSize 了,具體使用及顯示效果如下:
// code 10
MyColumn(modifier = Modifier.width(IntrinsicSize.Min)) {
Text(text = "watermelon")
Text("apple")
Divider(color = Color.Black, modifier = Modifier.height(2.dp).fillMaxWidth())
Text("orange")
}
如果不使用 Modifier.width(IntrinsicSize.Min)
固有特性測量大猛,則顯示的效果就會是這樣的:
所以如果需要自定義 Layout 適配固有特性測量扭倾,則需要實現(xiàn)相應的方法,個人覺得還是挺麻煩的挽绩。膛壹。。此外,在 code 9 中 minIntrinsicWidth
和 measure
方法中分別打上斷點模聋,可以發(fā)現(xiàn)肩民,Compose 確實是在 measure 父組件前,就先調用了 minIntrinsicWidth
方法去獲取了子組件的寬高链方。
而且這里還有個 bug持痰,如果設置的文案既有中文又有英文,則會換行祟蚀。工窍。。包含空格符也會換行暂题,有興趣的同學可以試一下移剪。不知道 Compose 何時才能修復這個 bug~
總結
Compose 為了避免傳統(tǒng) View 體系重復測量導致的性能問題,規(guī)定了只能測量一次子組件的規(guī)則薪者,否則會出現(xiàn)運行時異常纵苛。但是在有些需要多次測量的使用場景,Compose 提出了設置固有特性測量的解決方案言津。固有特性測量的設置攻人,就是允許父組件在正式測量自身寬高前,去獲取子組件的寬高信息悬槽,從而確定自己的寬高怀吻。從以上的例子可以看出,子組件可以根據(jù)自己的寬高信息來決定父組件的寬高信息初婆,從而影響其他子組件的布局和寬高信息蓬坡。
好了,固有特性測量就介紹到這里磅叛,歡迎關注我屑咳,解鎖更多 Android 開發(fā)新知識!