余一

纸上得来终觉浅,绝知此事要躬行。

0%

常用UI组件

Jetpack Compose中文文档

​ 在Compose中,每个组件都是一个带有@Composable注解的函数,被称为Composable。Compose已经预置了很多基础的Composable组件,它们都是基于Material Design规范设计,例如Button、TextField、TopAppBar等。

​ 在布局方面,Compose提供了Column、Row、Box三种布局组件,类似于传统视图开发中的LinearLayout(Vertical)、LinearLayout(Horizontal)、ReleativeLayout

Modifier修饰符

​ 在传统开发中,使用XML文件来描述组件的样式,而Jetpack Compose设计了一个精妙的东西,它叫作Modifier。

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

常用修饰符

​ 对于所有Composable组件都通用。

1.Modifier.size

用来设置被修饰组件的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Composable
fun View1(){
Row{
Image(
painterResource(id = R.drawable.e),
contentDescription = null,
modifier = Modifier
.size(60.dp) //with与height同时设置为60dp
.clip(CircleShape) //将图片裁剪为圆形
)
Spacer(modifier = Modifier.width(10.dp))
Image(
painterResource(id = R.drawable.e),
contentDescription = stringResource(id = androidx.compose.ui.R.string.on),
modifier = Modifier
.size(width = 100.dp, height = 100.dp) //with与height同时设置为100dp
.clip(CircleShape) //将图片裁剪为圆形
)
}
}

2.Modifier.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
27
28
29
@Composable
fun View2(){
Row{
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Red) //设置纯色背景
){
Text(text = "纯色",Modifier.align(Alignment.Center))
}
Spacer(modifier = Modifier.width(10.dp))
Box(
modifier = Modifier
.size(50.dp)
.background(brush = verticalGradientBrush) //设置渐变色背景
){
Text(text = "渐变色",Modifier.align(Alignment.Center))
}
}
}

//创建Brush渐变色
val verticalGradientBrush = Brush.verticalGradient(
colors = listOf(
Color.Red,
Color.Yellow,
Color.White,
)
)

​ 传统视图中View的background属性可以用来设置图片格式的背景,Compose的background修饰符只能设置颜色背景,图片背景需要使用Box布局配合Image组件实现。

3.Modifier.fillMaxSize

有的时候想要让组件在高度或者宽度上填满父空间,此时可以使用fillMaxXXX系列方法:

1
2
3
4
5
6
//填满整个父空间
Box(Modifier.fillMaxSize().background(Color.Red))
//高度填满父空间
Box(Modifier.fillMaxHeight().width(60.dp).background(Color.Red))
//宽度填满父空间
Box(Modifier.fillMaxWidth()height(60.dp).background(Color.Red))

4.Modifier.border & Modifier.padding

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

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

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

5.Modifier.offset

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

​ Modifier调用顺序会影响最终UI呈现的效果。

1
2
3
4
5
6
7
Box(modifier = Modifier
.size(80.dp)
.offset(x = 20.dp, y = 20.dp)
.background(Color.Red)
){
Text(text = "哈哈哈",Modifier.size(60.dp))
}

也可以使用offset的重载方法,返回一个IntOffset实例:

1
2
3
4
5
6
7
Box(modifier = Modifier
.size(80.dp)
.offset { IntOffset(20.dp.roundToPx(),15.dp.roundToPx()) }
.background(Color.Red)
){
Text(text = "哈哈哈",Modifier.size(60.dp))
}

作用域限定Modifier修饰符

​ Compose充分发挥了Kotlin的语法特性,让某些Modifier修饰符只能在特定作用域中使用,有利于类型安全地调用它们。所谓的“作用域”,在Kotlin中就是一个带有Receiver的代码块。例如Box组件参数中的conent就是一个Reciever类型为BoxScope的代码块,因此其子组件都处于BoxScope作用域中。

​ Receiver跨级访问会成为写代码时的“噪声”,加大出错的概率。Compose考虑到了这个问题,可以通过@LayoutScopeMarker注解来规避Receiver的跨级访问。常用组件Receivier作用域类型均已使用@LayoutScopeMarker注解进行了声明。

Compose作用域限定实现了Modifier的安全调用,我们只能在特定作用域中调用修饰符,就像只能在FrameLayout内使用toRightOf一样,如果换作LinearLayout将无法设置toRightOf

常见的作用域限定Modifier修饰符。

1.matchParentSize

matchParentSize是只能在BoxScope中使用的作用域限定修饰符。当使用matchParentSize设置尺寸时,可以保证当前组件的尺寸与父组件相同。而父组件默认的是wrapContent,会根据UserInfo的尺寸确定自身的尺寸。

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

2.weight

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

例如,我们希望白色方块、蓝色方块与红色方块共享一整块Column空间,其中每种颜色方块高度各占比1/3:

Modifier实现原理

​ Modifier会由于调用顺序不同而产生出不同的Modifier链,Compose会按照Modifier链来顺序完成页面测量布局与渲染。

1.接口实现

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
interface Modifier {

fun <R> foldIn(initial: R, operation: (R, Element) -> R): R

fun <R> foldOut(initial: R, operation: (Element, R) -> R): R

fun any(predicate: (Element) -> Boolean): Boolean

fun all(predicate: (Element) -> Boolean): Boolean

infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)

interface Element : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)

override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R =
operation(this, initial)

override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this)

override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this)
}


companion object : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
override fun any(predicate: (Element) -> Boolean): Boolean = false
override fun all(predicate: (Element) -> Boolean): Boolean = true
override infix fun then(other: Modifier): Modifier = other
override fun toString() = "Modifier"
}
}

​ Modifier实际是一个接口。它有三个具体实现,分别是一个Modifier伴生对象,Modifier. Element以及CombinedModifier

​ Modifier伴生对象是我们对Modifier修饰符进行链式调用的起点,即Modifier.xxx()中开头的那个Modifier。CombinedModifier用于连接Modifier链中的每个Modifier对象。Modifier. Element代表具体的修饰符。当我们使用Modifier.xxx()时,其内部实际上会创建一个Modifier实例。

​ Modifier.size()内部会创建一个SizeModifier实例,并使用then进行连接,then返回一个CombinedModifier,后者用来连接两个Modifier.Element。

​ 创建的各种Modifier本质上都是一个Modifier. Element。像LayoutModifier这类直接继承自Modifier. Element的接口,暂且称它们为Base Modifier。Base Modifier种类很多:

2.链的构建

