D08 / 怎麽做自己的 Modifier.padding? - Custom Layout Modifier

今天大概会聊到的范围

  • layout modifier

上一次讨论到 Modifier 时,觉得自己其实对物件如何绘制到画面上其实一知半解。今天打算继续研究负责调整布局的 Layout Modifier。

这次我们一样讲 padding 这个 Modifier,padding 是一个 LayoutModifier。他做的事情是在物件与其他物件之间保留空间。如果 jetpack compose 没有内建 padding,而是要我们自己实作的话,可以怎麽办呢?

首先,我们先将我们预想的 Preview 写出来

@Preview
@Composable
fun `Preview Modifier pad`(){
    
    Box() {
        Text("test", modifier = Modifier.padTop(4.dp))
    }
       
}

因为 padding 要写四个方位,为了简化今天的说明,先以 padding top 为范例。

当然啦,Modifier.padTop() 目前是无法 compile 的,我们需要替 Modifier 写一个 extension function

fun Modifier.padTop(top: Dp = 0.dp) {}

Layout Modifier

这边提到的 layout modifier 和之前提到的 LayoutModifier 不同,这边提到的是 Modifier 的 layout() function。Modifier.layout 这个 function 可以用来创造一个客制化的布局(最终仍是产生一个 LayoutModifier) 。

fun Modifier.padTop(top: Dp = 0.dp) = layout { measurable, constraints ->
    // MeasureScope
}

让我们来看看 layout 长什麽样子

fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this.then(
    LayoutModifierImpl(
        measureBlock = measure,
        inspectorInfo = debugInspectorInfo {
            name = "layout"
            properties["measure"] = measure
        }
    )
)

layout 本身会将 this 与新产生的 LayoutModifier 透过 then 串接起来,因此我们不需要额外做串接。
另外,我们可以看到 layout 接受的参数是一个以 MeasureScope 为 receiver 的 lambda,这个 lambda 需要回传 MeasureResult。并且,在这个 lambda 中,可以拿到 MeasurableConstraints 两个参数。

这些都和之前聊的一样:

  • Modifier.layout 会先取得一个 Measurable ,这是一个还不知道大小的 child (通常是正在 modify 的物件)
  • 同时会拿到一个 Constraints , 这个是前面各种运算後留下来,这个 layout 自己可以占的空间
  • Measurable 在执行 measure() 後,可以知道大小,变成一个知道大小但是还没摆放的 Placable
  • PlacablePlacementScope 中可以摆放
  • 摆放完所有的 Placeable 後,就可以知道自身的大小 ( MeasureResult )

了解这样的概念後,我们就可以来创造自己的 layout modifier 了!

首先,我们先做完基本的流程,并且什麽都不做调整:

fun Modifier.padTop(top: Dp = 0.dp) = this.then(
    layout { measurable, constraints ->
        
        // MeasureScope
        
        // 1.
        val placeable = measurable.measure(constraints)
        
        
        // 2. 
        layout(placeable.width, placeable.height) {
            
            // 3. 
            // layout function 中,
            placeable.placeRelative(0, 0)
            
        }
    }
)

第一步( 1. ),我们要测量 child 的大小。在 Compose 的系统中,每个 Child 都会被测量一次(而且只会量一次),测量时需要提供 constraintsmeasurable 知道自己最大可以到多大 。测量完後变成 placeable 可以摆放。

第二步( 2. ),框出自己的大小。在 MeasureScope 中,又有另一个 layout function,需要提供这个 function 预期的长、宽。

第三部( 3. ),实际摆放。 MeasureScope.layout 里是 PlacementScope,也就是可以摆放 Placable 的地方。在这边可以呼叫 Placable.place ( or placeRelative ) 来进行摆放。

最後 MeasureScope.layout 会回传 MeasureResult,也是 Modifier.layout trailing lambda 所需要的 return 值。

开始改动

