Jetpack Compose中Modifier方法介绍

前言

在 Compose 中,Modifier 的调用顺序是有影响的。

修饰符列表

https://android-dot-google-developers.gonglchuangl.net/jetpack/compose/modifiers-list?hl=zh-cn

状态栏和底部导航栏

顶部状态栏

1
.statusBarsPadding()

底部导航栏

1
.navigationBarsPadding()

尺寸设置

固定大小

layout_width & layout_height => Modifier.size() or (Modifier.width() & Modifier.height())

size: 用于设置组件的固定大小。

1
2
3
4
5
Modifier.size(width = 100.dp, height = 100.dp)

Modifier.size(100.dp)

Modifier.width(300.dp).height(200.dp)

自身大小

默认是 wrap_content

适配父大小

match_parent =>

1
2
3
.fillMaxWidth()
.fillMaxHeight()
.fillMaxSize()

fillMaxHeight() 修饰符的行为是占据其父组件分配给它的最大可用高度

具体表现取决于父组件的布局特性和其他同级组件的布局情况:

  1. 如果父组件是一个Column(垂直布局容器):

    当 Column 使用默认的 Arrangement.Top 时,fillMaxHeight() 会让当前组件占据 Column 剩余的全部高度(即父组件高度减去前面同级组件占用的高度不考虑后面的组件

    当 Column 使用 Modifier.fillMaxHeight() 且设置 verticalArrangement = Arrangement.SpaceEvenly 等分配方式时,会根据排列规则分配高度

  2. 如果父组件是Box(层叠布局):

    • fillMaxHeight() 会让组件直接占据 Box 的全部高度,不受其他同级组件的影响(因为 Box 中的组件是层叠关系而非顺序排列)

剩余空间(权重)

权重只能在Row和Column中使用。

注意有多个元素,其中一个元素要占用剩余所有空间,这时候最好用.weight(1f)

因为

.fillMaxSize()计算的时候是之前元素的剩余空间,如果有三个元素,中间的元素就会占用除下第一个元素后的所有空间,第三个元素就没法显示了。

所以只有元素是最后一个元素的时候才能使用.fillMaxSize(),为了不出岔子都建议使用.weight(1f)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Column(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.navigationBarsPadding()
.padding(start = CommonTheme.DpXl, end = CommonTheme.DpXl)
) {
// 顶部菜单
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.background(Color.Red)
) {

}
Spacer(Modifier.height(16.dp))
// 中部占用剩余空间
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.background(Color.Green)
) {

}

// 下部按钮
Spacer(Modifier.height(16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(46.dp)
.background(Color.Blue)
) {

}
Spacer(Modifier.height(16.dp))
}

背景

基本

1
Modifier.background(Color.Green)

背景渐变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.clip(mShape)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xff8AEFFD),
Color(0xff3BD9FD),
Color(0xff32C8FD),
Color(0xff36B3FE),
Color(0xff50B8FF),
), // 定义垂直渐变色
startY = 0f,
),
shape = mShape
)

水平渐变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Box(
Modifier
.width(136.dp)
.height(36.dp)
.clip(CommonTheme.CornerFull)
.background(
brush = Brush.horizontalGradient(
colors = listOf(
Color(0xff0085F7),
Color(0xff30E5FC),
), // 定义垂直渐变色
startX = 0f,
),
),
contentAlignment = Alignment.Center
) {
TextColorSizeComp("历史报告", Color.White, 16.sp)
}

渐变分割线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Composable
private fun LineVComp() {
Spacer(
modifier = Modifier
.width(0.5.dp)
.height(24.dp)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0x00ffffff),
Color(0xffffffff),
Color(0x00ffffff),
), // 定义垂直渐变色
startY = 0f,
),
)
)
}

内外边距和背景

在 Compose 中,背景色使用 Modifier.background() 进行设置。

在 Compose 中,Margin 和 Padding 都用 Modifier.padding() 来设置。

  • 没有background的时候是外边距
  • background的时候在background之前的是外边距,在background之后是内边距

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 背景色不包括 padding 的部分,效果类似 margin
Text(text = "Compose 学习", modifier = Modifier
.padding(8.dp)
.background(Color.Green))

// 背景色包括 padding 的部分,效果类似 padding
Text(text = "Compose 学习", modifier = Modifier
.background(Color.Green)
.padding(8.dp))

// 同时设置了 padding 和 margin 的效果
Text(
text = "Compose 学习", modifier = Modifier
.padding(8.dp)
.background(Color.Green)
.padding(8.dp)
)

background 还可以传入 shape 参数,来设置不同的背景形状。

Shape 对象也是一个通用的能力,例如,可以用于 clip 当中,进行裁切。

阴影

示例

