Compose知识分享

前言

"Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包。Compose 提供声明性 API,让您可在不以命令方式改变前端视图的情况下呈现应用界面,从而使编写和维护应用界面变得更加容易。"

以上是Compose官网中对于Compose这套全新的Android UI库的介绍,Compose的到来为Android引入了声明式、响应式的全新开发方式,直接以函数调用的形式使用组件,取代了原先冗长的Activity定义以及.xml文件编写;这一点也是当前TDF团队Kuikly框架同样所具备的能力,给予声明式、响应式的特性加速用户开发,并且Kuikly直接复用了Android的view体系,与Compose以全新的Composable以及Compose树而言,一定程度上可以加速新用户对于底层的理解。​​​​​​​

Compose执行生命周期

我们继续Compose,Compose在声明式函数的基础上,引入了重组的能力,结合后续的度量策略,Compose UI在发生变化时,会被系统的快照系统SnapManager监测到,并使用协程管道的方式获取信息,然后执行重组动作,重组动作会对状态变更的UI元素进行重新渲染,而不修改状态未改变的其余UI元素。

Compose程序的执行之后,一直到页面渲染出结果以及后续的持续UI变更,共可以分为4个阶段,分别是"组合 --> 布局 --> 渲染 <--> 重组";

组合阶段

"在组合阶段,Compose 运行时会执行可组合函数,并 输出一个表示界面的树结构。这个界面树由 包含下一阶段所需的所有信息的布局节点"(官方文档)

因此我们不难理解,Compose仍然是采用了树型结构作为UI加载、布局、渲染的底层结构,但是有所变更的是如今的Compose树的节点类型为ComposeUiNode,在组件树真正开始被载入时会变成LayoutNode类型的节点。LayoutNode是ComposeUiNode的子类,其继承了ComposeUiNode中所有组件设置的属性。对应的Row、Image、Column以及Text组件所形成的LayoutNode树显示如下:

因此我们可以明确了第一点,即组合阶段就是产生所有组件的LayoutNode树,为后续布局阶段提供遍历的内容。

结合源码我们梳理了组合阶段的执行流程,显示如下:

相关源码展示如下,setContent中内部的函数调用在此不做展示;可以结合流程图去检索源码即可了解其中的调用链。

Kotlin 复制代码
private object TutorialSnippet1 {

    class MainActivity : AppCompatActivity() {

        override fun onCreate(savedInstanceState: Bundle?) {

            super.onCreate(savedInstanceState)

            setContent {

                Text("Hello world!")

            }

        }

    }

}



public fun ComponentActivity.setContent(

    parent: CompositionContext? = null,

    content: @Composable () -> Unit

) {

    val existingComposeView = window.decorView

        .findViewById<ViewGroup>(android.R.id.content)

        .getChildAt(0) as? ComposeView



    if (existingComposeView != null) with(existingComposeView) {

        setParentCompositionContext(parent)

        setContent(content)

    } else ComposeView(this).apply {

        // Set content and parent **before** setContentView

        // to have ComposeView create the composition on attach

        setParentCompositionContext(parent)

        setContent(content)

        // Set the view tree owners before setting the content view so that the inflation process

        // and attach listeners will see them already present

        setOwners()

        setContentView(this, DefaultActivityContentLayoutParams)

    }

}

布局阶段

在布局阶段,Compose 会使用在组合阶段生成的界面树 作为输入,计算每个布局节点在界面树中的测量和放置位置,此过程中将遍历每个LayoutNode节点执行子节点测量、自我测量以及放置子节点三个过程。

以上图的LayoutNode节点树为例,此过程可描述为:

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1、Row 测量其子项 Image 和 Column。 2、系统测量了 Image。它没有任何子元素,因此它决定自己的 尺寸,并将尺寸报告给 Row。 3、接下来测量 Column。它会测量自己的子项(两个 Text 可组合项)。 4、系统将测量第一个 Text。由于没有任何子元素,因此它决定 自己的尺寸,并将其尺寸报告给 Column。系统测量第二个 Text。由于没有任何子元素,因此它决定 并将其报告给 Column。 5、Column 使用子测量值来确定自己的尺寸。它使用 最大子元素宽度及其子元素高度之和。 6、Column 会相对于自身放置其子项,并将其置于子项下方 相互垂直 7、Row 使用子测量值来确定自己的尺寸。它使用 子元素的最大高度及其子元素宽度之和。然后 子项。 |

采用深度遍历的方式执行LayoutNode树的遍历,基于constrains约束,树上每个组件所在的位置在遍历完成后都将计算完成,即完成布局阶段,等待后续的渲染阶段。

而布局阶段中,Modifier类已经开始在遍历的时候就被加载,并在宽高计算时被认为是待度量的因素。具体各组件的度量策略我附上一张表格。

!!! 目前核心关心的是Modifier 属性是怎么被加载的?

① 组成Modifier链

Kotlin 复制代码
Surface(

        modifier = Modifier
            .size(200.dp) // 设置组件大小
            .background(Color.LightGray) // 设置背景颜色
            .offset(x = 10.dp, y = 20.dp) // 设置偏移量

    ) {

        Text(
            text = "Hello, Jetpack Compose!",
            fontSize = 20.sp,
            color = Color.Black,
            modifier = Modifier.padding(8.dp) // 内部文本的额外内边距
        )

    }

以此代码为例,Surface 组件使用Modifier 伴生对象基于链式调用的方式声明了多个属性,也正如此Modifier 是Compose 中修饰 UI 组件的关键数据结构。

Kotlin 复制代码
interface Modifier {

    fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
    fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
    fun any(predicate: (Element) -> Boolean): Boolean
    fun all(predicate: (Element) -> Boolean): Boolean
    infix fun then(other: Modifier): Modifier = ...
    interface Element : Modifier {

