Compose 中嵌套原生 View 原理

Compose 是用于構(gòu)建原生 Android UI 的現(xiàn)代工具包,他只需要在 xml 布局中添加 ComposeView,或是通過 setContent 擴展函數(shù),即可將 Compose 組件繪制界面中。

Compose 天然就支持被原生 View 嵌套,但也支持嵌套原生 View,Compose 是通過自己的一套重組算法來構(gòu)建界面,測量和布局已經(jīng)脫離了原生 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 完美展現(xiàn)原生 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、創(chuàng)建 ViewFactoryHolder         
            ...
            val viewFactoryHolder = ViewFactoryHolder<T>(context, parentReference)
            // 2、factory 被賦值給了 ViewFactoryHolder
            viewFactoryHolder.factory = factory
            ...
            // 3、從 ViewFactoryHolder 拿到 LayoutNode
            viewFactoryHolder.layoutNode
        },
       ...
    )
  1. 創(chuàng)建了個 ViewFactoryHolder
  2. 將包裹原生 View 的 factory 函數(shù)賦值給 ViewFactoryHolder
  3. 從 ViewFactoryHolder 中拿到 LayoutNode 給 ComposeNode,后面會講解該操作

大家可能對 ComposeNode 有點陌生,如果你閱讀過 Compose 中組件源碼的話,例如 Text,在你一直跟蹤下去的時候會發(fā)現(xiàn),他們都有一個共同點,那就是都會走到 ComposeNode,并且,ComposeNode 函數(shù)中會拿到 factory 的返回值 LayoutNode 來創(chuàng)建一個 Node 節(jié)點來參與 Compose 的繪制。也即Compose 在排版和布局的時候,操控的就是 LayoutNode,并且這個 LayoutNode 能拿到 Compose 執(zhí)行中的一些回調(diào),例如 measure 和 layout 來改變自身的位置和狀態(tài)。

小結(jié):在 AndroidView 這個函數(shù)中我們發(fā)現(xiàn),原生 View 是通過外部包裹一層 Compose 組件參與到 Compose 布局中的

2、分析 ViewFactoryHolder

我們來看下,原生 View 的 factory 函數(shù),在賦值給 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 復(fù)制給幕后字段
            field = value
            // 2、factory 不為空 
          ->if (value != null) { 
                // 3、invoke factory 函數(shù),拿到原生 View 本身
                typedView = value(context)
                // 4、將原生 View 復(fù)制給 view
                view = typedView
            }
        }
    ...
}

在賦值發(fā)生時,會觸發(fā) ViewFactoryHolder 中 factory 的 set(value),value 就是嵌套原生 view 的 factory 函數(shù)

  1. 將 factory 函數(shù)賦值給幕后字段,也即 ViewFactoryHolder.factory = factory
  2. 判斷 factory 是否為空,我們提供了原生 ImageView 組件,這里為 true
  3. 執(zhí)行 factory 函數(shù),也即拿到我們的 ImageView 組件,賦值給全局變量的 typedView
  4. 并且也賦值給了 view

我們需要找到原生 ImageView 被誰持有,目前來看的話,typedView 被復(fù)制到了全局,沒有被其他變量持有,被復(fù)賦值的 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、觸發(fā)更新
                        runUpdate()
                    }
                }
        }
        ...
}
  1. 需要注意的是,AndroidViewHolder 是一個繼承自 ViewGroup 的原生組件
  2. 將原生 view 賦值給幕后字段,也即 view 的實體是 ImageView
  3. 移除所有的子 View,看來,AndroidViewHolder 只支持添加一個原生 View
  4. 判斷原生 view 是否為空,我們提供了 ImageView ,所以該判斷為 true
  5. 將原生 view 添加到當前的 ViewGroup,也即我們的 ImageView 被添加到了 AndroidViewHolder 中
  6. runUpdate 會觸發(fā) Compose 的一系列更新,我們先暫時不管他

小結(jié):我們提供的原生 View,最終會被 addView 到 ViewFactoryHolder 中,只是 addView 這個操作是發(fā)生在他的父類 AndroidViewHolder 中的,然后將原生 ImageView 賦值到全局變量 view 中

現(xiàn)在,我們還有一些疑問,原生 view 雖然被 addView 到 ViewFactoryHolder 中了,那 ViewFactoryHolder 這個 ViewGroup 是如何被添加到界面上的呢?ViewFactoryHolder 是如何測量和布局的呢?我們需要回到 AndroidView 的函數(shù)中,找到 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 回調(diào)
        layoutNode.onAttach = { owner ->
            // 2.1 重點: 將當前 ViewGroup 添加到 AndroidComposeView 中
            (owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)
            if (viewRemovedOnDetach != null) view = viewRemovedOnDetach
        }
        // 3、注冊 detach 回調(diào)
        layoutNode.onDetach = { owner ->
            // 3.1 重點: 將當前 ViewGroup 從 AndroidComposeView 中移除
            (owner as? AndroidComposeView)?.removeAndroidView(this)
            viewRemovedOnDetach = view
            view = null
        }
        // 4、注冊 measurePolicy 繪制策略回調(diào)
        layoutNode.measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                ...
                // 4.1、layoutNode 的測量,觸發(fā) AndroidViewHolder 的測量
                measure(
                    obtainMeasureSpec(constraints.minWidth, constraints.maxWidth,layoutParams!!.width),
                    obtainMeasureSpec(constraints.minHeight,constraints.maxHeight,layoutParams!!.height)
                )
                // 4.1、layoutNode 的布局,觸發(fā) AndroidViewHolder 的布局
            -> return layout(measuredWidth, measuredHeight) {
                    layoutAccordingTo(layoutNode)
                }
            }
           ...
        }
        // 5、返回 layoutNode 
        layoutNode
    }

