Jetpack Compose

Compose API 设计原则

视图树一旦生成便不可随意改变,视图的刷新依靠Composable函数的反复执行来实现

composable函数只能在composeable函数中调用

在Compose的世界中,一切组件都是函数,由于没有类的概念,因此不会有任何继承的层次结构,所有组件都是顶层函数

可以在DSL中直接调用

Composable作为函数相互没有继承关系,有利于促使开发者使用组合的视角去思考问题

基本概念啥的都有点不太一样,和之前学的

常用UI组件

Compose提供了Column,Row,Box三种布局组件,类似于传统视图中的LinearLayout(Vertical),LinearLayout(Horizontal),RelativeLayout

Modifier修饰符

Modifier允许我们同诺链式调用的写法来为组件应用一系列的样式设置,如边距,字体,位移等,在Compose中,每个基础的Composable组件都有一个modifier参数,通过传入自定义的Modifier来修改组件的样式

  1. size

设置组件大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Image(
painterResource(id = R.drawable.shiguang2),
contentDescription = null,
modifier = Modifier
.size(100.dp) // width与height同时设置为100dp
.clip(CircleShape) // 将图片裁切为圆形
)
// size也可以分开设置宽和高
Image(
painterResource(id = R.drawable.shiguang2),
contentDescription = null,
modifier = Modifier
.size(width = 200.dp, height = 500.dp) // 分别指定宽和高
)
  1. background

用来为修饰组件添加背景色,背景色支持设置color的纯色背景也可以使用brush设置渐变色背景,Brush是Compose提供的用来创建线性渐变色的工具

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
    Row {
Box(
Modifier
.size(50.dp)
.background(color = Color.Red) // 设置纯色背景
) {
Text("纯色", Modifier.align(Alignment.Center))
}
Spacer(modifier = Modifier.width(16.dp))
Box(
Modifier
.size(50.dp)
.background(brush = verticalGradientBrush)
) {
Text("渐变色", Modifier.align(Alignment.Center))
}
}

// 创建Brush渐变色-放在Composable方法的外面
val verticalGradientBrush = Brush.verticalGradient(
colors = listOf(
Color.Red,
Color.Yellow,
Color.Green
)
)

传统视图中的View的background属性可以用来设置图片格式的背景,但是这里是不支持的,Compose的background只能设置颜色背景

  1. fillMaxSize

size可以控制组件的大小,而fillMaxSize可以让组件在高度或者宽度上填满父空间,此时可以用fillMaxSize

1
2
3
4
5
6
7
8
9
10
11
12
13
Row(
Modifier.background(Color.Yellow).width(100.dp).height(200.dp)
) {
/*Box(
Modifier.fillMaxSize().background(Color.Red)
)*/
/*Box(
Modifier.fillMaxHeight().width(60.dp).background(Color.Green) // 填满高度,宽度自定义
)*/
Box(
Modifier.fillMaxWidth().height(50.dp).background(Color.Blue) // 填满宽度,高度自定义
)
}
  1. border&padding

border用来为被修饰组件添加边框,边框可以指定颜色,粗细,以及通过Shape指定形状,比如圆角矩形等,padding用来为被修饰组件增加间隙,可以在border前后各插入一个padding,区分对外和对内的比间距

1
2
3
4
5
6
7
8
9
10
11
12
Box(
modifier = Modifier
.padding(8.dp) // 外间隙
.border(2.dp, Color.Red, shape = RoundedCornerShape(2.dp)) // 边框
.padding(8.dp)
){
Spacer(
Modifier
.size(width = 100.dp, height = 10.dp)
.background(Color.Red)
)
}

20241221213559

相对于传统布局有Margin和Padding之分,Compose中只有padding这一种修饰符,根据在调用链的位置不同发挥不同的作用,概念更加简洁

  1. offset

offset修饰符用来移动被修饰组件的位置,我们在使用时只分别传入水平方向与垂直方向的偏移量

Modifier调用顺序会影响最终UI呈现的效果,这里应使用offset修饰符偏移,再使用background修饰符绘制背景色

