稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

降Compose十八掌之『见龙在田』| Modifier

通过前面的文章我们学会了如何使用元素来构建和填充我们的UI页面,但这只完成了一半,元素还需要装饰,以及进行动画和事件响应,这才能生成完整的UI。这就要用到Modifier,Jetpack Compose中的灵魂,它被用来装饰和增强Composables,让一个个平凡的元素变成鲜活的,好看的,可交互UI。我们来具体的看一下Modifier的使用方法。

对于Compose来说,每一个元素叫做Composable,它是一个函数,比如前面学过的布局(如Row)和小部件(如Text)等都是一个Composable,可以把它理解成为一个元素。

概念和基本使用方法

Modifier之于元素,犹如CSS之于HTML,但能做的更多,因为除了样式装饰以外,Modifier还能做很多事情,比如响应用户输入。

每一个Composable都可以接收一个Modifier参数,准确地说第一个参数都是Modifier,可以把一个Modifier对象作为第一个参数传给元素;也可以用命名参数如modifier = Modifier.padding(8.dp)。Modifier支持链式调用,它的每个方法都会把当前对象返回:

1
2
3
4
5
6
7
8
9
10
11
@Composable
private fun Greeting(name: String) {
    Column(
        modifier = Modifier
            .padding(24.dp)
            .fillMaxWidth()
    ) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

装饰元素的样式

重点来看一下如何用Modifier装饰元素的样式。需要注意的是Modifier是装饰元素的共性样式如尺寸,背景,边框和位移等等,而像具体元素特性,如Text中的文本样式,是无法用Modifier来修改的。

尺寸Size

尺寸对于一个UI元素来说是必要的,就是渲染之后的视觉上的宽度和高度。可以通过Modifier的下列函数来进行尺寸约束:

  • width.width(androidx.compose.ui.unit.Dp))/height.height(androidx.compose.ui.unit.Dp)) - 指定固定的偏好宽度和偏好高度,参数传入具体的数值如Modifier.width(320.dp).height(480.dp),就是指定某个元素的宽度是320dp,高度是480dp。偏好(preferred)的意思是可能会被其他约束条件覆盖,而最终值可能会不一样。
  • size.size(androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp)) - 同时指定宽度和高度为某一偏好的数值,只传一个参数就是一个正方形,如Modifier.size(100.dp),等同于Modifier.width(100.dp).height(100.dp);传两个参数时分别指定宽和高,如Modifier.size(320.dp, 480.dp)等同于Modifier.width(320.dp).height(480.dp)。
  • wrapContentWidth.wrapContentWidth(androidx.compose.ui.Alignment.Horizontal,kotlin.Boolean))/wrapContentHeight.wrapContentHeight(androidx.compose.ui.Alignment.Vertical,kotlin.Boolean))/wrapContentSize.wrapContentSize(androidx.compose.ui.Alignment,kotlin.Boolean)) - 让元素自己决定尺寸,无视最小尺寸限制(minimum_wdith/minimum_height),类似于XML中的wrap_content,这是比较严格的限制。
  • requiredWidth.requiredWidth(androidx.compose.ui.unit.Dp))/requiredHeight.requiredHeight(androidx.compose.ui.unit.Dp)) - 指定宽度和高度必须为某一数值,不可以被其他限制约束覆盖。带有In的函数可以指定一个范围。
  • requiredSize.requiredSize(androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp)) - 指定宽度和高度必须为某一数值,带有In的函数可以指定范围。
  • widthIn.widthIn(androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp))/heightIn/sizeIn.sizeIn(androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp)) - 带有in的函数,可以指定一个范围而非具体数值,比如Modifier.widthIn(10.dp, 100.dp),就是说限制此元素的宽度为10dp到100dp之间。
  • fillMaxWidth.fillMaxWidth(kotlin.Float))/fillMaxHeight.fillMaxHeight(kotlin.Float)) - 不固定具体的数值,按比例填充最大可用空间,比例为1.0时填满,类似于XML中的match_parent。
  • fillMaxSize.fillMaxSize(kotlin.Float)) - 按比例填充满可用空间。
  • weight.weight(kotlin.Float,kotlin.Boolean)) - 权重比例,仅在父布局是Row或者Column时,且尺寸与父布局约束一致时有效,比如在Row中,对width生效,在Column中对height生效。最终的占比是『权重比例 x 可用空间』,不指定weight则weight是0,比如一个Row中,有三个元素,其中两个元素A和B指定了weight为1和2,另一个没指定,那么A将占Row中剩余可用宽度的1/3,B将占2/3。与View中的LinearLayout中的权重是差不多的。