​ Modifier.size()内部会创建一个SizeModifier实例,并使用then进行连接。then返回一个CombinedModifier,后者用来连接两个Modifier.Element。

​ CombinedModifier连接的两个Modifier分别存储在outer与inner中,从CombinedModifier的数据结构可以联想到,Compose对Modifier的遍历,就像剥洋葱一样从外(outer)到内(inner)一层层访问。outer与inner作为private属性不能被外部直接访问,Modifier专门提供了foldOut()与foldIn()用来遍历Modifier链.

3.链的解析

​ Compose在绘制UI时,会遍历Modifier链获取配置信息。Compose使用foldOut()与foldIn()遍历Modifier链,就像Kotlin集合的fold操作符一样,链上的所有节点被“折叠”成一个结果后,传入视图树用于渲染。

常用的基础组件

文字组件

1.Text文本

​ Compose中,Text是遵循Material Design规范设计的上层文本组件,如果想脱离Material Design使用,也可以直接使用更底层的文本组件BasicText

Composable组件都是函数,所有的配置来自参数传递,通过参数列表就可以了解组件的所有功能

​ Text的基本功能是显示一段文字,可以为text参数中传入要显示的文字内容。Compose也提供了stringResource方法通过R资源文件获取字符串。

1
2
3
4
//指定字符串
Text(text = "Hello world.")
//指定文字资源
Text(text = stringResource(R.string.hello_world))

除了stringResource, Compose也提供了获取其他类型资源的方法,例如colorResource、integerResource、painterResource(Drawable类型资源)等

2.style文字样式

​ style参数接受一个TextStyle类型,TextStyle中包含一系列设置文字样式的字段,例如行高、间距、字体大小、字体粗细等。

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
@Preview(
showSystemUi = true
)
@Composable
fun HelloText(){
Column {
Text(
text = "Hello world\n" + "Goodbye World."
)
Text(
text = "Hello world\n" + "Goodbye World.",
style = TextStyle(
fontSize = 25.sp, //字体大小
fontWeight = FontWeight.Bold, //字体粗细
background = Color.Cyan,//字体颜色
lineHeight = 35.sp //行高
)
)
Text(
text = "Hello World",
style = TextStyle(
color = Color.Gray,
letterSpacing = 4.sp //字间距
)
)
Text(
text = "Hello World",
textDecoration = TextDecoration.LineThrough //删除线
)
Text(
text = "Hello World",
//TextStyle虽然是一个普通data class,但是它提供了data class那样的copy方法,可以非常高效地构造一个新实例。
//MaterialTheme.typography.h6是一个预置的TextStyle
style = MaterialTheme.typography.h6.copy(fontStyle = FontStyle.Italic)
)

}
}

​ Material Desgin 2的Typography规范,定义了各类文字样式,样式名称也体现了它们的使用场景,比如H1是一级标题,BUTTON是按钮文字等.

​ 如果项目采用了Material Design设计规范,那么可以为Text的style参数直接设置Typography中预置的TextStyle。也可以脱离Material Design来定义自己的文本样式。

​ TextStyle中的大部分字段也可以在Text参数中直接设置,例如fonteSize、fontWeight、fontStyle等。注意Text参数会覆盖对TextStyle同名属性的设置。

maxLines参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Text(
text = "你好世界,我正在使用Jetpack Compose框架来开发我的APP界面",
style = MaterialTheme.typography.body1
)
Text(
text = "你好世界,我正在使用Jetpack Compose框架来开发我的APP界面",
style = MaterialTheme.typography.body1,
//maxLines参数可以帮助我们将文本限制在指定的行数之间,当文本超过了参数设置的阈值时,文本会被截断。
maxLines = 1
)
Text(
text = "你好世界,我正在使用Jetpack Compose框架来开发我的APP界面",
style = MaterialTheme.typography.body1,
maxLines = 1,
//overflow可以处理文字过多的场景,在Ellipsis模式下会以...结尾
overflow = TextOverflow.Ellipsis
)

3.fontFamily字体风格

1
2
3
4
5
6
7
8
9
10
11
Text(
text = "Hello world"
)
Text(
text = "Hello world",
fontFamily = FontFamily.Monospace
)
Text(
text = "Hello world",
fontFamily = FontFamily.Cursive
)

​ 当使用系统没有的字体时,可以右击res文件夹,选择New-> Android Resource Directory > Resource type > font,创建font文件夹,然后将自己的字体文件拖入文件夹即可。

4.AnnotatedString多样式文字

​ 假如要在一段文字中对局部内容应用特别格式以示突出,比如一个超链接或者一个电话号码等,此时需要用到AnnotatedString。AnnotatedString是一个数据类,除了文本值,它还包含了一个SpanStyle和ParagraphStyle的Range列表。SpanStyle用于描述在文本中子串的文字样式,ParagraphStyle则用于描述文本中子串的段落样式,Range确定子串的范围。

​ 使用buildAnnotatedString{…},以DSL的方式构建一个AnnotatedString。其中append用来添加子串的文本,withStyle为append的子串指定文字或段落样式:

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
val annotatedString = buildAnnotatedString {
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(0xff59A869)
)
){
append("AnnotatedString")
}
}
}
Text(text = annotatedString)

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

SpanStyle或ParagraphStyle中的设置优先于整个TextStyle中的同名属性设置。

​ Compose提供了一种可点击文本组件ClickedText,可以响应我们对文字的点击,并返回点击位置。可以让AnnotatdString子串在相应的ClickedText中点击后,做出不同的动作。例如点击一个超链接样式子串可以打开浏览器、点击数字格式子串来拨打电话等在AnnotatedString中可以为子串添加一个tag标签,在处理onClick事件时,可以根据tag实现不同的逻辑。

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
val annotatedText = buildAnnotatedString {
withStyle(
style = ParagraphStyle(lineHeight = 25.sp)
){
//为pushStringAnnotation与pop之间的区域添加标签
pushStringAnnotation(tag = "URL", annotation = "https://jetpackcompose.cn/docs/elements/text")
withStyle(
style = SpanStyle(
fontWeight = FontWeight.W900,
textDecoration = TextDecoration.Underline,
color = Color(0xff59A869)
)
){
append("AnnotatedString")
}
}
//结束之前通过推入操作添加的样式或注释。
//pop()
}

ClickableText(
text = annotatedText,
onClick = { offset ->
//获取被点击区域的标签为URL的annotation并进行处理
annotatedText.getStringAnnotations(
tag = "URL",
start = offset,
end = offset
).firstOrNull()?.let {
//打开URL
openURL(it.item)
}

}
)

