- 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
1.前言
美图秀秀,天天P图,Photoshop,这些P图软件,一般都有交换图层顺序的功能,在Android中可以通过View#bringToFront()来让某个view显示在父控件最上层;
Compose中并不是直接叫:bringToFront,而是通过Modifier(修饰符)
的方式去控制子项绘制顺序
,虽然通过Compose中实现bringToFront功能很简单,但是简单并不意味着我们不用去思考里面的实现,我们了解里面做了什么,遇到问题才能更好的解决问题。
前面我们在Jetpack Compose UI创建布局绘制流程+原理,这篇文章中分析源码介绍了UI创建布局绘制的全部流程,如果还没有看过的,建议去看一看。
回到正文,实现bringToFront功能的话,我们需要用:Modifier.zIndex修饰符
简单看一下官方的解释:
控制子项的绘制顺序,zIndex比较大一点的,将绘制在小一点的zIndex之上
具有相同zIndex的根据摆放的顺序绘制,zIndex默认为0
2.简单演示的示例
一、简单写一个StickerNode方法
@Composable
fun StickerNode(modifier: Modifier, content: String, index: Int, onClick: () -> Unit) {
Box(
//使用Modifier一定要注意顺序哦
modifier.offset(if(index == 0) 30.dp else 100.dp,if(index == 0) 100.dp else 40.dp)
.clickable {
onClick()
}
.border(width = 1.dp, color = Color.Black)
.background(
if (index == 0) {
Color(android.graphics.Color.parseColor("#2196f4"))
} else {
Color(android.graphics.Color.parseColor("#fd9801"))
}
)
) {
Text(
text = content,
modifier = Modifier.padding(40.dp),
color = Color.White,
style = MaterialTheme.typography.subtitle2
)
}
}
二、简单示例调用如下:
val list = mutableListOf("许仙","白素贞")
var focusIndex by remember { mutableStateOf(0) }
Box(modifier = Modifier.fillMaxSize()) {
list.forEachIndexed {index,value->
StickerNode(modifier = Modifier.zIndex(if(focusIndex == index) 1F else 0F),content = value,index){
//更新显示在上层的index
focusedIndex = index
}
}
}
演示效果如下:
bringToFront
2.更新zIndex值
当更新Modifier.zIndex值之后,触发更改,建议大家可以去看Jetpack Compose UI创建布局绘制流程+原理,这篇文章;
下面,我们简单看一下部分执行的逻辑
//androidx.compose.runtime.Recomposer
suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->
while (shouldKeepRecomposing) {
......
try {
// 执行变更
toApply.fastForEach { composition ->
composition.applyChanges()
}
}
......
}
}
触发applyChanges
//androidx.compose.runtime.CompositionImpl
fun applyChanges(){
......
slotTable.write { slots ->
val applier = applier
changes.fastForEach { change ->
//会触发invoke,这里变更的是modifier
//所以后面会触发LayoutNode里面的setModifier方法
change(applier, slots, manager)
}
changes.clear()
}
......
}
执行到LayoutNode#setModifier,接下来会执行到requestRemeasure() 和parent?.requestRelayout,我们分别来简单的看一下,里面会执行什么
//androidx.compose.ui.node.LayoutNode#requestRemeasure
internal fun requestRemeasure() {
val owner = owner ?: return
if (!ignoreRemeasureRequests && !isVirtual) {
//此处的owner => AndroidComposeView
owner.onRequestMeasure(this)
}
}
内部会执行到requestRemeasure
//androidx.compose.ui.node.MeasureAndLayoutDelegate
fun requestRemeasure(layoutNode: LayoutNode): Boolean = when (layoutNode.layoutState) {
......
NeedsRelayout, Ready -> {
.......
layoutNode.layoutState = NeedsRemeasure
if (layoutNode.isPlaced || layoutNode.canAffectParent) {
val parentLayoutState = layoutNode.parent?.layoutState
if (parentLayoutState != NeedsRemeasure) {
//父节点的layoutState没有调用重新测量,需要添加到relayoutNodes列表中
relayoutNodes.add(layoutNode)
}
}
}
.......
}
执行完,接着执行parent?.requestRelayout,内部最终会执行下面代码
//androidx.compose.ui.node.MeasureAndLayoutDelegate#requestRelayout
fun requestRelayout(layoutNode: LayoutNode): Boolean = when (layoutNode.layoutState) {
......
Ready -> {
......
layoutNode.layoutState = NeedsRelayout
if (layoutNode.isPlaced) {
val parentLayoutState = layoutNode.parent?.layoutState
if (parentLayoutState != NeedsRemeasure && parentLayoutState != NeedsRelayout) {
//满足上面条件,添加到relayoutNodes列表
relayoutNodes.add(layoutNode)
}
}
......
}
......
}
上面的onRequestMeasure(layoutNode)和onRequestLayout(layoutNode)内部都会调用 AndroidComposeView#scheduleMeasureAndLayout(),会触发invalidate()
最终会触发AndroidComposeView#dispatchDraw调用
//androidx.compose.ui.platform.AndroidComposeView
override fun dispatchDraw(canvas: android.graphics.Canvas) {
......
//内部执行relayoutNodes按照树的深度优先级遍历node节点
measureAndLayout()
//执行LayoutNode绘制,内部会根据layoutNode里面的zIndex排序之后遍历执行绘制
canvasHolder.drawInto(canvas) { root.draw(this) }
......
}
我们看下面的流程图,dispatchDraw里面要执行的东西,大家可以根据下面的流程图,自己进去看代码里面的细节:
绘制方法调用的顺序
3.结论
(1). 更新Modifier.zIndex里面的值之后,会触发重组应用变更,执行CompositionImpl#applyChanges应用变更;
(2). 变更的是Modifier的值,所以会触发LayoutNode更新modifier值;
(3). 满足条件,会触发里面的requestRemeasure和LayoutNode内部的requestRelayout,将layoutNode添加到relayoutNodes列表中;
(4). AndroidComposeView#scheduleMeasureAndLayout() 内部会调用invalidate(),触发dispatchDraw(canvas)
(5). 在dispatchDraw方法内部会先遍历relayoutNodes列表,执行测量和布局,后面会遍历layoutNode列表,根据layoutNode里面的zIndex排序执行LayoutNode.draw(canvas)绘制节点
往期文章推荐:
1.Android跨进程传大图思考及实现——附上原理分析
2.Jetpack Compose UI创建布局绘制流程+原理 —— 内含概念详解(满满干货)
3.Jetpack App Startup如何使用及原理分析
4.Jetpack Compose - Accompanist 组件库
5.源码分析 | ThreadedRenderer空指针问题,顺便把Choreographer认识一下
6.源码分析 | 事件是怎么传递到Activity的?
7.聊聊CountDownLatch 源码
8.Android正确的保活方案,不要掉进保活需求死循环陷进