7. Compose 的渲染
7.1 Compose 渲染过程
对于任意一个 composable 的渲染主要分为三个阶段:
- Composition,在这一阶段决定哪些 composable 会被渲染并显示出来。
- Layout,在这一阶段会进行测量和布局,也就是确认 composable 的大小和摆放的位置。
- Drawing,在这一阶段主要是完成绘制工作,将要展示的 composable 绘制到 canvas 上。
Composition
Composition 分为 initial composition 和 recomposition 两个过程。初次加载 Compose 结构树的过程主要是决定哪些 composable 会被显示出来,以及完成 composable 与 state 对象的绑定工作。
Recomposition 是当 UI 已经显示出来后,由于 composable 持有的 state 在与用户交互过程中,发生了变化,而引起 UI 局部刷新的过程。这个局部刷新主要是以持有 state 状态发生变化的 composabe 为根,根据子 composable 的输入是否改变来向下递归的进行 UI 的刷新。
@Composable
fun CompositionExample() {
Log.d("composition", "CompositionExample")
var inputState by remember { mutableStateOf("") }
Column {
HomeScreen(inputState) {
inputState = it
}
HomeBottom()
}
}
@Composable
fun HomeBottom() {
Log.d("composition", "HomeBottom")
Text(text = "This is the bottom")
}
@Composable
fun HomeScreen(value: String, textChanged: (String) -> Unit) {
Log.d("composition", "HomeScreen")
TextField(value = value, onValueChange = { textChanged(it) })
}
上面的代码在完成 initial composition 的渲染后,如果在 TextFiled 输入框中输入新的内容,会引起 state 状态的变化,进而引发 recomposition 的刷新操作。
当在进行 recomposition 的刷新时,首先,直接持有 state 状态对象的 composable 会进行刷新,打印出 CompositionExample 日志;当 CompositionExample 在刷新的过程中,执行到 HomeScreen composable 时,发现其输入参数发生了变化,会递归到 HomeScreen 中进行刷新,此时,HomeScreen 日志会被打印;HomeScreen 执行完成后,执行到 HomeBottom composable 时,由于其没有输入参数,意味着此 composable 的输入没有发生改变,所以,不会执行 HomeBottom composable 及其子 composable。(这里 HomeScreen 和 HomeBottom 不一定是顺序调用,两者可能是并发同时在不同的线程被调用)
Layout
Layout 包含了两个步骤:测量和布局,也就是测量 composable 的大小及确定摆放的位置。
Layout 阶段主要包含三个步骤:
- Measure children(测量子 composable 节点的大小)
- Measure own size(测量自己的大小)
- Place children(为子 composable 指定摆放位置)
这三个步骤使用的是深度优先的递归策略执行的,如下图所示:
以 Box1 的 layout 过程为例:
- Box1 先测量其所有子 composable 的大小;
- Box1 测量自己的大小;
- Box1 为其所有子 composable 指定其摆放的位置。
MeasureScope.measure 主要是负责测量的操作;MeasureScope.layout 主要负责指定摆放位置的操作。
测量(measure)和定位(place)的独立性:
- 测量和定位两个步骤的操作是相互独立的;
- 如果在测量过程中,读取了 state 状态,由于测量通常都发生在自定义 Layout 过程中,而测量后,紧接着就是定位的操作,所以,当测量过程中读取的 state 状态发生变化时,会同时触发测量和定位两个操作。而当定位过程中,读取了 state 状态,由于定位可以直接在 composable 的 modifier 进行配置(如:
Modifier.offset{....} ),当其内部引用的 state 状态发生变化时,只会执行定位的操作,而不会触发测量的执行。
var offsetX by remember { mutableStateOf(8.dp) }
Text(
text = "Hello",
modifier = Modifier.offset {
IntOffset(offsetX.roundToPx(), 0)
}
)
Drawing
绘制通常使用两种方式实现,一种方式是直接创建 Canvas 对象,并调用 drawXXX 相关方法来进行自定义绘制;另一种方式是调用 Modifer.drawBehind{...} 或者 Modifier.drawContent{...} 来进行自定义绘制。
在进行自定义绘制过程中,如果引用了 state 状态对象,当 state 状态对象发生变化时,只会触发绘制阶段的逻辑,而不会触发测量或者定位阶段的逻辑。
var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
drawRect(color)
}
7.2 State 与 Layout 阶段的关系
- 如果 Composable function 或者 composable lambda 绑定的 state 发生了变化后,会触发 composition 来刷新 UI;在 composition 过程中,如果内容发生了变化,会执行对应 composable 的 layout 操作;在 layout 的过程中,如果 composable 的大小或者位置发生了变化,则会执行对应 composable 的 drawing 操作。
- 如果自定义 Layout 中或者 Modifier.offset 绑定的 state 发生了变化后,会触发 Layout 的操作对相应的 composable 进行测量和定位的操作;如果测量或者定位过程中,对应的大小或者位置发生了变化,则会触发 Drawing 的操作。
- 如果自定义绘制
Canvas 、Modifier.drawBehind 或者 Modifer.drawContent 所绑定的 state 对象发生了变化,则会触发对应 composable drawing 阶段的操作。
8. Modifier 、作用及其执行顺序
典型 Modifier 作为参数的用法及说明
@Composable
fun show(modifier: Modifier = Modifier) {
Box(modifier.background(Color.Red)) {
}
}
基本上来说,一般在定义一个 composable 的时候,都会将 modifier 作为参数传入,且会给一个默认值 Modifier 对象。= 左右两边的 Modifier 分别代表什么呢?
= 左边的 Modifier 实际上是一个 Modifier 接口,代表当前 modifier 的类型。
= 右边的 Modifier 实际上是一个 Modifier 接口的默认实现类,该默认实现其实什么也没有做,只是一个空的实现。
这种写的好处是:
每一个 Composable 都能接收到外部 Composable 对其的约束。 当外部对当前 Composable 没有任何要求时,会使用默认的 Modifier 对象,不会改变任何预期。
Modifier 的执行顺序
多个 Modifier 链式调用在编译过程中会被编译成一个嵌套的关系,其嵌套的原则是链头部分在嵌套的最外层,链尾部分在嵌套结构的最里层。
show(modifier = Modifier.padding(20.dp).size(80.dp))
当编译过后的执行过程是从嵌套的最内层依次往外层进行执行的,也就是从链的最右边为执行的起点,依次执行,直到链最左边的调用被执行完为止。
如上面 show 函数中的 modifier 的执行过程是:
先执行 .size(80.dp),再执行 .paading(20.dp),其执行结果为:
针对 size、width 和 height 等 布局相关的配置,如果同样的配置被重复配置,且值是不同的,则前面执行的配置会被后面的配置所覆盖。
show(modifier = Modifier.padding(20.dp).size(80.dp).size(10.dp))
由于先执行了 .size(10.dp) 后,再执行的 .size(80.dp),后面的将前面的配置覆盖了,所以,默认情况下,只有 .size(80.dp) 的配置才会被生效。
针对 size、width 和 height 等的配置,可以通过 requiredXXX 来改变其默认的执行结果
show(
modifier = Modifier
.padding(20.dp)
.background(Color.Green)
.size(80.dp)
.requiredSize(10.dp)
.background(Color.Red)
)
上面代码的执行结果为:
从上图可以看出,使用了 requiredSize 后,其配置显示出来了,但他影响的只是在它前面执行(也就是链后)的配置。在其它后面执行的 .size(80.dp) (背景为绿色部分)还是正常显示,并没有任何影响。
针对 size、width 和 height 等的配置,如果 requiredSize 的值比后面执行的 Size 的值要大,也会被 Size 给约束,如果有 padding 值的话,会变成 requiredSize 的一部分
9. Jetpack Compose 架构,各层的作用及如何添加依赖
9.1 Jetpack Compose 五层架构
依赖库包各层作用说明:
依赖关系图:
为什么 Button 会在 Material 库里面,而不是在 foundation 库里面?
因为在 Compose 中 Button 的组件的组成是非常灵活的,里面需要指定不同的组件及排列方式(如:Text()、Icon()、Column() 等等)。我们所使用的 Button 之所以放在 Material 包里面,是因为在 Material 库中指定了默认的排列顺序 Row()。
同一层中的多个包又是什么关系呢?
一般来说,我们只需要引入同一层中同名的库包,就会将其所属的其他包一起加入进来。如:androidx.compose.ui:ui:xxx 包就包含了 androidx.compose.ui:ui-text:xxx 、androidx.compose.ui:ui-graphics:xxx 和 androidx.compose.ui:ui-util:xxx 等库包。
例外情况:
androidx.compose.ui:ui:xxx 不包含 androidx.compose.ui:ui-tooling:xxx
androidx.compose.material:material:xxx 不包含 androidx.compose.material:material-icons-extended:xxx
9.2 五层架构的好处
- 灵活控制。层级越高的组件,使用起来更加简单,但相对限制更多;层级越低的组件,可扩展性超高,但使用起来也相对复杂。使用者可以根据需求灵活选择使用哪一层级的组件。
- 自定义简单。自定义高级别组件的时候,可以非常容易的通过组合低级别的组件来完成自定义的工作。比如:Material 层级的 Button 按钮就是通过组合 Material、Foundatation、Runtime 层级的组件来完成自定义功能的。
10. CompositionLocal
CompositionLocal 主要是为了解决 Composable 树结构中,多个底层分支依赖上层某个数据时,需要将对应的值通过函数参数不断向下传递的问题。
使用 CompositionLocal 的流程
- 创建 CompositionLocal 对象:通过
staticCompositionLocalOf 或者 compositionLocalOf 两种方式来创建该对象。
1.1 staticCompositionLocalOf
val ColorCompositionLocal = staticCompositionLocalOf<Color> {
error("No Color provided")
}
1.2 compositionLocalOf
val ColorCompositionLocal = compositionLocalOf<Color> {
error("No Color provided")
}
- 通过 CompositionLocalProvider 指定 CompositionLocal 的作用范围并绑定需要共享的带状态的对象或值
CompositionLocalProvider(LocalActivateUser provides user) {
UserProfile()
}
这里首先通过 CompositionLocalProvider 指定了 CompositionLocal 的作用范围为 UserProfile() 及其所有子 composable 函数。同时,绑定了 user 对象(带状态)作为共享的值来被 UserProfile() 及其所有子 composable 函数调用。
- 在对应的 Composable 函数中调用 CompositionLocal 共享的带状态的值
@Composable
fun UserProfile() {
Column {
Text(text = LocalActivateUser.current.name)
}
}
Note:CompositionLocal 对象的命名一般以 Local 开头。
staticCompositionLocalOf 和 compositionLocalOf 的区别
下面的图是一个使用 CompositionLocal 的例子,点击 click 按钮后,会更新带状态的 Color 的值。分别使用 staticCompositionLocalOf 或者 compositionLocalOf 对象来看看带状态的 Color 值变化后,两者的表现有什么区别。
- 使用 staticCompositionLocalOf
当带状态的 Color 值发生变化后,其被包含的所有 composable function 都会触发 recompose 操作。
- 使用 compositionLocalOf
当带状态的 Color 值发生变化后,只有直接引用了 Color 值的 Composable function 才会触发 recompose 操作。
var LocalColorComposition = compositionLocalOf<Color> { error("No color provided") }
var stateColor by mutableStateOf(Color.LightGray)
@Composable
fun CompositionLocalAndStaticCompositionLocal() {
Column {
Button(onClick = {
stateColor = if (stateColor == Color.LightGray) Color.Red else Color.LightGray
}) {
Text(text = "Update stateColor")
}
CompositionLocalProvider(LocalColorComposition provides stateColor) {
CoverCompossables1()
}
}
}
@Composable
fun CoverCompossables1() {
outsideCount++
MyBox(color = Color.Green, count = outsideCount) {
centerCount++
MyBox(color = LocalColorComposition.current, count = centerCount) {
insideCount++
MyBox(color = Color.White, count = insideCount) {
}
}
}
}
@Composable
fun MyBox(color: Color, count: Int, content: @Composable BoxScope.() -> Unit) {
Column(Modifier.background(color)) {
Text(text = "current value: $count")
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
content = content
)
}
}
Note:
如果这里没有抽取 MyBox Composable 函数,而是直接以层级形式直接展开的话,上面的特性会失效。无论使用 CompositionLocalOf 还是 StaticCompositionLocalOf,结果都会全部刷新。
未抽取 MyBox Composable 函数的代码:
var LocalColorComposition = compositionLocalOf<Color> { error("No color provided") }
var stateColor by mutableStateOf(Color.LightGray)
@Composable
fun CompositionLocalAndStaticCompositionLocal() {
Column {
Button(onClick = {
stateColor = if (stateColor == Color.LightGray) Color.Red else Color.LightGray
}) {
Text(text = "Update stateColor")
}
CompositionLocalProvider(LocalColorComposition provides stateColor) {
CoverCompossables()
}
}
}
@Composable
fun CoverCompossables() {
outsideCount++
Column(
Modifier
.size(1000.dp)
.background(Color.Green)
) {
Log.d("TAG", "outside")
Text(text = "current value: $outsideCount")
Box(
Modifier
.padding(16.dp)
.fillMaxSize(), contentAlignment = Alignment.Center
) {
centerCount++
Column(
Modifier
.size(800.dp)
.background(LocalColorComposition.current)
) {
Log.d("TAG", "center")
Text(text = "current value: $centerCount")
Box(
Modifier
.padding(16.dp)
.fillMaxSize(), contentAlignment = Alignment.Center
) {
insideCount++
Column(
Modifier
.size(600.dp)
.background(Color.White)
) {
Log.d("TAG", "inside")
Text(text = "current value: $insideCount")
}
}
}
}
}
}
作用
解决前的数据传递链路图:
解决 Composable 树结构中,多个底层分支依赖上层某个数据时,需要将对应的值通过函数参数不断向下传递的问题。
解决后的数据传递链路图:
与全局静态变量的区别
通过 CompositionLocal 共享的值,只能在共享该值的结点及其子结点才能使用。其它地方使用会抛异常。
CompositionLocalProvider 的实现
@Composable
@OptIn(InternalComposeApi::class)
fun CompositionLocalProvider(
vararg values: ProvidedValue<*>,
content: @Composable () -> Unit) {
currentComposer.startProviders(values)
content()
currentComposer.endProviders()
}
可以看到 CompositionLocalProvider 在 content 执行之前开始生效,而在 content 执行之后就被释放了。同时,可以看到,CompositionLocalProvider 接收多个对象或值的共享。
11. Migration(迁移)
11.1 如何获取 xml 资源文件的值
- dimensionResource(id) -> dimens.xml
- stringResource(id) -> strings.xml
- XxxResource(id) -> xxx.xml
11.2 Livedata 在 composable 中如何使用
使用 LiveData 的扩展函数 observeAsState 将其转换为 Composable 中的 State 对象。
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
val plant by plantDetailViewModel.plant.observeAsState()
plant?.let {
PlantDetailContent(it)
}
}
Note:由于 LiveData 可以发送 null 值,在使用的地方需要判空。
11.3 Compose 中无法显示 HTML 格式的文本
使用 AndroidView 来使用传统的 View System 中的控件并将其显示在 Compose 中。在 AndroidView 中有两个函数类型的参数,一个 factory 参数表示在此创建传统 View System 中的控件,当构建完成后,会回调到 update 函数,并当 factory 中创建的控件当作函数参数传入,此时,就可以对该控件进行设值等操作了。
同时,在 update 回调中引用的外部的 state 变量(如下面的 htmlDescription 值是一个 mutableStateOf 状态对象)变化后,update 会重新被调用。
@Composable
fun androidViewDemo() {
var htmlDescription by remember {
mutableStateOf(
HtmlCompat.fromHtml(
"HTML<br><br>description",
HtmlCompat.FROM_HTML_MODE_COMPACT
)
)
}
Column {
AndroidView(factory = { content ->
TextView(content)
}, update = {
it.text = htmlDescription
})
Button(onClick = {
htmlDescription =
HtmlCompat.fromHtml("HTML<br><br>update", HtmlCompat.FROM_HTML_MODE_COMPACT)
}) {
Text(text = "更改 text 的显示")
}
}
}
Note:在 AndroidView 的 update 回调中,引用的任何 State 状态对象,只要状态对象发生变化后,都会引起 update 回调方法再次执行,类似于 reComposition。
11.4 如何在 Compose 中使用传统 View System 中的 Theme
如果想要在 Compose 中使用传统 View System 中的 Theme ,需要使用 compose-theme-adapter 库,该库可以自动将 style 文件中的主题转换成 composable 类型的主题,并生成以 MdcTheme 固定名称 composable 主题。
@Composable
fun MdcTheme(
context: Context = AmbientContext.current,
readColors: Boolean = true,
readTypography: Boolean = true,
readShapes: Boolean = true,
setTextColors: Boolean = false,
content: @Composable () -> Unit
) {
val key = context.theme.key ?: context.theme
val themeParams = remember(key) {
createMdcTheme(
context = context,
readColors = readColors,
readTypography = readTypography,
readShapes = readShapes,
setTextColors = setTextColors
)
}
MaterialTheme(
colors = themeParams.colors ?: MaterialTheme.colors,
typography = themeParams.typography ?: MaterialTheme.typography,
shapes = themeParams.shapes ?: MaterialTheme.shapes,
content = content
)
}
11.5 如何在传统的 View System 中使用 Compose
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="hello world" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/acv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tv" />
</RelativeLayout>
findViewById<ComposeView>(R.id.acv).setContent {
Text(text = "ComposeView")
}
11.6 如何在 Compose 中使用传统的 View
动态创建传统 View:
setContent {
Column {
Text(text = "top")
AndroidView(factory = {
View(it).apply {
setBackgroundColor(android.graphics.Color.GRAY)
}
}, Modifier.size(30.dp)) {
}
Text(text = "bottom")
}
}
factory 的作用:用于创建由传统 View System 所构建的布局。只会被执行一次。
update 的作用:用于界面每次 Recompose 的时候刷新传统 View System 所构建的布局。会拿到一个在 factory 过程中生成的 View 引用对象,来进行操作。
11.7 Compose 中内部数据与 Compose 外部数据的交互
Compose 内部使用外部非 State 数据
- LiveData 数据更新触发 Recompose(Compose 使用外部数据)
val result = MutableLiveData(1)
setContent {
val num = result.observeAsState()
Text(text = "$num")
}
- 协程 Flow 发送数据触发 Recompose(Compose 使用外部数据)
val flowOjb = flow { emit(1) }
setContent {
val num = flowOjb.collectAsState(initial = 0)
Text(text = "$num")
}
Compose 外部实现使用 Compose 内部数据
在 Compose 的内部数据的主要表现形式是:State<T> ,这个对象是无法转换成其它对象(如:LiveData)的,所以,外部实现是无法使用 Compose 中的数据的。
解决方法:如果外部数据需要得到 Compose 内部数据的话,在一开始设计的时候,就需要将该数据结构定义在外部(如:LiveData)。
|