背景不能是半透明,阴影可以半透明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Box(
modifier = Modifier
.size(98.dp)
.shadow(
elevation = 8.dp, // 阴影半径/高度
shape = CircleShape, // 阴影形状(与Box形状匹配)
clip = true, // 是否裁剪内容到形状范围内
spotColor = Color(0x660F5FFF)
)
.background(Color.White),
contentAlignment = Alignment.Center
) {

}

注意

shadow设置要在尺寸之后,要在background之前。

不要和clip搭配,会把阴影剪裁掉。

设置shadow会自动剪裁背景,所以背景只用设置颜色就行不用设置形状。

如果没有空间显示阴影,要设置padding,这样会给阴影预留空间。

背景不能是半透明的会导致阴影非常难看。

参数说明

ambientColorspotColor 是用于更精细控制阴影效果的两个参数,主要用于 Modifier.shadow()

它们的作用如下:

  1. ambientColor(环境光阴影颜色)
    模拟环境光照射物体产生的「扩散阴影」,通常表现为范围较广、颜色较浅的阴影部分。
    它代表了周围环境散射光形成的阴影,给人一种柔和、均匀的阴影效果。
    默认值通常是带有透明度的黑色(如 Color.Black.copy(alpha = 0.1f)),可根据需求自定义。
  2. spotColor(点光源阴影颜色)
    模拟点光源(如单一方向的强光)照射物体产生的「聚焦阴影」,通常表现为范围较窄、颜色较深的阴影部分。
    它代表了定向光源形成的阴影,能增强物体的立体感和深度感。
    默认值通常是比 ambientColor 更深的透明黑色(如 Color.Black.copy(alpha = 0.2f))。

Card阴影

当我们不需要调整阴影颜色,又想快速有阴影效果,可以使用Card。

1
2
3
4
5
6
7
8
9
10
11
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
shape = CircleShape,
colors = CardDefaults.cardColors(
containerColor = Color.White,
),
modifier = Modifier
.size(98.dp),
) {

}

Card本身没有调整阴影颜色的属性,虽然可以使用Modifier来调整,但是没有意义,不如不用Card。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
),
modifier = Modifier
.padding(10.dp)
.size(140.dp, 140.dp)
.shadow(
elevation = 6.dp, // 阴影强度(值越大越扩散)
shape = RoundedCornerShape(16.dp), // 与Card形状保持一致
spotColor = Color(0xaaCCE7FF)
)
) {

}

offset

offset: 用于将组件从其默认位置移动指定的偏移量。

偏移的元素不会影响后续元素的位置,相当于只是视觉上位置变了,实际位置没变。

1
Modifier.offset(x = 20.dp, y = 20.dp)

pading和offset

先看一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.offset(y = (-100).dp),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Black)
)

Box(
modifier = Modifier
.padding(20.dp)
.size(100.dp)
.background(Color.Red)
.padding(20.dp) // 内边距,内容会向内收缩
)

Box(
modifier = Modifier
.offset(y = 20.dp)
.size(100.dp)
.background(Color.Green)
.padding(20.dp) // 内边距,内容会向内收缩
)

Box(
modifier = Modifier
.size(100.dp)
.padding(20.dp)
.background(Color.Blue)
.padding(20.dp) // 内边距,内容会向内收缩
)
}

如上

红色和绿色的图形都偏移了相同的位置,并且大小是一样的。

padding是比较特殊的,它放在不同的位置的含义是不一样的

  • 在size前不会影响组件大小和背景一样大、相当于css中的margin。
  • 在size后背景前会压缩背景的大小。
  • 在背景后相当于css中的padding。
  • 当作用是margin的时候会影响后续元素的位置。

offset相当于偏移

  • 在Box中偏移值就相当于绝对定位。
  • 在Row/Column中会相对于原位置偏移。
  • 偏移的元素不会影响后续元素的位置。

裁剪

clip: 用于裁剪组件的内容,以匹配指定的形状。

示例1

1
Modifier.clip(shape = CircleShape)

注意

剪裁要放在background之前,否则背景不会被剪裁。

示例2

Box中的AndroidView直接作为相机的渲染载体,那么填充父的大小就会失效,这时候我们要在父组件中设置剪裁,才能达到预期效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = leftRightSpace, end = leftRightSpace)
.clip(RectangleShape)
) {
AndroidView(
factory = {
previewView
},
modifier = Modifier
.fillMaxSize()
)
}

边框(border)

border: 用于向组件添加边框。

1
Modifier.border(width = 2.dp, color = Color.Black)

顺序

1
2
3
.clip(RoundedCornerShape(16.dp))
.background(Color.White.copy(0.5f),RoundedCornerShape(16.dp))
.border(1.dp, Color.White, RoundedCornerShape(16.dp))

