Jetpack Compose實現bringToFront功能——附上原理分析
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正確的保活方案,不要掉進保活需求死迴圈陷進
- 鴻蒙ArkUI如何開發跨平臺應用?
- HarmonyOS玩轉ArkUI動效 - 水母動畫
- Compose挑燈夜看 - 照亮手機螢幕裡面的書本內容
- 順手修復了Jetpack Compose官方文件中的一個多點觸控示例的Bug
- 正確實踐Jetpack SplashScreen API —— 在所有Android系統上使用總結,內含原理分析
- Jetpack Compose處理“導航欄、狀態列、鍵盤” 影響內容顯示的問題集錦
- 閒聊Android懸浮的“系統文字選擇選單”和“ActionMode解析”——附上原理分析
- Jetpack Compose實現bringToFront功能——附上原理分析
- Android跨程序傳大圖思考及實現——附上原理分析