Compose 中嵌套原生 View 原理

简介: 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)
                }
            })
        }
    }
}
复制代码

image.png

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 中
  1. 注册 LayoutNode 的 detach 回调,这个 detach 可以理解成 LayoutNode 从 Compose 布局中被移除触发的回调,和原生 View 从布局中移除,触发 onViewDetachedFromWindow 类似
  1. 将当前 ViewGroup 从 AndroidComposeView 中移除
  1. 注册 LayoutNode 的绘制策略回调,在 LayoutNode 被贴到 Compose 中,Compose 在重组控件的时候,会触发 LayoutNode 的绘制策略
  1. 触发 ViewGroup 的 measure  测量
  2. 触发 ViewGroup 的 layout 布局
  1. 返回 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 中
  3. 没有找到设置 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 中的,我们可以画个图来简单总结下:


image.png

  • 橙色:在 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 的位置与大小
目录
相关文章
|
2月前
|
前端开发 PHP
ThinkPHP6,视图的安装及模板渲染和变量赋值 view::fetch() ,view::assgin() ,助手函数
本文介绍了ThinkPHP6中视图的安装和使用,包括通过`composer`安装`topthink/think-view`,使用`view::fetch()`进行模板渲染和变量赋值,以及使用`view::assign()`进行全局模板变量赋值。还提到了助手函数作为`view::fetch()`和`view::assign()`的封装组合,但效率较低。
ThinkPHP6,视图的安装及模板渲染和变量赋值 view::fetch() ,view::assgin() ,助手函数
|
6月前
|
存储 小程序 程序员
嵌套的方式构建
嵌套的方式构建
27 0
|
6月前
|
存储 Dart 前端开发
为什么说 Compose 的声明式代码最简洁 ?Compose/React/Flutter/SwiftUI 语法对比
为什么说 Compose 的声明式代码最简洁 ?Compose/React/Flutter/SwiftUI 语法对比
213 1
|
前端开发 Go 图形学
Unity中查找子组件GameObject或Component的操作汇总
Unity中查找子组件GameObject或Component的操作汇总
165 0
|
小程序 前端开发 开发者
微信小程序web-view上覆盖原生组件,解决cover-view点击事件无法触发问题
微信小程序web-view上覆盖原生组件,解决cover-view点击事件无法触发问题
257 0
为什么在YII2.0的小部件里面一定要实现run()方法?底层原理是什么?
为什么在YII2.0的小部件里面一定要实现run()方法?底层原理是什么?
|
小程序 开发者 容器
讲述小程序之组件视图容器之view
讲述小程序之组件视图容器之view
131 0
讲述小程序之组件视图容器之view
|
小程序 容器
讲述小程序之组件视图容器之scroll-view(可滚动视图)
讲述小程序之组件视图容器之scroll-view(可滚动视图)
253 0
讲述小程序之组件视图容器之scroll-view(可滚动视图)
|
前端开发
react使用Context方法实现组件通信(案例说明嵌套传值+详细用法总结)
之前的文章我们介绍了在react中使用props实现组件通信,比如父子,祖孙通信,但是使用props一层层传递的时候还是很麻烦的,那么今天这篇文章就来介绍新的用法——使用Context实现组建通信
375 0
【Flutter】自定义 Flutter 组件 ( 创建自定义 StatelessWidget、StatefulWidget 组件 | 调用自定义组件 )(二)
【Flutter】自定义 Flutter 组件 ( 创建自定义 StatelessWidget、StatefulWidget 组件 | 调用自定义组件 )(二)
629 0
【Flutter】自定义 Flutter 组件 ( 创建自定义 StatelessWidget、StatefulWidget 组件 | 调用自定义组件 )(二)