//打开网页
fun openURL(url:String){
val intent= Intent(Intent.ACTION_VIEW, Uri.parse(url))
APP.appContext.startActivity(intent)
}

5.SelectionContainer选中文字

Text自身默认是不能被长按选择的。Compose提供了专门的SelectionContainer组件,对包裹的Text进行选中。

1
2
3
SelectionContainer {
Text(text = "我是可以被复制的文字")
}

6.TextField输入框

TextField组件是我们最常使用的文本输入框,它也遵循着Material Design设计准则。它也有一个低级别的底层组件,叫作BasicTextField,TextField有两种风格,一种是默认的,也就是filled,另一种是OutlinedTextField

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun TextFieldSample(){
//这个text是一个可以变化的文本,用来显示TextField输入框中当前输入的文本内容。
var text by remember {
mutableStateOf("")
}
TextField(
value = text,
onValueChange = { text = it },
label = { Text(text = "用户名") }//标签
)
}

​ 在onValueChange回调中可以获取来自软键盘的最新输入,利用这个信息来更新可变状态text,驱动界面刷新显示最新的输入文本。来自软键盘的输入内容不会直接更新TextField, TextField需要通过观察额外的状态更新自身,这也体现了声明式UI中“状态驱动UI”的基本理念

7.为输入框添加装饰

​ 输入框都可能带有前后图标或者按钮,例如在注册界面的表单中,密码一栏可能就含有可以隐藏或者显示密码的小按钮。

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
@Composable
fun TextFieldSample(){
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column {
TextField(
value = username,
onValueChange = { username = it },
label = { Text(text = "用户名") },
leadingIcon = {
Icon(
imageVector = Icons.Filled.AccountBox, contentDescription = stringResource(id = R.string.app_name)
)
}
)
TextField(
value = password,
onValueChange = { password = it },
label = { Text(text = "密码") },
trailingIcon = {
IconButton(
onClick = { }
) {
Icon(
painter = painterResource(id = R.drawable.image1), contentDescription = stringResource(id = R.string.app_name)
)
}
}
)
}
}

​ 在第一个输入框中,通过leadingIcon参数为输入框添加了前置小图标。小图标由专门的Icon组件来展示,trailingIcon可以添加后置图标

​ leadingIcon与trailingIcon都是@Composable(()-> Unit)类型,理论上可以摆放任意Composable组件。例如第二个输入框,在输入框尾部放置了一个IconButton,它除了显示Icon以外,还可以响应用户点击。

8.OutlinedTextField边框样式输入框

​ OutlinedTextField是按照Material Design规范设计的另一种风格的输入框,除了外观上它带有一个边框,其他用法和TextField基本一致。

1
2
3
4
5
6
7
8
9
@Composable
fun OutLineTextFieldSample(){
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text(text = "用户名") }
)
}

9.BasicTextField基本演示

​ BasicTextField是一个更低级别的Composable组件,与TextField、OutlinedTextField不同的是,BasicTextField拥有更多的自定义效果。由于TextField没有暴露足够的参数供我们设置,而BasicTextField可以支持大多数的定制需求.

BasicTextField提供了些可选参数:

​ 自定义的关键在于最后一个参数decorationBox。decorationBox是一个Composable,它回调了一个innerTextField函数给我们。innerTextField是框架定义好的东西,它就是文字输入的入口,所以需要创建好一个完整的输入框界面,并在合适的地方调用这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Composable
fun BasicTextFieldSample(){
var text by remember { mutableStateOf("") }
BasicTextField(
value = text,
onValueChange = {text = it},
decorationBox = {innerTextField ->
Column {
innerTextField()
Divider(
thickness = 2.dp,
modifier = Modifier.fillMaxWidth()
.background(Color.Black)
)
}
}
)
}

10.实战: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
@Composable
fun SearchBar(){
var text by remember { mutableStateOf("") }
Box(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.background(Color(0xffd3d3d3)),
contentAlignment = Alignment.Center //将Box里面的组件放置于Box容器中央
){
BasicTextField(
value = text,
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(id = R.string.app_name),
)
Box(
modifier = Modifier
.padding(horizontal = 10.dp),
//设置子元素为竖直方向上的中间,水平方向上的最左边
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(
id = R.string.app_name
)
)
}
}
}
},
modifier = Modifier
.padding(horizontal = 10.dp)
.background(Color.White, CircleShape)
.height(30.dp)
.fillMaxWidth()
)
}
}

图片组件

1.Icon图标

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Composable
fun Icon(
imageVector: ImageVector, //矢量图对象,可显示SVG格式的图标
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
)

@Composable
fun Icon(
bitmap: ImageBitmap, //位图对象,可显示JPG、PNG等格式图标
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
)

@Composable
fun Icon(
painter: Painter,//代表一个自定义画笔,可以使用画笔在Canvas上直接绘制图标
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
)

也可以通过res/下的图片资源来设置图标:

1
2
3
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_svg,contentDescription = "矢量图资源"))
Icon(bitmap = ImageBitmap.imageResource(id = R.drawable.ic_png,contentDescription = "图片资源"))
Icon(painter = painterResource(id = R.drawable.ic_both),contentDescription = "任意类型资源")

ImageVectorImageBitmap都提供了对应的加载Drawable资源的方法,vectorResource用来加载一个矢量XML, imageResource用来加载jpg或者png图片。painterResource对以上两种类型的Drawable都支持,内部会根据资源创建对应的画笔进行图标的绘制。

1
2
3
4
5
6
7
8
@Composable
fun IconSample(){
Column {
//使用Material包内置的Favorite矢量图标
Icon(imageVector = Icons.Filled.Favorite, contentDescription = null, tint = Color.Red)
Icon(imageVector = Icons.Filled.Favorite, contentDescription = null)
}
}

contentDescription参数服务于系统的无障碍功能,其中的文字会转换为语言供视障人士听取内容时使用,默认为空。

​ Material包每个图标都提供了五种风格可供使用,除了Filled,还包括OutlinedRoundedSharpTwo tone等,都可以通过Icons.xxx.xxx的方式调用。这五种风格在一些设计上的侧重点不同。

​ Material自带的包仅有一些常用的图标,如需使用其他所有的Material图标,可以添加依赖。

1
implementation "androidx.compose.material:material-icons-extended:$compose_version"

2.Image图片

​ Image组件用来显示一张图片。它和Icon一样也支持三种类型的图片设置:

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
@Composable
fun Image(
bitmap: ImageBitmap,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DefaultFilterQuality
)

