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)
}
})
}
}
}
Preview 效果
Compose 完美展现原生 View 效果,接下来,我们需要对 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
},
...
)
大家可能对 ComposeNode 有点陌生,如果你阅读过 Compose 中组件源码的话,例如 Text,在你一直跟踪下去的时候会发现,他们都有一个共同点,那就是都会走到 ComposeNode,并且,ComposeNode 函数中会拿到 factory 的返回值 LayoutNode 来创建一个 Node 节点来参与 Compose 的绘制。也即Compose 在排版和布局的时候,操控的就是 LayoutNode,并且这个 LayoutNode 能拿到 Compose 执行中的一些回调,例如 measure 和 layout 来改变自身的位置和状态。
小结:在 AndroidView 这个函数中我们发现,原生 View 是通过外部包裹一层 Compose 组件参与到 Compose 布局中的
我们来看下,原生 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 函数
ViewFactoryHolder.factory = factory
我们需要找到原生 ImageView 被谁持有,目前来看的话,typedView 被复制到了全局,没有被其他变量持有,被复赋值的 view 并不在 ViewFactoryHolder 中,那么,我们需要去 ViewFactoryHolder 的父类 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()
}
}
}
...
}
小结:我们提供的原生 View,最终会被 addView 到 ViewFactoryHolder 中,只是 addView 这个操作是发生在他的父类 AndroidViewHolder 中的,然后将原生 ImageView 赋值到全局变量 view 中
现在,我们还有一些疑问,原生 view 虽然被 addView 到 ViewFactoryHolder 中了,那 ViewFactoryHolder 这个 ViewGroup 是如何被添加到界面上的呢?ViewFactoryHolder 是如何测量和布局的呢?我们需要回到 AndroidView 的函数中,找到 AndroidView 中的 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
}
这段代码有点多,但却是最精华的核心部分:
在 2.1 的 attach 步骤中发现,我们的 ImageView 经过 AndroidViewHolder 的包裹,被 addAndroidView 到了 AndroidComposeView 中,这里我们又有个疑问,owner 转换成的 AndroidComposeView 是从哪来的?addAndroidView 做了哪些事情?
这里先小结下:AndroidViewHolder 中的 layoutNode 是一个不可见的 Compose 代理节点,他将 Compose 中触发的回调结果应用到 ViewGroup 中,以此来控制 ViewGroup 的绘制与布局
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
...
}
}
现在 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 方法:
-> 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
}
至此,我们分析完了原生 View 是如何添加进 Compose 中的,我们可以画个图来简单总结下:
然后我们遍历打印一下 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}
现在,我们可以来回答开头说的问题了: