引言
想要实现上面的列表的Item View,传统的开发方式是这样的
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="20dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="睡眠音律问题"
android:textAppearance="@style/Rainbow.TextAppearance.Body3"
android:textColor="@color/s1" />
<!--Image-->
</LinearLayout>
那Compose中,这些View的布局关系,和外观属性都是怎么被代码描述的呢?
Row(
modifier = Modifier
.fillMaxWidth() // 宽度填充到最大宽度,这里没有限制最大宽度,默认是父布局宽度
.padding(16.dp, 20.dp) // 设置水平和垂直的padding
.noRippleClickable(onViewClick),// 设置点击事件
horizontalArrangement = Arrangement.SpaceBetween,// 设置水平排列方式
verticalAlignment = Alignment.CenterVertically // 设置垂直排列方式
) {
Text(
text = title,
style = textStyle,
color = RainbowTheme.colors.s1,
modifier = Modifier.weight(1f, false)
)
// Icon
}
用途
设置view的布局,外观,行为
相当于LayoutParams
+ Background
+ Layout
+ Animation
+ Gesture
+ ..
developer.android.google.cn/jetpack/com…
类型安全机制
Xml 的方式如何确保view属性的类型安全?
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="20dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="睡眠音律问题"
// 编译器警告
android:layout_alignParentStart="true"
// 编译器不会报警告,但是实际是不生效的
android:maxWidth="100dp"
android:textAppearance="@style/Rainbow.TextAppearance.Body3"
android:textColor="@color/s1" />
<!--Image-->
</LinearLayout>
上述例子中,编译器能给出部分的检查警告⚠️,但不能涵盖所有的属性检查并警告。
在编写布局的时候,开发者通常根据语义设置对应的属性参数,比如maxWidth
,但结果却是不符合预期的。在日常的开发中往往会尝试不同的布局参数,这样给编写布局,增加了额外的记忆和调试成本。
在Android View 系统中,未实施任何类型安全机制。
Compose是如何做到强制的类型安全?
Row(
modifier = Modifier
.fillMaxWidth()
//Cannot access 'RowScopeInstance': it is internal in 'androidx.compose.foundation.layout'
.weight(1f,false)
.padding(start = 20.dp, top = 16.dp, end = 20.dp),
horizontalArrangement = Arrangement.SpaceBetween
){
Text(
text = title,
style = textStyle,
color = RainbowTheme.colors.s1,
modifier = Modifier.weight(1f, false)
)
Icon(
painter = painterResource(id = arrow),
tint = RainbowTheme.colors.s2,
contentDescription = "箭头图标",
)
}
Compose依赖Kotlin的动态语言特性,通过自定义作用域,强制实施类型安全机制
- 定义
RowScope
接口,接口中定义了Modifier
的扩展方法。
interface RowScope {
// ColumnScope 中定义的接口方法
@Stable
fun Modifier.weight(
/*@FloatRange(from = 0.0, fromInclusive = false)*/
weight: Float,
fill: Boolean = true
): Modifier
// ....
}
- 布局的content,是一个限定Scope的闭包。可以使用该Scope中定义的Modifier扩展方法,离开了该Scope,限定的方法就无法访问了。
@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
)
通过定义特定的作用域接口,使用扩展方法的方式,限定了特定属性作用域。
限定作用域的修饰符会将父项应知晓的关于子项的一些信息告知父项。这些修饰符通常称为“父项数据修饰符”。它们的内部构件与通用修饰符不同,但从使用角度来看,这些差异并不重要。
链式应用
一个modify的对象,可以从一个基础的修饰,通过不断地链式组合,发展成多种修饰样式
@Stable
fun Modifier._48(): Modifier = this.then(Modifier.defaultMinSize(100.dp, 48.dp))
@Stable
fun Modifier._44(): Modifier = this.then(Modifier.defaultMinSize(88.dp, 44.dp))
// 例如
fun Modifier.red_32_flat_circular():Modifier = this._32.then(Modifier.background(color = RainbowTheme.colors.p3,shape= ButtonCircleShape))
接口定义
modifiers的定义就是一条链式的组合应用关系。
interface Modifier {
// 折叠
fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
// 展开
fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
// 链式组合
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
}
接口的默认实现
// The companion object implements `Modifier` so that it may be used as the start of a
// modifier extension factory expression.
companion object : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
override fun any(predicate: (Element) -> Boolean): Boolean = false
override fun all(predicate: (Element) -> Boolean): Boolean = true
override infix fun then(other: Modifier): Modifier = other
override fun toString() = "Modifier"
}
默认接口的注释可以看出,设计者希望将一系列的Modifier设计成一个类似链表的结构,并且希望我们从Modifier的 companion实现开始进行构建链表。
链接
连接构建中的then
是构建链式的关键,需要看下CombinedModifier是如何做的。
class CombinedModifier(
private val outer: Modifier,
private val inner: Modifier
) : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
inner.foldIn(outer.foldIn(initial, operation), operation)
override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
outer.foldOut(inner.foldOut(initial, operation), operation)
override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
outer.any(predicate) || inner.any(predicate)
override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
outer.all(predicate) && inner.all(predicate)
override fun equals(other: Any?): Boolean =
other is CombinedModifier && outer == other.outer && inner == other.inner
override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()
override fun toString() = "[" + foldIn("") { acc, element ->
if (acc.isEmpty()) element.toString() else "$acc, $element"
} + "]"
}
每一个函数,都会对上一个函数返回的Modifier做修改。
顺序
您是否觉得Modifier的这种定义和使用方式,同协程中的CoroutineContext
有点类似呢?
// 接口的部分代码罗列
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext = ...
}
虽然它们有着类似的方法定义,但它们有着显著的一个区别:顺序
CoroutineContext
中的Element
是一种<Key,Value>的存储形式,互相没有顺序可言,这一点从plus操作符也能体现出来。
Modifier
是有着严格的顺序的,每一个函数,都会对上一个函数返回的Modifier
做修改。then(Modifier)
也显示着这点。
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.clickable(onClick = onClick)
.padding(padding)
.fillMaxWidth()
) {
// rest of the implementation
}
}
在上面的代码中,整个区域(包括周围的内边距)都是可点击的,因为
padding
修饰符应用在 clickable
修饰符后面。如果修饰符顺序相反,由 padding
添加的空间就不会响应用户输入:
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.padding(padding)
.clickable(onClick = onClick)
.fillMaxWidth()
) {
// rest of the implementation
}
}
从这点也能看出来,Compose是一个严谨的语义化的声明式的UI kit
作用域 VS 属性
我们在自定义Compose View的时候,这个Compose方法的参数设计上,到底是选择使用定义自定义的Scope,使用Modifier扩展函数的方式。还是定义属性类型,根据属性类型来设置View的Modifier?
CustomView(
// 为什么不定义一个CustomViewScope,内部定义Modifier.coontentBackground
//modifier = Modifier.background(RainbowTheme.colors.s10),
// 使用属性,定义content背景
backgroundColor = RainbowTheme.colors.s10
) {
// content
}
// 在RowScope中没有定义Modifier.horizontalArrangement(Arrangment)
// 而是使用属性参数的声明的方式?
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, top = 16.dp, end = 20.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
// ...
}
可能有下面的几个考虑因素
- Compose函数是否是一个容器。即是否支持传入可组合项?
- Compose函数是否足够复杂?简单的属性可以通过参数直接传入,如果过于复杂,则可以考虑使用定义Scope的Modifier扩展函数。
- 由于Modifier的扩展的方式,隐藏的层级较深。且在编译器提示的时候,排序是比一些通用的方法靠后的,不容易被用户发现。推荐使用直接传参的方式。
如何定义RainbowButton
// 代码举例
fun Modifier.red32FlatCircular():Modifier = this._32.then(Modifier.background(color = p3,shape= ButtonCircleShape))
Button(modifier = Modifier.red32FlatCircular())
OR
fun RainbowButton(
@StringRes text: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
style: ButtonStyle = red_48_flat
// 其他省略参数
)
class ButtonStyle(
val size: Int = 48,
private val style: Int = 0
)
RainbowButton(
enabled = enabled.value,
onClick = { /*TODO*/ } ,
text = "Button",
style = red_44_flat
)
是第一种方式,定义Modifier的扩展函数,给外部设置。还是使用第二种方式,定义ButtonStyle,内部去组装使用Modifier?
- RainbowButton不支持传入Compose可组合项。
- RainbowButton只是在原来Button的基础上通过modifier修改样式。
- 从API调用的关系上,使用ButtonStyle的设计,语义更加明确。
- 扩展的Modifier没有限定作用域,不适合暴露给外部使用。
最佳实践
提取和重用
// 定义可以重复使用的modifier
val itemModifier = Modifier
.padding(bottom = 12.dp)
.size(216.dp)
.clip(CircleShape)
// 在循环中重用,避免反复计算和创建
LazyColumn {
deviceAndIssue.issue.items.forEach {
IssueItem(modifier = itemModifier)
}
}