@Composable
fun Image(
imageVector: ImageVector,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
)

@Composable
fun Image(
painter: Painter,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
)

​ 跟Icon组件一样,可以使用painterResource加载一个本地图片资源传入painter参数。

contentScale参数用来指定图片在Image组件中的伸缩样式,类似传统视图ImageViewscaleType属性:

colorFilter参数用来设置一个ColorFilter,它可以通过对绘制的图片的每个像素颜色进行修改,以实现不同的图片效果。ColorFilter有三种修改方式:tintcolorMatrixlighting

1
2
3
4
5
6
7
8
9
10
11
12
//tint用BlendMode混合指定颜色。其中参数color将用来混合原图片每个像素的颜色
//参数blendMode是混合模式,blenModel有多种混合模式
//跟传统视图中用的Xfermode的PorterDuff.Model类似
ColorFilter.tint(color: Color, blendMode: BlendMode = BlendMode.SrcIn)

//colorMatrix通过传入一个RGBA四通道的4x5的数字矩阵去处理颜色变化
//比如可以降低图片饱和读,已到达图片灰化的目的
ColorFilter.colorMatrix(colorMatrix: ColorMatrix)

//lighting用来为图片应用一个简单的灯光效果
//它由两个参数定义,第一个用于颜色相乘,第二个用于添加到原图颜色
ColorFilter.lighting(multiply: Color, add: Color)

按钮组件

1.Button按钮

​ Button也是最常用的组件之一,它也是按照Material Design风格来实现的。

Button的参数列表:

​ Button的第一个参数onClick是必填项,这是其最重要的功能,通过回调响应用户点击事件。最后一个参数content也是一个必填项,也是其最重要的功能之一。Compose的Button默认没有任何UI。仅仅是一个响应onClick的容器,它的UI需要在content中通过其他组件来实现

可以根据需求,在content中实现各种复杂的Button样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Composable
fun ButtonSample(){
Column {
Button(
modifier = Modifier
.width(100.dp)
.height(60.dp)
.padding(3.dp),
onClick = { })
{
Icon(
imageVector = Icons.Filled.Done,
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize)
)
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Text(text = "确认")
}
}
}

​ Button有一个参数interactionSource,可以监听组件状态的事件源,通过它我们可以根据组件状态设置不同的样式,比如按钮按下时什么效果,正常时什么效果,类似传统视图中的Selector。

interactionSource通过以下方法获取当前组件状态:

  • interactionSource.collectIsPressedAsState()判断是否按下状态。
  • interactionSource.collectIsFocusedAsState()判断是否获取焦点的状态。
  • interactionSource.collectIsDraggedAsState()判断是否拖动

如,通常状态下按钮边框为白色,当处于选中状态时,框线将变为绿色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
val interactionSource = remember{
MutableInteractionSource()
}
val pressState = interactionSource.collectIsPressedAsState()
val borderColor = if (pressState.value) Color.Green else Color.White

Button(
modifier = Modifier
.width(100.dp)
.height(60.dp)
.padding(3.dp),
onClick = { },
border = BorderStroke(2.dp, color = borderColor),
interactionSource = interactionSource
)
{
Icon(
imageVector = Icons.Filled.Done,
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize)
)
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Text(text = "确认")
}

任何Composable组件都可以通过Modifier.clickable修饰符化身为可点击组件,而当Button被点击后,需要额外进行一些事件响应处理,比如显示Material Desgin风格的水波纹等,这些都是其内部通过拦截Modifier.clickable事件实现的处理,由于Modifier.clikable已经被内部实现所占用,Button需要提供单独的onClick参数供开发者使用。

​ Button的onClick在底层是通过覆盖Modifier.clickable实现的,所以不要为Button设置Modifier.clickable,即使设置了,也会因为被onClick覆盖而没有任何效果。

2.IconButton图标按钮

​ IconButton组件实际上只是Button组件的简单封装(一个可点击的图标),它一般用于应用栏中的导航或者其他行为。一般来说,我们需要在IconButton组件里提供一个图标组件,这个图标的默认尺寸一般为24×24dp

1
2
3
IconButton(onClick = {  }) {
Icon(imageVector = Icons.Filled.Favorite, contentDescription = null)
}

3.FloatingActionButton悬浮按钮

​ FloatingActionButton悬浮按钮(FAB)一般代表当前页面的主要行为。FAB组件也是需要提供一个Icon组件:

1
2
3
FloatingActionButton(onClick = { }) {
Icon(imageVector = Icons.Filled.Face, contentDescription = null)
}

Compose也提供了带有文字扩展的FAB,即ExtendedFloatingActionButton组件。

1
2
3
4
5
ExtendedFloatingActionButton(
icon = { Icon(imageVector = Icons.Filled.Favorite, contentDescription = null)},
text = { Text(text = "添加到我喜欢的") },
onClick = { }
)

选择器

1.Checkbox复选框

CheckBox允许用户从一个集合中选择一个或多个项目。复选框可以将一个选项打开或关闭。

1
2
3
4
5
6
7
Checkbox(
checked = checkedState.value,
onCheckedChange = { checkedState.value = it },
colors = CheckboxDefaults.colors(
checkedColor = Color(0xff0079d3)
)
)

2.TriStateCheckbox三态选择框

希望能够统一选择或者取消:

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
//为两个checkBox定义状态
val (state,onStateChange) = remember { mutableStateOf(true) }
val (state2,onStateChange2) = remember { mutableStateOf(true) }

//根据子CheckBox的状态来设置TriStateCheckbox状态
val parentState = remember(state,state2){
if (state && state2) ToggleableState.On
else if (! state && !state2) ToggleableState.Off
else ToggleableState.Indeterminate
}

//TriStateCheckbox可以为从属的复选框设置状态
val onParentClick = {
val s = parentState != ToggleableState.On
onStateChange(s)
onStateChange2(s)
}

TriStateCheckbox(
state = parentState,
onClick = onParentClick,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colors.primary
)
)
Column(
Modifier.padding(10.dp,0.dp,0.dp,0.dp)
) {
Checkbox(checked = state, onCheckedChange = onStateChange)
Checkbox(checked = state2, onCheckedChange = onStateChange2)
}

​ 在子复选框全选中时,TriCheckBox显示已完成的状态,而如果只有部分复选框选中时,TriCheckBox则显示不确定的状态,当我们在这个时候单击它,则会将剩余没有选中的复选框设置为选中状态。

3.Switch单选开关

Switch组件可以控制单个项目的开启或关闭状态。

