联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

Jetpack Compose Modifier2——LayoutModifier

作者:访客发布时间:2024-01-05分类:程序开发学习浏览:176


导读:Modifier.layout()&LayoutModifierNode注:本文源码基于:androidx.compose.ui:ui:1.5.4前置知识在...

Modifier.layout() & LayoutModifierNode

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

前置知识

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

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

3个阶段.webp

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

Compisition阶段.gif

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

Layout阶段.gif

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

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

layout-three-step-process.jpg

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

测量过程和摆放过程.jpg

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

Drawing阶段.gif


Modifier.layout()

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

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

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

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

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

// Measurable.kt
interface Measurable : IntrinsicMeasurable {
    fun measure(constraints: Constraints): Placeable
}

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

Constraints.jpg

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

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

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

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:

    MeasureeScope.kt
    interface MeasureeScope {
        fun layout(
            width: Int,
            height: Int,
            alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
            placementBlock: Placeable.PlacementScope.() -> Unit
        ) = object : MeasureResult { ... }
    }
    
    不干预测量和布局.jpg

修改测量过程

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

@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)
            }
        }
    )
}
正方形 Image.jpg

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

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

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:

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 布局
            }
	}
)
layout修饰符向右偏移20dp.jpg

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

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

元素偏移 VS 元素内容偏移.jpg

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

小结

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

Sample

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

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,就这么简单!

测试一下:

Text(
	text = "Hello, Compose!",
    fontSize = 38.sp,
    modifier = Modifier
		.background(Color.Yellow)
		.spacing(20.dp)
)
spacing.jpg

LayoutModifierNode

// 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。

修饰符size、padding、layout共同之处.jpg

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

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

/**
 * 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分类.jpg

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


LayoutNode 的测量流程

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

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

LayoutNode的remeasure方法跟源码-1.jpg

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的remeasure方法跟源码-2.jpg

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属性.jpg

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

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。

Code-Modifier链-Modifier.Node链.jpg

// 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 辅助对象,用于分层测量。

// 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

}

Modifier.Node链里面的NodeCoordinator.jpg

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

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

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

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 对应情况.jpg

上图中展示了 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) 方法,在里面做实际测量。

Modifier链顺序.jpg

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

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

约束向传递,尺寸向左传递.jpg

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

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

Box(Modifier.size(100.dp).size(200.dp))
Box(Modifier.size(200.dp).size(100.dp))

3

2

1

答案揭晓:

Box(Modifier.size(100.dp).size(200.dp)) // 最终大小 100 dp
Box(Modifier.size(200.dp).size(100.dp)) // 最终大小 200 dp

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

再来看一个例子:

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() 则会无视左边的约束,它们只会考虑自己的尺寸要求。

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)
    )
}
突破最大限制.jpg

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

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

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

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

突破最大限制-实际效果.png

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

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

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)
    )
}
突破最小尺寸限制.jpg

第一行文本,没有宽高约束,将宽设置为最小尺寸 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),主角以为自己瘸了走不了路,但实际上他行动并无问题。

突破最小尺寸限制-实际效果.jpg

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

Box(
    modifier = Modifier
    .background(Blue)
    .requiredSize(100.dp)
    .background(Origin)
    .requiredSize(50.dp)
)
50 dp 的橙色方块盖着 100 dp 的蓝色方块.jpg



参考:

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


标签:Compose喷气式飞机LayoutModifier


程序开发学习排行
最近发表
网站分类
标签列表