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 原理

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
        },
       ...
    )
  1. 创立了个 ViewFactoryHolder
  2. 将包裹原生 View 的 factory 函数赋值给 ViewFactoryHolder
  3. 从 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 函数

  1. 将 factory 函数赋值给暗地字段,也即 ViewFactoryHolder.factory = factory
  2. 判断 factory 是否为空,咱们供给了原生 ImageView 组件,这儿为 true
  3. 履行 factory 函数,也即拿到咱们的 ImageView 组件,赋值给全局变量的 typedView
  4. 而且也赋值给了 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()
                    }
                }
        }
        ...
}
  1. 需求留意的是,AndroidViewHolder 是一个承继自 ViewGroup 的原生组件
  2. 将原生 view 赋值给暗地字段,也即 view 的实体是 ImageView
  3. 移除一切的子 View,看来,AndroidViewHolder 只支撑增加一个原生 View
  4. 判断原生 view 是否为空,咱们供给了 ImageView ,所以该判断为 true
  5. 将原生 view 增加到当时的 ViewGroup,也即咱们的 ImageView 被增加到了 AndroidViewHolder 中
  6. 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
    }

这段代码有点多,但却是最精华的中心部分:

  1. 注释直接道破,这个 LayoutNode 会署理原生 View 的 measure、layout,将丈量和布局结果反应到 AndroidViewHolder 这个 ViewGroup 中
  2. 注册 LayoutNode 的 attach 回调,这个 attach 能够了解成 LayoutNode 被贴到了 Compose 布局中触发的回调,和原生 View 被增加到布局中,触发 onViewAttachedToWindow 类似
    1. 将当时 AndroidViewHolder 增加到 AndroidComposeView 中
  3. 注册 LayoutNode 的 detach 回调,这个 detach 能够了解成 LayoutNode 从 Compose 布局中被移除触发的回调,和原生 View 从布局中移除,触发 onViewDetachedFromWindow 类似
    1. 将当时 ViewGroup 从 AndroidComposeView 中移除
  4. 注册 LayoutNode 的制作战略回调,在 LayoutNode 被贴到 Compose 中,Compose 在重组控件的时候,会触发 LayoutNode 的制作战略
    1. 触发 ViewGroup 的 measure 丈量
    2. 触发 ViewGroup 的 layout 布局
  5. 回来 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
            ...
    }
}
  1. 将 AndroidViewsHandler 增加到 AndroidComposeView 中
  2. 将 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)
    }
}
  1. 调用 ComposeView 内部的 setContent 办法,将 compose 布局设置进去
  2. 调用 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
}
  1. 获取 ComposeView 的子 View 是否为 AndroidComposeView
  2. 假如获取为空,则创立个 AndroidComposeView,并调用 addView 将 AndroidComposeView 增加进 ComposeView
  3. 将 AndroidComposeView 设置到 WrappedComposition 中,并回来 Composition,这也便是为什么在 LayoutNode 中,能拿到 owner ,而且为 AndroidComposeView 的原因

三、总结

至此,咱们剖析完了原生 View 是怎么增加进 Compose 中的,咱们能够画个图来简单总结下:

Compose 中嵌套原生 View 原理

  • 橙色:在 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 的方位与巨细