Modifier的基本使用

Modifier修饰符是Jetpack Compose中用来修饰组件的,提供常用的属性,写布局时几乎所有Composable组件的大部分属性都可以用Modifier 来修饰。官方在开发Compose UI时,最初尝试过将所有属性全部以函数参数的形式提供,但是那样太多了,他们也尝试过像Flutter那样的方式,将属性也作为一个组件进行嵌套,但这样又很容易让人感到困惑,所以才诞生了Modifier,将大部分组件常用属性封装成Modifier的形式来提供,哪个组件需要就在哪个组件上应用。我认为Modifier是Compose中最优秀的设计点之一。

@Composable
fun ModifierExample() {Box(modifier = Modifier.size(200.dp)) { // size同时指定宽高大小Box(Modifier.fillMaxSize()  // 填满父空间.background(Color.Red))Box(Modifier.fillMaxHeight() // 高度填满父空间.width(60.dp) .background(Color.Blue))Box(Modifier.fillMaxWidth() // 宽度填满父空间.height(60.dp).background(Color.Green).align(Alignment.Center))Column(Modifier.clickable { } // 点击事件 .padding(15.dp) // 外间距.fillMaxWidth().background(MaterialTheme.colorScheme.primary) // 背景.border(2.dp, Color.Red, RoundedCornerShape(2.dp)) // 边框.padding(8.dp) // 内间距) {Text(text = "从基线到顶部保持特定距离",modifier = Modifier.paddingFromBaseline(top = 35.dp))Text(text = "offset设置偏移量", modifier = Modifier.offset(x = 14.dp) // 正offset会将元素向右移)} }
}

部分Modifier属性只能在特定组件的作用域范围内才能使用,避免了像传统xml布局中的属性那样对自身没有用的属性也能被写出来造成污染。例如 Modifier.matchParentSize() 只有在 Box 组件范围内才能使用:

 Box(modifier = Modifier.size(200.dp)) {Text(text = "aaa",modifier = Modifier.align(Alignment.Center).matchParentSize() // matchParentSize 仅在 BoxScope 中可用)
}

观察源码发现 Modifier.matchParentSize()Modifier.align() 被定义在了BoxScope接口的内部,所以只能在Boxlambda中使用,该lambda函数的类型是 @Composable BoxScope.() -> Unit,可见其定义了ReceiverBoxScope

interface BoxScope {@Stablefun Modifier.align(alignment: Alignment): Modifier@Stablefun Modifier.matchParentSize(): Modifier
}

可以在 RowColumn 中使用Modifier.weight,类比传统线性布局中的layout_weight属性,并且仅可在 RowScopeColumnScope 中使用。

@Composable
fun ArtistCard() {Row(modifier = Modifier.fillMaxWidth().size(150.dp)) {Image(painter = painterResource(id = R.drawable.ic_sky),contentDescription = null,contentScale = ContentScale.Crop,modifier = Modifier.weight(2f) // 占比2/3)Column(modifier = Modifier.weight(1f) // 占比1/3) {Text(text = "Hello", style = MaterialTheme.typography.titleSmall)Text(text = "Compose", style = MaterialTheme.typography.bodyMedium)}}
}

点击事件相关的Modifier属性:

Column{Box(Modifier.clickable { println("clickable") }.size(30.dp).background(Color.Red))Box(Modifier.size(50.dp).background(Color.Blue).combinedClickable(onLongClick = { println("onLongClick") },onDoubleClick = { println("onDoubleClick") },onClick = { println("onClick") }))Box(Modifier.size(50.dp).background(Color.Green).pointerInput(Unit) {detectTapGestures(onDoubleTap = { },onLongPress = { },onPress = { },onTap = {})detectDragGestures(onDragStart = { },onDragEnd = { },onDragCancel = { },onDrag = { change, dragAmount -> })})}

Modifier的复用

可以通过定义扩展函数复用常用的Modifier属性配置:

fun Modifier.redCircle(): Modifier = clip(CircleShape).background(Color.Red)

使用:

 Column {Box(Modifier.size(80.dp).redCircle()) }

可以提取和复用同一修饰符实例,并将其传递给可组合项,避免在每一帧重组中创建大量对象:

val reusableModifier = Modifier.padding(12.dp).background(Color.Gray)@Composable
fun LoadingWheelAnimation() {val animatedState = animateFloatAsState(...)LoadingWheel(// No allocation, as we're just reusing the same instancemodifier = reusableModifier,animatedState = animatedState.value)
}

提取和复用未限定作用域的修饰符
修饰符可以不限定作用域,也可以将作用域限定为特定可组合项。对于未限定作用域的修饰符,可以从任何可组合项之外提取它们作为简单变量:

val reusableModifier = Modifier.fillMaxWidth().background(Color.Red).padding(12.dp)@Composable
fun AuthorField() {HeaderText(// ...modifier = reusableModifier)SubtitleText(// ...modifier = reusableModifier)
}

与延迟布局结合使用时,这尤为有用。在大多数情况下,建议对所有潜在的重要项目使用完全相同的修饰符:

val reusableItemModifier = Modifier.padding(bottom = 12.dp).size(216.dp).clip(CircleShape)@Composable
private fun AuthorList(authors: List<Author>) {LazyColumn {items(authors) {AsyncImage(// ...modifier = reusableItemModifier,)}}
}

提取和复用限定作用域的修饰符
在处理作用域限定为特定可组合项的修饰符时,您可以将其提取到尽可能高的级别,并在适当的情况下重复使用:

Column(...) {val reusableItemModifier = Modifier.padding(bottom = 12.dp).align(Alignment.CenterHorizontally).weight(1f)Text1(modifier = reusableItemModifier,// ...)Text2(modifier = reusableItemModifier// ...)// ...
}

注意:只能将提取的限定作用域的修饰符传递给限定相同作用域的直接子项
例如:

Column(modifier = Modifier.fillMaxWidth()) {// Weight modifier is scoped to the Column composableval reusableItemModifier =  Modifier.weight(1f)// Weight 可以在这里正常应用因为 Text 是 Column 的一个直接子项Text(modifier = reusableItemModifier// ...)Box {// Weight 在这里不起作用,因为当前 Text 不是 Column 的直接子项Text(modifier = reusableItemModifier// ...)}
}

延长提取Modifier链
您可以通过调用 .then() 函数进一步链接或附加提取的Modifier链:

val reusableModifier = Modifier.fillMaxWidth().background(Color.Red).padding(12.dp)// Append to your reusableModifier
reusableModifier.clickable { … }// Append your reusableModifier
otherModifier.then(reusableModifier)

Modifier的分类

Modifier有很多属性,这些属性属于不同类型的Modifier,每种类型的Modifier负责处理一类的功能,就常用的属性而言可以分成LayoutModifierDrawModifier,如size、padding等背后的实现是基于LayoutModifier,而background、border等背后的实现是基于DrawModifier

Modifier的分类如下:

Modifier的自定义

可以利用 Modifier.composed 自定义有状态的 Modifier,例如:

// 显示360度旋转动画
fun Modifier.rotating(duration: Int): Modifier = composed {val transition = rememberInfiniteTransition()val angleRatio by transition.animateFloat(initialValue = 0f,targetValue = 1f,animationSpec = infiniteRepeatable(animation = tween(duration)))graphicsLayer {rotationZ = 360f * angleRatio}
}
// 点击的时候添加一个边框
fun Modifier.addBorderOnClicked() = composed {var width by remember { mutableStateOf(0.dp) }when(width) {0.dp -> Modifierelse -> Modifier.border(width, Color.Red)}.then(Modifier.padding(5.dp).clickable { width = 1.dp })
}

使用:

    Column {Box(Modifier.size(80.dp).background(Color.Blue).rotating(300))Text("aaa", Modifier.addBorderOnClicked())}

composed{…} 会使用 工厂函数 创建一个新的 Modifier 对象 , 它会在重组的时候被调用,例如:

val modifier = Modifier.composed { // composed 中必须返回一个Modifiervar padding by remember { mutableStateOf(8.dp) }Modifier.padding(padding).clickable { padding = 0.dp  } // 点击的时候将padding改成0dp
}
Column {Box(Modifier.background(Color.Red)) {Text("aaaaa", modifier)}Box(Modifier.background(Color.Blue)) {Text("bbbbbbbbb", modifier)}
}

composed与普通Modifier属性的区别是其状态是独享的在重组运行时才生效,因为其factory参数是一个Composable函数 @Composable Modifier.() -> Modifier,所以在{…}中可以使用remember,可以把它当成一个Composable组件。例如上面代码运行后点击其中一个Box的padding变成0dp,但是此时另一个Box的padding不会发生变化,作为对比可以运行如下代码:

 // 这样写下面两个组件会共享这个padding, 点击的时候会同时paddinng变成0var padding by remember { mutableStateOf(8.dp) }val modifier = Modifier.padding(padding).clickable { padding = 0.dp }Column {Box(Modifier.background(Color.Red)) {Text("aaaaa", modifier)}Box(Modifier.background(Color.Blue)) {Text("bbbbbbbbb", modifier)} }

composed的主要作用还是为了重用Modifier,延时使用

还可以利用 Modifier.layout() 自定义一些布局相关的属性,如组件的位置偏移、大小限制、或者padding等。

例如:

// 自定义类似Modifier.offset()类似的效果
fun Modifier.myOffset(x : Dp = 0.dp, y : Dp = 0.dp) = layout { measurable, constraints ->val placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {placeable.placeRelative(x.roundToPx(), y.roundToPx()) //设置偏移 支持RTL// placeable.place(0, 0) // 不支持RTL使用这个即可}
}
// 使用:
@Composable
fun LayoutModifierExample() {Box(Modifier.background(Color.Red)) {Text(text = "Offset", Modifier.myOffset(5.dp))}
}
// 自定义和Modifier.padding()类似的效果
fun Modifier.myPadding(myPadding : Dp) = layout { measurable, constraints ->val padding = myPadding.roundToPx()val placeable = measurable.measure(constraints.copy(maxWidth = constraints.maxWidth - padding * 2,maxHeight = constraints.maxHeight - padding * 2))val width =  placeable.width + padding * 2val height = placeable.height + padding * 2layout(width, height) {placeable.placeRelative(padding, padding)}
}
// 使用:
@Composable
fun LayoutModifierExample3() {Box(Modifier.background(Color.Green)){ Text(text = "padding", Modifier.myPadding(10.dp))}
}
// 自定义和Modifier.paddingFromBaseline()类似的效果
fun Modifier.paddingBaslineToTop(padding : Dp = 0.dp) = layout { measurable, constraints ->val placeable = measurable.measure(constraints)check(placeable[FirstBaseline] != AlignmentLine.Unspecified)val firstBaseline = placeable[FirstBaseline] // 基线高度val paddingTop = padding.roundToPx() - firstBaseline // [设置的基线到顶部的距离] - [基线的高度]// 仅改变高度为加上paddingToplayout(placeable.width, placeable.height + paddingTop) {placeable.placeRelative(0, paddingTop) // y坐标向下偏移paddingTop}
}
// 使用:
@Composable
fun LayoutModifierExample4() {Box(Modifier.background(Color.Green)){ Text(text = "paddingFromBaseline", Modifier.paddingBaslineToTop(25.dp))}
}

类似的我们也可以尝试模仿DrawModifier的相关属性自己写出类似的东西。

利用modifierElementOf进行自定义,例如:

@OptIn(ExperimentalComposeUiApi::class)
class Circle(var color: Color) : DrawModifierNode, Modifier.Node() {override fun ContentDrawScope.draw() {drawCircle(color)}
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.circle(color: Color) = this then modifierElementOf(key = color,create = { Circle(color) },update = { it.color = color },definitions = {name = "circle"properties["color"] = color}
)
@Preview
@Composable
fun ModifierElementOfExample() {Box(Modifier.size(100.dp).circle(Color.Red))
}
@ExperimentalComposeUiApi
class VerticalOffset(var padding: Dp) : LayoutModifierNode, Modifier.Node() {override fun MeasureScope.measure(measurable: Measurable,constraints: Constraints): MeasureResult {val paddingPx = padding.roundToPx()val placeable = measurable.measure(constraints.offset(vertical = -paddingPx))return layout(placeable.width, placeable.height + paddingPx) {placeable.placeRelative(0, paddingPx)}}
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.verticalOffset(padding: Dp) = this then modifierElementOf(key = padding,create = { VerticalOffset(padding) },update = { it.padding = padding },definitions = {name = "verticalPadding"properties["padding"] = padding}
)@Preview
@Composable
fun VerticalOffsetExample() {Box(Modifier.size(100.dp).background(Color.Gray).verticalOffset(20.dp)) {Box(Modifier.fillMaxSize().background(Color.DarkGray))}
}
class SizeLoggerNode(var id: String) : LayoutAwareModifierNode, Modifier.Node() {override fun onRemeasured(size: IntSize) {println("The size of $id was $size")}
}@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.logSize(id: String) = this then modifierElementOf(key = id,create = { SizeLoggerNode(id) },update = { it.id = id },definitions = {name = "logSize"properties["id"] = id}
)@Preview
@Composable
fun PositionLoggerPreview() {Box(Modifier.size(100.dp).logSize("box"))
}

modifierElementOf主要用于创建一个ModifierNodeElement对象,它用于绑定到Modifier.Node实例上面。

Modifier在Compose模块中所处的位置

Compose的库分为好几个模块,从上到下总共分为4层,上层依赖下层的,而每一层都可以单独使用。

Compose模块 package 说明
Material androidx.compose.material 提供基于Material Design设计主题的内置组件,如Button、Text、Icon等
Foundation androidx.compose.foundation 为下面的UI层提供一些基础的Composable组件,如Row、Column、Box等布局类的组件,以及特定手势识别等,这些Composable可以支持跨平台通用
UI androidx.compose.ui 包含很多模块如ui-text、ui-graphics、ui-tooling等,该层为上层的Composable提供运行基础,Composable的测量、布局、绘制、事件处理等都是在该层,而Modifier的管理就是位于该层
Runtime androidx.compose.runtime 提供对Compose的UI树的管理能力,自动重组UI,通过diff驱动界面刷新等

Modifier链的构建过程

Modifier 实际上是个接口,它有三个直接子类:

  • Modifier伴生对象: 我们在代码中使用 Modifier.xxx() 时,第一个开头的Modifier就是这个伴生对象, 当第一次调用Modifier的属性时,都是调用的这个伴生对象的then函数,它的then直接返回传入的Modifier对象。Modifier伴生对象默认没有任何效果,相当于提供一个白板,然后你再往上面加效果。
  • CombinedModifier: 用于合成 Modifier 链中的每个 Modifier 结点,如果在伴生对象Modifier后面连续调用,则第二个开始的then函数会返回一个CombinedModifier对象,它将左边的Modifier对象作为outer(即当前调用者),右边的Modifie对象作为inner(即新设置的属性)进行合并。
  • Modifier.Element内部子接口: 所有的其他类型的Modifier都是实现了该接口的子类(为方便合成CombinedModifier而存在)。

CombinedModifier 定义如下:

class CombinedModifier(internal val outer: Modifier,internal val inner: Modifier
) : Modifier {...
}

then函数如下:

interface Modifier {...infix fun then(other: Modifier): Modifier =if (other === Modifier) this else CombinedModifier(this, other)...companion object : Modifier {...// 伴生对象的then返回传入的Modifier对象override infix fun then(other: Modifier): Modifier = other}
}

可以看到Modifier 接口的then返回的是CombinedModifier,其伴生对象then返回的是传入的Modifier

例如 Modifier.size() 返回的是一个 SizeModifier,它是 LayoutModifier 的子类,而 LayoutModifier 实现了 Modifier.Element 接口

@Stable
fun Modifier.size(size: Dp) = this.then(SizeModifier(...)
)
private class SizeModifier( ...) : LayoutModifier {...
}
interface LayoutModifier : Modifier.Element {...
}

如果对 Modifier 连续调用then函数就会形成一个 Modifier 链条,例如如下代码:

Modifier.size(100.dp).background(Color.Red).padding(10.dp).pointerInput(Unit) {...}

会形成如下的链条:

所以Modifier 链条本质上是一个通过CombinedModifier连接起来的Modifier.Element链表:

另外,在Modifier接口中有两个重要的操作方法:

interface Modifier {fun <R> foldIn(initial: R, operation: (R, Element) -> R): Rfun <R> foldOut(initial: R, operation: (Element, R) -> R): R
}

Compose就是通过 foldIn()foldOut() 专门来遍历 Modifier 链的,例如对于上面链条的代码执行 foldIn() 和 foldOut() :

  • foldIn(): 正向遍历 Modifier 链,SizeModifier-> Background -> PaddingModifier -> ComposedModifier
  • foldOut(): 反向遍历 Modifier 链, ComposedModifier -> PaddingModifier -> Background ->SizeModifier

通过跟踪源码可以发现,我们调用的所有Composable组件最终都是调用了一个叫Layout的Composable:

@Composable
@UiComposable
inline fun Layout(modifier: Modifier = Modifier,measurePolicy: MeasurePolicy
) {...val materialized = currentComposer.materialize(modifier) // 重点ReusableComposeNode<ComposeUiNode, Applier<Any>>(factory = ComposeUiNode.Constructor,update = {...},)
}

继续跟进 Composer.materialize() 可以发现源码中使用了 foldIn() 方法进行遍历:

fun Composer.materialize(modifier: Modifier): Modifier {...val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->acc.then(if (element is ComposedModifier) { val factory = element.factory as Modifier.(Composer, Int) -> Modifierval composedMod = factory(Modifier, this, 0) // 生产 Modifiermaterialize(composedMod) // 递归处理} else element)}...return result
}

这里对 ComposedModifier 进行了特殊判断,因为 composed() 返回的 ComposedModifier 包含一个 可以构建 Modifier 的工厂函数 ,而这里想做的是将 Modifier 链中的所有 ComposedModifier 摊平,让其 factory 内部产生的 Modifier 也能加入到 Modifier 链中。

Modifier测量绘制原理初探

Compose通过ComposeView挂接到传统View视图体系中,ComposeView是一个ViewGroup,它的直接子View是一个AndroidComposeView对象(它也是一个ViewGroup),然后在AndroidComposeView中管理着一棵由LayoutNode组成的UI树,每个Composable最终都对应着LayoutNode树中的一个节点。

在Activity的onCreate方法中调用的setContent方法:

public fun ComponentActivity.setContent(parent: CompositionContext? = null,content: @Composable () -> Unit
) {val existingComposeView = window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0) as? ComposeView// 已存在ComposeView就直接调用其setContent方法,否则就创建一个if (existingComposeView != null) with(existingComposeView) {setParentCompositionContext(parent)setContent(content)} else ComposeView(this).apply {...setContent(content) // 重点...// 调用Activity的setContentView方法将自身添加进去setContentView(this, DefaultActivityContentLayoutParams) }
}

查看setContent方法,其中调用createComposition方法创建一个Composition对象来管理Compose的UI树:

class ComposeView @JvmOverloads constructor(...
) : AbstractComposeView(context, attrs, defStyleAttr) {/*** Set the Jetpack Compose UI content for this view.* Initial composition will occur when the view becomes attached to a window or when* [createComposition] is called, whichever comes first.*/fun setContent(content: @Composable () -> Unit) {shouldCreateCompositionOnAttachedToWindow = truethis.content.value = content // 保存onCreate中setContent的lambda返回的Composable组件if (isAttachedToWindow) {createComposition() // 重点}}
}

在createComposition()方法中会调用ensureCompositionCreated()方法,实际上当ComposeView被首次创建时,并不会直接调用createComposition()方法,而是在onAttachedToWindow()方法中调用了ensureCompositionCreated()方法:

abstract class AbstractComposeView @JvmOverloads constructor(...
) : ViewGroup(context, attrs, defStyleAttr) {override fun onAttachedToWindow() {super.onAttachedToWindow()previousAttachedWindowToken = windowTokenif (shouldCreateCompositionOnAttachedToWindow) {ensureCompositionCreated()}}fun createComposition() {...ensureCompositionCreated()}private fun ensureCompositionCreated() {if (composition == null) {try {creatingComposition = truecomposition = setContent(resolveParentCompositionContext()) {Content() // 返回保存的onCreate中填写的Composable组件}} finally {creatingComposition = false}}}
}

继续跟进这个在onAttachedToWindow()方法中的setContent方法,发现它是一个扩展函数:

// Wrapper.android.kt
internal fun AbstractComposeView.setContent(parent: CompositionContext,content: @Composable () -> Unit
): Composition {GlobalSnapshotManager.ensureStarted()// 创建AndroidComposeView添加到ComposeView当中,且AbstractComposeView只能有一个childval composeView =if (childCount > 0) {getChildAt(0) as? AndroidComposeView} else {removeAllViews(); null} ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) } return doSetContent(composeView, parent, content)
}private fun doSetContent(owner: AndroidComposeView,parent: CompositionContext,content: @Composable () -> Unit
): Composition {...val original = Composition(UiApplier(owner.root), parent) // 创建Composition用来管理UI树val wrapped = ...wrapped.setContent(content)return wrapped
}

注意到,这里创建Composition时,传入了一个owner.root参数,从名字就可以猜出来,它就是整棵LayoutNode树的根节点:

 //AndroidComoseView.android.ktoverride val root = LayoutNode().also {it.measurePolicy = RootMeasurePolicyit.density = density// Composed modifiers cannot be added here directlyit.modifier = Modifier.then(semanticsModifier).then(rotaryInputModifier).then(_focusManager.modifier).then(keyInputModifier)}
 //AndroidComoseView.android.ktprivate val measureAndLayoutDelegate = MeasureAndLayoutDelegate(root)override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {...measureAndLayoutDelegate.updateRootConstraints(constraints) // 更新根节点的约束条件,同时会将root添加到relayoutNodes中measureAndLayoutDelegate.measureOnly()...}override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {measureAndLayoutDelegate.measureAndLayout(resendMotionEventOnLayout) // 遍历relayoutNodes中的节点执行measureAndLayout...}

代理类的measureAndLayout方法会遍历保存在其relayoutNodes集合中的每个节点(该集合保存了所有需要进行测量和布局的LayoutNode节点,包括root在内),然后执行其doRemeasureplace方法。

     // MeasureAndLayoutDelegate.ktfun measureAndLayout(onLayout: (() -> Unit)? = null): Boolean { performMeasureAndLayout {if (relayoutNodes.isNotEmpty()) {relayoutNodes.popEach { layoutNode ->val sizeChanged = remeasureAndRelayoutIfNeeded(layoutNode)...} }}...}private fun remeasureAndRelayoutIfNeeded(layoutNode: LayoutNode): Boolean {var sizeChanged = false...sizeChanged = doRemeasure(layoutNode, constraints)...layoutNode.replace()...}

Compose的测量绘制分为三个阶段:重组、布局、绘制

其中Layout阶段包含了我们在传统View中的测量和布局的概念,最后一步就是用Canvas进行绘制。

看一下 doRemeasure() 方法:

    // MeasureAndLayoutDelegate.ktprivate fun doRemeasure(layoutNode: LayoutNode, constraints: Constraints?): Boolean {val sizeChanged = if (constraints != null) {layoutNode.remeasure(constraints) // 重点} else {layoutNode.remeasure()}...}

可以看到这里将约束条件传给了 layoutNode 中的 remeasure() 方法中:

 // LayoutNode.ktprivate val measurePassDelegateget() = layoutDelegate.measurePassDelegateinternal val layoutDelegate = LayoutNodeLayoutDelegate(this)internal fun remeasure(constraints: Constraints? = layoutDelegate.lastConstraints): Boolean {return if (constraints != null) {... measurePassDelegate.remeasure(constraints) // 重点} else {false}}
    // LayoutNodeLayoutDelegate.ktinner class MeasurePassDelegate : Measurable, Placeable(), AlignmentLinesOwner {...remeasure(constraints)}fun remeasure(constraints: Constraints): Boolean {...performMeasure(constraints)...        }private fun performMeasure(constraints: Constraints) {...layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(layoutNode,affectsLookahead = false) {outerCoordinator.measure(constraints) // 重点} if (layoutState == LayoutState.Measuring) {markLayoutPending() }}

这里的outerCoordinator是LayoutNode中NodeChain中的对象:

internal class LayoutNodeLayoutDelegate(private val layoutNode: LayoutNode,
) {val outerCoordinator: NodeCoordinatorget() = layoutNode.nodes.outerCoordinator
}
// LayoutNode.kt
internal val nodes = NodeChain(this)

NodeChain是一个链表结构,其中的head和tail分别是Modifier.Node类型:

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

其中的 NodeCoordinator 是用来辅助Ndode节点处理测量和布局的,其中包含measure和placeAt的方法逻辑。NodeChain链表上的每一个Node都会对应的绑定一个NodeCoordinator 对象来辅助处理。

那么NodeChain这个链表什么时候会被更新呢,我们可以在LayoutNode中看到其成员对象modifier的set方法被覆写了:

    // LayoutNode.ktoverride var modifier: Modifier = Modifierset(value) { ...field = valuenodes.updateFrom(value)...}

这里调用了NodeChainupdateFrom方法,该方法将根据Modifier链来更新对应的NodeChain,也就是说每当有Modifier对象被设置到LayoutNode上面时,都会调用updateFrom方法进行更新对应的NodeChain。

updateFrom方法中,会调用Modifier.fillVector方法先将嵌套的Modifier按顺序进行展平成一个数组,随后根据展平结果将Modifier封装成Modifier.Node再串成一个双向链表。每个Composable对应的LayoutNode都拥有一个NodeChain链表,而NodeChain链表中的每个Modifier.Node节点都持有一个NodeCoordinator辅助对象。每当Modifier链更新时,会同步更新该链表,同时会同步每个Modifier.Node对应的NodeCoordinator。

Modifier.fillVector方法如下:

private fun Modifier.fillVector(result: MutableVector<Modifier.Element>
): MutableVector<Modifier.Element> {val stack = MutableVector<Modifier>(result.size).also { it.add(this) }while (stack.isNotEmpty()) {when (val next = stack.removeAt(stack.size - 1)) {is CombinedModifier -> {stack.add(next.inner)stack.add(next.outer)}is Modifier.Element -> result.add(next)else -> next.all {result.add(it)true}}}return result
}

注意,从1.3.0+版本开始,Compose中不再使用foldIn foldOut方法对Modifier进行遍历了,在1.3.0之前的版本LayoutNode源码中是通过foldOut遍历+头插法处理,而现在是通过fillVector方法处理达到类似的效果。


在进行测量时,Compose会遍历处理这个链表的每个Node对应的NodeCoordinator 的measure方法,对于布局也是类似,会调用placeAt方法。

由于Modifier是以NodeChain链表的形式挂到LayoutNode上面的,所以在重组时,如果只是更改了Modifier属性,将只会更新该Modifier对应在NodeChain链表中的某个Node节点,而不是重建整个Node链。

Modifier链的顺序对结果的影响

总的来说Modifier的执行顺序是按照从左到右,左边修饰符的尺寸和布局将影响右边的修饰符。可组合对象的最终大小取决于作为参数传递的所有修饰符。首先,修饰符将从左到右更新约束,然后从右到左返回大小。

例如来看如下代码的执行结果:

Box(Modifier.border(1.dp, Color.Red).size(32.dp).padding(8.dp).border(1.dp, Color.Blue))


首先会绘制一个32dp大小的红色边框,接着会将【32dp大小的约束】向右边传递,然后会在32dp的内部添加8dp的边距,接着将【32dp大小且8dp内边距的约束】继续传给Box组件,并在上面绘制出一个32dp-8dp*2=16dp大小的蓝色边框。

如果现在把 .size().padding() 的顺序交换一下:

Box(Modifier.border(1.dp, Color.Red).padding(8.dp).size(32.dp).border(1.dp, Color.Blue))


可以看到,结果是先应用了8dp的间距,在8dp的内部再显示了32dp大小的蓝色边框,或者可以理解为在32dp大小的基础之上添加了8dp的外间距,所以红色边框的大小是32dp+8dp*2=48dp。

因此,从执行顺序上看是从左到右,但生效结果的顺序是从右到左,是逆序的,即后执行的先生效。

但这样的顺序也有好处,来看下面这个例子:

@Composable
fun MyFancyButton(modifier: Modifier = Modifier) {Text(text = "Ok",modifier = modifier.clickable(onClick = { /*do something*/ }).background(Color.Blue, RoundedCornerShape(4.dp)).padding(8.dp))
}

只要将modifier作为Composable的参数传入,当前组件就允许其父组件对其添加额外的Modifier属性来修饰,例如父组件额外设置一个padding,因为最后添加的Modifier属性会先生效,因此组件内部的边框和内间距不会受到外部的影响。

再来看几个例子,以加深理解

下面的调用链会先绘制红色背景,后绘制蓝色背景,因此后绘制的蓝色会盖住红色背景,所以最终效果是一个50dp大小的蓝色块:

Box(Modifier.background(Color.Red).background(Color.Blue).size(50.dp))

而下面的代码调用链的结果会是40dp的蓝色块盖在80dp的红色块之上:

Box(Modifier.background(Color.Red).requiredSize(80.dp).background(Color.Blue).requiredSize(40.dp))

如果将上面代码中的 requiredSize(80.dp)requiredSize(40.dp) 对换位置:

Box(Modifier.background(Color.Gray).fillMaxSize(), // 规定父组件的大小才能看出效果contentAlignment = Alignment.Center
) {     Box(Modifier.background(Color.Red).requiredSize(40.dp).background(Color.Blue).requiredSize(80.dp))
}

这将会得到一个80dp的蓝色块,这是因为requiredSize属性不会使用左边传入的constraints约束条件进行约束,该多大就是多大,因此是80dp的蓝色块盖在40dp的红色块之上。

如果此时再将requiredSize换成size:

Box(Modifier.background(Color.Red).size(40.dp).background(Color.Blue).size(80.dp))

这将会得到一个40dp的蓝色块,因为此时左边的约束条件会传递给右边,而左边的约束条件更严格。或者从效果上也可以理解为是80dp的蓝色块上裁剪出一块40dp的大小。

OnRemeasuredModifier 和 OnPlacedModifier

OnRemeasuredModifier: Composable的remeasure方法执行完毕被回调,每次测量之后调用,可以用来获取测量后的尺寸大小。类比原生View的onMeasure()。

@Composable
fun OnRemeasuredModifierExample() {Box(Modifier.background(Color.Gray).size(200.dp),contentAlignment = Alignment.Center) {Text(text = "AAAAAAAAAAAAdddddddddddddddddddddddddddddddddddddd",Modifier.then(object : OnRemeasuredModifier {override fun onRemeasured(size: IntSize) {println(size)}}))}
}

可以使用Modifier.onSizeChanged来达到同样的效果,因为其内部就是基于OnRemeasuredModifier 封装实现的。

@Composable
fun OnRemeasuredModifierExample() {Box(Modifier.background(Color.Gray).size(200.dp),contentAlignment = Alignment.Center) {Text(text = "BBBBBBBBBBBhhhhhhhhhhhhhh",Modifier.onSizeChanged { size ->println(size)})}
}

OnPlacedModifier: 可以拿到坐标、尺寸等信息,类比原生View的onLayout()。它与OnRemeasuredModifier相比,它获得的信息更全,但是OnRemeasuredModifier发生的更早。

@Composable
fun OnPlacedModifierExample() {Box(Modifier.background(Color.Gray).size(200.dp),contentAlignment = Alignment.Center) {Text(text = "AAA",Modifier.onPlaced { layoutCoordinates ->val posInParent = layoutCoordinates.positionInParent()val posInWindow = layoutCoordinates.positionInWindow()val posInRoot = layoutCoordinates.positionInRoot()val size = layoutCoordinates.sizeval parentLayCoordinates = layoutCoordinates.parentLayoutCoordinatesprintln("posInParent: $posInParent")println("posInWindow: $posInWindow")println("posInRoot: $posInRoot")println("size: $size")println("parentLayCoordinates.size: ${parentLayCoordinates?.size}")})}
}

注意OnRemeasuredModifierOnPlacedModifier都是用来获取通知的,并不是用来执行measurelayout操作,而是在这些操作执行完毕后被通知的。

ParentDataModifier

ParentDataModifier: 一个继承自Modifier.Element的接口,它是一个可以为父布局提供数据的修饰符。可以在测量和布局期间通过IntrinsicMeasurable.parentData 读取到设置的数据值。parentData 通常用于通知父类如何测量和定位子类布局。

interface ParentDataModifier : Modifier.Element { fun Density.modifyParentData(parentData: Any?): Any?
}

例如,以下代码利用ParentDataModifier实现了一个简易版的Row/Column中的weight属性效果:

// 自定义weight
interface VerticalScope {@Stablefun Modifier.weight(weight: Float) : Modifier
}
class WeightParentData(val weight: Float=0f) : ParentDataModifier {override fun Density.modifyParentData(parentData: Any?) = this@WeightParentData
}
object VerticalScopeInstance : VerticalScope {@Stableoverride fun Modifier.weight(weight: Float): Modifier = this.then(WeightParentData(weight))
}@Composable
fun WeightedVerticalLayout(modifier: Modifier = Modifier,content: @Composable VerticalScope.() -> Unit
) {val measurePolicy = MeasurePolicy { measurables, constraints ->val placeables = measurables.map {it.measure(constraints)}// 获取各weight值val weights = measurables.map {(it.parentData as WeightParentData).weight}val totalHeight = constraints.maxHeightval totalWeight = weights.sum()// 宽度:最宽的一项val width = placeables.maxOf { it.width }layout(width, totalHeight) {var y = 0placeables.forEachIndexed() { i, placeable ->placeable.placeRelative(0, y)// 按比例设置大小y += (totalHeight * weights[i] / totalWeight).toInt()}}}Layout({ VerticalScopeInstance.content() }, modifier, measurePolicy)
}@Composable
fun WeightedVerticalLayoutExample() {WeightedVerticalLayout(Modifier.padding(16.dp).height(200.dp)) {Box(modifier = Modifier.width(40.dp).weight(1f).background(Color.Red))Box(modifier = Modifier.width(40.dp).weight(2f).background(Color.Green))Box(modifier = Modifier.width(40.dp).weight(7f).background(Color.Blue))}
}
@Preview(showBackground = true)
@Composable
fun WeightedVerticalLayoutExamplePreview() {WeightedVerticalLayoutExample()
}

运行效果:

参考资料:

  • 图解Modifier
  • Compose Modifiers deep dive
  • ParentData
  • 《Jetpack Compose从入门到实战》- 机械工业出版社 - 2022年9月

Jetpack Compose中的Modifier相关推荐

  1. 详解Jetpack Compose中的Modifier修饰符

    前言 本文将会介绍Jetpack Compose中的Modifier.在谷歌官方文档中它的描述是这么一句话:Modifier元素是一个有序.不可变的集合,它可以往Jetpack Compose UI元 ...

  2. Jetpack Compose 中的架构思想

    Jetpack Compose 中的架构总览 如果应用打算使用 Jetpack Compose 来开发,那么就可以跟以前的MVC.MVP.MVVM等乱七八糟的架构全部说拜拜,这些名词也将在Androi ...

  3. Jetpack Compose 中使用 Lottie 动画

    从事 Android 开发的 都知道 airbnb 的 Lottie 库,如今它也支持在 Jetpack Compose 中使用了. http://airbnb.io/lottie/#/android ...

  4. 42. Compose1.4 如何在Jetpack Compose中为文本应用描边效果(OpenAi翻译)

    探索Jetpack Compose中文本笔画效果的DrawStyle API Jetpack Compose最近为TextStyle增加了很多新的自定义选项,TextStyle.drawStyle就是 ...

  5. 了解 Jetpack Compose 中的重组

    对事件变化的响应对于创建最佳用户界面很重要.在许多情况下,用户不会输入正确的内容,并且当用户更改首选项或位置时,UI 必须更改. 为了使用户界面保持最新,UI 框架 Jetpack Compose 提 ...

  6. 在 Jetpack Compose 中安全地使用数据流

    /   今日科技快讯   / 11月17日下午,暴雪中国官方微博发布公告称,各位暴雪游戏的国服玩家,我们很遗憾地通知大家,随着我们与网之易公司现有授权协议的到期,自2023年1月24日0点起,所有&l ...

  7. Jetpack Compose中的手势操作

    点击事件 监听点击事件非常简单,使用 clickable 和 combinedClickable 修饰符即可满足需求: @OptIn(ExperimentalFoundationApi::class) ...

  8. Jetpack Compose中的Accompanist

    accompanist是Jetpack Compose官方提供的一个辅助工具库,以提供那些在Jetpack Compose sdk中目前还没有的功能API. 权限 依赖配置: repositories ...

  9. Jetpack Compose 中的重组作用域和性能优化

    只有读取可变状态的作用域才会被重组 这句话的意思是只有读取 mutableStateOf() 函数生成的状态值的那些 Composable 函数才会被重新执行.注意,这与 mutableStateOf ...

最新文章

  1. R语言dplyr包排序及序号函数实战(row_number、ntile、min_rank、dense_rank、percent_rank、cume_dist)
  2. 列出对象锁(lock)信息及其被杀的会话(sid,serial#)信息
  3. gsea结果分析图怎么看_数据分析怎么做?看这篇就够了!
  4. 一个Web OS - Windows4All
  5. ie php文件,IE(HTTPS):从php文件生成pdf不起作用
  6. osg-3D世界到屏幕
  7. 欢迎来到 C# 9.0(Welcome to C# 9.0)
  8. PHP 基础 自动类型转换之比较运算符
  9. oracle的todate函数 不用英文,关于在mybaties 和 oracle的to_date函数的问题?
  10. 使用lua实现nginx rewrite
  11. java程序打包exe
  12. 位图和矢量图转换工具推荐
  13. WIN7 Activation
  14. win10家庭版添加“本地安全策略”
  15. vc开发记牌器的两种思路
  16. 一步一步教你写股票走势图——分时图一(概述)
  17. 达梦数据库的备份还原
  18. 升级pip 升级pip3的快速方法
  19. java基础--while循环实现A4纸折叠次数到珠穆朗玛峰高度
  20. python内置库求复数的辐角_根据下列选项,回答 30~34 题: A.杜仲B.黄柏C.厚朴D.肉桂E.牡丹皮 第 30 题 断面较平坦,粉...

热门文章

  1. 程序是怎样跑起来的-读书文摘
  2. Python 02 Selenium 账号密码登录CSDN
  3. 神武4手游服务器维护,神武4手游快速升级攻略 神武4手游新区老区冲级攻略
  4. 《NAO机器人程序设计》---第四章 运动控制
  5. AI+智能服务机器人应用基础【学习报告】
  6. import-html-entry]: error occurs while executing entry script, 乾坤加载子应用路由报错
  7. ABAP:BTE的查找及使用
  8. 用计算机进行会计核算与手工会计核算,会计核算软件与手工会计核算软件有什么区别...
  9. 求解两点间最短路径的算法
  10. Android App内部防截屏技术