1
2
3
4
val checkState = remember {
mutableStateOf(true)
}
Switch(checked = checkState.value, onCheckedChange = { checkState.value = it })

4.Slider滑竿组件

Slider类似于传统视图的Seekbar,可用来做音量、亮度之类的数值调整或者进度条。

colors参数用来设置滑竿各部位的颜色。滑竿组件中可设置颜色的区域很多,例如滑竿小圆球的颜色、滑竿进度颜色、滑竿底色等。step参数将进度条平分成(steps+1)段。比如当分成2段时,进度条在第一段之间拉动,超过第一段的一半就自动到第一段,没超过就退回到开始位置。

1
2
3
4
5
6
@Composable
fun MySlideDemo(){
var sliderPosition by remember { mutableStateOf(0f) }
Text(text = "%.1f".format(sliderPosition * 100) + "%")
Slider(value = sliderPosition, onValueChange = { sliderPosition = it })
}

对话框

1.Dialog对话框

content允许通过传入自己的Composable组件来描述Dialog页面:

如:Dialog宽度不受限制,达到全屏的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Composable
fun DialogSample(){
Column {
Dialog(onDismissRequest = { },
properties = DialogProperties(
usePlatformDefaultWidth = false
)
) {
androidx.compose.material.Surface(
modifier = Modifier.fillMaxSize(),
color = Color.Gray
) {
Text(text = "你好")
}
}

}
}

properties参数定制一些对话框特有的行为:

Compose的对话框不像传统视图的对话框那样通过show()、dismiss()等命令式的方式显隐,它像不同的Composable组件一样,显示与否要看是否在重组中被执行,所以它的显示与否要依赖状态控制。Dialog和普通Composable组件的不同在于其底层需要依赖独立的Window进行显示。

用状态控制Dialog的显隐:

1
2
3
4
5
6
7
8
9
10
val openDialog = remember {
mutableStateOf(true)
}
val dialogWidth = 200.dp
val dialogHeight = 50.dp
if (openDialog.value){
Dialog(onDismissRequest = { openDialog.value = false }) {
Box(modifier = Modifier.size(dialogWidth, dialogHeight).background(Color.White))
}
}

​ 在Dialog组件显示过程中,当点击对话框以外区域时,onDismissRequest会触发执行,修改openDialog状态为false,触发DialogSample重组,此时判断openDialog为false, Dialog无法被执行,对话框消失。

2.AlertDialog警告对话框

AlertDialog组件是Dialog组件的更高级别的封装,同时遵守着Material Design设计标准。它已经定位好了标题、内容文本、按钮组的位置。只需要提供相应的组件即可。

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
@Composable
fun AlertDialogSample(){
val openDialog = remember {
mutableStateOf(true)
}
if (openDialog.value){
AlertDialog(
onDismissRequest = { },
title = {
Text(text = "开启位置服务")},
text = {
Text(text = "这将意味着,我们会给您提供精准的位置服务,并且您将接受关于您订阅的位置信息。")
},
confirmButton = {
TextButton(onClick = { openDialog.value = false }) {
Text(text = "同意")
}
},
dismissButton = {
TextButton(onClick = { openDialog.value = false }) {
Text(text = "取消")
}
}
)
}
}

3.进度条

​ Compose自带了两种Material Design的进度条,分别是圆形直线的进度条,它们都有两种状态,一种是无限加载的,另一种是根据值来动态显示的,

圆形的进度条:

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 CircularProgressSample(){
//创建一个进度值
var progress by remember {
mutableStateOf(0.1f)
}

//创建一个动画,根据progress变量
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
)
Column {
Spacer(modifier = Modifier.requiredHeight(30.dp))
CircularProgressIndicator(progress = animatedProgress)
//圆形进度条指示器
Spacer(modifier = Modifier.requiredHeight(30.dp))
OutlinedButton(onClick = {
if (progress < 1f) progress += 0.1f
}) {
Text(text = "增加进度")
}
}
}

​ 代码中每次单击按钮,进度就会增加10%。当不设置progress时,就是无限加载的进度条。另外还有直线进度条(LinearProgressIndicator)可供选择,使用方法完全一致。

常用的布局组件

线性布局

​ 线性布局也是Android中最常用的布局方式,对应了传统视图中的LinearLayout,Compose根据orientation的不同又分为ColumnRow

1.Column

Column是一个垂直线性布局组件,它能够将子项按照从上到下的顺序垂直排列

1
2
3
4
5
6
7
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
)

verticalArrangmenthorizontalAlignment参数分别可以安排子项的垂直/水平位置,在默认的情况下,子项会以垂直方向上靠上(Arrangment. Top),水平方向上靠左(Alignment. Start)来布置。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Composable
fun ColumnSample(){
Column(
modifier = Modifier.border(1.dp, Color.Black),
) {
Text(
text = "Hello Compose",
style = MaterialTheme.typography.h6
)
Image(painter = painterResource(id = R.drawable.image1), contentDescription = "野比大雄")
Text(text = "野比大雄")
}
}

只有指定了高度或者宽度,才能使用verticalArrangement或horizontalAlignment来定位子项在Column中的位置。如果高度与宽度都指定了,就可以同时使用以上的两参数来定位子项的水平/垂直位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Composable
fun ColumnSample(){
Column(
modifier = Modifier
.border(1.dp, Color.Black)
.size(150.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Hello Compose",
style = MaterialTheme.typography.h6
)
Image(painter = painterResource(id = R.drawable.image1), contentDescription = "野比大雄")
Text(text = "野比大雄")
}
}

