Jetpack Compose Modifier2——LayoutModifier

Modifier.layout() & LayoutModifierNode

注:本文源码基于:androidx.compose.ui:ui:1.5.4

前置知识

在 Compose 中,将数据渲染到屏幕上,一共会经历 3 个阶段:

  1. 组合 Compositrion
  2. 布局 Layout
  3. 绘制 Drawing

组合(Composition)阶段,Composable 函数会被执行,输出表示界面的树形数据结构:LayoutNode 树,也叫做 UI 树,每个 Composable 函数都对应一个节点 LayoutNode。

布局(Layout)阶段,树中的每个元素都会测量其子元素(如果有的话),并将它们摆放到可用的某个位置

界面树的每个节点在布局阶段都有 3 个步骤:

  1. 测量所有子项(如果有);
  2. 确定自己的尺寸;
  3. 摆放其子项。

一般来说,我们会将布局阶段的 3 个 步骤看作是 2 个过程:1.测量过程;2.布局(摆放)过程

最后一个阶段------绘制(Drawing)阶段,树中的每个节点会在屏幕上绘制像素:

Modifier.layout()

Compose 里面有一个 layout() 修饰符,可用于修改元素的测量和布局方式,从而影响元素的尺寸和位置。

kotlin 复制代码
// LayoutModifier.kt
fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this then LayoutElement(measure)

layout() 修饰符有一个函数类型参数 measure

  • 接收者类型是 MeasureScope;
  • 接受两个参数,类型分别为 Measurable 和 Constraints;
  • 返回类型为 MeasureResult。
kotlin 复制代码
Image(
    painter = painterResource(id = R.drawable.android), 
    contentDescription = null,
    modifier = Modifier.layout { measurable, constraints ->
		// 在这里修改元素的测量和布局过程
		// 最后需要返回 MeasureResult
    }
)

先来看看 lambda 表达式里的两个参数,第一个参数 Measurable,"可被测量的",它就是被 layout() 修饰符所修饰的元素,对于上面的例子来说,这个 measurable 其实就是 Image 元素。

kotlin 复制代码
// Measurable.kt
interface Measurable : IntrinsicMeasurable {
    fun measure(constraints: Constraints): Placeable
}

可以看到 Measurable 只有一个 measure() 方法,参数的类型是 Constraints。恰好,lambda 表达式的第二个参数就是 Constraints,它是父元素对当前元素的约束条件:

最后,lambda 表达式要求返回类型为 MeasureResult,这又是什么?从它的名字就可以看出来,这是"测量结果",里面保存了宽高和对齐线,还有一个 placeChildren() 方法,用于在布局过程被调用。

kotlin 复制代码
// MeasureResult.kt
interface MeasureResult {
    val width: Int
    val height: Int
    val alignmentLines: Map<AlignmentLine, Int>
    fun placeChildren()
}

说了这么多,这个 layout() 修饰符到底怎么使用啊?先看一下最简单的使用方式,即不修改元素原本的测量和布局方式:

kotlin 复制代码
Image(
    painter = painterResource(id = R.drawable.android), 
    contentDescription = null,
    modifier = Modifier.layout { /* 拥有 MeasureScope 上下文 */ 
        measurable, constraints ->
        val placeable = measurable.measure(constraints)
        layout(placeable.width, placeable.height) {
            placeable.placeRelative(0, 0)
        }
    }
)
  • 首先,调用 measurable.measure(constraints) 来测量当前元素,也就是让 Image 元素进行自我测量,得到一个 Placeable 实例。

  • 然后使用 MeasureScope 的 layout() 函数来保存元素的尺寸,这里传入了 placeable.widthplaceable.height,也就是使用了自我测量得到的尺寸。另外,还传入了一个 lambda 表达式,在里面调用 placeable.placeRelative(0, 0),将元素内容摆放到 (0, 0) 位置。

    MeasureScope 的 layout() 函数返回值类型就是我们需要的 MeasureResult:

    kotlin 复制代码
    MeasureeScope.kt
    interface MeasureeScope {
        fun layout(
            width: Int,
            height: Int,
            alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
            placementBlock: Placeable.PlacementScope.() -> Unit
        ) = object : MeasureResult { ... }
    }

