Modifier是Compose中的重要概念,能够让UI呈现更加专业、好看的视觉效果。
1. 为什么使用Modifier?
常规的View体系中,控件以实例对象的形式存在,控件可以在实例化之后再动态配置属性,但是Composable本质上是函数,只能在调用的同时通过参数传递进行配置,如果没有Modifier,参数签名会变得很长(虽然Kotlin支持默认参数)。
使用Modiifer可以很好地解决这个问题,它就像Composable的配置文件,可以在此对Composable的样式和行为进行统一配置。
2. Modifier是一组有序的链式调用
Modifier通过链式调用“装饰”我们的Composable,为其添加Background
、Padding
、onClick
事件等。
链上的每个操作符都创建一个Element
,整个调用链是一组Element的有序执行单元:
Text(
"Hello, World!",
Modifier.padding(16.dp) // Outer padding; outside background
.background(color = Color.Green) // Solid element background color
.padding(16.dp) // Inner padding; inside background, around text
)
如上,调用链上的两个padding
不是覆盖关系,而是按照顺序发挥作用。
padding创建PaddingModifier
,
class PaddingModifier(val start: Dp, val top : d'p...) : Modifier.Element
fun Modifier.padding(all: Dp) = this.then(PaddingModifier(start = all, top = all, xxx))
then
用来组合两个Modifier并保持顺序执行。
open infix fun then(other: Modifier): Modifier
Modifier类似RxJava的Observable,基于 函数式编程思想,将操作符串联成一组可执行函数,在Composable渲染的时候才执行。
3. 使用Modifier装饰Composable
Modifier的操作符(API)虽然数量多,但是语义明确,上手不难。下面通过一个例子带大家体验一下如何使用Modifier装饰我们的Composable。
我们试着用Compose实现一个关注列表的Item,如下
@Composable
fun Plain() {
Row(modifier = Modifier.fillMaxWidth()) {
Image(
modifier = Modifier.size(40.dp),
bitmap = imageResource(id = R.drawable.miku),
contentDescription = null, // decorative
)
Column(modifier = Modifier.weight(1f)) {
Text(text = name, maxLines = 1)
Text(text = desc, maxLines = 1)
}
Text("Follow", Modifier.padding(6.dp))
}
}
接下来,我们一步步通过Modifier对其进行装饰,在文章最后,UI将达到下面第二张图片的效果。
3.1 整体布局
Modifier.padding
我们使用Row
、Column
对Item内的元素进行了基本布局,但是元素之间缺少间距
Compose通过Modifier在Composable之间添加Padding
@Composable
fun Decorated() {
Row(
modifier = Modifier
.fillMaxWidth()
.preferredHeightIn(min = 64.dp)
.padding(8.dp) //外间隙
.border(1.dp, Color.LightGray, RoundedCornerShape(4.dp))
.padding(8.dp) //内间隙
) {
...
}
}
如上,我们对Item整体添加Padding。border
前后各有一个padding
,分别表示对外和对内的间距。相对于传统布局有Margin
和Padding
区分,Modifier中只有padding
,根据调用链中的位置不同发挥不同作用,使用更加简单。
Modifier.border
border
用来定义边框,RoundedCornerShape
是一个Shape类型,用来指定边框的形状为圆角矩形。
我们还可以调用两次background
来实现border的效果:
modifier = Modifier
.background(Color.LightGray)
.padding(1.dp) //两个backgound之间形成边框
.background(Color.White)
Modifier.preferredHeight / Modifier.preferredHeightIn
preferedXXX
等用来设置初始的size
,例如preferedHeight
可以设置Composable的默认高度,这个值可能被其他约束覆盖,若想要高度不被覆盖,就使用Modifier.height
设置固定值
本例中使用preferedHeightIn
,可以设置minHeight
和maxHeight
。
Modifier.fillMaxWidth
fillMaxWidth
表示填充整个父容器,相当于传统布局的match_parent
3.2 参数中传入Modifier
填充Row
中的内容,从左往右依次是,头像、文字、按钮
@Composable
fun Decorated() {
Row(
modifier = Modifier
.fillMaxWidth()
.preferredHeight(64.dp)
.padding(8.dp)
.border(1.dp, Color.LightGray, RoundedCornerShape(4.dp))
.padding(8.dp)
) {
Avatar( //头像部分
modifier = Modifier
.padding(4.dp)
.align(Alignment.CenterVertically)
)
Info( //文字部分
Modifier
.weight(1f)
.align(Alignment.CenterVertically)
)
FollowBtn( //按钮
Modifier.align(Alignment.CenterVertically)
)
}
}
我们将具体实现抽成独立的Composable,在Row中调用并传入Modifier。
在Compose中定义Composable时,为Modifier预留参数位置是一个好习惯
Modifier为调用方提供了修改子元素样式的机会,但更重要的是有一些操作符只能在外部调用。
Modifier.align
Modifier的操作符都是扩展函数,并不是定义在一起。操作符定义在不同的空间中,可以限制某些操作符只能在特定父Comopsable中使用,避免误用。
interface RowScope {
fun Modifier.align(alignment: Alignment.Vertical)
}
如上,align
只能在Row中调用,用来设置子元素在垂直方向如何对齐。子元素不关心其在父容器中如何对齐,因此在外部设置align(Alignment.CenterVertically)
后,传给子元素继续使用。
Modifier.weight
weight
同样只能在Row中调用,为子元素分配在Row中的占比,类似于LinearLayout
的layout_weight
。本例中让中间的文字部分占据所有所有空间
3.3 头像图片
我们对头像图片做圆形处理并添加边框,提升整体视觉效果。
@Composable
fun Avatar(modifier: Modifier) {
Image(
modifier = modifier
.size(50.dp)
.clip(CircleShape)
.border(
shape = CircleShape,
border = BorderStroke(
width = 2.dp,
brush = Brush.linearGradient(
colors = listOf(blue, teal200, green200, orange),
start = Offset( 0f, 0f),
end = Offset(100f,100f)
)
)
)
.border(
shape = CircleShape,
border = BorderStroke(4.dp, SolidColor(Color.White))
),
bitmap = imageResource(id = R.drawable.miku),
contentDescription = null, // decorative
)
}
Modifier.size
首先size(50.dp)
设置图片的整体大小
Modifier.clip
clip
用来将图片裁剪成指定形状,例子中clip(CircleShape)
将图片裁剪成圆形
Modifier.border调用顺序
图片的边框由两部分组成,外层带颜色的部分,和内层的白色边框,因此调用链中出现了两个border()
。两个border的调用顺序需要特备注意,border表示为后面的调用添加边框,所以在前面调用的后生效。所以例子中的border调用顺序如下:
Modifier
.border() //2dp 颜色边框
.border() //4dp 白色边框
BorderStroke & Brush
border使用BorderStroke
填充边框颜色。
外边框使用Brush.linearGradient
填充多种颜色组成的渐变色,start
和end
表示颜色范围
BroderStroke(
brush = Brush.linearGradient(
colors = listOf(blue, teal200, green200, orange),
start = Offset( 0f, 0f),
end = Offset(100f,100f)
)
)
内边框使用SolidColor
填充固定颜色
BorderStroke(brush = SolidColor(Color.White))
3.4 文字部分
@Composable
fun Info(modifier: Modifier) {
Column(modifier = modifier) {
Text(
text = name,
color = Color.Black,
maxLines = 1,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
letterSpacing = 0.15.sp
)
)
Text(
text = desc,
color = Color.Black.copy(alpha = 0.75f),
maxLines = 1,
style = TextStyle( // here
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
letterSpacing = 0.25.sp
)
)
}
}
许多字体的样式不借助Modifier,而是通过Text自身的属性以及TextStyle
设置
文字颜色
color
设置文字颜色,Compose的Color
类功能强大, 例如这里可以设置透明度:Color.Black.copy(alpha = 0.75f)
TextStyle
TextStyle可以设置字体、字号等,例子中通过fontWeight
设置了粗体
textDecoration
虽然本例中没有使用,但是Text还有一个重要属性textDecoration
,对文字进行更有针对性的“装饰”,例如添加下划线、删除线等
textDecoration = TextDecoration.Underline
3.5 按钮
虽然有Compose提供了专门的Button
实现按钮,使用Text同样可以实现按钮,而且可定制性更高。
@Composable
fun FollowBtn(modifier: Modifier) {
val backgroundShape: Shape = RoundedCornerShape(4.dp)
Text(
text = "Follow",
style = typography.body1.copy(color = Color.White),
textAlign = TextAlign.Center,
modifier =
modifier
.preferredWidth(80.dp)
.clickable(onClick = {})
.shadow(3.dp, shape = backgroundShape)
.clip(backgroundShape)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Red500,
orange700,
),
startY = 0f,
endY = 80f
)
)
.padding(6.dp)
)
}
Modifier.background
为按钮添加了渐变的背景色以及阴影后,显得更加拟物、有质感background
中同样通过Brush
添加渐变色
Modifier.shadow
shadow
添加阴影,需要特别shadow在调用链中的位置,阴影本身也是占用面积的,所以要在background之前调用,避免阴影进入背景色区域中
Modifier.click
Text之所以可以替代Button实现按钮,是因为Modifier提供了click
,可以让Composable处理onClick
事件
4. 最后
通过上面的例子,相信大家已经掌握了Modifier的基本使用方式,Modifier还有很多高级的API,后续有机会陆续分享给大家。Modifier在设计上吸取了装饰器模式、FP等多种编程思想,思路巧妙,值得大家学习。
<br/>
【参考】