到目前为止,我们应该可以写出一个完全无用的 Modifier。接下来,我们要将 top padding 纳入考量。

  1. 首先,我们需要将 top padding 的 dp 转成 px 来运作
  2. 内容物的边界不需要改变。因此,我们用同一个 constraint 对他进行测量即可。
  3. 再来,我们的边界需要向上( top ) 延伸。因此,在 MeasureScope.layout 这边,提供 height 时要增加一段 top padding
  4. 因为是 top padding。因次,在 place 时,我们需要将物件往下移动 top padding 的量

因此,调整後我们的 Modifier 会变成这样

fun Modifier.padTop(top: Dp = 0.dp) = this.then(
    layout { measurable, constraints ->
        
        // MeasureScope
        
        // 0.
        val topPx = top.roundToPx()
        
        // 1.
        val placeable = measurable.measure(constraints)
        
        // 2.
        layout(placeable.width, placeable.height + topPx) {
            
            // 3.
            placeable.placeRelative(0, topPx)
        }
    }
)

限缩使用范围

我们顺利的建立了一个客制化的 top padding modifier。但是假设今天我们的 Modifier 很特别,只在特殊的场景适用,不希望其他人在其他场景误用的话,该怎麽办呢?

这边要使用讲到之前说过的 Modifier Scope 概念。建立一个 Scope 并且 Modifier 只在这个范围内能呼叫的到、能产生作用。

在那之前,我们先定义一个 Composable,作为我们限缩 Modifier 的 container。

@Composable
fun MyLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Surface(modifier = modifier) {
           content() 
    }
}

很单纯的 Composable,基本上只是将 content 包装在 Surface 内而已。

接下来才是有趣的部分,我们需创造一个 Scope 来限缩 modifier。

interface MyScope

// ---

@Composable
fun MyLayout(
    modifier: Modifier = Modifier,
    content: @Composable MyScope.() -> Unit // << 改成以 MyScope 做 receiver 
) {

将 Modifier 的定义搬进 MyScope 中:

interface MyScope {
    fun Modifier.padTop(top: Dp = 0.dp): Modifier
}

因为是 interface,所以这边还不会有实作。但是将 Modifier.padTop 放在 MyScope 中,我们就可以确保今天不是在 MyLayout 的 composable 都不会有这个 Modifier。

接下来,我们将 MyScope 实作,并且将 padTop 的实作也放进 MyScope 实作中。

object MyScopeInstance: MyScope {
    override fun Modifier.padTop(top: Dp) = layout { measurable, constraints ->
            // ...   
        }
}

最後,因为 MyLayout 的 content 需要一个 receiver。因此我们用 MyScopeInstance 作 receiver 呼叫他

@Composable
fun MyLayout(
    modifier: Modifier = Modifier,
    content: @Composable MyScope.() -> Unit
) {
    Surface(modifier = modifier) {
        MyScopeInstance.content() // << 改以 MyScopeInstance 呼叫 
    }
}

这次模仿实作一次,其实大多都是跟着官方文件实作的。但这个实作等於是将之前试图了解的 Layout 流程与 Modifier Scope 观念全部串在一起,算是不错的练习。


<<:  day8 储存设备 (雷)大家都不一样呢

>>:  [ Day 7 ] - 判断与流程控制

新新新手阅读 Angular 文件 - Router - pathMatch(2) - Day28

本文内容 接续,Day27 的内容,纪录阅读有关 Angular Route 的 pathMatch...

Ruby on Rails CRUD 之 U(Update)

更新资料常⽤的有 save 、 update 、 update_attribute 及 update...

DAY06随机森林演算法(续3)

昨天,我们把分类函数算法算完,那今天,我打算建立决策树: 有了第一个最佳分类点和数值後,接下来就要找...

全端入门Day01_前言

今天是铁人赛的第一天,这是我第一次参赛,之前听了很多同学说这是个需要有毅力的比赛,我相信我一定能够撑...

Day 7:git 版本控制

本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...