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 {
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(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(val root: T) : Applier {
private val stack = mutableListOf()
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.runtime
和compose.compiler
最为核心,它们是支撑声明式UI的基础。
而我们上面分析的AndroidComposeView
这一部分,属于compose.ui
部分,它主要负责Android
设备相关的基础UI
能力,例如 layout
、measure
、drawing
、input
等
但这一部分是可以被替换的,compose.runtime
提供了 NodeTree
管理等基础能力,此部分与平台无关,在此基础上各平台只需实现UI
的渲染就是一套完整的声明式UI
框架
基于compose.runtime
可以实现任意一套声明式UI
框架,关于compose.runtime
的详细介绍可参考fundroid
大佬写的:Jetpack Compose Runtime : 声明式 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
来实现水波纹效果