在给Column定义了大小之后,我们能够使用Modifier.align修饰符来独立设置子项的对齐规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Composable
fun ColumnSample(){
Column(
modifier = Modifier
.border(1.dp, Color.Black)
.size(150.dp),
verticalArrangement = Arrangement.Center,
) {
Text(
text = "Hello Compose",
style = MaterialTheme.typography.h6,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Image(
painter = painterResource(id = R.drawable.image1),
contentDescription = "野比大雄",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Text(text = "野比大雄")
}
}

注:在对齐效果的影响下,Modifier.align修饰符会优先于Column的horizontalAlignment参数。对于垂直布局中的子项,Modifier.align只能设置自己在水平方向的位置,反之水平布局的子项,只能设置自己在垂直方向的位置。

2.Row

Row组件能够将内部子项按照从左到右的方向水平排列

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
@Composable
fun ColumnSample2(){
androidx.compose.material.Surface(
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(horizontal = 12.dp) //设置Surface的外边距
.fillMaxWidth(),
elevation = 10.dp
) {
Column(
modifier = Modifier.padding(12.dp) //里面内容的外边距
) {
Text(
text = "Jetpack Compose 是什么?",
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.padding(vertical = 5.dp))
Text(text = "Jetpack Compose是用于构建原生Android界面的新工具包。" +
"它可简化并加快Android上的界面开发,使用更少的代码、强大的工具和直观的Kotlin API,让应用生动而精彩。")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
IconButton(onClick = { }) {
Image(painter = painterResource(id = R.drawable.image1), contentDescription = null)
}
IconButton(onClick = { }) {
Image(painter = painterResource(id = R.drawable.image2), contentDescription = null)
}
IconButton(onClick = { }) {
Image(painter = painterResource(id = R.drawable.image4), contentDescription = null)
}
}
}
}
}

RowhorizontalArrangement参数帮助我们合理配置了按钮的水平位置。Arrangment定义了很多子项位置的对齐方式,除了Center(居中)、Start(水平靠左)、End(水平靠右)等常见的对齐方式,还有一些特定场景下可能用到的对齐方式,例如Space Between、Space Evenly等。

帧布局

1.Box

Box组件是一个能够将里面的子项依次按照顺序堆叠的布局组件,在使用上类似于传统视图中的FrameLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
fun BoxSample(){
Box(
modifier = Modifier
.size(150.dp)
.background(Color.Green)
)
Box(
modifier = Modifier
.size(80.dp)
.background(Color.Red)
)
Text(
text = "世界"
)
}

2.Surface

Surface从字面上来理解,是一个平面,在Material Design设计准则中也同样如此,我们可以将很多的组件摆放在这个平面之上,可以设置这个平面的边框、圆角、颜色等。

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
@Composable
fun SurfaceSample(){
Surface(
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.width(300.dp)
.height(100.dp)
.padding(6.dp),
elevation = 10.dp,
) {
Row(
modifier = Modifier.clickable { }
) {
Image(
painter = painterResource(id = R.drawable.image1),
contentDescription = null,
modifier = Modifier.size(100.dp),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.padding(horizontal = 12.dp))
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.Center
) {
Text(text = "Patient", style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.padding(horizontal = 8.dp))
Text(text = "耐心")
}
}
}
}

Surface与Box之间的区别:

  • 如果我们需要快速设置界面的形状、阴影、边框、颜色等,则用Surface更为合适,它可以减少Modifier的使用量。
  • 如果只是需要简单地设置界面的背景颜色、大小,且有时候需要简单安排里面布局的位置,则可以使用Box。

Spacer留白

​ 让两个组件之间留有空白的间隔,这个时候就可以使用Spacer组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Composable
fun SpacerSample(){
Row {
Box(modifier = Modifier
.size(100.dp)
.background(Color.Red))
Spacer(modifier = Modifier.width(20.dp)) //也可使用Modifier.padding(horizontal = xx.dp)
Box(modifier = Modifier
.size(100.dp)
.background(Color.Magenta))
Spacer(modifier = Modifier.weight(1f))
Box(modifier = Modifier.size(100.dp).background(Color.Black))
}
}

​ 其实当Box没有content时,完全可以用Spacer替换。另外,还可以给Spacer做如下封装,可以更方便地用在水平或垂直布局中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Composable
fun WidthSpacer(
value:Dp
) = Spacer(modifier = Modifier.width(value))

@Composable
fun HeightSpacer(
value:Dp
) = Spacer(modifier = Modifier.height(value))

Row{
Button{ ... }
WidthSpacer(value = 10.dp)
Text{ ... }
}

ConstraintLayout约束布局

​ 使用约束布局可以有效降低视图树高度,使视图树扁平化。约束布局在测量布局耗时上,比传统的相对布局具有更好的性能表现,并且约束布局可以根据百分比自适应各种尺寸的终端设备。因为约束布局ConstraintLayout十分好用,所以官方为我们迁移到了Compose平台。

在build.gradle(app)脚本中添加Compose版本ConstraintLayout对应的依赖项。

1
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"

1.创建与绑定引用

​ 在Compose版本的ConstraintLayout中,可以主动创建引用并绑定至某个具体组件上,从而实现资源ID相似的功能。每个组件都可以利用其他组件的引用获取到其他组件的摆放位置信息,从而确定自己应摆放的位置。

​ 在Compose中有两种创建引用的方式:createRef()和createRefs()。字面意思非常清楚,createRef()每次只会创建一个引用,而createRefs()每次可以创建多个引用(最多16个)。

1
2
3
4
5
6
7
8
9
10
11
//createRef()
val portraitImageRef = remember {
createRef()
}
val usernameTextRef = remember {
createRef()
}
//createRefs()
val (portraitImageRef,usernameTextRef) = remember {
createRefs()
}

​ 接下来可以使用Modifier.constrainAs()修饰符将前面创建的引用绑定到某个具体组件上。可以在constrainAs尾部Lambda内指定组件的约束信息。只能在ConstraintLayout尾部的Lambda中使用createRef()、createRefs()创建引用,并使用Modifier.constrainAs()来绑定引用,这是因为ConstrainScope尾部Lambda的Reciever是一个ConstraintLayoutScope作用域对象。

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
@Composable
fun ConstraintLayoutSample(){
ConstraintLayout(
modifier = Modifier
.width(300.dp)
.height(100.dp)
.padding(10.dp)
) {
//类似于创建了两个id引用
val portraitImageRef = remember {
createRef()
}
val portraitImageRef2 = remember {
createRef()
}
Image(
painter = painterResource(id = R.drawable.image1),
contentDescription = null,
modifier = Modifier
.size(80.dp)
.constrainAs(portraitImageRef) {
//居左显示
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
}
)
Text(
text = "野比大雄",
fontSize = 20.sp,
modifier = Modifier.constrainAs(portraitImageRef2){
//居中显示
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
start.linkTo(parent.start)
}
)

}
}

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
@Composable
fun ConstraintLayoutSample2(){
ConstraintLayout(
modifier = Modifier
.width(300.dp)
.height(100.dp)
.padding(10.dp)
) {
val (portraitImageRef,usernameTextRef,desTextRef) = remember {
createRefs()
}
Image(
painter = painterResource(id = R.drawable.image1),
contentDescription = stringResource(id = R.string.ybdx),
modifier = Modifier
.size(80.dp)
.constrainAs(portraitImageRef) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
}
)
Text(
text = "A Compose technophile",
fontSize = 16.sp,
maxLines = 1,
textAlign = TextAlign.Left,
modifier = Modifier
.constrainAs(usernameTextRef){
top.linkTo(portraitImageRef.top)
start.linkTo(portraitImageRef.end,10.dp)
}
)
Text(
text = "我的个人描述...",
fontSize = 14.sp,
color = Color.Gray,
fontWeight = FontWeight.Light,
modifier = Modifier.constrainAs(desTextRef){
top.linkTo(usernameTextRef.bottom,5.dp)
start.linkTo(portraitImageRef.end,10.dp)
}
)

}
}

