当谈到 Jetpack Compose 时,你可能会想到:这是个 UI 库。但实际上,Compose 包含多个子库,也就是:
上面的五个基本都和 UI 有关,但今天我们不看它们,我们来玩一玩下面两个:compose.compiler
和 compose.runtime
。基于它们,我们可以自己做一个客户端库(Client Library)。
在这里,我们要讨论的是 Client Library。大致上,它就是 “用于编写应用程序的API” 。比如,在 Jetpack Compose 中,它的各个组成部分有这样的层级
自上而下, Material 用到了 Foundation,Foundation 用到了 UI,UI 又依赖于 Runtime。前三者都是 Client Library。而今天,我们要做的,就是基于 Runtime 写一个 “compose.ui” 出来
让我们先看看 Compose 编译器吧。在这里,我们不会很深入的涉及各项细节,而是给你一个大致的把控。基本上,Compose 编译器是:
@Composable
函数类似于 suspend
函数的处理,Compose 编译器在背后为我们做了许多事情。比如说:
Compose Runtime 可以说是 Compose 的大脑,没有它,就不会有 Jetpack Compose。它负责管理各种状态,是整个 Compose 编程的基石。
我们将会从下面四个方面简要介绍它。
Composer 是 Composable 和 Slot Table 间的桥梁。它由 Compose 编译器注入,所有与它相关的代码都由 Compose 编译器处理好了。
Composer 负责:
到现在为止,我们的 Composable 经历了这么些流程:
我们的 Composable 经过 Compose 编译器,现在多了个 Composer 作为参数。当我们开始运行时,又发生了什么呢?我们上面说,Composer 是 Composable 和 Slot Table 间的桥梁,现在就是和 Slot Table 交互的时机了
什么是 Slot Table?简单地说,它是一种线性的数据结构,能够高效地处理 “插入” 的过程。它存储着 Composition 的当前状态,也就是说,哪个 Composition 需要重新执行,哪个不需要,这个是记录在 Slot Table 里面的。
Slot Table 的工作依赖于两个概念:Groups
和 Slots
。
Groups 用于将内存中上实际存储的线性结构转换为 UI 所需的树形结构。比方说,对于数组 array = [ComposableA, ComposableB, ComposableC]
,要能变成
- ComposableA
- ComposableB
- ComposableC
Slots 则负责存储 Group 中的真实数据,比如说 rememebr { "FunnySaltyFish" }
,里面的 FunnySaltyFish
就实际存储在 Slots 中。
如需详细了解 Slot Table,可参阅 fundroid 大佬写的 探索 Jetpack Compose 内核:深入 SlotTable 系统 或者由 Google 工程师写的 Under the hood of Jetpack Compose — part 2 of 2
现在,我们把目光重新转回去。我们现在知道,Composable 在 runtime 中被调用,它和 Slot Table 交互,而 Slot Table 中又包含着来自 Composable 的数据。但好像这都和用户没关系诶?目前的这些都是 Compose 内部在互相玩耍。接下来会发生什么呢?
我们已经提到过,Composer 会记录状态的改变,比如说首次的 Composition,或者后来的 Re-composotion,它记录到了这些变化,然后,这些变化会被交给 Applier
来使用。来看看 Applier 吧
Applier 负责应用那些 Composition 中被提交的基于树的操作。也就是说,Composer 提交了这些变化,Applier 就负责在 Node Tree 上执行它们。这些操作主要包含:插入、移动、删除(或者甚至清空整棵树)
有趣的是,如果你查看 Applier 的源码,你会发现它是个接口:
/**
* An Applier is responsible for applying the tree-based operations that get emitted during a
* composition. Every [Composer] has an [Applier] which it uses to emit a [ComposeNode].
*
* A custom [Applier] implementation will be needed in order to utilize Compose to build and
* maintain a tree of a novel type.
*/
@JvmDefaultWithCompatibility
interface Applier<N> {
xxx
}
而它有个实现的抽象类 AbstractApplier
/**
* An abstract [Applier] implementation.
*/
abstract class AbstractApplier<T>(val root: T) : Applier<T> {
xxx
}
因此,如果我们需要写自定义的 Applier 的话,就需要继承这个抽象类。
好嘞,现在我们有了 Applier,我们知道它会把 change 作用到 Compose Node Tree 上。整个流程现在变成了这样:
那么新的问题来了,这个 Compose Node Tree 是怎样被创建的呢?
假设我们现在有两个 Composable,Hi
和 Dev
现在,这两个 Composable 都会提交一个 Node 给 Applier,Applier 获取到了这个节点,就可以按情况创建或者更新 Compose Node Tree。
那么,Compose Node 又是怎么被提交的呢?我们说 “提交一个节点”,大致上就是在说 “创建一个你自己的节点,让它代表一个 Composable”。为了让 Compose 知道我们想要提交什么类型的节点,我们需要选择合适的 Node 类型,它们包括:ComposableNode 和 ReusableComposeNode
从字面上可以看到,ReusableComposeNode
就是 “可以被重新使用的 Compose Node”,即:避免重新创建一个新 Node,而是直接复用某个节点。它们的代码差异如下:
/**
* Emits a node into the composition of type [T].
*
* This function will throw a runtime exception if [E] is not a subtype of the applier of the
* [currentComposer].
*
* @param factory A function which will create a new instance of [T]. This function is NOT
* guaranteed to be called in place.
* @param update A function to perform updates on the node. This will run every time emit is
* executed. This function is called in place and will be inlined.
*
*/
// ComposeNode is a special case of readonly composable and handles creating its own groups, so
// it is okay to use.
@Suppress("NONREADONLY_CALL_IN_READONLY_COMPOSABLE", "UnnecessaryLambdaCreation")
@Composable inline fun <T : Any, reified E : Applier<*>> ComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
// 注意看,这里是 startNode
currentComposer.startNode()
if (currentComposer.inserting) {
currentComposer.createNode { factory() }
} else {
currentComposer.useNode()
}
Updater<T>(currentComposer).update()
currentComposer.endNode()
}
/**
* Emits a recyclable node into the composition of type [T].
*
* This function will throw a runtime exception if [E] is not a subtype of the applier of the
* [currentComposer].
* @param factory A function which will create a new instance of [T]. This function is NOT
* guaranteed to be called in place.
* @param update A function to perform updates on the node. This will run every time emit is
* executed. This function is called in place and will be inlined.
*/
// ComposeNode is a special case of readonly composable and handles creating its own groups, so
// it is okay to use.
@Suppress("NONREADONLY_CALL_IN_READONLY_COMPOSABLE", "UnnecessaryLambdaCreation")
@Composable inline fun <T : Any, reified E : Applier<*>> ReusableComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
// 而这里是 startReusableNode
currentComposer.startReusableNode()
if (currentComposer.inserting) {
currentComposer.createNode { factory() }
} else {
currentComposer.useNode()
}
Updater<T>(currentComposer).update()
currentComposer.endNode()
}
上面两个函数都是泛型函数,也就是说,你需要自己指定类型,告诉 Compose 你需要提交什么类型的 Node。
例如,在刚刚的示例中,我们提交一个自定义的 TextNode
,它们会交给 Applier,根据情况创建或更新 Compose Node Tree。
现在我们已经知道 Compose Node Tree 是怎么创建或更新的了。但现在还差一步:我们还没画 UI,还没显示任何东西。到这里,Compose Runtime 已经都给你准备好了,最后的一步就是让它显示出来而已。这也就是我们今天的主题:做一个显示在 PPT 里的 UI 库。
我们要做的最后一步叫做 Client Intergration(客户端集成)
,具体来说,它包括:
我们选择了 PowerPoint 作为客户端,其实你也可以选其他的,用 View 啊、用 Canvas 啊、用 HTML 啊或者什么乱七八糟的,但我们这选 PPT 做演示。For Fun!
我们希望能呈现这样一个效果:一页幻灯片(Slide),里面包含一行文本
首先,我们需要定义一些节点,比如 TextNode
、SlideNode
,用来表示文本和幻灯片(当然也可以有其他的,比如 Video、Image,但我们这先演示这俩);其次,我们需要自定义的 Applier
,让 Compose 知道如何去创建/插入我们的节点,如何更新之类的;再然后,我们需要 Composable
,这里也是两个, Text
和 Slide
;最后,我们需要做 Client Integration
。
让我们开始吧~
我们先定义一个抽象类,表示咱们的 PPTNode
它有一个成员 children
, 表示子节点;一个方法 render
用于渲染,在我们这,就是渲染成 PPT。
SlideNode
表示一张幻灯片,有点类似于 布局
。它的代码就简单了,render
方法就循环调用一遍 children.render
。
TextNode
略有不同,我们希望它有一些数据(text),并把它渲染出来。它的代码如下:
这里用的是 Apache POI 实现的渲染,我们不会深入细节。如果你感兴趣,可以查看对应源代码、
下一步是自定义 Applier
,也就是继承 AbstractApplier
。实际上,AbstractApplier
已经实现了一些逻辑,因此我们需要重写一些方法。它们主要包括:
AbstractApplier
提供了两个 insert
方法,分别是自顶向下和自底向上,我们只需要按要求实现即可。这里我们只用自顶向下的 insert 就行
其他操作 包括 remove/move/clear 之类的
下一步就是创建 Composable
。听起来很简单,但它实际上和你平时写的有点不一样。因为,现在我们现在需要提交我们刚刚创建的 Node。我们调用 ComposeNode
函数,并传入刚刚写好的 Node
和 Applier
。我们先从 Slide
开始吧
这里的几个参数,
factory
传入了 SlideNode 的构造函数update
会在每次 Composable 被重新调用时触发,但这里由于没有任何数据,这里就不写了Text
的代码类似,如下:
不同的是,由于 Text
持有数据(text),因此需要更新一下。
好,现在最后一步。我们在这儿要给 Composable 一个入口(类似于 setContent {})。在这儿,我们用 runBlocking
执行一些逻辑
我们首先需要创建一个 frameClock,这里调用的是 Compose 已经给出的实现;然后是 Recomposer 和 rootNode(在这里,也就是 SlideNode)
然后是创建 Composition、设置 content 为我们传入的 content
接下来我们需要观察快照的变更、运行 Recomposer、向 clock 不断发送帧……最后调用 rootNode.render() 完成渲染
好耶,我们现在就完成啦~
比如说,在 main
函数调用我们的入口函数,即可
当你运行时,你就发现生成了对应的 PPT
本项目的源代码在:fgiris/composePPT: An experimental UI toolkit for generating PowerPoint presentation files using Compose,欢迎建议和 PR。我们最后做个总结吧:
这就是今天的内容了,感谢大家的观看(在这里是阅读哈哈)。
阅读量:1233
点赞量:0
收藏量:0