1
2
3
4
5
6
7
8
9
10
11
12
        Box(
Modifier
.size(100.dp)
// .offset(x = 200.dp, y = 150.dp)
.offset {
IntOffset(
200.dp.roundToPx(),
150.dp.roundToPx()
)
} // 可以使用offset的重载方法,返回一个IntOffset实例
.background(Color.Red)
)

作用域限定Modifier修饰符

某些Modifier修饰符只能在特定作用域中使用,有利于类型安全地调用它们,所谓作用域,在Kotlin中就是一个带有Receiver的代码,例如Box组件参数中的conent就是一个Receiver类型为BoxScope的代码块,因此其子组件都处于BoxScope作用域中

  1. matchParentSize

matchParentSize是只能在BoxScope中使用的作用域限定修饰符,当使用matchParentSize设置尺寸时,可以保证当前组件的尺寸与父组件相同,而父组件默认的是wrapContent,这个相当于根据内层组件的大小来确定自己的大小

但是如果使用fillMaxSize来取代matchParentSize,那么该组件的尺寸会被设置为父组件所允许的最大尺寸,这样会导致背景铺满整个屏幕

  1. weight

在RowScope与ColumnScope中可以使用专属的weight修饰符来设置尺寸,与size修饰符不同的是,weight修饰符允许组件通过百分比设置尺寸,也就是允许组件可以自适应适配各种屏幕尺寸的移动终端设备

案例:希望让白色方块、蓝色方块和红色方块共享一整块Column空间,其中每种颜色方块高度各占比1/3,使用weight修饰符可以很容易地实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Composable
fun WeightModifierDemo() {
Column(
Modifier
.width(300.dp)
.height(200.dp)
) {
Box(
Modifier
.weight(1f)
.fillMaxWidth()
.background(Color.Green))
Box(
Modifier
.weight(1f)
.fillMaxWidth()
.background(Color.Blue))
Box(
Modifier
.weight(1f)
.fillMaxWidth()
.background(Color.Red))
}
}

20241221224548

Modifier实现原理

Modifier调用顺序会影响到最终UI的呈现效果,这是因为Modifier会由于调用顺序不同而产生不同的Modifier链,Compose会按照Modifier链来顺序完成页面测量布局与渲染

Modifier实际上是一个接口:

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
@Suppress("ModifierFactoryExtensionFunction")
@Stable
@JvmDefaultWithCompatibility
interface Modifier {
/**
* Accumulates a value starting with [initial] and applying [operation] to the current value
* and each element from outside in.
*
* Elements wrap one another in a chain from left to right; an [Element] that appears to the
* left of another in a `+` expression or in [operation]'s parameter order affects all
* of the elements that appear after it. [foldIn] may be used to accumulate a value starting
* from the parent or head of the modifier chain to the final wrapped child.
*/
fun <R> foldIn(initial: R, operation: (R, Element) -> R): R

/**
* Accumulates a value starting with [initial] and applying [operation] to the current value
* and each element from inside out.
*
* Elements wrap one another in a chain from left to right; an [Element] that appears to the
* left of another in a `+` expression or in [operation]'s parameter order affects all
* of the elements that appear after it. [foldOut] may be used to accumulate a value starting
* from the child or tail of the modifier chain up to the parent or head of the chain.
*/
fun <R> foldOut(initial: R, operation: (Element, R) -> R): R

/**
* Returns `true` if [predicate] returns true for any [Element] in this [Modifier].
*/
fun any(predicate: (Element) -> Boolean): Boolean
}

嗯,这一段,等会用了再看吧,先会用,再理解概念

常用的基础组件

  1. Text文本

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Composable
fun Text(
text: String, // 要显示的文本
modifier: Modifier = Modifier, // 修饰符
color: Color = Color.Unspecified, // 文字颜色
fontSize: TextUnit = TextUnit.Unspecified, // 文字大小
fontStyle: FontStyle? = null, // 字体变体,例如斜体
fontWeight: FontWeight? = null, // 粗细
fontFamily: FontFamily? = null, // 字体
letterSpacing: TextUnit = TextUnit.Unspecified, // 间距
textDecoration: TextDecoration? = null, // 装饰,例如下划线
textAlign: TextAlign? = null, // 对齐方式
lineHeight: TextUnit = TextUnit.Unspecified, // 文本的间距
overflow: TextOverflow = TextOverflow.Clip, // 文本溢出的视觉效果
softWrap: Boolean = true, // 控制文本是否能够换行
maxLines: Int = Int.MAX_VALUE, // 文本最多可以有几行
minLines: Int = 1,
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
style: TextStyle = LocalTextStyle.current
)