​ 也可以在ConstrainScope中为指定组件的宽高信息,在ConstrainScope中直接设置width与height即可,有几个可选值可供使用:

​ 当用户名过长时,可以通过设置end来指定组件最大所允许的宽度,并将width设置为preferredWrapContent,这意味着当用户名较短时,实际宽度会随着长度进行自适应调整:

1
2
3
4
5
6
7
8
9
10
11
12
Text(
text = "A Compose technophile A Compose technophile A Compose technophile ",
fontSize = 16.sp,
textAlign = TextAlign.Left,
modifier = Modifier
.constrainAs(usernameTextRef){
top.linkTo(portraitImageRef.top)
start.linkTo(portraitImageRef.end,10.dp)
end.linkTo(parent.end,10.dp)
width = Dimension.preferredWrapContent
}
)

Compose版本的ConstraintLayout同样也继承了一些优质特性,例如BarrierGuidelineChain等,方便我们完成各种复杂场景的布局需求。

2.Barrier分界线

​ 假如将两个输入框左对齐摆放,且距离文本组件中最长者仍保持10dp的间隔。当用户名、密码等发生变化时,输入框的位置能够自适应调整。在这个需求场景下,就需要使用到Barrier特性,在两个文本结束处添加一条分界线即可:

使用createEndBarrier创建一条结尾分界线,此时分界线位置位于两个文本中较长文本的结尾处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ConstraintLayout(
modifier = Modifier
.width(300.dp)
.height(100.dp)
.padding(10.dp)
) {
val (usernameTextRef,pwdTextRef,usernameInputRef,passwordInputRef,dividerRef) = remember {
createRefs()
}
//创建一条结尾分界线,此时分界线位置位于两个文本中较长文本的结尾处
var barrier = createEndBarrier(usernameTextRef,pwdTextRef)
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.constrainAs(usernameInputRef){
start.linkTo(barrier,10.dp)
top.linkTo(usernameTextRef.top)
bottom.linkTo(usernameTextRef.bottom)
height = Dimension.fillToConstraints
}
)
}

3.Guideline引导线

​ Barrier分界线是需要依赖其他引用,从而确定自身位置的。可以使用Guideline不依赖任何引用,凭空创建出一条引导线。

​ 假设我们希望将用户头像摆放在距离屏幕顶部2∶8的高度位置,头像以上的部分为用户背景,头像以下的部分为用户信息:

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
@Composable
fun GuidelineSample(){
ConstraintLayout(

) {
val (userPortraitBackgroundRef,userPortraitImageRef,usernameTextRef,pwdTextRef) = remember {
createRefs()
}
//首先可以使用createGuidelineFromTop创建从顶部出发的引导线。
val guideline = createGuidelineFromTop(0.2f)
//接下来的用户背景就可以依赖这条引导线确定宽高了。
Box(
modifier = Modifier
.constrainAs(userPortraitBackgroundRef) {
top.linkTo(parent.top)
bottom.linkTo(guideline)
height = Dimension.fillToConstraints
width = Dimension.matchParent
}
.background(Color(0xff1e9fff))
)
//摆放头像位置也很简单,仅需将top与bottom连接至引导线。
Image(
painter = painterResource(id = R.drawable.image1),
contentDescription = null,
modifier = Modifier
.constrainAs(userPortraitImageRef){
top.linkTo(guideline)
bottom.linkTo(guideline)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
.size(100.dp)
.clip(CircleShape)
.border(width = 2.dp, color = Color(0xff5fb878), shape = CircleShape)
)
}
}

4.Chain链接约束

ConstraintLayout另一个非常好用的特性就是Chain链接约束,通过链接约束可以允许多个组件平均分配布局空间,这个功能类似于weight修饰符。

如:在界面上展示四句诗词:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ConstraintLayout {
//首先需要创建四个引用对应这四句诗词
val (quotesFirstLineRef,quotesSecondLineRef,quotesThirdLineRef,quotesForthLineRef) = remember {
createRefs()
}
//接着创建一条垂直的链接约束将四句诗词连接起来,末尾参数需要传入一个ChainStyle,以表示我们期望链接布局样式
createVerticalChain(
quotesFirstLineRef,
quotesSecondLineRef,
quotesThirdLineRef,
quotesForthLineRef,
chainStyle = ChainStyle.Spread
)
}

Compose提供了三种Chain Style:

  • Spread:链条中每个元素平分整个parent空间。
  • SpreadInside:链条中首尾元素紧贴边界,剩下每个元素评分整个parent空间。
  • Packed:链条中所有元素聚集到中间。

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
@Composable
fun Poetry(){
ConstraintLayout {
//首先需要创建四个引用对应这四句诗词
val (quotesFirstLineRef,quotesSecondLineRef,quotesThirdLineRef,quotesForthLineRef) = remember {
createRefs()
}
//接着创建一条垂直的链接约束将四句诗词连接起来,末尾参数需要传入一个ChainStyle,以表示我们期望链接布局样式
createVerticalChain(
quotesFirstLineRef,
quotesSecondLineRef,
quotesThirdLineRef,
quotesForthLineRef,
chainStyle = ChainStyle.Spread
)
Text(
text = "寄蜉蝣于天地,",
color = Color.Black,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesFirstLineRef){
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
Text(
text = "渺沧海之一粟。",
color = Color.Black,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesSecondLineRef){
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
Text(
text = "哀吾生之须臾,",
color = Color.Black,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesThirdLineRef){
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
Text(
text = "羡长江之无穷。",
color = Color.Black,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesForthLineRef){
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
}
}

Scaffold脚手架

Scaffold组件实现了Material Design的布局结构,通过配合其他Material组件可以轻松地构建Material Design风格的界面。

一个带有TopAppBar的Scaffold例子:

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 ScaffoldSample(){
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "主页")
},
navigationIcon = {
IconButton(onClick = { }) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
}
}
)
}
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "主页界面")
}
}
}

