Jetpack-Compose 學習筆記(四)—— Intrinsic 固有特性測量是個啥枚驻?看完這篇就知道了

終于可以寫寫技術文了~ 最近忙著各種總結,想必大家也是一樣的吧株旷?今年年初的規(guī)劃再登,現(xiàn)在完成的怎么樣了呢?是不是也像我一樣“虎頭蛇尾”晾剖?哈哈锉矢!至少竹子比去年進步了不少,這是今年的最后一篇啦齿尽!希望2022年大家一起加油沽损!一起進步!

這一篇是為了填上一篇學習筆記三中提到的 Compose 也可多次測量的“坑”循头,那就是固有特性測量绵估。

Google 起的這名字個人感覺太不直觀了,第一次看到這個官方的翻譯真的讓我一頭霧水卡骂,這是個啥国裳?其實,這個東西主要作用就是全跨,調節(jié)需要展示的 Composable 組件的寬高大小缝左。

固有特性測量的基本用法

前面文章中也提到了,Compose 有一項規(guī)則螟蒸,即子元素只能測量一次盒使,測量兩次就會引發(fā)運行時異常。但是七嫌,有時又需要先收集一些關于子組件的信息,然后再測量父組件苞慢。那么诵原,借助固有特性,就可以先查詢子組件挽放,然后再進行實際測量绍赛。下面是一個栗子。

假如需要像下面展示的那樣:

圖 1

根據(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”。實際展示居然是這樣的:

圖2

嗯纯出?蚯妇?怎么中間的分割線“放飛自我”了敷燎?這是因為 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方法吴藻,同樣的還有 maxIntrinsicHeightminIntrinsicWidth笑诅、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")
                }
圖 3

如果不使用 Modifier.width(IntrinsicSize.Min)固有特性測量大猛,則顯示的效果就會是這樣的:

圖 4

所以如果需要自定義 Layout 適配固有特性測量扭倾,則需要實現(xiàn)相應的方法,個人覺得還是挺麻煩的挽绩。膛壹。。此外,在 code 9 中 minIntrinsicWidthmeasure方法中分別打上斷點模聋,可以發(fā)現(xiàn)肩民,Compose 確實是在 measure 父組件前,就先調用了 minIntrinsicWidth方法去獲取了子組件的寬高链方。

而且這里還有個 bug持痰,如果設置的文案既有中文又有英文,則會換行祟蚀。工窍。。包含空格符也會換行暂题,有興趣的同學可以試一下移剪。不知道 Compose 何時才能修復這個 bug~

總結

Compose 為了避免傳統(tǒng) View 體系重復測量導致的性能問題,規(guī)定了只能測量一次子組件的規(guī)則薪者,否則會出現(xiàn)運行時異常纵苛。但是在有些需要多次測量的使用場景,Compose 提出了設置固有特性測量的解決方案言津。固有特性測量的設置攻人,就是允許父組件在正式測量自身寬高前,去獲取子組件的寬高信息悬槽,從而確定自己的寬高怀吻。從以上的例子可以看出,子組件可以根據(jù)自己的寬高信息來決定父組件的寬高信息初婆,從而影響其他子組件的布局和寬高信息蓬坡。

好了,固有特性測量就介紹到這里磅叛,歡迎關注我屑咳,解鎖更多 Android 開發(fā)新知識!

參考文獻

  1. https://developer.android.google.cn/codelabs/jetpack-compose-layouts?continue=https%3A%2F%2Fdeveloper.android.google.cn%2Fcourses%2Fpathways%2Fcompose%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-layouts#10
  2. https://mp.weixin.qq.com/s/ESJHNzXXjdF95jCf9CNA6Q
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末弊琴,一起剝皮案震驚了整個濱河市兆龙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌敲董,老刑警劉巖紫皇,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異腋寨,居然都是意外死亡聪铺,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門萄窜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來计寇,“玉大人,你說我怎么就攤上這事脂倦》” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵赖阻,是天一觀的道長蝶押。 經常有香客問我,道長火欧,這世上最難降的妖魔是什么棋电? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮苇侵,結果婚禮上赶盔,老公的妹妹穿的比我還像新娘。我一直安慰自己榆浓,他們只是感情好于未,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著陡鹃,像睡著了一般烘浦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萍鲸,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天闷叉,我揣著相機與錄音,去河邊找鬼脊阴。 笑死握侧,一個胖子當著我的面吹牛,可吹牛的內容都是我干的嘿期。 我是一名探鬼主播品擎,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼秽五!你這毒婦竟也來了孽查?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤坦喘,失蹤者是張志新(化名)和其女友劉穎盲再,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓣铣,經...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡答朋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了棠笑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梦碗。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出洪规,到底是詐尸還是另有隱情印屁,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布斩例,位于F島的核電站雄人,受9級特大地震影響,放射性物質發(fā)生泄漏念赶。R本人自食惡果不足惜础钠,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叉谜。 院中可真熱鬧旗吁,春花似錦、人聲如沸停局。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翻具。三九已至履怯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間裆泳,已是汗流浹背叹洲。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留工禾,地道東北人运提。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像闻葵,于是被迫代替她去往敵國和親民泵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內容