修改测量过程

如果要创建一个总是显示为正方形的自定义 Image 组件,可以在 Image 完成自我测量后,从长和宽中取最小值作为正方形的边长,保存为尺寸:

kotlin 复制代码
@Composable
fun SquareImage(painter: Painter) {
    Image(
        painter = painter,
        contentDescription = null,
        modifier = Modifier.layout { measurable, constraints ->
            val placeable = measurable.measure(constraints)
            val size = min(placeable.width, placeable.height)
            layout(size, size) {
                placeable.placeRelative(0, 0)
            }
        }
    )
}

可以看到,虽然我们的图片是长方形,但由于使用了 Modifier.layout() 对 Image 自我测量的结果进行修改,最终显示出来的是正方形图像。

在传统 View 里面,等效的写法,需要继承 ImageView,重写 onMeasure() 方法:

kotlin 复制代码
class SquareImageView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec) // 先让 ImageView 自我测量
        val size = min(measuredWidth, measuredHeight) // 取长宽最小值
        setMeasuredDimension(size, size) // 保存尺寸
    }
}

这里只是针对提出的简单场景,对比 Compose 和 View 的写法。Compose 的 Modifier.layout() 并不等价于 View 里面的 onMeasure()。对于元素的测量过程而言,Modifier.layout() 只能修改"测量前的约束条件"或"测量后得到的尺寸"。它不能 100% 修改元素测量过程,也不能像 onMeasure() 那样测量子元素(子 View),只能对元素自身的测量过程进行简单的修改(修饰)。

修改布局过程

Modifier.layout() 除了能修改元素的测量方式,还能修改元素的布局方式。比如将元素内容向右偏移 20 dp:

kotlin 复制代码
Text(
	text = "Hello Android!",
    fontSize = 38.sp,
    modifier = Modifier
    	.background(Color.Yellow)
    	.layout { measurable, constraints ->
        	val placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) {
            	placeable.placeRelative(20.dp.roundToPx(), 0)
                // 其实这里使用 placeable.place() 也是可以的,
                // 只是 placeable.placeRelative() 支持 RTL 布局
            }
	}
)

不知道你是否发现,刚才说的是"对元素内容进行偏移",而不是"对元素进行偏移",注意二者的区别。

元素内容偏移,参照物是元素本身;而元素偏移,参照物是父元素。

为什么 placeable.placeRelative() 摆放的不是元素自身,而是元素内容呢?我们换个角度思考,一个元素的摆放是由它的父元素决定的,而我们现在用的是 Modifier.layout(),Modifier 是用于修改被修饰元素的外观和行为的,不应该干预父元素的行为,这样看事情似乎就变得合理了。

小结

Modifier.layout() 修饰符,只适用于需要对元素自身测量过程和布局过程进行简单修改的场景。简单来说,你对某个元素的测量和布局方式没有大刀阔斧修改的需求,只想微调一下尺寸,挪挪位置,那么 Modifier.layout() 修饰符就能派上用场了。

Sample

现在我们使用 layout() 修饰符来自定义一个功能类似 padding() 的修饰符,用于为元素添加内边距。

kotlin 复制代码
fun Modifier.spacing(spacing: Dp): Modifier = layout { measurable, constraints ->
    val spacingInPx = spacing.roundToPx()
    val placeable = measurable.measure(constraints.copy(
        maxWidth = constraints.maxWidth - spacingInPx * 2,
        maxHeight = constraints.maxHeight - spacingInPx * 2
    ))
    val width = placeable.width + spacingInPx * 2
    val height = placeable.height + spacingInPx * 2
    layout(width, height) {
        placeable.placeRelative(spacingInPx, spacingInPx)
    }
}

首先,在元素进行自我测量前,需要修改约束条件,最大可用高度和宽度,需要减去内边距 * 2,因为对于元素实际内容来说,它可用空间变小了。

