Compose 中嵌套原生 View 原理
Compose 是用于构建原生 Android UI 的现代工具包,他只需要在 xml 布局中添加 ComposeView,或是通过 setContent 扩展函数,即可将 Compose 组件绘制界面中。
Compose 天然就支持被原生 View 嵌套,但也支持嵌套原生 View,Compose 是通过自己的一套重组算法来构建界面,测量和布局已经脱离了原生 View 体系。既然脱离了这套体系,那 Compose 是如何完美支持嵌套原生 View 的呢?脱离了原生 View 布局体系的 Compose,是如何对原生 View 进行测量和布局的呢?
带着疑问我们从示例 demo 开始,然后再翻阅源码.
一、示例
Compose 通过 AndroidView 组件来嵌套原生 View,示例如下:
TimeAssistantTheme {
Surface {
Column {
// Text 为 Compose 组件
Text(text = "hello world")
// AndroidView 为 Compose 组件
AndroidView(factory = {context->
// 原生 ImageView
ImageView(context).apply {
setImageResource(R.mipmap.ic_launcher)
}
})
}
}
}
Compose 完美展现原生 View 效果,接下来,我们需要对 AndroidView 一探究竟。
二、源码分析
1、分析 AndroidView
AndroidView 通过 factory 闭包来拿到我们的 ImageView,我们在探索 AndroidView 源码的时候,只需要观察这个 factory 究竟被谁使用了:
@Composable
fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
) {
...
ComposeNode<LayoutNode, UiApplier>(
factory = {
//1、创建 ViewFactoryHolder
...
val viewFactoryHolder = ViewFactoryHolder<T>(context, parentReference)
// 2、factory 被赋值给了 ViewFactoryHolder
viewFactoryHolder.factory = factory
...
// 3、从 ViewFactoryHolder 拿到 LayoutNode
viewFactoryHolder.layoutNode
},
...
)
- 创建了个 ViewFactoryHolder
- 将包裹原生 View 的 factory 函数赋值给 ViewFactoryHolder
- 从 ViewFactoryHolder 中拿到 LayoutNode 给 ComposeNode,后面会讲解该操作
大家可能对 ComposeNode 有点陌生,如果你阅读过 Compose 中组件源码的话,例如 Text,在你一直跟踪下去的时候会发现,他们都有一个共同点,那就是都会走到 ComposeNode,并且,ComposeNode 函数中会拿到 factory 的返回值 LayoutNode 来创建一个 Node 节点来参与 Compose 的绘制。也即Compose 在排版和布局的时候,操控的就是 LayoutNode,并且这个 LayoutNode 能拿到 Compose 执行中的一些回调,例如 measure 和 layout 来改变自身的位置和状态。
小结:在 AndroidView 这个函数中我们发现,原生 View 是通过外部包裹一层 Compose 组件参与到 Compose 布局中的
2、分析 ViewFactoryHolder
我们来看下,原生 View 的 factory 函数,在赋值给 ViewFactoryHolder 做了些什么:
@OptIn(ExperimentalComposeUiApi::class)
internal class ViewFactoryHolder<T : View>(
context: Context,
parentContext: CompositionContext? = null
) : AndroidViewHolder(context, parentContext), ViewRootForInspector {
internal var typedView: T? = null
override val viewRoot: View? get() = parent as? View
var factory: ((Context) -> T)? = null
...
set(value) {
// 1、将 factory 复制给幕后字段
field = value
// 2、factory 不为空
->if (value != null) {
// 3、invoke factory 函数,拿到原生 View 本身
typedView = value(context)
// 4、将原生 View 复制给 view
view = typedView
}
}
...
}
在赋值发生时,会触发 ViewFactoryHolder 中 factory 的 set(value),value 就是嵌套原生 view 的 factory 函数
- 将 factory 函数赋值给幕后字段,也即
ViewFactoryHolder.factory = factory
- 判断 factory 是否为空,我们提供了原生 ImageView 组件,这里为 true
- 执行 factory 函数,也即拿到我们的 ImageView 组件,赋值给全局变量的 typedView
- 并且也赋值给了 view
我们需要找到原生 ImageView 被谁持有,目前来看的话,typedView 被复制到了全局,没有被其他变量持有,被复赋值的 view 并不在 ViewFactoryHolder 中,那么,我们需要去 ViewFactoryHolder 的父类 AndroidViewHolder 看看了
3、分析 AndroidViewHolder
跟进 view 字段:
@OptIn(ExperimentalComposeUiApi::class)
\internal abstract class AndroidViewHolder(
context: Context,
parentContext: CompositionContext?
// 1、AndroidViewHolder 是一个继承自 ViewGroup 的原生组件
) : ViewGroup(context) {
...
/**
* The view hosted by this holder.
*/
-> var view: View? = null
internal set(value) {
if (value !== field) {
// 2、将 view 赋值给幕后字段
field = value
// 3、移除所有子 View
removeAllViews()
// 4、原生 view 不为空
-> if (value != null) {
// 5、将原生 view 添加到当前的 ViewGroup
addView(value)
// 6、触发更新
runUpdate()
}
}
}
...
}
- 需要注意的是,AndroidViewHolder 是一个继承自 ViewGroup 的原生组件
- 将原生 view 赋值给幕后字段,也即 view 的实体是 ImageView
- 移除所有的子 View,看来,AndroidViewHolder 只支持添加一个原生 View
- 判断原生 view 是否为空,我们提供了 ImageView ,所以该判断为 true
- 将原生 view 添加到当前的 ViewGroup,也即我们的 ImageView 被添加到了 AndroidViewHolder 中
- runUpdate 会触发 Compose 的一系列更新,我们先暂时不管他
小结:我们提供的原生 View,最终会被 addView 到 ViewFactoryHolder 中,只是 addView 这个操作是发生在他的父类 AndroidViewHolder 中的,然后将原生 ImageView 赋值到全局变量 view 中
现在,我们还有一些疑问,原生 view 虽然被 addView 到 ViewFactoryHolder 中了,那 ViewFactoryHolder 这个 ViewGroup 是如何被添加到界面上的呢?ViewFactoryHolder 是如何测量和布局的呢?我们需要回到 AndroidView 的函数中,找到 AndroidView 中的 viewFactoryHolder.layoutNode 进行源码跟进
4、分析 ViewFactoryHolder.layoutNode
layoutNode 字段也在 ViewFactoryHolder 的父类 AndroidViewHolder 中:
val layoutNode: LayoutNode = run {
// 1、一句注释直接讲透
// Prepare layout node that proxies measure and layout passes to the View.
-> val layoutNode = LayoutNode()
...
// 2、注册 attach 回调
layoutNode.onAttach = { owner ->
// 2.1 重点: 将当前 ViewGroup 添加到 AndroidComposeView 中
(owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)
if (viewRemovedOnDetach != null) view = viewRemovedOnDetach
}
// 3、注册 detach 回调
layoutNode.onDetach = { owner ->
// 3.1 重点: 将当前 ViewGroup 从 AndroidComposeView 中移除
(owner as? AndroidComposeView)?.removeAndroidView(this)
viewRemovedOnDetach = view
view = null
}
// 4、注册 measurePolicy 绘制策略回调
layoutNode.measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
...
// 4.1、layoutNode 的测量,触发 AndroidViewHolder 的测量
measure(
obtainMeasureSpec(constraints.minWidth, constraints.maxWidth,layoutParams!!.width),
obtainMeasureSpec(constraints.minHeight,constraints.maxHeight,layoutParams!!.height)
)
// 4.1、layoutNode 的布局,触发 AndroidViewHolder 的布局
-> return layout(measuredWidth, measuredHeight) {
layoutAccordingTo(layoutNode)
}
}
...
}
// 5、返回 layoutNode
layoutNode
}
这段代码有点多,但却是最精华的核心部分:
- 注释直接道破,这个 LayoutNode 会代理原生 View 的 measure、layout,将测量和布局结果反应到 AndroidViewHolder 这个 ViewGroup 中
- 注册 LayoutNode 的 attach 回调,这个 attach 可以理解成 LayoutNode 被贴到了 Compose 布局中触发的回调,和原生 View 被添加到布局中,触发 onViewAttachedToWindow 类似
- 将当前 AndroidViewHolder 添加到 AndroidComposeView 中
- 注册 LayoutNode 的 detach 回调,这个 detach 可以理解成 LayoutNode 从 Compose 布局中被移除触发的回调,和原生 View 从布局中移除,触发 onViewDetachedFromWindow 类似
- 将当前 ViewGroup 从 AndroidComposeView 中移除
- 注册 LayoutNode 的绘制策略回调,在 LayoutNode 被贴到 Compose 中,Compose 在重组控件的时候,会触发 LayoutNode 的绘制策略
- 触发 ViewGroup 的 measure 测量
- 触发 ViewGroup 的 layout 布局
- 返回 LayoutNode
在 2.1 的 attach 步骤中发现,我们的 ImageView 经过 AndroidViewHolder 的包裹,被 addAndroidView 到了 AndroidComposeView 中,这里我们又有个疑问,owner 转换成的 AndroidComposeView 是从哪来的?addAndroidView 做了哪些事情?
这里先小结下:
AndroidViewHolder 中的 layoutNode 是一个不可见的 Compose 代理节点,他将 Compose 中触发的回调结果应用到 ViewGroup 中,以此来控制 ViewGroup 的绘制与布局
5、分析 AndroidComposeView.addAndroidView
internal class AndroidComposeView(context: Context) :
ViewGroup(context), Owner, ViewRootForTest, PositionCalculator, DefaultLifecycleObserver {
...
internal .val androidViewsHandler: AndroidViewsHandler
get() {
if (_androidViewsHandler == null) {
_androidViewsHandler = AndroidViewsHandler(context)
// 1、将 AndroidViewsHandler addView 到 AndroidComposeView 中
addView(_androidViewsHandler)
}
return _androidViewsHandler!!
}
// Called to inform the owner that a new Android View was attached to the hierarchy.
-> fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
androidViewsHandler.holderToLayoutNode[view] = layoutNode
// 2、AndroidViewHolder 被添加到 AndroidViewsHandler 中
androidViewsHandler.addView(view)
androidViewsHandler.layoutNodeToHolder[layoutNode] = view
...
}
}
- 将 AndroidViewsHandler 添加到 AndroidComposeView 中
- 将 AndroidViewHolder 添加到 AndroidViewsHandler 中
现在 addView 的逻辑已经走到了 AndroidComposeView,我们现在还需要知晓 AndroidComposeView 从何而来
这次,我们需要先从 ComposeView 开始分析:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AndroidWidget()
}
}
在 Activity 的 onCreate 方法中,我们通过 setContent 将 ComposeView 应用到界面上,我们需要跟踪这个 setContent 拓展函数一探究竟:
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
...
if (existingComposeView != null) with(existingComposeView) {
...
// 1、设置 compose 布局
setContent(content)
} else ComposeView(this).apply {
...
// 1、设置 compose 布局
setContent(content)
...
// 2、调用 Activity 的 setContentView 方法,布局为 ComposeView
setContentView(this, DefaultActivityContentLayoutParams)
}
}
- 调用 ComposeView 内部的 setContent 方法,将 compose 布局设置进去
- 调用 Activity 的 setContentView 方法,布局为 ComposeView,这也是 Activity 中没有找到设置 setContentView 的原因,因为拓展函数已经做了这个操作
我们需要跟踪下 ComposeView 的 setContent 方法:
-> fun setContent(content: @Composable () -> Unit)
-> fun createComposition()
-> fun ensureCompositionCreated()
-> internal fun ViewGroup.setContent(
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
val composeView =
// 1、获取 ComposeView 的子 View 是否为 AndroidComposeView
-> if (childCount > 0) {
getChildAt(0) as? AndroidComposeView
} else {
removeAllViews(); null
// 2、如果为空,则创建个 AndroidComposeView,并调用 addView 将 AndroidComposeView 添加进 ComposeView
} ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
return doSetContent(composeView, parent, content)
}
-> private fun doSetContent(
owner: AndroidComposeView,
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
...
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
// 3、将 AndroidComposeView 设置到 WrappedComposition 中,并返回 Composition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
wrapped.setContent(content)
return wrapped
}
- 获取 ComposeView 的子 View 是否为 AndroidComposeView
- 如果获取为空,则创建个 AndroidComposeView,并调用 addView 将 AndroidComposeView 添加进 ComposeView
- 将 AndroidComposeView 设置到 WrappedComposition 中,并返回 Composition,这也就是为什么在 LayoutNode 中,能拿到 owner ,并且为 AndroidComposeView 的原因
三、总结
至此,我们分析完了原生 View 是如何添加进 Compose 中的,我们可以画个图来简单总结下:
- 橙色:在 Compose 中嵌套 AndroidView 才会有,如果没有使用,则没有橙色层级
- 黄色: 嵌套的原生 View,此处演示的为示例的 ImageView
- 绿色:Compose 的控件,也即 LayoutNode
然后我们遍历打印一下 view 树,以此来确认我们的跟踪的是否正确
System.out: viewGroup --> android.widget.FrameLayout{47cc49 V.E...... ........ 0,95-1080,2400 #1020002 android:id/content}
System.out: viewGroup --> androidx.compose.ui.platform.ComposeView{134250 V.E...... ........ 0,0-232,257}
System.out: viewGroup --> androidx.compose.ui.platform.AndroidComposeView{8e162e1 VFED..... ........ 0,0-232,257}
System.out: viewGroup --> androidx.compose.ui.platform.AndroidViewsHandler{fbb7614 V.E...... ......ID 0,0-232,257}
System.out: viewGroup --> androidx.compose.ui.viewinterop.ViewFactoryHolder{4b0e4aa V.E...... ......I. 0,59-198,257}
System.out: view --> android.widget.ImageView{8438ebd V.ED..... ........ 0,0-198,198}
现在,我们可以来回答开头说的问题了:
- Compose 是通过 addView 的方式,将原生 View 添加到 AndroidComposeView 中的,他依然使用的是原生布局体系
- 嵌套原生 View 的测量与布局,是通过创建个代理 LayoutNode ,然后添加到 Compose 中参与组合,并将每次重组返回的测量信息设置到原生 View 上,以此来改变原生 View 的位置与大小
链接:https://juejin.cn/post/7100162348069879839
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。