圆形边框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Box(
modifier = Modifier
.size(100.dp)
// 圆形边框:宽度为2dp,颜色为蓝色
.border(
width = 2.dp,
color = Color.Blue,
shape = CircleShape
)
.clip(CircleShape)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text("圆形边框")
}

这点要注意的是

必须要先调用clip,再设置背景background,否则背景不会被剪裁,跟我们的直觉是相反的。

渐变边框

系统属性

垂直渐变

1
2
3
4
5
6
7
8
9
10
11
12
13
.border(
width = 0.5.dp,
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF70A0FF),
Color(0x00ffffff),
Color(0xFF5E5BFC)
),
start = androidx.compose.ui.geometry.Offset(0f, 0f),
end = androidx.compose.ui.geometry.Offset(0f, Float.POSITIVE_INFINITY)
),
shape = mShape
)

注意

Offset 的值不是0f100f,是 0fFloat.POSITIVE_INFINITY

水平渐变

1
2
3
4
5
6
7
8
9
10
11
12
.border(
width = 0.5.dp,
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF4C617D),
Color(0x004B5F7B),
),
start = Offset(0f, 0f),
end = Offset(Float.POSITIVE_INFINITY, 0f)
),
shape = CommonTheme.CornerS
)

自己绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

/**
* 为组件添加渐变边框的Modifier
* @param brush 渐变画笔
* @param borderWidth 边框宽度
* @param cornerRadius 圆角半径
*/
fun Modifier.gradientBorder(
brush: Brush,
borderWidth: Dp,
cornerRadius: Dp = 12.dp
) = this.drawWithContent {
// 先绘制内容
drawContent()

// 计算边框的尺寸和位置(考虑边框宽度的一半作为偏移)
val borderWidthPx = borderWidth.toPx()
val cornerRadiusPx = cornerRadius.toPx()
val mRadius = CornerRadius(cornerRadiusPx, cornerRadiusPx)

// 创建边框路径
val path = Path().apply {
addRoundRect(
RoundRect(
left = borderWidthPx / 2,
top = borderWidthPx / 2,
right = size.width - borderWidthPx / 2,
bottom = size.height - borderWidthPx / 2,
topLeftCornerRadius = mRadius,
topRightCornerRadius = mRadius,
bottomLeftCornerRadius = mRadius,
bottomRightCornerRadius = mRadius
)
)
}

// 绘制渐变边框
drawPath(
path = path,
brush = brush,
style = Stroke(
width = borderWidthPx,
cap = StrokeCap.Round,
join = StrokeJoin.Round
)
)
}

/**
* 带渐变边框的组件示例
*/
@Composable
fun GradientBorderBox(
modifier: Modifier = Modifier,
borderWidth: Dp = 1.dp,
cornerRadius: Dp = 12.dp,
content: @Composable () -> Unit
) {
// 定义渐变颜色
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF70A0FF),
Color(0x00ffffff),
Color(0xFF5E5BFC)
),
start = Offset(0f, 0f),
end = Offset(0f, Float.POSITIVE_INFINITY),
)

Box(
modifier = modifier
.gradientBorder(
brush = gradientBrush,
borderWidth = borderWidth,
cornerRadius = cornerRadius,
)
) {
content()
}
}

使用

1
2
3
4
5
6
GradientBorderBox(
modifier = Modifier
.fillMaxWidth()
.height(106.dp)
) {
}

透明度

1
Modifier.alpha(0.5f)

对齐

内部对齐

Box

1
2
3
4
5
6
7
8
9
10
11
12
contentAlignment = Alignment.Center
contentAlignment = Alignment.TopStart // 左上
contentAlignment = Alignment.TopCenter // 上中
contentAlignment = Alignment.TopEnd // 右上
contentAlignment = Alignment.BottomStart // 左下
contentAlignment = Alignment.BottomCenter // 下中
contentAlignment = Alignment.BottomEnd // 右下

contentAlignment = Alignment.CenterStart // 左中
contentAlignment = Alignment.CenterEnd // 右中

contentAlignment = Alignment.Center // 中间

Row

1
2
3
verticalAlignment = Alignment.Top
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.Bottom

Column

1
2
3
horizontalAlignment = Alignment.Start
horizontalAlignment = Alignment.CenterHorizontally
horizontalAlignment = Alignment.End

子相对父对齐

align: 用于指定组件在其父容器中的对齐方式。

align 方法用于指定组件在其父容器中的对齐方式。它适用于容器类组件,如 BoxColumnRow 等,以及具有布局属性的组件,如 BoxWithConstraints

设置组件内的元素的对齐方式不能用Modifier,会有专门的属性来配置。

align 方法生效的情况取决于父容器的布局方式。

通常情况下,父容器需要使用相应的布局修饰符,如 Box 中的 BoxScopeColumn 中的 ColumnScopeRow 中的 RowScope

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Red)
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Blue)
.align(Alignment.Center)
) {
// Content
}
}