其次,让元素进行自我测量,将得到的长和宽都加上内边距 * 2,再保存为尺寸,因为内边距也是算在尺寸里面的。

最后,在摆放元素内容时,向右下偏移。That's all,就这么简单!

测试一下:

kotlin 复制代码
Text(
	text = "Hello, Compose!",
    fontSize = 38.sp,
    modifier = Modifier
		.background(Color.Yellow)
		.spacing(20.dp)
)

LayoutModifierNode

kotlin 复制代码
// LayoutNodofoer.kt
fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this then LayoutElement(measure)

layout() 修饰符背后使用了 LayoutElement,与 size() 修饰符、padding() 修饰符背后的 SizeElemrnt、PaddingElement 对比,会发现它们都继承了 ModifierNodeElement<N : Modifier.Node> ,这个类的泛型类型 上界是 Modifier.Node。

Modifier.Node 是什么?我们都知道,代码 Modifier.size(100.dp).padding(10.dp) 会创建出一条 Modifier 链,每个节点都是一个 Modifier。这个 Modifier 链条被真正使用前,会被遍历处理,生成另外一条 Modifier.Node 的双向链条,每个节点都是一个 Modifier.Node。

不过我们也看到,SizeNode、PaddingNode...除了继承自 Modifier.Node,还实现了 LayoutModifierNode 接口,这个接口又是什么?查看它的注释:

kotlin 复制代码
/**
 * A [Modifier.Node] that changes how its wrapped content is measured and laid out.
 * It has the same measurement and layout functionality as the [androidx.compose.ui.layout.Layout]
 * component, while wrapping exactly one layout due to it being a modifier. In contrast,
 * the [androidx.compose.ui.layout.Layout] component is used to define the layout behavior of
 * multiple children.
 *
 * This is the [androidx.compose.ui.Modifier.Node] equivalent of
 * [androidx.compose.ui.layout.LayoutModifier]
 */
interface LayoutModifierNode : DelegatableNode { ... }

大概意思就是,LayoutModifierNode 代表这个 Modifier.Node 能改变其包装内容的测量和布局方式。它与 Layout() 函数具有相同的测量和布局功能,不过它终究只是一个 Modifier,所以只能封装一个布局,而 Layout() 函数可用于定义多个子元素的布局测量方式。另外,文档里提到,这个 LayoutModifierNode 和 LayoutModifier 是等价的,LayoutModifier 是 1.0.0 版本就有的,后来因为要做性能优化,Compose 团队就对 Modifier 的代码进行逐步重构,1.3.0 版本后就有了 LayoutModifierNode,它俩的功能是一样的。

到这里就不得不提一嘴 Modifier 修饰符的分类了:

Modifier 修饰符有很多,不过我们可以按功能来分类:影响元素的测量与布局过程的修饰符,像 size()padding()layout()... 它们都是基于 LayoutModifier 实现的(新版本基于 LayoutModifierNode);而影响元素绘制流程的修饰符,像 background()border() 则基于 DrawModifier 实现(新版本基于 DrawModifierNode)。我们最常用的 Modifier 修饰符基本都属于前面的两个分类,此外还有很多其他种类。

LayoutNode 的测量流程

LayoutModifierNode 是如何改变元素的测量与布局方式的呢?要探究这个问题,首先得了解元素是怎么进行测量与布局的。每个 Composable 函数,经过 Compose 编译器处理后,都会生成对应的 LayoutNode 对象,LayoutNode 的 remeasure() & replace() 方法做的就是测量 & 布局工作。

下面来扒一下 LayoutNode 的 remeasure() 关键源码:

LayoutNode 的 remeasure() 方法里,调用了 measurePassDelegate 的 remeasure() 方法,而这个 measurePassDelegate 的类型是 LayoutNodeLayoutDelegate.MeasurePassDelegate,所以我们应该跟踪到 LayoutNodeLayoutDelegate.MeasurePassDelegate 的 remeasure() 方法。

LayoutNodeLayoutDelegate.MeasurePassDelegate 的 remeasure() 方法里,调用了外部类 LayoutNodeLayoutDelegate 的 performMeasure() 方法。

