Jetpack Compose實現bringToFront功能——附上原理分析

語言: CN / TW / HK

theme: arknights highlight: androidstudio


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方法 kotlin @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 ) } } 二、簡單示例呼叫如下:

kotlin 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建立佈局繪製流程+原理,這篇文章;
下面,我們簡單看一下部分執行的邏輯

```kotlin //androidx.compose.runtime.Recomposer

suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock -> while (shouldKeepRecomposing) { ...... try { // 執行變更 toApply.fastForEach { composition -> composition.applyChanges() } } ...... } } ``` 觸發applyChanges

kotlin //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,我們分別來簡單的看一下,裡面會執行什麼 ```kotlin //androidx.compose.ui.node.LayoutNode#requestRemeasure

internal fun requestRemeasure() { val owner = owner ?: return if (!ignoreRemeasureRequests && !isVirtual) { //此處的owner => AndroidComposeView owner.onRequestMeasure(this) } } ``` 內部會執行到requestRemeasure

```kotlin //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,內部最終會執行下面程式碼

kotlin //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呼叫

kotlin //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 原始碼
7.Android正確的保活方案,不要掉進保活需求死迴圈陷進