Modifier的基本使用
Modifier修饰符是Jetpack Compose中用来修饰组件的,提供常用的属性,写布局时几乎所有Composable组件的大部分属性都可以用Modifier 来修饰。官方在开发Compose UI时,最初尝试过将所有属性全部以函数参数的形式提供,但是那样太多了,他们也尝试过像Flutter那样的方式,将属性也作为一个组件进行嵌套,但这样又很容易让人感到困惑,所以才诞生了Modifier,将大部分组件常用属性封装成Modifier的形式来提供,哪个组件需要就在哪个组件上应用。我认为Modifier是Compose中最优秀的设计点之一。
@Composable
fun ModifierExample() {
Box(modifier = Modifier.size(200.dp)) {
// size同时指定宽高大小
Box(Modifier.fillMaxSize() // 填满父空间
.background(Color.Red))
Box(Modifier.fillMaxHeight() // 高度填满父空间
.width(60.dp)
.background(Color.Blue))
Box(Modifier.fillMaxWidth() // 宽度填满父空间
.height(60.dp)
.background(Color.Green)
.align(Alignment.Center))
Column(Modifier.clickable {
} // 点击事件
.padding(15.dp) // 外间距
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primary) // 背景
.border(2.dp, Color.Red, RoundedCornerShape(2.dp)) // 边框
.padding(8.dp) // 内间距
) {
Text(
text = "从基线到顶部保持特定距离",
modifier = Modifier.paddingFromBaseline(top = 35.dp))
Text(
text = "offset设置偏移量",
modifier = Modifier.offset(x = 14.dp) // 正offset会将元素向右移
)
}
}
}
部分Modifier属性只能在特定组件的作用域范围内才能使用,避免了像传统xml布局中的属性那样对自身没有用的属性也能被写出来造成污染。例如 Modifier.matchParentSize() 只有在 Box 组件范围内才能使用:
Box(modifier = Modifier.size(200.dp)) {
Text(
text = "aaa",
modifier = Modifier
.align(Alignment.Center)
.matchParentSize() // matchParentSize 仅在 BoxScope 中可用
)
}
观察源码发现 Modifier.matchParentSize() 与 Modifier.align() 被定义在了BoxScope接口的内部,所以只能在Box的lambda中使用,该lambda函数的类型是 @Composable BoxScope.() -> Unit,可见其定义了Receiver是BoxScope
interface BoxScope {
@Stable
fun Modifier.align(alignment: Alignment): Modifier
@Stable
fun Modifier.matchParentSize(): Modifier
}
可以在 Row 和 Column 中使用Modifier.weight,类比传统线性布局中的layout_weight属性,并且仅可在 RowScope 和 ColumnScope 中使用。
@Composable
fun ArtistCard() {
Row(
modifier = Modifier
.fillMaxWidth()
.size(150.dp)
) {
Image(
painter = painterResource(id = R.drawable.ic_sky),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.weight(2f) // 占比2/3
)
Column(
modifier = Modifier.weight(1f) // 占比1/3
) {
Text(text = "Hello", style = MaterialTheme.typography.titleSmall)
Text(text = "Compose", style = MaterialTheme.typography.bodyMedium)
}
}
}
点击事件相关的Modifier属性:
Column{
Box(Modifier
.clickable {
println("clickable") }
.size(30.dp)
.background(Color.Red))
Box(Modifier
.size(50.dp)
.background(Color.Blue)
.combinedClickable(
onLongClick = {
println("onLongClick") },
onDoubleClick = {
println("onDoubleClick") },
onClick = {
println("onClick") }
))
Box(Modifier
.size(50.dp)
.background(Color.Green)
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
},
onLongPress = {
},
onPress = {
},
onTap = {
})
detectDragGestures(
onDragStart = {
},
onDragEnd = {
},
onDragCancel = {
},
onDrag = {
change, dragAmount -> }
)
})
}
Modifier的复用
可以通过定义扩展函数复用常用的Modifier属性配置:
fun Modifier.redCircle(): Modifier = clip(CircleShape).background(Color.Red)
使用:
Column {
Box(Modifier.size(80.dp).redCircle())
}
可以提取和复用同一修饰符实例,并将其传递给可组合项,避免在每一帧重组中创建大量对象:
val reusableModifier = Modifier
.padding(12.dp)
.background(Color.Gray)
@Composable
fun LoadingWheelAnimation() {
val animatedState = animateFloatAsState(...)
LoadingWheel(
// No allocation, as we're just reusing the same instance
modifier = reusableModifier,
animatedState = animatedState.value
)
}
提取和复用未限定作用域的修饰符
修饰符可以不限定作用域,也可以将作用域限定为特定可组合项。对于未限定作用域的修饰符,可以从任何可组合项之外提取它们作为简单变量:
val reusableModifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(12.dp)
@Composable
fun AuthorField() {
HeaderText(
// ...
modifier = reusableModifier
)
SubtitleText(
// ...
modifier = reusableModifier
)
}
与延迟布局结合使用时,这尤为有用。在大多数情况下,建议对所有潜在的重要项目使用完全相同的修饰符:
val reusableItemModifier = Modifier
.padding(bottom = 12.dp)
.size(216.dp)
.clip(CircleShape)
@Composable
private fun AuthorList(authors: List<Author>) {
LazyColumn {
items(authors) {
AsyncImage(
// ...
modifier = reusableItemModifier,
)
}
}
}
提取和复用限定作用域的修饰符
在处理作用域限定为特定可组合项的修饰符时,您可以将其提取到尽可能高的级别,并在适当的情况下重复使用:
Column(...) {
val reusableItemModifier = Modifier
.padding(bottom = 12.dp)
.align(Alignment.CenterHorizontally)
.weight(1f)
Text1(
modifier = reusableItemModifier,
// ...
)
Text2(
modifier = reusableItemModifier
// ...
)
// ...
}
注意:只能将提取的限定作用域的修饰符传递给限定相同作用域的直接子项
例如:
Column(modifier = Modifier.fillMaxWidth()) {
// Weight modifier is scoped to the Column composable
val reusableItemModifier = Modifier.weight(1f)
// Weight 可以在这里正常应用因为 Text 是 Column 的一个直接子项
Text(modifier = reusableItemModifier
// ...
)
Box {
// Weight 在这里不起作用,因为当前 Text 不是 Column 的直接子项
Text(modifier = reusableItemModifier
// ...
)
}
}
延长提取Modifier链
您可以通过调用 .then() 函数进一步链接或附加提取的Modifier链:
val reusableModifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(12.dp)
// Append to your reusableModifier
reusableModifier.clickable {
… }
// Append your reusableModifier
otherModifier.then(reusableModifier)
Modifier的分类
Modifier有很多属性,这些属性属于不同类型的Modifier,每种类型的Modifier负责处理一类的功能,就常用的属性而言可以分成LayoutModifier和DrawModifier,如size、padding等背后的实现是基于LayoutModifier,而background、border等背后的实现是基于DrawModifier。
Modifier的分类如下:
Modifier的自定义
Modifier.composed 自定义
Modifier.composed 是一种可以支持有状态的 Modifier,可以将很多行为延时到重组后执行,而不是状态变化后立即执行,例如:
// 点击的时候添加一个边框
fun Modifier.addBorderOnClicked() = composed {
var width by remember {
mutableStateOf(0.dp) }
when(width) {
0.dp -> Modifier
else -> Modifier.border(width, Color.Red)
}.then(
Modifier
.padding(5.dp)
.clickable {
width = 1.dp }
)
}
使用:
Column {
Text("ccccccccccccc", Modifier.addBorderOnClicked())
Text("ddddddd", Modifier.addBorderOnClicked())
}
效果:
composed{…} 会使用 工厂函数 创建一个新的 Modifier 对象 , 它会在重组的时候被调用, composed与普通Modifier属性的区别是其状态是独享的,在重组运行时才生效,因为其factory参数是一个Composable函数 @Composable Modifier.() -> Modifier,所以在{…}中可以使用remember,可以把它当成一个Composable组件。
可以运行下面的例子,来感受它和普通Modifier的不同:
@Composable
fun ComposedBackgroundExample() {
Column(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy((8.dp))
) {
var counter by remember {
mutableStateOf(0) }
Button(
onClick = {
counter++ },
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Increase $counter")
}
Text("Modifier.composed")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Box(Modifier
.composedBackground(150.dp, 20.dp, 0)
.width(150.dp)) {
Text(text = "Recomposed $counter")
}
Box(Modifier
.composedBackground(150.dp, 20.dp, 1)