前言
Compose
正式發布1.0已經相當一段時間了,但相信很多同學對Compose
還是有很多迷惑的地方 Compose
跟原生的View
到底是什么關系?是跟Flutter
一樣完全基于Skia
引擎渲染,還是說還是View
的那老一套? 相信很多同學都會有下面的疑問
下面我們就一起來看下下面這個問題
現象分析
我們先看這樣一個簡單布局
class TestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
ComposeBody()
}
}
}
@Composable
fun ComposeBody() {
Column {
Text(text = "這是一行測試數據", color = Color.Black, style = MaterialTheme.typography.h6)
Row() {
Text(text = "測試數據1!", color = Color.Black, style = MaterialTheme.typography.h6)
Text(text = "測試數據2!", color = Color.Black, style = MaterialTheme.typography.h6)
}
}
}
如上所示,就是一個簡單的布局,包含Column
,Row
與Text
然后我們打開開發者選項中的顯示布局邊界
,效果如下圖所示:
我們可以看到Compose
的組件顯示了布局邊界,我們知道,Flutter
與WebView H5
內的組件都是不會顯示布局邊界的,難道Compose
的布局渲染其實還是View
的那一套?
我們下面再在onResume
時嘗試遍歷一下View
的層級,看一下Compose
到底會不會轉化成View
override fun onResume() {
super.onResume()
window.decorView.postDelayed({
(window.decorView as? ViewGroup)?.let { transverse(it, 1) }
}, 2000)
}
private fun transverse(view: View, index: Int) {
Log.e("debug", "第${index}層:" + view)
if (view is ViewGroup) {
view.children.forEach { transverse(it, index + 1) }
}
}
通過以上方式打印頁面的層級,輸出結果如下:
E/debug: 第1層:DecorView@c2f703f[RallyActivity]
E/debug: 第2層:android.widget.LinearLayout{4202d0c V.E...... ........ 0,0-1080,2340}
E/debug: 第3層:android.view.ViewStub{2b50655 G.E...... ......I. 0,0-0,0 #10201b1 android:id/action_mode_bar_stub}
E/debug: 第3層:android.widget.FrameLayout{9bfc86a V.E...... ........ 0,90-1080,2340 #1020002 android:id/content}
E/debug: 第4層:androidx.compose.ui.platform.ComposeView{1b4d15b V.E...... ........ 0,0-1080,2250}
E/debug: 第5層:androidx.compose.ui.platform.AndroidComposeView{a8ec543 VFED..... ........ 0,0-1080,2250}
如上所示,我們寫的Column
,Row
,Text
并沒有出現在布局層級中,跟Compose
相關的只有ComposeView
與AndroidComposeView
兩個View
而ComposeView
與AndroidComposeView
都是在setContent
時添加進去的Compose
的容器,我們后面再分析,這里先給出結論
Compose
在渲染時并不會轉化成
View,而是只有一個入口
View,即
AndroidComposeView我們聲明的
Compose布局在渲染時會轉化成
NodeTree,
AndroidComposeView中會觸發
NodeTree的布局與繪制 總得來說,
Compose會有一個
View的入口,但它的布局與渲染還是在
LayoutNode上完成的,基本脫離了
View
總得來說,純Compose
頁面的頁面層級如下圖所示:
原理分析
前置知識
我們知道,在View
系統中會有一棵ViewTree
,通過一個樹的數據結構來描述整個UI
界面 在Compose
中,我們寫的代碼在渲染時也會構建成一個NodeTree
,每一個組件就是一個ComposeNode
,作為NodeTree
上的一個節點
Compose
對 NodeTree
管理涉及 Applier
、Composition
和 ComposeNode
: Composition
作為起點,發起首次的 composition
,通過 Compose
的執行,填充 Slot Table
,并基于 Table
創建 NodeTree
。渲染引擎基于 Compose Nodes
渲染 UI
, 每當 recomposition
發生時,都會通過 Applier
對 NodeTree
進行更新。 因此
Compose
的執行過程就是創建Node
并構建NodeTree
的過程。
為了了解NodeTree
的構建過程,我們來介紹下面幾個概念
Applier
:增刪 NodeTree
的節點
簡單來說,Applier
的作用就是增刪NodeTree
的節點,每個NodeTree
的運算都需要配套一個Applier
。 同時,Applier
會提供回調,基于回調我們可以對 NodeTree
進行自定義修改:
interface Applier<N> {
val current: N // 當前處理的節點
fun onBeginChanges() {}
fun onEndChanges() {}
fun down(node: N)
fun up()
fun insertTopDown(index: Int, instance: N) // 添加節點(自頂向下)
fun insertBottomUp(index: Int, instance: N)// 添加節點(自底向上)
fun remove(index: Int, count: Int) //刪除節點
fun move(from: Int, to: Int, count: Int) // 移動節點
fun clear()
}
如上所示,節點增刪時會回調到Applier
中,我們可以在回調的方法中自定義節點添加或刪除時的邏輯,后面我們可以一起看下在Android
平臺Compose
是怎樣處理的
Composition
: Compose
執行的起點
Composition`是`Compose`執行的起點,我們來看下如何創建一個`Composition
val composition = Composition(
applier = NodeApplier(node = Node()),
parent = Recomposer(Dispatchers.Main)
)
composition.setContent {
// Composable function calls
}
如上所示
-
Composition
中需要傳入兩個參數,Applier
與Recomposer
-
Applier
上面已經介紹過了,Recomposer
非常重要,他負責Compose
的重組,當重組后,Recomposer
通過調用Applier
完成NodeTree
的變更 -
Composition#setContent
為后續Compose
的調用提供了容器
通過上面的介紹,我們了解了NodeTree
構建的基本流程,下面我們一起來分析下setContent
的源碼
setContent
過程分析
setContent
入口
setContent
的源碼其實比較簡單,我們一起來看下:
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
//判斷ComposeView是否存在,如果存在則不創建
if (existingComposeView != null) with(existingComposeView) {
setContent(content)
} else ComposeView(this).apply {
//將Compose content添加到ComposeView上
setContent(content)
// 將ComposeView添加到DecorView上
setContentView(this, DefaultActivityContentLayoutParams)
}
}
上面就是setContent
的入口,主要作用就是創建了一個ComposeView
并添加到DecorView
上
Composition
的創建
下面我們來看下AndroidComposeView
與Composition
是怎樣創建的 通過ComposeView#setContent
->AbstractComposeView#createComposition
->AbstractComposeView#ensureCompositionCreated
->ViewGroup#setContent
最后會調用到doSetContent
方法,這里就是Compose
的入口:Composition
創建的地方
private fun doSetContent(
owner: AndroidComposeView, //AndroidComposeView是owner
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
//..
//創建Composition,并傳入Applier與Recomposer
val original = Composition(UiApplier(owner.root), parent)
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
//將Compose內容添加到Composition中
wrapped.setContent(content)
return wrapped
}
如上所示,主要就是創建一個Composition
并傳入UIApplier
與Recomposer
,并將Compose content
傳入Composition
中
UiApplier
的實現
上面已經創建了Composition
并傳入了UIApplier
,后續添加了Node
都會回調到UIApplier
中
internal class UiApplier(
root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {
//...
override fun insertBottomUp(index: Int, instance: LayoutNode) {
current.insertAt(index, instance)
}
//...
}
如上所示,在插入節點時,會調用current.insertAt
方法,那么這個current
到底是什么呢?
private fun doSetContent(
owner: AndroidComposeView, //AndroidComposeView是owner
): Composition {
//UiApplier傳入的參數即為AndroidComposeView.root
val original = Composition(UiApplier(owner.root), parent)
}
abstract class AbstractApplier<T>(val root: T) : Applier<T> {
private val stack = mutableListOf<T>()
override var current: T = root
}
}
可以看出,UiApplier
中傳入的參數其實就是AndroidComposeView
的root
,即current
就是AndroidComposeView
的root
# AndroidComposeView
override val root = LayoutNode().also {
it.measurePolicy = RootMeasurePolicy
//...
}
如上所示,root
其實就是一個LayoutNode
,通過上面我們知道,所有的節點都會通過Applier
插入到root
下
布局與繪制入口
上面我們已經在AndroidComposeView
中拿到NodeTree
的根結點了,那Compose
的布局與測量到底是怎么觸發的呢?
# AndroidComposeView
override fun dispatchDraw(canvas: android.graphics.Canvas) {
//Compose測量與布局入口
measureAndLayout()
//Compose繪制入口
canvasHolder.drawInto(canvas) { root.draw(this) }
//...
}
override fun measureAndLayout() {
val rootNodeResized = measureAndLayoutDelegate.measureAndLayout()
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
如上所示,AndroidComposeView
會通過root
,向下遍歷它的子節點進行測量布局與繪制,這里就是LayoutNode
繪制的入口
小結
-
Compose
在構建NodeTree
的過程中主要通過Composition
,Applier
,Recomposer
構建,Applier
會將所有節點添加到AndroidComposeView
中的root
節點下 - 在
setContent
的過程中,會創建ComposeView
與AndroidComposeView
,其中AndroidComposeView
是Compose
的入口 -
AndroidComposeView
在dispatchDraw
中會通過root
向下遍歷子節點進行測量布局與繪制,這里是LayoutNode
繪制的入口 - 在
Android
平臺上,Compose
的布局與繪制已基本脫離View
體系,但仍然依賴于Canvas
Compose
與跨平臺
上面說到,Compose
的繪制仍然依賴于Canvas
,但既然這樣,Compose
是怎么做到跨平臺的呢? 這主要是通過良好的分層設計
Compose
在代碼上自下而上依次分為6層:
其中compose.runtime
和compose.compiler
最為核心,它們是支撐聲明式UI的基礎。
而我們上面分析的AndroidComposeView
這一部分,屬于compose.ui
部分,它主要負責Android
設備相關的基礎UI
能力,例如 layout
、measure
、drawing
、input
等 但這一部分是可以被替換的,compose.runtime
提供了 NodeTree
管理等基礎能力,此部分與平臺無關,在此基礎上各平臺只需實現UI
的渲染就是一套完整的聲明式UI
框架
Button
的特殊情況
上面我們介紹了在純Compose
項目下,AndroidComposeView
不會有子View
,而是遍歷LayoutnNode
來布局測量繪制 但如果我們在代碼中加入一個Button
,結果可能就不太一樣了
@Composable
fun ComposeBody() {
Column {
Text(text = "這是一行測試數據", color = Color.Black, style = MaterialTheme.typography.h6)
Row() {
Text(text = "測試數據1!", color = Color.Black, style = MaterialTheme.typography.h6)
Text(text = "測試數據2!", color = Color.Black, style = MaterialTheme.typography.h6)
}
Button(onClick = {}) {
Text(text = "這是一個Button",color = Color.White)
}
}
}
然后我們再看看頁面的層級結構
E/debug: 第1層:DecorView@182e858[RallyActivity]
E/debug: 第2層:android.widget.LinearLayout{397edb1 V.E...... ........ 0,0-1080,2340}
E/debug: 第3層:android.widget.FrameLayout{e2b0e17 V.E...... ........ 0,90-1080,2340 #1020002 android:id/content}
E/debug: 第4層:androidx.compose.ui.platform.ComposeView{36a3204 V.E...... ........ 0,0-1080,2250}
E/debug: 第5層:androidx.compose.ui.platform.AndroidComposeView{a8ec543 VFED..... ........ 0,0-1080,2250}
E/debug: 第6層:androidx.compose.material.ripple.RippleContainer{28cb3ed V.E...... ......I. 0,0-0,0}
E/debug: 第7層:androidx.compose.material.ripple.RippleHostView{b090222 V.ED..... ......I. 0,0-0,0}
可以看到,很明顯,AndroidComposeView
下多了兩層子View
,這是為什么呢?
我們一起來看下RippleHostView
的注釋
Empty View that hosts a RippleDrawable as its background. This is needed as RippleDrawables cannot currently be drawn directly to a android.graphics.RenderNode (b/184760109), so instead we rely on View's internal implementation to draw to the background android.graphics.RenderNode. A RippleContainer is used to manage and assign RippleHostViews when needed - see RippleContainer.getRippleHostView.
意思也很簡單,Compose
目前還不能直接繪制水波紋效果,因此需要將水波紋效果設置為View
的背景,這里利用View
做了一個中轉 然后RippleHostView
與RippleContainer
自然會添加到AndroidComposeView
中,如果我們在Compose
中使用了AndroidView
,效果也是一樣的 但是這種情況并沒有違背我們上面說的,純Compose
項目下,AndroidComposeView
下沒有子View
,因為Button
并不是純Compose
的
總結
本文主要分析回答了Compose
到底有沒有完全脫離View
系統這個問題,總結如下:
-
Compose
在渲染時并不會轉化成View
,而是只有一個入口View
,即AndroidComposeView
,純Compose
項目下,AndroidComposeView
沒有子View
- 我們聲明的
Compose
布局在渲染時會轉化成NodeTree
,AndroidComposeView
中會觸發NodeTree
的布局與繪制,AndroidComposeView#dispatchDraw
是繪制的入口 - 在
Android
平臺上,Compose
的布局與繪制已基本脫離View
體系,但仍然依賴于Canvas
- 由于良好的分層體系,
Compose
可通過compose.runtime
和compose.compiler
實現跨平臺 - 在使用
Button
時,AndroidComposeView
會有兩層子View
,這是因為Button
中使用了View
來實現水波紋效果
作者:程序員江同學
轉載來源于:https://juejin.cn/post/7017811394036760612
如有侵權,請聯系刪除!