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 的位置与大小
目录
相关文章
|
Ubuntu Linux API
libusb简介及在Linux中安装libusb
最近做了关于在X86环境下通过FT232H芯片实现的USB转SPI的相关驱动,接触到了libusb。libusb是一个开源的用C实现的,应用程序与用户的USB设备进行通信的库。它是可移植的,对外使用统一的API,支持Windows、macOS、Linux、Android。它是用户模式(user-mode),应用程序与USB设备通信不需要高权限,但是在Android下好像有些接口需要root权限才能调用成功。它支持所有版本的USB协议。它的License是LGPL,源码地址在https://github.com/libusb/libusb,最新发布版本为1.0.23。
libusb简介及在Linux中安装libusb
|
存储 JSON 监控
APM监控 · 入门篇 · Android端测监控平台建设(1)
APM 全称 Application Performance Management & Monitoring (应用性能管理/监控) 性能问题是导致 App 用户流失的罪魁祸首之一,如果用户在使用我们 App 的时候遇到诸如页面卡顿、响应速度慢、发热严重、流量电量消耗大等问题的时候,很可能就会卸载掉我们的 App。这也是我们在目前工作中面临的巨大挑战之一,尤其是低端机型。
4708 0
APM监控 · 入门篇 · Android端测监控平台建设(1)
|
监控 数据可视化 安全
如何使用webgl(three.js)实现煤矿隧道、井下人员定位、掘进面、纵采面可视化解决方案——第十九课(一)
three.js、webgl、3D煤矿隧道、三维井下人员定位、掘进面三维可视化、纵采面可视化、采集面可视化展示、设备检测、数字孪生、物联网3D、3d建筑、3d库房,bim管理系统
1044 1
|
算法 数据安全/隐私保护 C语言
XXTEA加密算法
XXTEA加密算法
1387 0
|
机器学习/深度学习 算法 搜索推荐
基于FPGA的中值滤波算法的实现
中值滤波法是一种非线性平滑技术,它将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值.
|
存储 小程序 算法
【微信小程序】粤语教学平台-粤言粤语(上)
【微信小程序】粤语教学平台-粤言粤语
1058 0
|
7月前
|
人工智能 监控 网络协议
从Clawdbot、Moltbot到OpenClaw,快速部署Clawdbot傻瓜式教程,跟着教程一步步走~
OpenClaw(原Clawdbot/Moltbot)是开源AI智能体,支持阿里云一键部署。无需代码基础,5分钟即可拥有专属AI员工,调用大模型自动处理邮件、监控文件、生成报告等任务,新手首月仅9.9元。(239字)
731 0
|
2月前
|
人工智能 IDE API
TokenPlan 套餐焕新上线!四大 AI 场景组合购,算力 + 云产品成套采购,最低 43.45 元起
阿里云TokenPlan焕新上线,推出四大AI场景组合购(Coding/Agent托管/轻量部署等),43.45元起,一站式集成Qwen3.8-Max旗舰模型(2.4T参数)、算力Credits与云资源,支持夜间错峰折扣,开箱即用,大幅降低AI开发配置与成本。
|
6月前
|
弹性计算 人工智能 缓存
阿里云新用户、老用户活动集锦,新购、续费云服务器最新优惠政策与活动
2026年,阿里云为新老用户打造了极具吸引力的优惠政策体系。新用户可享38元轻量服务器抢购、u2i实例3折起等优惠,新用户还有专属折扣券、特定产品特价等权益。对于老用户,阿里云提供“长效特惠”模式,如99元/199元实例新购续费同价,解决“续费贵”问题,同时常规续费也有折扣。用户应厘清身份、把握首购特惠、关注续费与升级、善用优惠组合,以最优成本获取稳定可靠的云服务器资源。
|
7月前
|
人工智能 缓存 安全
阿里云Tokens怎么收费?最新AI焕新季优惠活动专享万亿Tokens扶持,免费领取1728元代金券
阿里云Tokens按输入/输出分别计费,价格因模型、产品及区域而异,官方活动精准报价:https://t.aliyun.com/U/FzmsXA 千问-Plus约¥0.0008/千tokens(输入),通义点金高级版¥0.1/千tokens。现推出AI焕新季活动,享万亿Tokens扶持及1728元代金券,开通百炼更可免费领7000万Tokens!
1367 1