        ...

    }

    companion object : Modifier {

        ...

    }

}

Modifier 接口有三个直接实现类或接口:伴生对象 Modifier 、内部子接口Modifier.Element 、CombinedModifier

  1. 伴生对象 Modifier :最常用的 Modifier 即在代码中使用 Modifier.xxx()
  2. 内部子接口 Modifier.Element 当我们使用Modifier.xxx() 时,其内部实际会创建一个Modifier 实例。如使用 Modifier.size(100.dp) 时,内部会创建一个 SizeModifier 实例;

其背后的继承关系是

而类似LayoutModifier 的中间类显示如下,不同的中间Modifier.Element 子类将使得Modifier 链建立时使用CombinedModifier 来链接;

② Modifier 链的构建过程

  1. then()

size 属性定义时建立的SizeModifier 实例被当作参数传入 then() 方法中。而这个 then() 方法就是 Modifier 间相互连接的关键方法。

Kotlin 复制代码
Modifier.size(100.dp)


fun Modifier.size(size: Dp) = this.then( // 关键方法

    SizeModifier(
        ...
    )

)

// 返回待连接的Modifier

companion object : Modifier {

    ...

    override infix fun then(other: Modifier): Modifier = other

}
  1. 继续链式调用后续属性,BackGround, padding
Kotlin 复制代码
Modifier
    .size(100.dp)
    .background(Color.Red)

fun Modifier.background(
    color: Color,
    shape: Shape = RectangleShape
) = this.then( // 当前 this 指向 SizeModifier 实例
    Background(
        ...
    )

)
  1. 而此时Modifier 链也将引入新的类型CombinedModifier 来链接所对应的中间Modifier.Element 子类不一致时的Modifier
Kotlin 复制代码
interface Modifier {

    infix fun then(other: Modifier): Modifier =
    if (other === Modifier) this else CombinedModifier(this, other)

}


class CombinedModifier(
    private val outer: Modifier,
    private val inner: Modifier

) : Modifier

形成以下的样式

所有属性全部上链会形成

至此Modifier 链已经构建完成,而在LayoutNode 遍历时会基于nodes 属性访问管理Modifier 链的Nodechain

Kotlin 复制代码
internal val nodes = NodeChain(this)

而辅助遍历时的数据结构是

Kotlin 复制代码
internal val innerCoordinator: NodeCoordinator
    get() = nodes.innerCoordinator


internal val layoutDelegate = LayoutNodeLayoutDelegate(this)    

internal val outerCoordinator: NodeCoordinator
    get() = nodes.outerCoordinator

innerCoordinator是指最内层的协调器,直接与LayoutNode关联,负责处理Modifier链的内部操作,包括处理内部Modifier、管理LayoutNode子LayoutNode,协助他们的度量工作以及传递事件;

outerCoordinator是指最外层的协调器,负责处理Modifier链的外部操作,包括处理外部Modifier,协调Modifier的调用顺序以及管理LayoutNode父节点;

其中内部Modifier和外部Modifier的分辨规则是:

  1. 内部 Modifier 是指那些主要影响组件的布局和测量的修饰符。这些修饰符通常在LayoutNode的内部进行操作,改变组件的尺寸、位置和布局行为。( padding, size, fillMaxWidth / fillMaxHeight, wrapContentSize, aspectRatio, weight)
  2. 外部 Modifier 是指那些主要影响组件的绘制和偏移的修饰符。这些修饰符通常在LayoutNode的外部进行操作,改变组件的外观、透明度和位置偏移等。( background, border, offset, alpha, clip, shadow);

而其中访问Modifier链的过程将基于foldin与foldout方法取到Modifier上所有的设置信息,从而将外部 or 内部 属性的定义值反馈到当前LayoutNode的width, height上,并最后在父节点的constrains限制下输出最大的width和height;

Kotlin 复制代码
fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
// foldIn(): 正向遍历 Modifier 链,SizeModifier-> Background -> PaddingModifier


fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
// foldOut(): 反向遍历 Modifier 链, PaddingModifier -> Background ->SizeModifier

绘制阶段

绘制代码期间的状态读取会影响绘制阶段。常见示例包括 Canvas()、Modifier.drawBehind 和 Modifier.drawWithContent。

Modifier知识补充

Compose UI 组件 Modifier 子类 类图

Compose 常用UI 组件 LayoutNode 、Modifier 、MeasurePolicy 汇总

相关推荐
wavky17 天前
零经验选手,Compose 一天开发一款小游戏!
compose
亚林瓜子1 个月前
Minio安装(Docker Compose方式)
运维·docker·容器·minio·compose
氦客3 个月前
Android Compose 显示底部对话框 (ModalBottomSheet),实现类似BottomSheetDialog的效果
android·dialog·ui·compose·modal·bottomsheet·底部对话框
小林爱3 个月前
【Compose multiplatform教程06】用IDEA编译Compose Multiplatform常见问题
android·java·前端·kotlin·intellij-idea·compose·多平台
小林爱3 个月前
【Compose multiplatform教程12】【组件】Box组件
前端·kotlin·android studio·框架·compose·多平台
俺不理解4 个月前
Android Compose 悬浮窗
android·生命周期·compose·悬浮窗
许三多20204 个月前
Compose Navigation快速入门
compose·navigation·compose 导航
许多仙5 个月前
【Android Compose原创组件】可拖动滚动条的完美实现
android·compose·scrollbar·compose快速滚动条
大福是小强5 个月前
005-Kotlin界面开发之程序猿初试Composable
kotlin·界面开发·桌面应用·compose·jetpack·可组合
敲代码不忘补水6 个月前
将 Docker Run 命令转换为 Docker Compose 配置:在线工具操作指南
docker·容器·yml·compose·在线转换工具