最佳实践:Text组件的参数会按照其使用频度排序,并尽量添加默认实现,便于在单元测试或者预览中使用,我们自定义的Composable组件也应该遵循这样的参数设计原则

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
@Composable
fun TextDemo() {
Column {
Spacer(Modifier.size(100.dp))
Text(
text = "Hello Android"
)
Text(
text = "Hello Android",
style = TextStyle(
fontSize = 25.sp, // 字体大小
fontWeight = FontWeight.Bold, // 粗细
background = Color.Cyan,
lineHeight = 35.sp // 行高
)
)
Text(
text = "Hello Android",
style = TextStyle(
color = Color.Gray,
letterSpacing = 4.sp // 字体间距
)
)
Text(
text = "Hello Android",
style = TextStyle(
textDecoration = TextDecoration.LineThrough //删除线
)
)
Text(
text = "Hello Android",
style = MaterialTheme.typography.headlineLarge.copy(fontStyle = FontStyle.Italic)
)
}
}

20241221230905

style中的部分参数也可以直接在Text中直接设置,例如字体大小,粗细,且Text参数会覆盖掉style中的样式

AnnotatedString多样式文字

在一段文字中对局部内容应用特别格式一示突出

  • AnnotatedString
    • SpanStyle:用于描述在文本中子串的文字样式
    • ParagraphStyle:用于描述文本中子串额段落格式
    • Range:确定子串的范围
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
Text(
// 一大段文字
text = buildAnnotatedString {
// withStyle参数可以设置文本的样式,而方法体中可以append上这个样式应用在什么文本上
withStyle(style = SpanStyle(fontSize = 24.sp)) {
append("你现在学习的章节是")
}
withStyle(
style = SpanStyle(
fontWeight = FontWeight.W900,
fontSize = 24.sp
)
) {
append("Text")
}
append("\n")
withStyle(style = ParagraphStyle(lineHeight = 25.sp)) {
append("在刚刚讲的内容中,我们学会了如何应用文字样式,以及如何限制文本的行数和处理溢出的视觉效果")
append("\n")
append("现在,我们正在学习")
withStyle(
style = SpanStyle(
fontWeight = FontWeight.W900,
textDecoration = TextDecoration.Underline,
color = Color(0xFF59AB69)
)
) {
append("AnnotatedString")
}
}
}
)

20241222095316

SpanStyle继承了TextStyle中关于文字样式相关的字段,而ParagraphStyle继承了TextStyle中控制段落的样式,例如textAligh,lineHeight等,某种意义上二者拆分了TextStyle,可以对子串分别进行文字以及段落样式设置


Compose提供了一种可以点击的文本组件ClickedText,可以响应我们对文字的点击,并返回点击位置

Text自身默认是不能被长按选择的,否则在Button中使用,又会出现”可粘贴的Button”的例子

Compose提供了专门的SelectionContainer组件,对包裹的Text进行选中

1
2
3
4
// 可复制的文字
SelectionContainer {
Text("这是可复制的文字")
}

TextField输入框

最常用的文本输入框,具有两种风格,一种是默认,也就是filled,另一种是OutlinedTextField

使用var text by remember { mutableStateOf("") }报错时,可能是没有导入下面两个依赖

1
2
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
1
2
3
4
5
6
7
8
9
@Composable
fun TextFiledDemo() {
var text by remember { mutableStateOf("") }
TextField(value = text,
onValueChange = { // it:String
text = it
},
label = { Text("用户名") }) // 标签
}

就是这个样子的:
20241222104026

为输入框添加修饰

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
@Composable
fun TextFiledSample() {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column {
TextField(
value = username,
onValueChange = {
username = it
},
label = {
Text("用户名")
},
leadingIcon = {
Icon(
imageVector = Icons.Filled.AccountBox,
contentDescription = stringResource(R.string.description)
)
},
maxLines = 1
)

OutlinedTextField(
value = password,
onValueChange = {
password = it
},
label = {
Text("密码")
},
trailingIcon = {
IconButton(onClick = {}) {
Icon(
painter = painterResource(id = R.drawable.shiguang2),
contentDescription = stringResource(R.string.description)
)
}
},
maxLines = 1
)
}
}

