前言
Compose
正式發(fā)布1.0已經(jīng)相當(dāng)一段時(shí)間了鳖枕,但相信很多同學(xué)對(duì)Compose
還是有很多迷惑的地方 Compose
跟原生的View
到底是什么關(guān)系?是跟Flutter
一樣完全基于Skia
引擎渲染呼股,還是說(shuō)還是View
的那老一套? 相信很多同學(xué)都會(huì)有下面的疑問(wèn)
下面我們就一起來(lái)看下下面這個(gè)問(wèn)題
現(xiàn)象分析
我們先看這樣一個(gè)簡(jiǎn)單布局
class TestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
ComposeBody()
}
}
}
@Composable
fun ComposeBody() {
Column {
Text(text = "這是一行測(cè)試數(shù)據(jù)", color = Color.Black, style = MaterialTheme.typography.h6)
Row() {
Text(text = "測(cè)試數(shù)據(jù)1!", color = Color.Black, style = MaterialTheme.typography.h6)
Text(text = "測(cè)試數(shù)據(jù)2!", color = Color.Black, style = MaterialTheme.typography.h6)
}
}
}
如上所示耕魄,就是一個(gè)簡(jiǎn)單的布局,包含Column
,Row
與Text
然后我們打開(kāi)開(kāi)發(fā)者選項(xiàng)中的顯示布局邊界
彭谁,效果如下圖所示:
我們可以看到Compose
的組件顯示了布局邊界,我們知道允扇,Flutter
與WebView H5
內(nèi)的組件都是不會(huì)顯示布局邊界的缠局,難道Compose
的布局渲染其實(shí)還是View
的那一套?
我們下面再在onResume
時(shí)嘗試遍歷一下View
的層級(jí),看一下Compose
到底會(huì)不會(huì)轉(zhuǎn)化成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) }
}
}
通過(guò)以上方式打印頁(yè)面的層級(jí),輸出結(jié)果如下:
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}
如上所示考润,我們寫(xiě)的Column
,Row
,Text
并沒(méi)有出現(xiàn)在布局層級(jí)中狭园,跟Compose
相關(guān)的只有ComposeView
與AndroidComposeView
兩個(gè)View
而ComposeView
與AndroidComposeView
都是在setContent
時(shí)添加進(jìn)去的Compose
的容器,我們后面再分析糊治,這里先給出結(jié)論
Compose
在渲染時(shí)并不會(huì)轉(zhuǎn)化成
View唱矛,而是只有一個(gè)入口
View,即
AndroidComposeView我們聲明的
Compose布局在渲染時(shí)會(huì)轉(zhuǎn)化成
NodeTree,
AndroidComposeView中會(huì)觸發(fā)
NodeTree的布局與繪制 總得來(lái)說(shuō)井辜,
Compose會(huì)有一個(gè)
View的入口绎谦,但它的布局與渲染還是在
LayoutNode上完成的,基本脫離了
View
總得來(lái)說(shuō)粥脚,純Compose
頁(yè)面的頁(yè)面層級(jí)如下圖所示:
原理分析
前置知識(shí)
我們知道窃肠,在View
系統(tǒng)中會(huì)有一棵ViewTree
,通過(guò)一個(gè)樹(shù)的數(shù)據(jù)結(jié)構(gòu)來(lái)描述整個(gè)UI
界面 在Compose
中,我們寫(xiě)的代碼在渲染時(shí)也會(huì)構(gòu)建成一個(gè)NodeTree
,每一個(gè)組件就是一個(gè)ComposeNode
,作為NodeTree
上的一個(gè)節(jié)點(diǎn)
Compose
對(duì) NodeTree
管理涉及 Applier
刷允、Composition
和 ComposeNode
: Composition
作為起點(diǎn)冤留,發(fā)起首次的 composition
,通過(guò) Compose
的執(zhí)行树灶,填充 Slot Table
纤怒,并基于 Table
創(chuàng)建 NodeTree
。渲染引擎基于 Compose Nodes
渲染 UI
天通, 每當(dāng) recomposition
發(fā)生時(shí)泊窘,都會(huì)通過(guò) Applier
對(duì) NodeTree
進(jìn)行更新。 因此
Compose
的執(zhí)行過(guò)程就是創(chuàng)建Node
并構(gòu)建NodeTree
的過(guò)程土砂。
為了了解NodeTree
的構(gòu)建過(guò)程州既,我們來(lái)介紹下面幾個(gè)概念
Applier
:增刪 NodeTree
的節(jié)點(diǎn)
簡(jiǎn)單來(lái)說(shuō),Applier
的作用就是增刪NodeTree
的節(jié)點(diǎn),每個(gè)NodeTree
的運(yùn)算都需要配套一個(gè)Applier
萝映。 同時(shí),Applier
會(huì)提供回調(diào)吴叶,基于回調(diào)我們可以對(duì) NodeTree
進(jìn)行自定義修改:
interface Applier<N> {
val current: N // 當(dāng)前處理的節(jié)點(diǎn)
fun onBeginChanges() {}
fun onEndChanges() {}
fun down(node: N)
fun up()
fun insertTopDown(index: Int, instance: N) // 添加節(jié)點(diǎn)(自頂向下)
fun insertBottomUp(index: Int, instance: N)// 添加節(jié)點(diǎn)(自底向上)
fun remove(index: Int, count: Int) //刪除節(jié)點(diǎn)
fun move(from: Int, to: Int, count: Int) // 移動(dòng)節(jié)點(diǎn)
fun clear()
}
如上所示,節(jié)點(diǎn)增刪時(shí)會(huì)回調(diào)到Applier
中序臂,我們可以在回調(diào)的方法中自定義節(jié)點(diǎn)添加或刪除時(shí)的邏輯蚌卤,后面我們可以一起看下在Android
平臺(tái)Compose
是怎樣處理的
Composition
: Compose
執(zhí)行的起點(diǎn)
Composition`是`Compose`執(zhí)行的起點(diǎn),我們來(lái)看下如何創(chuàng)建一個(gè)`Composition
val composition = Composition(
applier = NodeApplier(node = Node()),
parent = Recomposer(Dispatchers.Main)
)
composition.setContent {
// Composable function calls
}
如上所示
-
Composition
中需要傳入兩個(gè)參數(shù)实束,Applier
與Recomposer
-
Applier
上面已經(jīng)介紹過(guò)了,Recomposer
非常重要逊彭,他負(fù)責(zé)Compose
的重組咸灿,當(dāng)重組后,Recomposer
通過(guò)調(diào)用Applier
完成NodeTree
的變更 -
Composition#setContent
為后續(xù)Compose
的調(diào)用提供了容器
通過(guò)上面的介紹侮叮,我們了解了NodeTree
構(gòu)建的基本流程避矢,下面我們一起來(lái)分析下setContent
的源碼
setContent
過(guò)程分析
setContent
入口
setContent
的源碼其實(shí)比較簡(jiǎn)單,我們一起來(lái)看下:
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
//判斷ComposeView是否存在囊榜,如果存在則不創(chuàng)建
if (existingComposeView != null) with(existingComposeView) {
setContent(content)
} else ComposeView(this).apply {
//將Compose content添加到ComposeView上
setContent(content)
// 將ComposeView添加到DecorView上
setContentView(this, DefaultActivityContentLayoutParams)
}
}
上面就是setContent
的入口审胸,主要作用就是創(chuàng)建了一個(gè)ComposeView
并添加到DecorView
上
Composition
的創(chuàng)建
下面我們來(lái)看下AndroidComposeView
與Composition
是怎樣創(chuàng)建的 通過(guò)ComposeView#setContent
->AbstractComposeView#createComposition
->AbstractComposeView#ensureCompositionCreated
->ViewGroup#setContent
最后會(huì)調(diào)用到doSetContent
方法,這里就是Compose
的入口:Composition
創(chuàng)建的地方
private fun doSetContent(
owner: AndroidComposeView, //AndroidComposeView是owner
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
//..
//創(chuàng)建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內(nèi)容添加到Composition中
wrapped.setContent(content)
return wrapped
}
如上所示卸勺,主要就是創(chuàng)建一個(gè)Composition
并傳入UIApplier
與Recomposer
,并將Compose content
傳入Composition
中
UiApplier
的實(shí)現(xiàn)
上面已經(jīng)創(chuàng)建了Composition
并傳入了UIApplier
砂沛,后續(xù)添加了Node
都會(huì)回調(diào)到UIApplier
中
internal class UiApplier(
root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {
//...
override fun insertBottomUp(index: Int, instance: LayoutNode) {
current.insertAt(index, instance)
}
//...
}
如上所示,在插入節(jié)點(diǎn)時(shí)曙求,會(huì)調(diào)用current.insertAt
方法碍庵,那么這個(gè)current
到底是什么呢?
private fun doSetContent(
owner: AndroidComposeView, //AndroidComposeView是owner
): Composition {
//UiApplier傳入的參數(shù)即為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
中傳入的參數(shù)其實(shí)就是AndroidComposeView
的root
静浴,即current
就是AndroidComposeView
的root
# AndroidComposeView
override val root = LayoutNode().also {
it.measurePolicy = RootMeasurePolicy
//...
}
如上所示,root
其實(shí)就是一個(gè)LayoutNode
,通過(guò)上面我們知道芽淡,所有的節(jié)點(diǎn)都會(huì)通過(guò)Applier
插入到root
下
布局與繪制入口
上面我們已經(jīng)在AndroidComposeView
中拿到NodeTree
的根結(jié)點(diǎn)了马绝,那Compose
的布局與測(cè)量到底是怎么觸發(fā)的呢?
# AndroidComposeView
override fun dispatchDraw(canvas: android.graphics.Canvas) {
//Compose測(cè)量與布局入口
measureAndLayout()
//Compose繪制入口
canvasHolder.drawInto(canvas) { root.draw(this) }
//...
}
override fun measureAndLayout() {
val rootNodeResized = measureAndLayoutDelegate.measureAndLayout()
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
如上所示,AndroidComposeView
會(huì)通過(guò)root
,向下遍歷它的子節(jié)點(diǎn)進(jìn)行測(cè)量布局與繪制挣菲,這里就是LayoutNode
繪制的入口
小結(jié)
-
Compose
在構(gòu)建NodeTree
的過(guò)程中主要通過(guò)Composition
,Applier
,Recomposer
構(gòu)建,Applier
會(huì)將所有節(jié)點(diǎn)添加到AndroidComposeView
中的root
節(jié)點(diǎn)下 - 在
setContent
的過(guò)程中富稻,會(huì)創(chuàng)建ComposeView
與AndroidComposeView
,其中AndroidComposeView
是Compose
的入口 -
AndroidComposeView
在dispatchDraw
中會(huì)通過(guò)root
向下遍歷子節(jié)點(diǎn)進(jìn)行測(cè)量布局與繪制,這里是LayoutNode
繪制的入口 - 在
Android
平臺(tái)上白胀,Compose
的布局與繪制已基本脫離View
體系椭赋,但仍然依賴于Canvas
Compose
與跨平臺(tái)
上面說(shuō)到,Compose
的繪制仍然依賴于Canvas
或杠,但既然這樣哪怔,Compose
是怎么做到跨平臺(tái)的呢? 這主要是通過(guò)良好的分層設(shè)計(jì)
Compose
在代碼上自下而上依次分為6層:
其中compose.runtime
和compose.compiler
最為核心,它們是支撐聲明式UI的基礎(chǔ)向抢。
而我們上面分析的AndroidComposeView
這一部分认境,屬于compose.ui
部分,它主要負(fù)責(zé)Android
設(shè)備相關(guān)的基礎(chǔ)UI
能力挟鸠,例如 layout
叉信、measure
、drawing
艘希、input
等 但這一部分是可以被替換的硼身,compose.runtime
提供了 NodeTree
管理等基礎(chǔ)能力硅急,此部分與平臺(tái)無(wú)關(guān),在此基礎(chǔ)上各平臺(tái)只需實(shí)現(xiàn)UI
的渲染就是一套完整的聲明式UI
框架
Button
的特殊情況
上面我們介紹了在純Compose
項(xiàng)目下佳遂,AndroidComposeView
不會(huì)有子View
,而是遍歷LayoutnNode
來(lái)布局測(cè)量繪制 但如果我們?cè)诖a中加入一個(gè)Button
营袜,結(jié)果可能就不太一樣了
@Composable
fun ComposeBody() {
Column {
Text(text = "這是一行測(cè)試數(shù)據(jù)", color = Color.Black, style = MaterialTheme.typography.h6)
Row() {
Text(text = "測(cè)試數(shù)據(jù)1!", color = Color.Black, style = MaterialTheme.typography.h6)
Text(text = "測(cè)試數(shù)據(jù)2!", color = Color.Black, style = MaterialTheme.typography.h6)
}
Button(onClick = {}) {
Text(text = "這是一個(gè)Button",color = Color.White)
}
}
}
然后我們?cè)倏纯错?yè)面的層級(jí)結(jié)構(gòu)
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
荚板,這是為什么呢?
我們一起來(lái)看下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.
意思也很簡(jiǎn)單,Compose
目前還不能直接繪制水波紋效果吩屹,因此需要將水波紋效果設(shè)置為View
的背景啸驯,這里利用View
做了一個(gè)中轉(zhuǎn) 然后RippleHostView
與RippleContainer
自然會(huì)添加到AndroidComposeView
中,如果我們?cè)?code>Compose中使用了AndroidView
祟峦,效果也是一樣的 但是這種情況并沒(méi)有違背我們上面說(shuō)的,純Compose
項(xiàng)目下徙鱼,AndroidComposeView
下沒(méi)有子View
,因?yàn)?code>Button并不是純Compose
的
總結(jié)
本文主要分析回答了Compose
到底有沒(méi)有完全脫離View
系統(tǒng)這個(gè)問(wèn)題,總結(jié)如下:
-
Compose
在渲染時(shí)并不會(huì)轉(zhuǎn)化成View
宅楞,而是只有一個(gè)入口View
,即AndroidComposeView
,純Compose
項(xiàng)目下袱吆,AndroidComposeView
沒(méi)有子View
- 我們聲明的
Compose
布局在渲染時(shí)會(huì)轉(zhuǎn)化成NodeTree
,AndroidComposeView
中會(huì)觸發(fā)NodeTree
的布局與繪制,AndroidComposeView#dispatchDraw
是繪制的入口 - 在
Android
平臺(tái)上厌衙,Compose
的布局與繪制已基本脫離View
體系,但仍然依賴于Canvas
- 由于良好的分層體系绞绒,
Compose
可通過(guò)compose.runtime
和compose.compiler
實(shí)現(xiàn)跨平臺(tái) - 在使用
Button
時(shí)婶希,AndroidComposeView
會(huì)有兩層子View
,這是因?yàn)?code>Button中使用了View
來(lái)實(shí)現(xiàn)水波紋效果
作者:程序員江同學(xué)
轉(zhuǎn)載來(lái)源于:https://juejin.cn/post/7017811394036760612
如有侵權(quán)蓬衡,請(qǐng)聯(lián)系刪除喻杈!