/ 今日科技快讯 /
最近,小米相关负责人表示,商业用地早在2018年上海总部规划,上海总部职能包括手机研发、金融、互联网等业务,是租赁物业分散办公室,土地未来建设规划是这些团队,而不是外部猜测。
/ 作者简介 /
本文来自RicardoMJiang文章主要分析比较了投稿Compose和View系统的相互关系,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
RicardoMJiang博客地址:
https://juejin.cn/user/668101431009496
/ 前言 /
Compose正式发布1.0已经很久了,但我相信很多学生都是对的Compose还有很多困惑。
Compose跟原生的View到底是什么关系?是和吗?Flutter同样完全基于Skia引擎渲染,或者View老一套?
相信很多同学都会有以下问题。
让我们来看看下面的问题。
/ 现象分析 /
让我们先看看这样一个简单的布局:
classTestActivity:ComponentActivity(){ overridefunonCreate(savedInstanceState:Bundle?){ setContent{ ComposeBody() } } } @Composable funComposeBody(){ 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。
overridefunonResume(){ super.onResume() window.decorView.postDelayed({ (window.decorViewas?ViewGroup)?.let{transverse(it,1)} },2000) } privatefuntransverse(view:View,index:Int){ Log.e("debug","第${index}层:" view) if(viewisViewGroup){ 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的节点,每个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。
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的源码其实比较简单,我们一起来看下:
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上。
下面我们来看下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中。
上面已经创建了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框架。
基于compose.runtime可以实现任意一套声明式UI框架,关于compose.runtime的详细介绍可参考fundroid大佬写的:Jetpack Compose Runtime : 声明式 UI 的基础。
Jetpack Compose Runtime : 声明式 UI 的基础:
https://juejin.cn/post/6976435919666544653
/ 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的注释。
意思也很简单,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来实现水波纹效果
推荐阅读:
我的新书,《第一行代码 第3版》已出版!
带倒计时RecyclerView的设计心路历程
Android 12上焕然一新的小组件
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注