两种风格的输入框,都自带动效
20241222105721

需要注意的是,TextField和OutlinedTextField都是遵循Material Desingn准则的,所以无法直接修改输入框的高度,如果尝试修改高度,会看到输入区域被截断

这时就可以使用更基础的BasicTextField,这种输入框有更多的可自定义的参数

B站风格搜索框

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
/**
* B站样式搜索框
*/
@Composable
fun SearchBar() {
var text by remember { mutableStateOf("") }
Box(
Modifier
.fillMaxSize()
.background(Color(0xFFD3D3D3)),
contentAlignment = Alignment.Center // 将Box里面的组件放置于Box容器的中央
) {
BasicTextField(
value = text,
maxLines = 1,
onValueChange = {
text = it
},
decorationBox = { innerTextField ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 2.dp, horizontal = 8.dp)
) {
// 最左侧搜索图标
Icon(
imageVector = Icons.Filled.Search,
contentDescription = stringResource(R.string.description)
)
// 搜索框本体
Box(
modifier = Modifier
.padding(horizontal = 10.dp)
.weight(1f), // 使用weight使文本占用剩余空间
contentAlignment = Alignment.CenterStart,
) {
if (text.isEmpty()) {
Text(
text = "输入点东西看看吧~",
style = TextStyle(
color = Color(0, 0, 0, 128)
)
)
}
innerTextField()
}
if (text.isNotEmpty()) {
IconButton(
onClick = { text = "" },
modifier = Modifier.size(16.dp)
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.description)
)
}
}
}
},
modifier = Modifier
.padding(horizontal = 10.dp)
.background(Color.White, CircleShape)
.height(30.dp)
.fillMaxWidth()
)
}
}

说实话,并没有感觉这玩意比前端好写,甚至感觉这写起来比前端麻烦多了,而且结构不清晰

20241222135643

图片组件

  1. Icon图标

Icon组件用于显示一系列小图标,Icon组件支持三种不同类型的图片设置

Icon可以传入Resource中的资源

  1. imageVector
  2. imageBitmap
  3. vectorResource
  4. imageResource
  5. painterResource

可以直接使用Material包中的图标

1
2
3
4
5
6
7
8
@Composable
fun IconSample() {
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = null,
tint = Color.Red // 填充颜色
)
}

20241222140543

Icon组件还可以加载网络上下载的图标库,google图标库

  1. Image图片

image组件中有一个contentScale参数用来指定图片在Image组件中的伸缩样式

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun ImageSample() {
Image(
painterResource(id = R.drawable.shiguang2),
contentDescription = null,
modifier = Modifier.size(width = 100.dp, height = 200.dp),
// contentScale = ContentScale.Crop // 居中裁切
// contentScale = ContentScale.Fit // 不裁切,使用一边的大小
// 还有充满高的 FillHeight, 等 FillWidth
contentScale = ContentScale.FillBounds // 拉伸
)
}

colorFilter参数用于设置一个ColorFilter,它可以通过对绘制的图片的每个像素的颜色进行修改,实现不同的图片效果

  1. tint
  2. colorMatrix
  3. lighting 灯光效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var colorMatrix = ColorMatrix().apply {
setToSaturation(0f)
}

@Composable
fun ImageSample() {
Image(
painterResource(id = R.drawable.shiguang2),
contentDescription = null,
modifier = Modifier.size(width = 100.dp, height = 200.dp),
contentScale = ContentScale.Crop, // 居中裁切
// contentScale = ContentScale.Fit // 不裁切,使用一边的大小
// 还有充满高的 FillHeight, 等 FillWidth
// contentScale = ContentScale.FillBounds // 拉伸
// colorFilter = ColorFilter.tint(Color.Blue, BlendMode.Multiply)
// colorFilter = ColorFilter.colorMatrix(colorMatrix)
colorFilter = ColorFilter.lighting(
multiply = Color.Red,
add = Color.Blue
)
)
}