Modifier修饰尺寸的函数比较多,容易学杂了,需要梳理一下:size相当于快捷方式,可以同时约束宽和高;带有In的函数可以为某个约束指定范围。宽高是一种限制性约束,不同的函数的限制严格性是不一样的,带有required是最严格的限制约束,优先级最高,如有冲突,以它为准;wrapContentWidth/wrapContentHeight/wrapContentSize是较严格的限制,仅次于required;width/height/size是中等严格,较wrapContent再次之;weight再次之;fillMax则是最弱的限制。

间隔

间隔(padding)是在元素与其边界之间添加的额外的空白空间,Modifier.padding.padding(androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp))不影响元素的尺寸,它是在测量之前就应用生效。

对于有XML经验的同学来说,以往的间隔有两个,一个是margin控制着元素边界之外的间隔,padding控制着边界与元素本身内容间隔。对于Modifier来说,只有一个函数,在不同的位置调用padding函数会有不同的效果,如果在尺寸之后调用padding,则是调整边界与内容之间的间隔,如果是在尺寸之前调用,则是调整边界与外部的间隔。另外,就是padding不可以传负值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
fun PaddingDemo(modifier: Modifier = Modifier.fillMaxSize()) {
    Box(modifier = Modifier
        .fillMaxSize()
        .background(Color.LightGray)) {
        Text(
            text = "降龙十八掌",
            style = MaterialTheme.typography.headlineLarge,
            modifier = Modifier
                .padding(16.dp) // As margin: outside space beyond border
                .background(Color.Cyan)
                .size(360.dp, 120.dp)
                .padding(10.dp) // As padding: space between border and content
        )
    }
}

padding demo

位移Offset

函数Modifier.offset.offset(kotlin.Function1))用来给水平和垂直方向加一个位移,数值可正可负,注意仅是增加位移,并不会改变元素的尺寸。与View中的translateX和translateY是类似的。它有两个函数,只有一个参数时是水平和垂直方向都加上相同的位移;两个参数时是分别指定水平方向和垂直方向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
fun ModifierDemo(modifier: Modifier = Modifier.fillMaxSize()) {
    var offset by remember { mutableStateOf(0) }
    Column(
        modifier = modifier
            .padding(8.dp)
            .clickable { offset += 8 }
    ) {
        Text(
            text = "降龙十八掌",
            style = MaterialTheme.typography.headlineLarge,
            color = MaterialTheme.colorScheme.primary,
            modifier = Modifier
                .offset(offset.dp, offset.dp)
                .background(Color.Cyan)
                .padding(16.dp)
        )
    }
}

offset demo

因为offset并不改变元素的尺寸,仅是在原位置上进行偏移,所以多用于点击效果,或者点击动画。

背景Background

使用Modifier.background.background(androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Shape))函数来修改元素的背景颜色,唯一需要注意的是padding的影响,background是给尺寸所指定的区域加背景色,所以padding调用的位置会有影响。

边框Border

Modifier.bodrer.border(androidx.compose.foundation.BorderStroke,androidx.compose.ui.graphics.Shape))函数可以指定边框的样式,如形状,粗细,线条和颜色。需要注意的是它也是跟尺寸一样的,受padding的影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Composable
fun BorderWithShape(modifier: Modifier = Modifier.fillMaxSize()) {
    Box {
        Text(
            text = "降龙十八掌",
            style = MaterialTheme.typography.headlineLarge,
            modifier = Modifier
                .padding(10.dp)
                .border(2.dp, SolidColor(Color.Green), RoundedCornerShape(20.dp))
                .padding(10.dp)
        )
    }
}

border demo

变幻