這段代碼有點多,但卻是最精華的核心部分:

  1. 注釋直接道破,這個 LayoutNode 會代理原生 View 的 measure、layout,將測量和布局結(jié)果反應(yīng)到 AndroidViewHolder 這個 ViewGroup 中
  2. 注冊 LayoutNode 的 attach 回調(diào),這個 attach 可以理解成 LayoutNode 被貼到了 Compose 布局中觸發(fā)的回調(diào),和原生 View 被添加到布局中,觸發(fā) onViewAttachedToWindow 類似
    1. 將當前 AndroidViewHolder 添加到 AndroidComposeView 中
  3. 注冊 LayoutNode 的 detach 回調(diào),這個 detach 可以理解成 LayoutNode 從 Compose 布局中被移除觸發(fā)的回調(diào),和原生 View 從布局中移除,觸發(fā) onViewDetachedFromWindow 類似
    1. 將當前 ViewGroup 從 AndroidComposeView 中移除
  4. 注冊 LayoutNode 的繪制策略回調(diào),在 LayoutNode 被貼到 Compose 中,Compose 在重組控件的時候,會觸發(fā) LayoutNode 的繪制策略
    1. 觸發(fā) ViewGroup 的 measure 測量
    2. 觸發(fā) ViewGroup 的 layout 布局
  5. 返回 LayoutNode

在 2.1 的 attach 步驟中發(fā)現(xiàn),我們的 ImageView 經(jīng)過 AndroidViewHolder 的包裹,被 addAndroidView 到了 AndroidComposeView 中,這里我們又有個疑問,owner 轉(zhuǎn)換成的 AndroidComposeView 是從哪來的?addAndroidView 做了哪些事情?

這里先小結(jié)下: AndroidViewHolder 中的 layoutNode 是一個不可見的 Compose 代理節(jié)點,他將 Compose 中觸發(fā)的回調(diào)結(jié)果應(yīng)用到 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 中

現(xiàn)在 addView 的邏輯已經(jīng)走到了 AndroidComposeView,我們現(xiàn)在還需要知曉 AndroidComposeView 從何而來

這次,我們需要先從 ComposeView 開始分析:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AndroidWidget()
        }
}

在 Activity 的 onCreate 方法中,我們通過 setContent 將 ComposeView 應(yīng)用到界面上,我們需要跟蹤這個 setContent 拓展函數(shù)一探究竟:

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    ...
    
    if (existingComposeView != null) with(existingComposeView) {
        ...
         // 1、設(shè)置 compose 布局
        setContent(content)
    } else ComposeView(this).apply {
        ...
         // 1、設(shè)置 compose 布局
        setContent(content)
        ...
        // 2、調(diào)用 Activity 的 setContentView 方法,布局為 ComposeView
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}
  1. 調(diào)用 ComposeView 內(nèi)部的 setContent 方法,將 compose 布局設(shè)置進去
  2. 調(diào)用 Activity 的 setContentView 方法,布局為 ComposeView,這也是 Activity 中沒有找到設(shè)置 setContentView 的原因,因為拓展函數(shù)已經(jīng)做了這個操作

我們需要跟蹤下 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、如果為空,則創(chuàng)建個 AndroidComposeView,并調(diào)用 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 設(shè)置到 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. 如果獲取為空,則創(chuàng)建個 AndroidComposeView,并調(diào)用 addView 將 AndroidComposeView 添加進 ComposeView
  3. 將 AndroidComposeView 設(shè)置到 WrappedComposition 中,并返回 Composition,這也就是為什么在 LayoutNode 中,能拿到 owner ,并且為 AndroidComposeView 的原因

三、總結(jié)

至此,我們分析完了原生 View 是如何添加進 Compose 中的,我們可以畫個圖來簡單總結(jié)下:

  • 橙色:在 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}

現(xiàn)在,我們可以來回答開頭說的問題了:

  • Compose 是通過 addView 的方式,將原生 View 添加到 AndroidComposeView 中的,他依然使用的是原生布局體系
  • 嵌套原生 View 的測量與布局,是通過創(chuàng)建個代理 LayoutNode ,然后添加到 Compose 中參與組合,并將每次重組返回的測量信息設(shè)置到原生 View 上,以此來改變原生 View 的位置與大小
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容