Column中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Column(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Blue)
.align(Alignment.CenterHorizontally)
) {
// Content
}
}

事件

clickable: 用于使组件可点击,并指定点击事件的处理程序。

1
Modifier.clickable(onClick = { /* 点击事件处理 */ })

pointerInput: 用于处理指针输入事件,例如触摸或鼠标事件。

1
Modifier.pointerInput { /* 处理指针输入事件的逻辑 */ }

文字大小

文字大小使用函数参数(fontSize)设置,而不是 Modifier

滚动

垂直滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
val vState = remember { ScrollState(0) }

// 垂直滚动示例
Column(
modifier = Modifier
.verticalScroll(vState)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
for (i in 1..20) {
Text("Item $i", modifier = Modifier.padding(8.dp))
}
}

注意

垂直滚动的区域的子元素不能有高度填充父元素的,也不能有没设置高度的LazyColumn 或 LazyVerticalGrid,否则会报错崩溃。

注意一定要加remember { },否则渲染的时候会重置位置。

即使设置.wrapContentHeight()也不行

1
2
3
4
5
6
7
8
9
10
11
12
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalArrangement = Arrangement.spacedBy(8.dp), // 水平间距
verticalArrangement = Arrangement.spacedBy(8.dp), // 垂直间距
) {
items(mList.size) {
ResultUserImageItem(mList[it])
}
}

必须设定高度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
private fun ResultUserImageList(mList: List<String>) {
val space = 8
val lineNum = ceil(mList.size / 3.0).toInt()
val height = (lineNum * 72).dp + ((lineNum - 1) * space).dp
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
.fillMaxWidth()
.height(height),
// 设置项之间的间距(水平和垂直方向)
horizontalArrangement = Arrangement.spacedBy(space.dp), // 水平间距
verticalArrangement = Arrangement.spacedBy(space.dp), // 垂直间距
) {
items(mList.size) {
ResultUserImageItem(mList[it])
}
}
}

也就是说

可垂直滚动的容器内,如果元素也会垂直滚动,必须设置明确的高度。

水平滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
val hState = remember { ScrollState(0) }

// 水平滚动示例
Row(
modifier = Modifier
.horizontalScroll(hState)
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
for (i in 1..20) {
Text("Item $i", modifier = Modifier.padding(8.dp))
}
}

自动滚动到最后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 记住滚动状态,避免重组时重置
val scrollState = remember { ScrollState(0) }
LaunchedEffect(vm.resultMdStr.value) {
// 等待布局完成后再滚动
scrollState.animateScrollTo(scrollState.maxValue)
}
Column(
Modifier
.fillMaxWidth()
.weight(1f)
.padding(bottom = 16.dp, end = 26.dp)
.verticalScroll(scrollState)
) {
MarkdownText(markdown = vm.resultMdStr.value)
}

常用工具

颜色转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import androidx.compose.ui.graphics.Color

object ZColorUtils {
fun hexToColor(hex: String): Color {
val tempStr = hex.removePrefix("#")
if (tempStr.length == 6) {
val r: Int = tempStr.substring(0, 2).toLong(16).toInt()
val g: Int = tempStr.substring(2, 4).toLong(16).toInt()
val b: Int = tempStr.substring(4, 6).toLong(16).toInt()
return Color(r, g, b, 255)
} else if (tempStr.length == 8) {
val r: Int = tempStr.substring(0, 2).toLong(16).toInt()
val g: Int = tempStr.substring(2, 4).toLong(16).toInt()
val b: Int = tempStr.substring(4, 6).toLong(16).toInt()
val a = tempStr.substring(6, 8).toLong(16).toInt()
return Color(r, g, b, a)
}
return Color.White
}
}

扩展

不显示波纹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import kotlinx.coroutines.delay

@SuppressLint("ModifierFactoryUnreferencedReceiver")
inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }) {
onClick()
}
}

防连点

这里准确来说不是防抖,是让点击事件直接触发,后续500毫秒内不能再触发。

而防抖是:让函数在事件停止触发后,延迟一段时间再执行;如果在延迟期间事件再次触发,则重新计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import kotlinx.coroutines.delay

@SuppressLint("ModifierFactoryUnreferencedReceiver")
fun Modifier.debouncedClickable(delay: Long = 500, onClick: () -> Unit) = composed {
//按钮是否可点击
var canClick by remember {
mutableStateOf(true)
}
LaunchedEffect(key1 = canClick, block = {
if (!canClick) {
delay(delay)
canClick = true
}
})

Modifier.clickable(canClick) {
canClick = false
onClick()
}
}

使用方法

1
2
3
4
Modifier
.debouncedClickable {

},