compose--初入compose、资源获取、标准控件与布局
compose正式发布已经一年多了,越来越多的开发人员选择使用它,声明式UI
也是未来的一个主流趋势,本人也是一年前学习后,并没有真正的使用,所以本着边学习,边分享的心态,准备写个compose系列的文章
首先compose目前只支持kotlin
,基于google对移动端的鸿图,未来应该也不会支持其他语言,和传统安卓的xml
布局不同,compose是通过kotlin
定义一个一个组件,由于是通过代码定义的组件,每个组件都可以很方便的重用
,这点在UI开发时确实便利了不少。至于声明式UI和命令式UI的区别,相信你会在后续实际使用时有很大的感触
一、认识compose
通过官方文档我们可以了解到compose的编程思想。官方地址:https://developer.android.google.cn/jetpack/compose/mental-model
我这边也是根据官方文档,对重要的部分和自己的想法进行融合,来介绍什么是compose。这部分内容都是概念性的,但是贯穿整个compose的学习,应该进行着重深入
理解
1. 重组
1.1 安卓传统UI
先来说在安卓传统UI,大致的流程就是xml
中我们定义了一系列的布局(组件)和控件后,由Activity
的onCreate()
触发xml
解析,生成View树
:DecorView
,并Activity
的handleResumeActivity()
与ViewRootImpl
绑定,通过Binder
通信,交由由WindowManagerService
创建surface
进行渲染,最终呈现在手机屏幕
当然了,我们只需要关注在onCreate()
中设置xml
即可,由于布局是一次性加载的,即生成View树
的过程是同步
进行的
1.2 compose UI
对与compose
而言,每个可组合函数(组件)的调用可能发生在与调用方不同的线程上,即每个组件添加至View树
的过程,都是通过协程进行的,上树的过程未必按代码调用的顺序执行
1.3 什么是重组?
在compose中,每个可组合函数调用直至渲染完成,称之为重组
通过异步上树虽然带来了性能的提升,但是管理方面变得困难,所以compose规定,每个可组合函数都是独立运行的存在,可组合函数内部应该仅处理的UI操作,重组的发生的时机并不由我们控制,而是由compose内部自动管理,后续我们可以使用状态
来通知compose进行重组
二、创建compose项目
推荐使用最新的android studio
,低版本并不支持compose,也可以查看官方文档-快速入门:https://developer.android.google.cn/jetpack/compose/setup
1.创建项目
我这边尝鲜使用MD3
风格的项目,实际开发中google也推荐:UI设计从MD2转变为MD3
2.BOM
对于compose的版本管理,官方推荐使用BOM
,导入BOM后的好处是:导入compose其他库组,都将使用BOM中定义的版本,后续更新,我们只需要更新BOM的版本即可。下面是官方给出的BOM:compose
版本对应关系:
库组 | 版本 (2022.10.00) | 版本 (2022.11.00) |
---|---|---|
androidx.compose.animation:animation | 1.3.0 | 1.3.1 |
androidx.compose.animation:animation-core | 1.3.0 | 1.3.1 |
androidx.compose.animation:animation-graphics | 1.3.0 | 1.3.1 |
androidx.compose.foundation:foundation | 1.3.0 | 1.3.1 |
androidx.compose.foundation:foundation-layout | 1.3.0 | 1.3.1 |
androidx.compose.material:material | 1.3.0 | 1.3.1 |
androidx.compose.material:material-icons-core | 1.3.0 | 1.3.1 |
androidx.compose.material:material-icons-extended | 1.3.0 | 1.3.1 |
androidx.compose.material:material-ripple | 1.3.0 | 1.3.1 |
androidx.compose.material3:material3 | 1.0.0 | 1.0.1 |
androidx.compose.material3:material3-window-size-class | 1.0.0 | 1.0.1 |
androidx.compose.runtime:runtime | 1.3.0 | 1.3.1 |
androidx.compose.runtime:runtime-livedata | 1.3.0 | 1.3.1 |
androidx.compose.runtime:runtime-rxjava2 | 1.3.0 | 1.3.1 |
androidx.compose.runtime:runtime-rxjava3 | 1.3.0 | 1.3.1 |
androidx.compose.runtime:runtime-saveable | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-geometry | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-graphics | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-test | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-test-junit4 | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-test-manifest | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-text | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-text-google-fonts | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-tooling | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-tooling-data | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-tooling-preview | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-unit | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-util | 1.3.0 | 1.3.1 |
androidx.compose.ui:ui-viewbinding | 1.3.0 | 1.3.1 |
工程中导入:
dependencies {
def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
implementation composeBom
...
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose'
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.ui:ui-tooling-preview"
implementation 'androidx.compose.material3:material3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.3.1"
debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-test-manifest"
}
3.kotlin-compose compiler版本对应
BOM中不包含Compose编译器库,所以我们需要手动对应下kotlin版本与compose compiler版本,下面是两者的兼容关系,官网也可以查询到最新的对应关系:
https://developer.android.google.cn/jetpack/androidx/releases/compose-kotlin
Compose Compiler 版本 | 兼容的 Kotlin 版本 |
---|---|
1.4.0-alpha01 | 1.7.20 |
1.3.2 | 1.7.20 |
1.3.1 | 1.7.10 |
1.3.0 | 1.7.10 |
1.3.0-rc02 | 1.7.10 |
1.3.0-rc01 | 1.7.10 |
1.3.0-beta01 | 1.7.10 |
1.2.0 | 1.7.0 |
1.2.0-rc02 | 1.6.21 |
1.2.0-rc01 | 1.6.21 |
1.2.0-beta03 | 1.6.21 |
1.2.0-beta02 | 1.6.21 |
1.2.0-beta01 | 1.6.21 |
1.2.0-alpha08 | 1.6.20 |
1.2.0-alpha07 | 1.6.10 |
1.2.0-alpha06 | 1.6.10 |
1.2.0-alpha05 | 1.6.10 |
1.2.0-alpha04 | 1.6.10 |
1.2.0-alpha03 | 1.6.10 |
1.2.0-alpha02 | 1.6.10 |
1.2.0-alpha01 | 1.6.10 |
1.1.1 | 1.6.10 |
1.1.0 | 1.6.10 |
1.1.0-rc03 | 1.6.10 |
1.1.0-rc02 | 1.6.10 |
1.1.0-rc01 | 1.6.0 |
1.1.0-beta04 | 1.6.0 |
1.1.0-beta03 | 1.5.31 |
1.1.0-beta02 | 1.5.31 |
1.1.0-beta01 | 1.5.31 |
1.1.0-alpha06 | 1.5.31 |
1.1.0-alpha05 | 1.5.31 |
1.0.5 | 1.5.31 |
1.0.4 | 1.5.31 |
1.1.0-alpha04 | 1.5.30 |
1.1.0-alpha03 | 1.5.30 |
1.0.3 | 1.5.30 |
1.1.0-alpha02 | 1.5.21 |
1.1.0-alpha01 | 1.5.21 |
1.0.2 | 1.5.21 |
1.0.1 | 1.5.21 |
1.0.0 | 1.5.10 |
1.0.0-rc02 | 1.5.10 |
1.0.0-rc01 | 1.5.10 |
我这边使用的是1.3.1,对应kotlin版本是1.7.10,工程中build.gradle
:
android {
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.1"
}
kotlinOptions {
jvmTarget = "1.8"
}
}
主工程中build.gradle
:
plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
}
4.预览compose函数与启动
4.1 预览compose函数
引入了ui-tooling-preview
库组后,我们可以使用@Preview
注解可组合函数,并实现预览组件
4.2 启动
启动到模拟器的效果:
三、资源获取
在xml
中,我们常常会使用资源id
获取到资源文件,比如:color、drawable、string等,在compose中,通过以下函数获取,这些函数都位于androidx.compose.ui.res
包下:
当然我们并不需要使用里面全部的类,掌握下面列出的即可:
资源获取方式 | 描述 |
---|---|
stringResource | 获取对应id的string资源,并支持传入多个参数,来实现字符串格式化 |
colorResource | 获取对应id的color资源 |
painterResource | 获取对应id的图片资源,可以是一个vector,也可以是drawable |
dimensionResource | 获取对应id的dimen资源,由于compose推荐使用md主题设置dimen,用的也不多 |
四、标准控件
compose本身内置了一些组件,官方说法所有组件都是可组合函数,这边仅仅是便于传统开发理解,分成控件和布局来介绍,这些内置可组合函数分散在各个不同的库组内,如:androidx.compose.foundation
,androidx.compose.foundation.layout
,androidx.compose.material3
。
其中控件大多位于md包下,他们都具有MD风格,也是官方推荐使用的组件:
1.Text
Text
用于呈现一段文字,是使用最多的组件,官方也详细的介绍了该组件:https://developer.android.google.cn/jetpack/compose/text
1.1 基本使用
所有compose函数都要由@Composable
注解,并且每个可组合函数都是可以重用的组件:
@Composable
@Preview
fun MyText() {
Text(text = "hello world!")
}
预览效果:
1.2 使用资源获取文本
通过stringResource(id)
获取String
,可以达到同样的效果
@Composable
@Preview
fun MyText() {
Text(text = stringResource(id = R.string.hello))
}
1.3 AnnotatedString
传统UI的TextView
,可以通过Span来改变文本的内嵌样式,比如个别字颜色设置、设置背景颜色等效果
compose
中可以使用AnnotatedString
来达到这种效果,通过buildAnnotatedString()
构建一个AnnotatedString
,AnnotatedString
可以包含多个 SpanStyle(点击跳转API) 和 ParagraphStyle(点击跳转API)
-
SpanStyle
:设置文本的内嵌样式 -
ParagraphStyle
:设置文本的行高,对齐方式,文字方向和文字缩进样式
例子:
@Composable
@Preview
fun MyText() {
Text(
text = buildAnnotatedString {
withStyle(
style = ParagraphStyle(
lineHeight = 30.sp,//行高
textAlign = TextAlign.Left,//左对齐
textIndent = TextIndent(firstLine = 10.sp)//缩进
)
) {
withStyle(
style = SpanStyle(
fontSize = 20.sp,
color = Color.Red,//设置颜色为红色
fontWeight = FontWeight.Medium//加粗
)
) {
append("hi\n")
}
}
withStyle(
style = ParagraphStyle(
lineHeight = 60.sp,
)
) {
withStyle(
style = SpanStyle(
color = Color.Red,
shadow = Shadow(//设置阴影
color = Color.Blue,//阴影颜色
blurRadius = 3f,//虚化
offset = Offset(5f, 20f)//x,y轴的偏移
)
)
) {
append("你好\n")
}
}
}
)
}
预览效果:
1.4 其他参数
其他参数可以通过源码查看:
@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,//最大行数
onTextLayout: (TextLayoutResult) -> Unit = {},//文本变化导致重组的回调
style: TextStyle = LocalTextStyle.current//更丰富的字体样式,包含上面大多数设置,以及SpanStyle和ParagraphStyle
) {
...
}
其中Modifier
后续会详细介绍,举例使用里面的几个参数设置,如使用TextStyle
去除首行的顶部行间距:
<string name="hello">hello!\nworld</string>
@Composable
@Preview
fun MyText() {
Text(
text = stringResource(id = R.string.hello),
fontWeight = FontWeight.Medium,
overflow = TextOverflow.Clip,
//将当前的style和另一个合并,以另一个设置的属性为优先
style = LocalTextStyle.current.merge(
TextStyle(
lineHeight = 2.5.em,
platformStyle = PlatformTextStyle(
includeFontPadding = false//配合trim
),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
// trim生效需要includeFontPadding = false
// trim是指将行间距尽可能的去除
// FirstLineTop:将第一行顶部的行间距去除
trim = LineHeightStyle.Trim.FirstLineTop
)
)
)
)
}
预览效果:
2.Image
Image
用于展现图片
2.1 基本使用
必传入参为图片资源对象painter
和内容描述contentDescription
,contentDescription
主要是为了残疾人使用的,国外对于残疾人使用也非常的重视,此外使用python
自动化测试也可以通过contentDescription
找到该组件:
@Composable
@Preview
fun MyImage() {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),//指定图片资源
contentDescription = "my image" //描述,残疾人以及自动化测试使用
)
}
预览效果:
2.2 其他参数
相较于Text
,Image
的参数少很多:
@Composable
fun Image(
painter: Painter,
contentDescription: String?,
modifier: Modifier = Modifier,//修饰符
alignment: Alignment = Alignment.Center,//图片对齐方式
contentScale: ContentScale = ContentScale.Fit,//图片的拉伸方式
alpha: Float = DefaultAlpha,//图片透明度
colorFilter: ColorFilter? = null//通过ColorFilter对颜色矩阵进行变换
) {
}
参数还是比较简单的,ContentScale
的几种方式可以通过官网认识:ContentScale介绍(点击跳转),其中ColorFilter
和传统UI自定义控件时,使用的高级渲染效果相同,ColorFilter
分别拥有三个伴生方法,对应不同的渲染方式:
-
tint(color: Color, blendMode: BlendMode = BlendMode.SrcIn)
:高级渲染,参考Xfermod(点击跳转) -
colorMatrix(colorMatrix: ColorMatrix)
:颜色矩阵变换,参考ColorMatrixColorFilter(点击跳转) -
lighting(multiply: Color, add: Color)
:颜色向量增加,参考LightingColorFilter(点击跳转)
使用tint
例子,使用SrcIn
模式合成一个红色:
@Composable
@Preview
fun MyImage() {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "my image",
colorFilter = ColorFilter.tint(
color = Color.Red,
blendMode = BlendMode.SrcIn
)
)
}
预览效果:
使用colorMatrix
例子,颜色增强:
@Composable
@Preview
fun MyImage() {
Row {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = "my image1",
colorFilter = ColorFilter.colorMatrix(
ColorMatrix().apply {
setToScale(1.2f, 1.2f, 1.2f, 1f)//颜色增强
}
)
)
Spacer(modifier = Modifier.width(10.dp))
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = "my image2",
)
}
}
预览效果,左边为颜色增强后:
使用lighting
例子,添加红色向量:
@Composable
@Preview
fun MyImage() {
Row {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = "my image1",
// 红色向量添加255,红色加绿色 = 黄色
colorFilter = ColorFilter.lighting(
Color(red = 0xff, green = 0xff, blue = 0xff),
Color(red = 0xff, green = 0, blue = 0)
)
)
Spacer(modifier = Modifier.width(10.dp))
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = "my image2",
)
}
}
预览效果,左边为添加红色向量后:
2.3 Icon
同样用于显示图标,Icon
功能比Image
少,只支持tint
,并且该tint
为一个Color
对象,不支持模式,只支持染色:
@Composable
@Preview
fun MyImage() {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "icon",
tint = Color.Blue // 将图标染成蓝色
)
}
预览效果:
3.TextField
TextField
就是输入框,并且需要用到state
,关于state
后续会详细介绍
3.1 基本使用
TextField
必须传入的两个参数,一个是value
,一个是onValueChange
,结合之前的重组
概念来理解,每次重组都会重新调用可组合函数
,所以输入框内容value
必须是一个全局对象,在compose中,可以使用remember函数
来使得一个变量成为全局变量,从而不受重组时代码调用导致重新初始化操作的影响
此外,只有state
的改变才能通知compose进行重组,所以value
又必须是一个state
对象,并在onValueChange
中对state
进行改变,才能够进行组件的刷新
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyTextField() {
var text by remember { mutableStateOf("") }// 定义state对象:text ,并设为全局
TextField(
value = text,//text 与TextField进行绑定
onValueChange = { text = it },//当输入框值发生变换时,改变text值,从而引起状态的刷新,进而重组
label = { Text("hint") }//提示
)
}
效果:
3.2 TextFieldValue
value
的参数类型除了支持String
外,还支持TextFieldValue
,TextFieldValue
具有更好的自定义性,如使用AnnotatedString
使文本具有样式、TextRange
指定光标位置:
@Immutable
class TextFieldValue constructor(
val annotatedString: AnnotatedString,//带样式的字符串
selection: TextRange = TextRange.Zero,//
composition: TextRange? = null
) {
...
}
例子:
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Preview
@Composable
fun TextFieldValuePreview(
) {
val textFieldValueState = remember {
mutableStateOf(
TextFieldValue(
annotatedString = buildAnnotatedString {
append("hi")
withStyle(
style = SpanStyle(
color = Color.Red,
//设置阴影
shadow = Shadow(
color = Color.Blue,//阴影颜色
blurRadius = 3f,//虚化
)
)
) {
append("你好\n")
}
},
selection = TextRange(2)// 光标默认显示在第二个字符位置
)
)
}
val showKeyboard = remember { mutableStateOf(true) }
val focusRequester = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current
// 显示键盘
LaunchedEffect(focusRequester) {
if (showKeyboard.value) {
focusRequester.requestFocus()
delay(100)
keyboard?.show()
}
}
TextField(
modifier = Modifier.focusRequester(focusRequester),
value = textFieldValueState.value,
onValueChange = {
}
)
}
效果:
3.3 其他参数
@ExperimentalMaterial3Api
@Composable
fun TextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,// 是否可用
readOnly: Boolean = false,// 是否只读
textStyle: TextStyle = LocalTextStyle.current,// 和Text一样支持的TextStyle
label: @Composable (() -> Unit)? = null,//提示,有内容时自动缩小并上移
placeholder: @Composable (() -> Unit)? = null,//提示,有内容时自动消失
leadingIcon: @Composable (() -> Unit)? = null,//文本前的图标
trailingIcon: @Composable (() -> Unit)? = null,//文本尾的图标
supportingText: @Composable (() -> Unit)? = null,//文本下方的文本
isError: Boolean = false,//是否错误,错误会将label、下划线、下方文本、文本尾的图标的图标染红
visualTransformation: VisualTransformation = VisualTransformation.None,//输入内容的视觉类型,如密码显示*
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,//键盘类型和imeAction
keyboardActions: KeyboardActions = KeyboardActions.Default,//imeAction触发时的回调
singleLine: Boolean = false,//是否单行
maxLines: Int = Int.MAX_VALUE,//最大行数
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },//传入状态,从而监听用户触摸操作,如点击、拖拽
shape: Shape = TextFieldDefaults.filledShape,//设置背景形状
colors: TextFieldColors = TextFieldDefaults.textFieldColors()// 颜色集,通过设置相应的颜色,可以改变如错误发生时的颜色
) {
...
}
例子:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyTextField() {
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
placeholder = { Text("haha") },
leadingIcon = {//设置文本前图片
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "leadingIcon"
)
},
trailingIcon = {//设置文本后图片
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "leadingIcon"
)
},
supportingText = {//设置文本下的文本
Text("supportingText")
},
isError = true,// 设置发生错误
visualTransformation = PasswordVisualTransformation(),//视觉为密码
shape = RoundedCornerShape(10.dp),//背景为圆角
colors = TextFieldDefaults.textFieldColors(//错误时,下划线显示黄色
errorIndicatorColor = Color.Yellow
)
)
}
效果:
3.4 OutlinedTextField
OutlinedTextField
是含有一个边框的输入框,其他用法和TextField
相同
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyTextField() {
var text by remember { mutableStateOf("") }
OutlinedTextField(
modifier = Modifier.padding(start = 10.dp, top = 10.dp),
value = text,
onValueChange = { text = it },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions {
}
)
}
效果:
4. Button
Button
需要传入一个点击事件onClick
的lambda
表达式,和一个content
内容组件的lambda
表达式,border
边框支持Shader(点击跳转详情),其他参数说明如下:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,// 是否可用
shape: Shape = ButtonDefaults.shape,// 背景形状
colors: ButtonColors = ButtonDefaults.buttonColors(),//颜色集,背景、内容的可用和非可用颜色
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),//阴影,默认、按下、不可用等状态下的阴影
border: BorderStroke? = null,//边框,支持Shader
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,// 内容组件的padding
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },//触摸事件的状态改变
content: @Composable RowScope.() -> Unit//按钮的内容组件
) {
}
4.1 基本使用
Button
的content
是一个RowScope
的作用域,也就是以行来摆放组件
例子:
@Preview
@Composable
fun MyButton() {
Button(
onClick = { /*TODO*/ },
colors = ButtonDefaults.buttonColors(
containerColor = Color.Cyan,
contentColor = Color.Red
),
elevation = ButtonDefaults.buttonElevation(defaultElevation = 3.dp),
border = BorderStroke(
1.dp,
Brush.linearGradient(
0f to Color.Transparent,
1f to Color.DarkGray
)
),
contentPadding = PaddingValues(
start = 10.dp,
top = 5.dp
),
content = {
Text("点我")
Text("点我")
}
)
}
预览效果:
4.2 IconButton
IconButton
的content
需要传入一个Icon
组件,其他用法和Button
相同:
@Composable
fun MyIconButton() {
IconButton(
onClick = { /*TODO*/ },
colors = IconButtonDefaults.iconButtonColors(contentColor = Color.Green),
content = {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "icon"
)
}
)
}
预览效果:
4.3 IconToggleButton
IconToggleButton
具有选中和未选中状态,checked
入参需要配合state
对象使用,onCheckedChange
用于选中状态切换的处理,其他用法和Button
相同:
@Preview
@Composable
fun MyIconToggleButton() {
var checked by remember { mutableStateOf(false) }
IconToggleButton(
checked = checked,
onCheckedChange = {
checked = it
},
modifier = Modifier
.width(100.dp)
.height(100.dp),
colors = IconButtonDefaults.iconToggleButtonColors(
contentColor = Color.Green,//选中为绿色
checkedContentColor = Color.Red//非选中为红色
),
content = {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "icon"
)
}
)
}
效果:
4.4 Switch
Switch
为开关样式的IconToggleButton
组件,thumbContent
参数支持指定开关按钮的Icon
,其他用法与IconToggleButton
相同:
@Preview
@Composable
fun MySwitch() {
var checked by remember { mutableStateOf(false) }
Switch(
checked = checked,
onCheckedChange = { checked = it },
thumbContent = {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "icon"
)
}
)
}
效果:
4.5 RadioButton
RadioButton
为单选框
@Preview
@Composable
fun MyRadioButton() {
var selected by remember { mutableStateOf(false) }
RadioButton(
selected = selected,
onClick = { selected = !selected }
)
}
效果:
4.6 Checkbox
Checkbox
为复选框
@Preview
@Composable
fun MyCheckbox() {
var selected by remember { mutableStateOf(false) }
Checkbox(
checked = selected,
onCheckedChange = { selected = it }
)
}
效果:
4.7 ExtendedFloatingActionButton
ExtendedFloatingActionButton
为悬浮按钮,控制expanded
参数可以展开和缩小,此外还支持shape
设置背景形状、elevation
设置阴影:
@Composable
fun ExtendedFloatingActionButton(
text: @Composable () -> Unit,// 文字
icon: @Composable () -> Unit,// 图标
onClick: () -> Unit,
modifier: Modifier = Modifier,
expanded: Boolean = true,// 是否展开
shape: Shape = FloatingActionButtonDefaults.extendedFabShape,//背景形状
containerColor: Color = FloatingActionButtonDefaults.containerColor,//容器颜色
contentColor: Color = contentColorFor(containerColor),//内容组件颜色
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),//阴影
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
...
}
例子:
@Preview
@Composable
fun MyExtendedFloatingActionButton() {
var expanded by remember { mutableStateOf(false) }
ExtendedFloatingActionButton(
text = { Text(text = "点我") },
icon = { /*TODO*/ },
onClick = { expanded = !expanded },
expanded = expanded,
shape = RoundedCornerShape(30.dp)
)
}
效果:
5.Spacer
Spacer
表示间距,用来代表一片隔离区域,隔离组件与组件
@Preview
@Composable
fun MySpacer() {
Row {
Text("hi")
Spacer(modifier = Modifier.width(20.dp))
Text("hi")
}
}
预览效果:
6.Divider
Divider
可以用来表示一条分割线,默认是一条横向的,所以通过Modifier
来改变
@Preview
@Composable
fun MyDivider() {
Row() {
Text(
"hi",
modifier = Modifier.weight(1f)
)
Divider(
color = Color.Blue,
modifier = Modifier
.fillMaxHeight()//充满整个组件
.width(1.dp)//宽度为1dp
)
Text(
"hi",
modifier = Modifier.weight(1f)
)
}
}
预览效果:
6.1 IntrinsicSize
从上面的预览效果可以知道,将Divider
设置为最大高度后,MyDivider
组件充满了整个屏幕,如果想到达到Divider
的高度不计入MyDivider
的高度,并随着MyDivider
的高度进行填充,就需要用到IntrinsicSize
IntrinsicSize
表示允许父组件优先查询下子组件的高度,所以设置给父组件,这边给Row
设置Modifier
:
@Preview
@Composable
fun MyDivider2() {
Row(modifier = Modifier.height(IntrinsicSize.Min)) {//高度设置为IntrinsicSize
Text(
"hi",
modifier = Modifier.weight(1f)
)
Divider(
color = Color.Red,
modifier = Modifier
.fillMaxHeight()//充满整个组件
.width(1.dp)//宽度为1dp
)
Text(
"hi",
modifier = Modifier.weight(1f)
)
}
}
预览效果:
五、标准布局
compose
中的布局也不多,最基础的为Column
、Row
、Box
,官方给出的定义如下图:
1.Row
上面我们使用过一个Row
,它的作用域是RowScope
,同横向LinearLayout
@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,// 内容组件水平排列方式
verticalAlignment: Alignment.Vertical = Alignment.Top,//内容组件垂直对齐方式
content: @Composable RowScope.() -> Unit// 内容组件
) {
}
1.1 Arrangement
关于Arrangement
的几种方式,官方给出的图示:
1.2 基本使用
@Preview
@Composable
fun MyRow() {
Row(
modifier = Modifier.width(100.dp),
horizontalArrangement = Arrangement.End,//内容组件往右对齐
verticalAlignment = Alignment.CenterVertically//内容组件垂直居中
) {
Text("hi")
Text("你好\n 张三")
}
}
预览效果:
2.Column
Column
就是竖直方向摆放组件的布局,用法上和Row
相同,同竖向LinearLayout
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,//内容组件垂直对齐方式
horizontalAlignment: Alignment.Horizontal = Alignment.Start,//内容组件水平对齐方式
content: @Composable ColumnScope.() -> Unit//内容组件
) {
}
2.1 基本使用
@Preview
@Composable
fun MyColumn() {
Column(
modifier = Modifier.height(100.dp),
horizontalAlignment = Alignment.CenterHorizontally,//内容组件水平居中
verticalArrangement = Arrangement.SpaceBetween//内容组件垂直分布到两侧
) {
Text("hi")
Text("你好\n 张三")
}
}
预览效果:
3.Box
Box
类似FrameLayout
,可以堆叠摆放子组件
@Composable
inline fun Box(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,//内容组件的对齐方式
propagateMinConstraints: Boolean = false,//是否指定内容组件使用该组件的最小约束(最小宽高)
content: @Composable BoxScope.() -> Unit
) {
}
3.1 基本使用
下面两个Image
的宽高设定为40dp,由于Box
设置了最小约束为50dp和70dp,所以Image
变大了:
@Preview
@Composable
fun MyBox() {
Box(
modifier = Modifier
.sizeIn(50.dp, 70.dp),//设置内容组件的最小宽度和高度为50dp、70dp,配合propagateMinConstraint=true使用
propagateMinConstraints = true,//使内容组件最小宽度和高度生效
contentAlignment = Alignment.BottomEnd
) {
// propagateMinConstraints,内部需要一个组件撑大整体的大小
Box(Modifier.size(50.dp,150.dp).background(Color.Cyan))
Image(
modifier = Modifier.size(40.dp, 40.dp),
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
contentScale = ContentScale.FillHeight // 图片高度拉伸
)
Image(
modifier = Modifier.size(40.dp, 40.dp),
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
contentScale = ContentScale.FillHeight // 图片高度拉伸
)
}
}
预览效果:
4.Scaffold
Scaffold
预设了很多槽位(存放子组件)和功能,Scaffold
的学习可以通过官网:Scaffold官方示例(有些参数只有MD2版本才有)
4.1 topBar
槽位topBar
就是给顶部子组件准备的,如:TopAppBar:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold() {
Scaffold(
topBar = {
TopAppBar(
title = {//标题
Text(
modifier = Modifier.padding(start = 10.dp),
text = "topBar"
)
},
navigationIcon = {//导航图标
Icon(Icons.Default.ArrowBack, contentDescription = null)
},
actions = {//右侧按钮
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Search, contentDescription = null)
}
},
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
)
}
) { paddings ->
Box(modifier = Modifier.padding(paddings))
}
}
预览效果:
4.2 bottomBar
和topBar
对应,bottomBar
是用来存放底部子组件的槽位,如:BottomAppBar、BottomNavigation:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold() {
Scaffold(
topBar = {
...
},
bottomBar = {
BottomAppBar(
containerColor = MaterialTheme.colorScheme.primaryContainer,
tonalElevation = 2.dp,
) {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Home, contentDescription = null)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.ShoppingCart, contentDescription = null)
}
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Info, contentDescription = null)
}
}
}
) { paddings ->
Box(modifier = Modifier.padding(paddings))
}
}
效果:
4.3 floatingActionButton
floatingActionButton
是专门为FloatingActionButton
准备的槽位,配合floatingActionButtonPosition
可以改变槽位的位置,目前只支持底部居中和底部靠右:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold() {
Scaffold(
topBar = {
...
},
bottomBar = {
...
},
floatingActionButton = {
FloatingActionButton(onClick = { /*TODO*/ }) {
Text(text = "hi")
}
},
floatingActionButtonPosition = FabPosition.Center
) { paddings ->
Box(modifier = Modifier.padding(paddings))
}
}
效果:
4.4 snackbarHost
snackbarHost
槽位用于展示一个提示SnackbarHost
,需要通过SnackbarHostState
来控制该子组件的显示:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold2() {
val scaffoldState by remember { mutableStateOf(SnackbarHostState()) }
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
...
},
bottomBar = {
...
},
floatingActionButton = {
FloatingActionButton(onClick = {
scope.launch {
scaffoldState.showSnackbar("hi,this is snack bar")
}
}) {
Text(text = "hi")
}
},
snackbarHost = { SnackbarHost(hostState = scaffoldState) },
) { paddings ->
Box(modifier = Modifier.padding(paddings))
}
}
效果:
SnackbarHostState
还支持显示的时长,相应的点击动作,基于协程返回消失或点击动作的结果:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold2() {
val scaffoldState by remember { mutableStateOf(SnackbarHostState()) }
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
...
},
bottomBar = {
...
},
floatingActionButton = {
FloatingActionButton(onClick = {
scope.launch {
val result = scaffoldState.showSnackbar(
message = "hi,this is snack bar",
duration = SnackbarDuration.Short,
actionLabel = "click"
)
when (result) {
SnackbarResult.ActionPerformed -> {
/* Handle snackbar action performed */
scaffoldState.currentSnackbarData?.dismiss()
}
SnackbarResult.Dismissed -> {
/* Handle snackbar dismissed */
}
}
}
}) {
Text(text = "hi")
}
},
snackbarHost = { SnackbarHost(hostState = scaffoldState) },
) { paddings ->
Box(modifier = Modifier.padding(paddings))
}
}
效果:
4.5 MD2-drawerContent
drawerContent
是抽屉菜单的槽位,它是一个ColumnScope
,注意目前MD3
版本并不支持,如果要使用,需要MD2
的Scaffold
:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffoldDrawer() {
val drawerState = rememberScaffoldState()
val scope = rememberCoroutineScope()
androidx.compose.material.Scaffold(
topBar = {
TopAppBar(
title = {//标题
Text(
modifier = Modifier.padding(start = 10.dp),
text = "topBar"
)
},
navigationIcon = {//导航图标
Icon(
modifier = Modifier.clickable {
scope.launch {
drawerState.drawerState.apply {
if (isClosed) open() else close()
}
}
},
imageVector = Icons.Default.List,
contentDescription = null
)
},
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
)
},
drawerContent = {
Text("title")
Row(modifier = Modifier.padding(start = 10.dp,top = 10.dp)) {
Image(Icons.Default.Phone, contentDescription = null)
Text(text = "my phone")
}
Row(modifier = Modifier.padding(start = 10.dp,top = 10.dp)) {
Image(Icons.Default.Call, contentDescription = null)
Text(text = "call")
}
Row(modifier = Modifier.padding(start = 10.dp,top = 10.dp)) {
Image(Icons.Default.Delete, contentDescription = null)
Text(text = "delete cache")
}
},
scaffoldState = drawerState
) { paddings ->
Box(modifier = Modifier.padding(paddings))
}
}
效果:
5. MD2-ModalDrawer
ModalDrawer
仅仅是抽屉栏,同样只在MD2
中才有,需要DrawerState
控制展开和收起:
@Preview
@Composable
fun MyModalDrawer() {
val drawerState =
androidx.compose.material.rememberDrawerState(androidx.compose.material.DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalDrawer(
drawerState = drawerState,
drawerContent = {
// Drawer content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("hi")
}
}
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// Screen content
Button(onClick = {
scope.launch {
drawerState.apply {
if (isClosed) open() else close()
}
}
}) {
Text("点我展开抽屉")
}
}
}
}
效果:
此外BottomDrawer
代表底部的抽屉栏,用法上和ModalDrawer
差不多
6.MD2-ModalBottomSheetLayout
ModalBottomSheetLayout
是底部菜单,需要使用ModalBottomSheetState
控制显示和消失:
@OptIn(ExperimentalMaterialApi::class)
@Preview
@Composable
fun MyModalBottomSheetLayout() {
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
// Sheet content
Box(
modifier = Modifier.height(400.dp),
contentAlignment = Alignment.Center
) {
Text("Sheet")
}
}
) {
// Screen content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(onClick = {
scope.launch {
sheetState.apply {
if (isVisible) hide() else show()
}
}
}) {
Text("点我展开")
}
}
}
}
效果:
此外,BottomSheetScaffold
代表带有底部sheetContent
槽位的Scaffold
,用法和Scaffold
差不多
7.MD2-BackdropScaffold
BackdropScaffold
官方的说法为背景幕
,就是两个布局可以堆叠,并前面的布局可以下移隐藏,通过BackdropScaffoldState
控制是否隐藏:
@OptIn(ExperimentalMaterialApi::class)
@Preview
@Composable
fun MyBackdropScaffold() {
val scaffoldState = rememberBackdropScaffoldState(
BackdropValue.Concealed
)
val scope = rememberCoroutineScope()
BackdropScaffold(
scaffoldState = scaffoldState,
appBar = {
// Top app bar
androidx.compose.material.TopAppBar(
title = {//标题
Text(
modifier = Modifier.padding(start = 10.dp),
text = "topBar"
)
},
navigationIcon = {//导航图标
Icon(
modifier = Modifier.clickable {
scope.launch {
scaffoldState.apply {
if (isConcealed) reveal() else conceal()
}
}
},
imageVector = Icons.Default.List,
contentDescription = null
)
}
)
},
backLayerContent = {
// Back layer content
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
}
},
frontLayerContent = {
// Front layer content
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Magenta)
) {
}
}
)
}
效果:
8.MD3-ModalNavigationDrawer
ModalNavigationDrawer
是MD3中的抽屉栏,配合ModalDrawerSheet
组件,可以达到抽屉栏列表MD3风格的样式:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyPermanentNavigationDrawer() {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
modifier = Modifier.fillMaxHeight(),
drawerContent = {
ModalDrawerSheet() {
NavigationRailItem(
selected = true,
onClick = {},
icon = { Icon(Icons.Default.Home, contentDescription = null) },
label = { Text("home") }
)
NavigationRailItem(
selected = false,
onClick = {},
icon = { Icon(Icons.Default.Info, contentDescription = null) },
label = { Text("info") }
)
NavigationRailItem(
selected = false,
onClick = {},
icon = { Icon(Icons.Default.Call, contentDescription = null) },
label = { Text("call") }
)
}
},
) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = { Text("CenterAlignedTopAppBar") },
navigationIcon = {
Icon(
modifier = Modifier.clickable {
scope.launch {
drawerState.apply {
if (isClosed) open() else close()
}
}
},
imageVector = Icons.Default.List,
contentDescription = null
)
},
actions = { /* App bar actions */ })
},
) { paddings ->
Box(modifier = Modifier.padding(paddings))
}
}
}
效果:
六、总结
最后总结下这篇文章的各个组件的作用,当然了compose中还有其他的组件,以及后续会出更多的新组件,目前也介绍了大部分组件的使用:
组件 | 分类 | 备注 |
---|---|---|
Text | 文本 | |
TextField | 文本输入 | |
OutlinedTextField | 文本输入 | 带边框 |
Image | 图片 | |
Icon | 图标 | 渲染方式比Image少 |
Button | 按钮 | |
IconButton | 图标按钮 | |
IconToggleButton | 选中状态图标按钮 | 通过State 切换是否选中 |
Switch | 开关样式图标按钮 | 通过State 切换是否选中 |
RadioButton | 单选按钮 | 通过State 切换是否选中 |
Checkbox | 复选按钮 | 通过State 切换是否选中 |
FloatingActionButton | 悬浮按钮 | |
ExtendedFloatingActionButton | 可展开悬浮按钮 | 通过State 切换是否展开 |
SnackbarHost | 提示消息 | 通过SnackbarHostState 是否显示 |
Spacer | 间距 | |
Divider | 分割线 | |
Row | 横向布局 | |
Column | 纵向布局 | |
Box | 堆叠布局 | |
Scaffold | 槽位布局 | 通过ScaffoldState 切换是否展开抽屉栏 |
TopAppBar | 标题导航栏 | |
CenterAlignedTopAppBar | 标题居中导航栏 | |
BottomAppBar | 底部导航栏 | |
BottomNavigation | 底部导航栏 | |
ModalDrawer | 抽屉栏 | 通过DrawerState 切换是否展开抽屉栏 |
ModalBottomSheetLayout | 底部抽屉菜单栏 | 通过ModalBottomSheetState 切换是否显示菜单栏 |
BackdropScaffold | 背景幕 | 通过BackdropScaffoldState 切换是否前幕布下移 |
ModalNavigationDrawer | 抽屉栏 | 通过DrawerState 切换是否展开抽屉栏 |
ModalDrawerSheet | 抽屉栏菜单布局 |