Modifier还能对元素进行一些变幻,如透明度(Alpha),旋转(Rotate)和缩放(Scale)。通常用来实现一些非动画的静态特效。

Modifier.alpha.alpha(kotlin.Float))指定透明度,0是透明,1是完全不透明,默认值是1.0。

Modifier.rotate.rotate(kotlin.Float))实现旋转,参数是一个角度,顺时针旋转为正值,逆时针旋转为负值,默认值是0度。

Modifier.scale.scale(kotlin.Float,kotlin.Float))是以元素的几何中心为中心点进行缩放负值会进行水平和垂直方向翻转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Composable
fun TransformationDemo(modifier: Modifier = Modifier.fillMaxSize()) {
    Box(
        modifier = Modifier.fillMaxSize()
            .background(Color.LightGray)
    ) {
        Text(
            text = "降龙十八掌",
            style = MaterialTheme.typography.headlineLarge,
            color = MaterialTheme.colorScheme.primary,
            modifier = Modifier
                .padding(16.dp)
                .alpha(0.618f)
                .rotate(11.8f)
                .scale(0.618f)
                .background(Color.Cyan)
                .size(360.dp, 120.dp)
                .offset(10.dp, 10.dp)
        )
    }
}

transformation demo

事件监听

Modifier.clickable.clickable(kotlin.Boolean,kotlin.String,androidx.compose.ui.semantics.Role,kotlin.Function0))函数用来指定点击事件响应。此外,还可以用于指定元素是否可以点击。

另外,还可以通过Modifier.scrollable.scrollable(androidx.compose.foundation.gestures.ScrollableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.gestures.FlingBehavior,androidx.compose.foundation.interaction.MutableInteractionSource))来指定元素是否可以滑动。

最佳实践

Modifier是非常强大的,也是非常复杂的,前面列出的都是最为常用的一些函数。接下再来学习一下使用Modifier时需要注意的事项。

顺序很重要

Modifier有很多很多函数,修改着同一个对象实例,有些函数会相互影响,因此这些函数的调用顺序就变得相当重要,特别是涉及尺寸强相关的装饰特性时,如background和border,它们会受到padding以及变幻的影响。比如前面变幻小节的例子,可以试着修改函数的调用顺序,就会发现结果会不一样。在实例使用时,如果出现与预期不一致的结果时,就尝试调整一下Modifiier函数的顺序,看是否是顺序 导致的。

留意上下文

在Compose中,有一些Modifier函数只能在特定的元素中使用,这就涉及了Compose上下文(Scope)。比如说像Modifier.align.align(androidx.compose.ui.Alignment))只能在BoxScope中使用,也就是说只能在Box的子元素中使用。对其他任何布局来说都是不能用的,所以在使用的时候也要注意元素所在的父布局。

尽可能的复用

每个元素在渲染的时候都需要一个Modifier对象,通常情况下都是通过Modifier的函数进行对象创建。但对于一些循环的场景,且Modifier对象没啥变化 时,这时就应该复用对象,而非每次都创建。比如说动画,以及像集合性布局的子布局,这时都应该在其父布局缓存Modifier对象,直接传给子元素,而不是让其每次都创建新对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
val modifier = Modifier
        .padding(12.dp)
        .background(Color.Gray)
@Composable
fun LoadingWheelAnimation() {
    val animatedState = animateFloatAsState(/*...*/)

    LoadingWheel(
        // 把Modifier对象缓存到上一级的父布局中,以免每帧动画都创建一个Modifier对象
        modifier = reusableModifier,
        animatedState = animatedState
    )
}

对于集合性布局也最好是能复用Modifier对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,
            )
        }
    }
}

保持一致性

一致性对于代码的可维护性和可扩展性是非常重要的,因为每个元素都需要Modifier对象,在Compose中到处都可以看到Modifier,在实际使用中保持一致性就非常重要。比如说Modifier要作为Composable的第一个参数,参数的命名应该是modifier,并且最好要有默认值,可以查阅Compose本身的代码,可以发现其所有的元素都遵循此约定。另外,就是对于共性的装饰要提升到父布局中进行统一设定,比如说根布局统一设定padding,而不是每个子布局进行分别设定,等等。

参考资料

Comments