LayoutNodeLayoutDelegate 的 performMeasure() 里,调用了 outerCoordinator.measure(),outerCoordinator 是谁?是 layoutNode.nodes.outerCoordinator,这时候我们得回头找 LayoutNode 的 nodes,找到 nodes 再继续看它里面的 outerCoordinator,因为上一步就是调用了这个 outerCoordinator 的 measure() 方法。

LayoutNode 的 nodes 属性,类型是 NodeChain,继续深入,找到 NodeChain 里面的 outerCoordinator,发现 outerCoordinator 实际上指向了 innerCoordinator,innerCoordinator 的实际类型是 InnerCoordinator,至此终于找到了上一步执行的 measure() 方法,它就是 InnerCoordinator 里的 measure() 方法,在这个方法里,调用了 MeasureScope.measure() 方法,得到了 MeasureResult。

山路十八弯,兜兜转转,我们从 LayoutNode 的 remeasure() 方法,一路跟踪,最后发现是调用了 InnerCoordinator 的 measure() 方法,在这里面做最终的、实际的测量,从而得到 MeasureResult。

也许你对 InnerCoordinator 的 measure() 方法还有很多疑惑,不过现在你只需知道:Compose 组件例如 Text 组件,它的内部定义了具体的测量算法,当使用 Text 组件时,Compose 会生成对应的 LayoutNode 对象,里面自然也包含了测量的具体算法。在测量阶段, LayoutNode 对象的 remeasure() 方法就会被执行,里面会调用 InnerNodeCoordinator 的 measure() 方法,在这个方法里会执行组件的实际测量算法。

LayoutModifierNode 如何影响测量流程

虽然我们简单了解了 LayoutNode 测量流程,但在此过程中,似乎并没有看到哪里和"LayoutModifierNode 改变元素的测量与布局方式"有关系,甚至连 Modifier 的影子都没见着。

让我们换个角度,我们都知道 Composable 函数会生成 LayoutNode 对象,而 LayoutNode 里面有一个 modifier 变量,存储的就是修饰 Composable 函数的 Modifier。

我们不妨从 LayoutNode 的 modifier 属性入手,看一看这个 modifier 的 set 方法,如果它被设置为 Modifier.size(100.dp),将在哪个地方影响到元素的测量与布局。

kotlin 复制代码
internal class LayoutNode(...) : ... {
    
    internal val nodes = NodeChain(this)
    
    override var modifier: Modifier = Modifier
		set(value) {
    		...
    		nodes.updateFrom(value) // 📌 重点
    		...
		}
}

我们主要看 set 方法里面的 nodes.updateFrom(value),这个 nodes 是一个 NodeChain 实例,NodeChain.updateFrom(modifier) 就是根据所设置的 Modifier 链来更新 NodeChain。

NodeChain 其实就是存储 Modifier.Node 的双向链表。也就是前面提到的,Modifier 链会被用于生成 Modifier.Node 双向链,所谓的 Modifier.Node 双向链就是 NodeChain。

kotlin 复制代码
// NodeChain.kt
internal class NodeChain(val layoutNode: LayoutNode) {
    internal val innerCoordinator = InnerNodeCoordinator(layoutNode)
    internal var outerCoordinator: NodeCoordinator = innerCoordinator
    
    internal val tail: Modifier.Node = innerCoordinator.tail
    internal var head: Modifier.Node = tail
}

从源码里看到,这个双向链表 NodeChain 除了头尾节点 headtail,还有两个 NodeCoordinator:innerCoordinatorouterCoordinator,NodeCoordinator 又是啥?

其实每一个 Modifier.Node 都有一个对应的 NodeCoordinator 辅助对象,用于分层测量。

kotlin 复制代码
// Modifier.kt
interface Modifier {
    abstract class Node : DelegatableNode {
        internal var parent: Node? = null                 // 父节点
        internal var child: Node? = null                  // 子节点
        internal var coordinator: NodeCoordinator? = null // 对应的 NodeCoordinator

        internal open fun updateCoordinator(coordinator: NodeCoordinator?) {
            this.coordinator = coordinator
        }
    }
}


