Jetpack Compose实现bringToFront功能——附上原理分析

2,217 阅读4分钟

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正确的保活方案,不要掉进保活需求死循环陷进