前言
在前面的文章里,我介绍了LayoutModifier
和DrawModifier
在Compose测量布局绘制的过程中的作用,介绍了ParentDataModifier
,用于提供数据给父容器做布局逻辑处理,那么本节我将会介绍组件测量过程中发生变化时,如何能够监听到。
1 OnRemeasureModifier
惯例还是看官方解释:
当布局的内容被重新测量的时候会被调用,通常会使用onSizeChanged来监听
/**
* A modifier whose [onRemeasured] is called when the layout content is remeasured. The
* most common usage is [onSizeChanged].
*
* Example usage:
* @sample androidx.compose.ui.samples.OnSizeChangedSample
*/
@JvmDefaultWithCompatibility
interface OnRemeasuredModifier : Modifier.Element {
/**
* Called after a layout's contents have been remeasured.
*/
fun onRemeasured(size: IntSize)
}
其实就是在组件的内容,例如宽高发生变化的时候,onRemeasured
函数会被重新调用,此时会拿到一个新的组件状态值IntSize
,里面包含了宽高信息。
1.1 OnRemeasuredModifier使用
在官方文档中,建议我们使用onSizeChanged
监听,例如点击一个按钮之后,将Text
的宽度改变,看onSizeChanged
是否会被回调。
var textSize by remember {
mutableStateOf(200.dp)
}
Column {
Text(text = "Text", modifier = Modifier
.width(textSize)
.background(Color.Red)
.onSizeChanged {
Log.d("TAG", "onCreate: onSizeChanged ${it.width}")
})
Button(onClick = {
textSize = 250.dp
}) {
Text(text = "点击改变Text宽度")
}
}
经过测试,初始化和点击按钮时,都会回调onSizeChanged
。
com.lay.composestudy D onCreate: onSizeChanged 688
其实onSizeChanged
调用时,会创建OnSizeChangedModifier
,OnSizeChangedModifier
其实就是实现了OnRemeasuredModifier
。
@Stable
fun Modifier.onSizeChanged(
onSizeChanged: (IntSize) -> Unit
) = this.then(
OnSizeChangedModifier(
onSizeChanged = onSizeChanged,
inspectorInfo = debugInspectorInfo {
name = "onSizeChanged"
properties["onSizeChanged"] = onSizeChanged
}
)
)
private class OnSizeChangedModifier(
val onSizeChanged: (IntSize) -> Unit,
inspectorInfo: InspectorInfo.() -> Unit
) : OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
private var previousSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
override fun onRemeasured(size: IntSize) {
if (previousSize != size) {
onSizeChanged(size)
previousSize = size
}
}
// ......
}
1.2 OnRemeasuredModifier原理分析
一旦讲到Modifier的原理,就需要再一次回顾Modifier的初始化流程。
当Modifier初始化的时候,首先会将Modifier.Element
转换为Modifier.Node
,采用头插法加到NodeChain
的双向链表中。
private fun createAndInsertNodeAsParent(
element: Modifier.Element,
child: Modifier.Node,
): Modifier.Node {
val node = when (element) {
is ModifierNodeElement<*> -> element.create().also {
it.kindSet = calculateNodeKindSetFrom(it)
}
else -> BackwardsCompatNode(element)
}
check(!node.isAttached)
node.insertedNodeAwaitingAttachForInvalidation = true
return insertParent(node, child)
}
在NodeChain
中的Modifier.Node
中,如果是ModifierNodeElement
类型的,例如LayoutModifier
或者DrawModifier
,就按照自身的具体实现类型创建对应的Modifier.Node
;其他类型,就统一创建为BackwardsCompatNode
。
实例化完成之后,在LayoutNode
测量布局的时候,会执行remeasure
函数,拿1.1 小节这个例子:
其中OnRemeasuredModifier
是依附在InnerCoordinator
这个Node上,那么在remeasure函数调用的时候,会从外层的LayoutModifierNodeCoordinator
开始测量(执行measure函数),一层一层,所以我这边就直接看内层的InnerNodeCoordinator
中的measure函数即可。
// InnerNodeCoordinator.kt
override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
// before rerunning the user's measure block reset previous measuredByParent for children
layoutNode.forEachChild {
it.measuredByParent = LayoutNode.UsageByParent.NotUsed
}
measureResult = with(layoutNode.measurePolicy) {
measure(layoutNode.childMeasurables, constraints)
}
// 核心代码
onMeasured()
return this
}
看下属于这节的核心代码,onMeasured
函数:
fun onMeasured() {
if (hasNode(Nodes.LayoutAware)) {
Snapshot.withoutReadObservation {
visitNodes(Nodes.LayoutAware) {
it.onRemeasured(measuredSize)
}
}
}
}
首先会判断,是否存在Nodes.LayoutAware
类型的节点,这个节点是什么类型?我查了一下在calculateNodeKindSetFrom
函数中,并没有一个对应的OnRemeasureModifierNode
,
internal fun calculateNodeKindSetFrom(node: Modifier.Node): Int {
var mask = Nodes.Any.mask
// ......
if (node is LayoutAwareModifierNode) {
mask = mask or Nodes.LayoutAware
}
}
但是,BackwardsCompatNode
其实是实现了LayoutAwareModifierNode
,所以OnRemeasuredModifier
就是这个类型。
internal class BackwardsCompatNode(element: Modifier.Element) :
// ......
LayoutAwareModifierNode,
然后在onMeasured
函数中,会执行onRemeasured
并把重新测量的值传递过来。
当然这是一个情况,如果在外层的outerCoordinator
中也有OnRemeasuredModifier
,那么在测量的时候,原理其实基本是一样,所以需要注意的一点就是OnRemeasuredModifier
只会回调它依附于的那一层LayoutModifierNodeCoordinator测量的变化。
Text(text = "Text", modifier = Modifier
.width(textSize)
.background(Color.Red)
.padding(padding1)
.onSizeChanged {
Log.d("TAG", "onCreate: onSizeChanged ${it.width}")
}
.padding(padding2))
Button(onClick = {
padding1 = 30.dp
}) {
Text(text = "点击改变Text宽度")
}
例如当padding1的值发生变化时,onSizeChanged
是不会有回调的结果过来的,除非发生了重组。
2 OnPlacedModifier
官方解释:
onPlaced函数的调用时机是在父容器摆放完成之后,任意子组件摆放之前调用
@JvmDefaultWithCompatibility
interface OnPlacedModifier : Modifier.Element {
/**
* [onPlaced] is called after parent [LayoutModifier] and parent layout gets placed and
* before any child [LayoutModifier] is placed.
*
* [coordinates] provides [LayoutCoordinates] of the [OnPlacedModifier]. Placement in both
* parent [LayoutModifier] and parent layout can be calculated using the [LayoutCoordinates].
*/
fun onPlaced(coordinates: LayoutCoordinates)
}
准确的来说,就是父容器的LayoutModifier
执行之后,在子组件的LayoutModifier
执行之前会回调onPlaced
函数。调用的时机在组合阶段,子组件的位置发生变化,都是进行onPlaced函数的回调。
2.1 OnPlacedModifier使用
例如对Text
组件设置了onSizeChanged
,onPlaced
,layout
,其实padding也是LayoutModifier,所以这3个函数的执行顺序为:onPlaced
> onSizeChanged
> layout
。
Text(text = "Text", modifier = Modifier
.width(textSize)
.background(Color.Red)
.onSizeChanged {
Log.d("TAG", "onCreate: onSizeChanged ${it.width}")
}
.onPlaced {
Log.d("TAG", "onCreate: onPlaced ${it.parentCoordinates}")
}
.layout { measurable, constraints ->
Log.d("TAG", "onCreate: call child layout")
layout(100, 100) {
}
}
.padding(padding2))
在onPlaced
函数的回调中,除了组件的大小之外,还可以拿到组件的位置信息,比OnRemeasureModifier
拿到的信息要更多。
@Composable
fun TestOnPlaceModifier() {
Box {
Text(text = "文字", Modifier.size(200.dp).background(Color.Red).onPlaced {
val positionInParent = it.positionInParent()
if (positionInParent.x > 20 && positionInParent.y > 20){
//TODO 动效展示,或者文字展示
}
})
}
}
所以通过onPlaced
拿到详细的位置信息,可以根据产品的需求做不同的效果展示。
2.2 OnPlacedModifier原理分析
在Compose测量阶段,会调用LayoutNode的remeasure
函数;在摆放阶段,会执行replace
函数。
/**
* Place this layout node again on the same position it was placed last time
*/
internal fun replace() {
if (intrinsicsUsageByParent == UsageByParent.NotUsed) {
// This LayoutNode may have asked children for intrinsics. If so, we should
// clear the intrinsics usage for everything that was requested previously.
clearSubtreePlacementIntrinsicsUsage()
}
try {
relayoutWithoutParentInProgress = true
// 核心代码
measurePassDelegate.replace()
} finally {
relayoutWithoutParentInProgress = false
}
}
如果看过remeasure
函数,那么在replace
函数中执行到核心代码时,最终会执行outerCoordinator
的replace
函数。
fun Placeable.place(x: Int, y: Int, zIndex: Float = 0f) =
placeApparentToRealOffset(IntOffset(x, y), zIndex, null)
@Suppress("NOTHING_TO_INLINE")
internal inline fun Placeable.placeApparentToRealOffset(
position: IntOffset,
zIndex: Float,
noinline layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
placeAt(position + apparentToRealOffset, zIndex, layerBlock)
}
跟一下代码,看下placeAt
函数的实现,这个是一个抽象接口,具体调用还是LayoutModifierNodeCoordinator
和InnerNodeCoordinator
调用这个函数。
override fun placeAt(
position: IntOffset,
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
super.placeAt(position, zIndex, layerBlock)
// The coordinator only runs their placement block to obtain our position, which allows them
// to calculate the offset of an alignment line we have already provided a position for.
// No need to place our wrapped as well (we might have actually done this already in
// get(line), to obtain the position of the alignment line the coordinator currently needs
// our position in order ot know how to offset the value we provided).
if (isShallowPlacing) return
// 核心代码
onPlaced()
PlacementScope.executeWithRtlMirroringValues(
measuredSize.width,
layoutDirection,
this
) {
measureResult.placeChildren()
}
}
最终执行还是调用onPlaced
,因为OnPlacedModifier
的类型依然是Nodes.LayoutAware
,所以会调用onPlaced
函数,然后会把NodeCoordinator
对象传递进去,在这个对象中已经保存了Node的全部属性信息。
// NodeCoordinator.kt
@OptIn(ExperimentalComposeUiApi::class)
fun onPlaced() {
val lookahead = lookaheadDelegate
if (lookahead != null) {
visitNodes(Nodes.LayoutAware) {
it.onLookaheadPlaced(lookahead.lookaheadLayoutCoordinates)
}
}
visitNodes(Nodes.LayoutAware) {
it.onPlaced(this)
}
}
这里需要关注一个点,在调用了OnPlaced
函数之后,才调用了MeasureResult
的placeChildren
函数,因为在这个函数中才是真正做组件的摆放逻辑的,印证了2.1小节开头的结论。
3 OnGloballyPositionedModifier
官方解释:
当每个LayoutModifierNodeCoordinator的全局内容发生变化的时候,例如位置,大小等,onGloballyPositioned函数会被执行,这个过程会在组合完成后。
/**
* A modifier whose [onGloballyPositioned] is called with the final LayoutCoordinates of the
* Layout when the global position of the content may have changed.
* Note that it will be called after a composition when the coordinates are finalized.
*
* Usage example:
* @sample androidx.compose.ui.samples.OnGloballyPositioned
*/
@JvmDefaultWithCompatibility
interface OnGloballyPositionedModifier : Modifier.Element {
/**
* Called with the final LayoutCoordinates of the Layout after measuring.
* Note that it will be called after a composition when the coordinates are finalized.
* The position in the modifier chain makes no difference in either
* the [LayoutCoordinates] argument or when the [onGloballyPositioned] is called.
*/
fun onGloballyPositioned(coordinates: LayoutCoordinates)
}
3.1 OnGloballyPositionedModifier使用
在Modifier中提供了onGloballyPositioned
函数,在调用这个函数的时候,会创建OnGloballyPositionedModifierImpl
,它是继承自OnGloballyPositionedModifier
。
@Stable
fun Modifier.onGloballyPositioned(
onGloballyPositioned: (LayoutCoordinates) -> Unit
) = this.then(
OnGloballyPositionedModifierImpl(
callback = onGloballyPositioned,
inspectorInfo = debugInspectorInfo {
name = "onGloballyPositioned"
properties["onGloballyPositioned"] = onGloballyPositioned
}
)
)
private class OnGloballyPositionedModifierImpl(
val callback: (LayoutCoordinates) -> Unit,
inspectorInfo: InspectorInfo.() -> Unit
) : OnGloballyPositionedModifier, InspectorValueInfo(inspectorInfo) {
override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
callback(coordinates)
}
// ......
}
例如下面的例子:
Column {
Text(text = "Text", modifier = Modifier
.width(textSize)
.background(Color.Red)
.onGloballyPositioned {
Log.d("TAG", "onGloballyPositioned: $it")
}
.padding(padding2))
Button(onClick = {
padding1 = 30.dp
}) {
Text(text = "点击改变Text宽度")
}
}
首先,OnGloballyPositionedModifier
是依附于padding(xx)
这块区域的,当这块区域的位置大小发生变化的时候,就会回调onGloballyPositioned
函数,等会,怎么和OnPlacedModifier
是一样的?甚至返回都是一样的。
那么这两者有什么区别呢?其实onGloballyPositioned
会在组件相对于窗口的位置发生变化的时候回调;而onPlaced
会在组件相对于父容器的位置发生变化的时候回调;综合来说,onGloballyPositioned
的回调会更加频繁一些,因此考虑到性能问题,需要谨慎使用onGloballyPositioned
。
3.2 OnGloballyPositionedModifier原理分析
因为OnGloballyPositionedModifier
涉及到窗口的变动,因此需要从原生View开始看,就是AndroidComposedView
,在measureAndLayout
函数调用时,会执行measureAndLayoutDelegate
的dispatchOnPositionedCallbacks
函数,从字面意思上看,就是进行位置分发。
// AndroidComposedView.kt
override fun measureAndLayout(sendPointerUpdate: Boolean) {
trace("AndroidOwner:measureAndLayout") {
val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
if (rootNodeResized) {
requestLayout()
}
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
}
最终会执行OnPositionedDispatcher
的dispatch
函数,因为前面的文章中我讲过,Compose的UI体系基础就是LayoutNode树,每一个组件对应一个LayoutNode
,这里会遍历LayoutNode
树,执行dispatchHierarchy
函数。
// OnPositionedDispatcher.kt
fun dispatch() {
// sort layoutNodes so that the root is at the end and leaves are at the front
layoutNodes.sortWith(DepthComparator)
layoutNodes.forEachReversed { layoutNode ->
if (layoutNode.needsOnPositionedDispatch) {
dispatchHierarchy(layoutNode)
}
}
layoutNodes.clear()
}
最终执行到LayoutNode
中的dispatchOnPositionedCallbacks
函数,在这个函数中会从head遍历到tail,碰到Nodes.GlobalPositionAware
类型的节点,也就是OnGloballyPositionedModifier
,就会执行其onGloballyPositioned
函数。
// LayoutNode.kt
@OptIn(ExperimentalComposeUiApi::class)
internal fun dispatchOnPositionedCallbacks() {
if (layoutState != Idle || layoutPending || measurePending) {
return // it hasn't yet been properly positioned, so don't make a call
}
if (!isPlaced) {
return // it hasn't been placed, so don't make a call
}
nodes.headToTail(Nodes.GlobalPositionAware) {
it.onGloballyPositioned(it.requireCoordinator(Nodes.GlobalPositionAware))
}
}
所以从源码来看,OnPlacedModifier
是在LayoutNode
进行测量布局的时候调用,而OnGloballyPositionedModifier
则是整个窗口测量布局之后,才会调用,后者是更加重量级的刷新。