// NodeCoordinator.kt
internal abstract class NodeCoordinator(
    override val layoutNode: LayoutNode,
) : Measurable, ... {
    abstract val tail: Modifier.Node

    internal var wrapped: NodeCoordinator? = null   // 内层 NodeCoordinator
    internal var wrappedBy: NodeCoordinator? = null // 外层 NodeCoordinator

}

在测量过程中,Compose 会遍历 Modifier.Node 链中的每个 NodeCoordinator,调用 NodeCoordinator 的 measure() 方法,从而影响元素的测量。同理,在布局过程则遍历调用 NodeCoordinator 的 placeAt() 方法。

上图中的例子里,外三层的 NodeCoordinator 负责对应 LayoutModifier 修饰符的测量工作,而最里层的 InnerNodeCoordinator 则负责元素 Box 的测量。

明白了这些,再来看 NodeChain 的 updateFrom(modifier) 方法就很清晰了:

kotlin 复制代码
internal class NodeChain(val layoutNode: LayoutNode) {
    internal val innerCoordinator = InnerNodeCoordinator(layoutNode)
    internal var outerCoordinator: NodeCoordinator = innerCoordinator
    internal val tail: Modifier.Node = innerCoordinator.tail
    internal var head: Modifier.Node = tail

    internal fun updateFrom(m: Modifier) {
        var coordinatorSyncNeeded = false
        val paddedHead = padChain()
        var before = current
        val beforeSize = before?.size ?: 0
        // 📌 Modifier.fillVector() 会将 Modifier 展平
        val after = m.fillVector(buffer ?: mutableVectorOf())
        var i = 0
        if (after.size == beforeSize) { // 检测更新差异
            ...
        } else if (!layoutNode.isAttached && beforeSize == 0) { // 第一次组装 Modifier.Node 双向链表
            coordinatorSyncNeeded = true
            var node = paddedHead
            while (i < after.size) { // 遍历 after 组装 Modifier.Node 双向链表
                val next = after[i]
                val parent = node
                node = createAndInsertNodeAsChild(next, parent)
                logger?.nodeInserted(0, i, next, parent, node)
                i++
            }
            syncAggregateChildKindSet()
        } else if (after.size == 0) { // 删除所有 modifier
            checkNotNull(before) { "expected prior modifier list to be non-empty" }
            var node = paddedHead.child
            while (node != null && i < before.size) {
                logger?.nodeRemoved(i, before[i], node)
                node = detachAndRemoveNode(node).child
                i++
            }
            innerCoordinator.wrappedBy = layoutNode.parent?.innerCoordinator
            outerCoordinator = innerCoordinator
        } else { ... }
        current = after
        buffer = before?.also { it.clear() }
        head = trimChain(paddedHead) // 更新头节点
        if (coordinatorSyncNeeded) {
            syncCoordinators() // 📌 关联 Modifier.Node 和 NodeCoordinator
        }
    }


    fun syncCoordinators() {
        var coordinator: NodeCoordinator = innerCoordinator
        var node: Modifier.Node? = tail.parent
        while (node != null) { // 尾 -> 头,遍历 Modifier.Node 双向链表
            val layoutmod = node.asLayoutModifierNode()
            if (layoutmod != null) { // 如果 Modifier.Node 属于 LayoutModifierNode
                val next = if (node.coordinator != null) { // LayoutModifierNode 已经有对应的 NodeCoordinator 了
                    val c = node.coordinator as LayoutModifierNodeCoordinator
                    val prevNode = c.layoutModifierNode
                    c.layoutModifierNode = layoutmod
                    if (prevNode !== node) c.onLayoutModifierNodeChanged()
                    c
                } else { // LayoutModifierNode 还没有对应的 NodeCoordinator
                    // 创建一个 LayoutModifierNodeCoordinator 与 LayoutModifierNode 关联
                    val c = LayoutModifierNodeCoordinator(layoutNode, layoutmod)
                    node.updateCoordinator(c)
                    c
                }
                // 将当前 LayoutModifierNode 对应的 NodeCoordinator 与上一个 NodeCoordinator 串起来
                coordinator.wrappedBy = next
                next.wrapped = coordinator
                coordinator = next
            } else { // Modifier.Node 不属于 LayoutModifierNode
                // 直接和上一个 NodeCoordinator 关联
                node.updateCoordinator(coordinator)
            }
            node = node.parent
        }
        // 链条所有节点都和对应的 NodeCoordinator 关联完成,最后更新 outerCoordinator
        coordinator.wrappedBy = layoutNode.parent?.innerCoordinator
        outerCoordinator = coordinator
    }
}

