前言
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
如有侵权,请联系删除!