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 汇总

相关推荐
tangweiguo030519876 天前
Android Compose Activity 页面跳转动画详解
android·compose
tangweiguo030519876 天前
在 Jetpack Compose 中实现 iOS 风格输入框
android·compose
tangweiguo0305198714 天前
Android Compose 权限申请完整指南
compose
tangweiguo0305198719 天前
androd的XML页面 跳转 Compose Activity 卡顿问题
compose
tangweiguo0305198719 天前
iOS 风格弹框组件集 (Compose版)
compose
tangweiguo0305198719 天前
Android Material Design 3 主题配色终极指南:XML 与 Compose 全解析
compose
tangweiguo0305198720 天前
Android Compose 中获取和使用 Context 的完整指南
android·compose
tangweiguo0305198722 天前
Jetpack Compose 自定义组件完全指南
compose
tangweiguo0305198723 天前
打破界限:Android XML与Jetpack Compose深度互操作指南
android·kotlin·compose
wavky1 个月前
零经验选手,Compose 一天开发一款小游戏!
compose