在Scaffold组件中添加一个底部导航栏(BottomNavigation):

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
@Composable
fun ScaffoldSample1(){
var selectedItem by remember {
mutableStateOf(0)
}
val items = listOf<Item>(
Item("主页",Icons.Filled.Home),
Item("列表",Icons.Filled.List),
Item("设置",Icons.Filled.Settings)
)
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "主页")
},
navigationIcon = {
IconButton(onClick = { }) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
}
}
)
},
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
selected = selectedItem == index,
onClick = { selectedItem = index },
icon = { Icon(imageVector = item.imageVector, contentDescription = null )},
alwaysShowLabel = false,
label = { Text(text = item.name) }
)
}
}
}
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "主页界面")
}
}
}

BottomNavigation组件创造了底部导航栏的总体布局,比如高度等,这些都是按照Material Design风格设计的,要修改整体的颜色,可以在BottomNavigation的其他参数中修改。BottomNavigationItem组件则创建了具体的导航图标以及标签,在BottomNavigationItem组件中可以设置一些其他参数,例如选中/未选中时的图标颜色。设置alwaysShowLabel=false,也就是只有当前的页面才显示标签文字。

​ 通过Scaffold组件创建一个侧边栏很简单,Scaffold有一个drawerContent参数,只需要传递一个自定义的Composable的content即可:

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
@Composable
fun ScaffoldSample1(){
var selectedItem by remember {
mutableStateOf(0)
}
val items = listOf<Item>(
Item("主页",Icons.Filled.Home),
Item("列表",Icons.Filled.List),
Item("设置",Icons.Filled.Settings)
)
Scaffold(
//标题导航栏
topBar = {
TopAppBar(
title = {
Text(text = "主页")
},
navigationIcon = {
IconButton(onClick = { }) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
}
}
)
},
//底部导航栏
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
selected = selectedItem == index,
onClick = { selectedItem = index },
icon = { Icon(imageVector = item.imageVector, contentDescription = null )},
alwaysShowLabel = false,
label = { Text(text = item.name) }
)
}
}
},
//侧边栏
drawerContent = {
Text(text = "Hello")
}
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "主页界面")
}
}
}

​ 此时如果按下了系统返回键,应用会直接退出。我们希望此时只是关闭侧边栏。Compose提供了用于拦截系统返回键的组件BackHandler。此外,通过ScaffoldState可以监听侧边栏是否已打开。

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
@Composable
fun ScaffoldSample1(){
var selectedItem by remember {
mutableStateOf(0)
}
val items = listOf<Item>(
Item("主页",Icons.Filled.Home),
Item("列表",Icons.Filled.List),
Item("设置",Icons.Filled.Settings)
)
//通过ScaffoldState可以监听侧边栏是否已打开
//通过rememberScaffoldState()获取包含侧边栏状态的ScaffoldState
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
//标题导航栏
topBar = {
TopAppBar(
title = {
Text(text = "主页")
},
navigationIcon = {
IconButton(onClick = { }) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
}
}
)
},
//底部导航栏
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
selected = selectedItem == index,
onClick = { selectedItem = index },
icon = { Icon(imageVector = item.imageVector, contentDescription = null )},
alwaysShowLabel = false,
label = { Text(text = item.name) }
)
}
}
},
//侧边栏
drawerContent = {
Text(text = "Hello")
},
scaffoldState = scaffoldState

) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "主页界面")
}
}
//拦截系统返回键的组件BackHandler
BackHandler(
//当侧边栏被打开时,scaffoldState.drawerState.isOpen被更新为true,
// 此时,BackHandler开始监听系统返回键事件,返回键被按下则会通过scaffoldState来关闭侧边栏。
enabled = scaffoldState.drawerState.isOpen
) {
scope.launch {
//通过rememberCoroutineScope()创建了一个协程作用域,因为close()是一个挂起函数
scaffoldState.drawerState.close()
}
}
}

列表

​ 很多产品中都有展示组数据的需求场景,如果数据数量是可以枚举的,则仅需通过Column组件来枚举列出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Composable
fun MenuSample(
options:List<Options>,
expanded:Boolean,
onDismissRequest:()->Unit
){
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
Column {
options.forEach{ option ->
ListItem(text = { Text(text = option.text)})
}
}
}
}

​ 可以通过给ColumnModifier添加verticalScroll()方法来让列表实现滑动。

LazyComposables

​ 如果列表过长,众多的内容会占用大量的内存。然而更多的内容对于用户其实都是不可见的,没必要加载到内存。所以Compose提供了专门用于处理长列表的组件,这些组件只会在能看到的列表部分进行重组和布局,它们分别是LazyColumnLazyRow。其作用类似于传统视图中的ListView或者RecyclerView

LazyListScope作用域

LazyColumnLazyRow内部都是基于LazyList组件实现的,虽然这是一个internal的内部组件,我们无法直接使用它。LazyList和其他布局类组件不同,不能在它的content里面直接裸写子Composable组件。它的content是一个LazyListScope.()-> Unit类型的作用域代码块,在内部通过LazyListScope提供的item等方法来描述列表中的内容,整体符合DSL的代码风格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Composable
fun MenuSample(){
LazyColumn {
//在lazyColumn中添加一个Text
item {
Text(text = "第一项内容")
}
//在第一个text之后添加五个text
items(5){index ->
Text(text = "第${index + 2}项内容")
}
//添加另一个Text
item {
Text(text = "最后一项")
}
}
}

​ 除了itemitems(Int),LazyListScope还提供了items(List< T> )以及itemsIndexed(List< T> )扩展函数,允许直接传入一个List对象。比如像下面这样创建一个菜单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Composable
fun MenuSample(
options:List<Options>,
expanded:Boolean,
onDismissRequest:()->Unit
){
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
LazyColumn {
items(options){ option ->
ListItem(text = { Text{ option.text } })
}
}
}
}

内容填充

​ 有的时候也需要在列表中为内容设置外边距,这也非常容易,Lazy组件提供了contentPadding参数。

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
@Composable
fun MenuSample2(){
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray),
contentPadding = PaddingValues(35.dp)
){
items(50){ index ->
ContentCard(index = index)
}
}
}

@Composable
fun ContentCard(index:Int){
Card(
elevation = 8.dp,
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(15.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "我是序号第${index}位的卡片",
style = MaterialTheme.typography.h5
)
}
}
}

​ 还能通过Arrangement设置Lazy组件中每个项目之间的间隔(水平/竖直):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Composable
fun MenuSample2(){
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray),
contentPadding = PaddingValues(35.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
){
items(50){ index ->
ContentCard(index = index)
}
}
}