上图中展示了 3 种情况下 Modifier.Node 与 NodeCoordinator 对应的场景:

  1. 不设置 Modifier:只有 InnerNodeCoordinator 用于测量元素自身;
  2. 设置的 Modifier 全部都是 LayoutModifier 修饰符:除了 InnerNodeCoordinator 用于测量元素自身,每一个 LayoutModifierNode 都有一个对应的 LayoutModifierNodeCoordinator,用于测量 LayoutModifier 修饰符;
  3. 设置的 Modifier 里面既有 LayoutModifier 修饰符,也有 DrawModifier 修饰符:Draw 修饰符会和邻近的 LayoutModifier 修饰符共用同一个 LayoutModifierNodeCoordinator。

Modifier 链顺序

在使用 LayoutModifierNode 修饰符的时候,我们都知道先调用的修饰符会影响后调用的修饰符,具体是如何影响的呢?

回顾前面 LayoutNode 的测量流程,LayoutNode 的 remeasure(constraints) 方法,最后会调用最外层的 outerCoordinator 的 measure(constraints) 方法,在里面做实际测量。

结合图不难看出:约束条件会从外层 NodeCoordinator 往内层更新传递。对于代码 Modifier.size().padding().padding() 而言,约束条件则是从左往右更新传递,换而言之,右边的修饰符会受到左边修饰符传递过来的约束限制。

写在最后的 modifier 修饰符反而在最内侧,距离元素最近。

修饰符从左往右更新传递约束条件,然后从右往左传递返回确定的尺寸。

现在思考一个问题,以下两个 Box 最终大小分别是多少?

kotlin 复制代码
Box(Modifier.size(100.dp).size(200.dp))
Box(Modifier.size(200.dp).size(100.dp))

3

2

1

答案揭晓:

kotlin 复制代码
Box(Modifier.size(100.dp).size(200.dp)) // 最终大小 100 dp
Box(Modifier.size(200.dp).size(100.dp)) // 最终大小 200 dp

为什么呢?因为约束条件从左往右传递,右边修饰符的测量会受到左边修饰符传递过来的约束限制。

再来看一个例子:

kotlin 复制代码
Box(
    modifier = Modifier
        .size(100.dp)
    	.background(Blue)
    	.size(50.dp)
    	.background(Origin)
)

这个 Box 最终效果是?

A. 100 dp 的蓝色方块盖着 50 dp 的橙色方块;

B. 100 dp 的橙色方块盖着 50 dp 的蓝色方块;

C. 50 dp 的蓝色方块盖着 100 dp 的橙色方块;

D. 50 dp 的橙色方块盖着 100 dp 的蓝色方块.

3

2

1

答案是 E. 100 dp 橙色方块盖着 100 dp 的蓝色方块。

从测量的角度看,右边的 size(50.dp) 受到左边 size(100.dp) 的约束限制,size(50.dp) 已经失去作用,两次划定尺寸都是 100 dp;从绘制角度看,先绘制的蓝色,后绘制的橙色。那么结果自然就是 100 dp 橙色方块盖着 100 dp 的蓝色方块。

required modifiers

如果就是想让 50 dp 的橙色方块盖着 100 dp 的蓝色方块呢?有什么办法让右边的 Layout 修饰符不受左边 Layout 修饰符的约束限制?还真有办法,那就是 required modifiers 修饰符。

日常使用的 width()height()size() 修饰符它们都会考虑左边传递过来的约束,而 requiredWidth()requiredHeight()requiredSize() 则会无视左边的约束,它们只会考虑自己的尺寸要求。

kotlin 复制代码
val columnWidth = 200.dp
Column(
    modifier = Modifier
    .width(columnWidth)
    .border(1.dp, red)
) {
    Text(
        text = "width = parent + 50",
        modifier = Modifier
        .width(columnWidth + 50.dp)
        .background(Color.LightGray)
    )
    Text(
        text = "requiredWidth = parent + 50",
        modifier = Modifier
        .requiredWidth(columnWidth + 50.dp)
        .background(Color.LightGray)
    )
}

Column 的宽度被设置为 200 dp,那么它的所有子项都会接收到限制:喂,记得测量的时候,宽别超过 200 dp,不然爸爸要撑死啦;

第一个 Text 子项想要 250 dp 宽测量自己,但是听说上级要求最多 200 dp,好吧,那就只要 200 dp 吧;

第二个子项用 requiredWidth() 要求用 250 dp 测量自己,什么?最大 200 dp,我才不管,我就要 250 dp。

实际显示到屏幕上,看到的就是:

注意第二个子项,它自认为拥有了 250 dp 宽,所以把内容按照 250 dp 宽的规格来画,但实际上只是掩耳盗,200 dp 范围外的内容都是别人看不见的。

以上例子使用 requiredWidth() 修饰符突破了最大尺寸限制,再来看一个突破最小尺寸限制的例子:

kotlin 复制代码
val min = 150.dp
val max = 200.dp
Column {
    Text(
        text = "width = minWidth",
        modifier = Modifier
        .border(.5.dp, blue)
        .width(min)
        .background(Color.LightGray)
    )
    Text(
        text = "width = minWidth - 50",
        modifier = Modifier
        .border(.5.dp, blue)
        .widthIn(min, max)
        .width(min - 50.dp)
        .background(Color.LightGray)
    )

    Text(
        text = "requiredWidth = minWidth - 50",
        modifier = Modifier
        .border(.5.dp, blue)
        .widthIn(min, max)
        .requiredWidth(min - 50.dp)
        .background(Color.LightGray)
    )
}

第一行文本,没有宽高约束,将宽设置为最小尺寸 150 dp,OK;

第二行文本,宽约束为 [150 dp, 200 dp],width() 要求 100 dp 宽来测量自己,但因为受到约束(最低限制 150 dp),所以最终还是以 150 dp 宽来自我测量;

第三行文本, 宽约束为 [150 dp, 200 dp],requiredWidth() 要求 100 dp 宽来测量自己,无视约束(最低限制 150 dp),所以最终以 100 dp 宽来自我测量。

同理,这里要注意的是,第三个 Text 只是在测量时,自认为自己仅拥有 100 dp,所以内容按照 100 dp 宽的规格来画,但在屏幕上这个组件实际所占的宽就是 150 dp。有点像电影里面演的创伤后应激障碍(PTSD),主角以为自己瘸了走不了路,但实际上他行动并无问题。

到这里,回顾最初的问题,想让 50 dp 的橙色方块盖着 100 dp 的蓝色方块,怎么写?

kotlin 复制代码
Box(
    modifier = Modifier
    .background(Blue)
    .requiredSize(100.dp)
    .background(Origin)
    .requiredSize(50.dp)
)

参考:

Jetpack Compose中的Modifier------川峰

Compose:LayoutModifier

How Jetpack Compose Measuring Works

Custom layouts

Jetpack Compose - Order of Modifiers

Android Jetpack Compose width / height / size modifier vs requiredWidth / requiredHeight / requiredSize

相关推荐
服装学院的IT男2 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms2062 小时前
android 全面屏最底部栏沉浸式
android
服装学院的IT男2 小时前
【Android 源码分析】Activity生命周期之onStop-1
android
人间有清欢5 小时前
十、kotlin的协程
kotlin
吾爱星辰5 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
ChinaDragonDreamer5 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
网络研究院7 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下7 小时前
android navigation 用法详细使用
android
小比卡丘10 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭11 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android