在上篇《Jetpack Compose技术快速上手》一文中简单介绍了Compose,那么这边我们就来学习下Compose的布局。由于布局这块涉及内容较多,会分开写。
布局主要包括:布局基础知识、Material组件和布局、自定义布局、Compose中使用ConstraintLayout。
本文重点讲解Material组件和布局。
主要涉及:Material中常用组件
和Material布局
。如下图:
Material中常用组件
常用组件包括:按钮
、文字 Text
、输入框
、图标 Icon
、分割线 Divider
、复选框 CheckBox
、切换组件 Switch
、滑块 Slider
、进度条 ProgressIndicator
、信息提示组件 SnackBar
、Tab 和TabRow
。
按钮
按钮包括:Button 、TextButton、IconButton、IconToggleButton、RadioButton、FloatingActionButton、ExtendedFloatingActionButton。
使用示例:
Column {
Row(Modifier.fillMaxWidth()
.horizontalScroll(rememberScrollState())
) {
//1 纯文字按钮
TextButton(onClick = {}) {
Text("TextButton")
}
Spacer(modifier = Modifier.width(5.dp))
//2 普通按钮
Button(onClick = { /*TODO*/ }) {
Text(text = "Button")
}
Spacer(modifier = Modifier.width(5.dp))
// 3 带Icon的按钮
IconButton(onClick = { /*TODO*/ }, modifier = Modifier.requiredWidth(120.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Favorite, contentDescription = "Favorite",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(text = "IconButton")
}
}
}
Row {
Spacer(modifier = Modifier.width(5.dp))
// 4 IconToggleButton
IconToggleButton(checked = true, onCheckedChange = {},
modifier = Modifier.requiredWidth(120.dp)
) {
Text(text = "IconToggleButton")
}
Spacer(modifier = Modifier.width(5.dp))
//5 OutlinedButton
OutlinedButton(onClick = { /*TODO*/ }) {
Text(text = "OutlinedButton")
}
Spacer(modifier = Modifier.width(5.dp))
// 6 RadioButton
RadioButton(selected = true, onClick = { /*TODO*/ })
}
//扩展悬浮按钮
ExtendedFloatingActionButton(
icon = { Icon(imageVector = Icons.Default.Favorite, contentDescription = "") },
text = { Text("Like") },
onClick = { },
)
}
预览效果:
文本 Text
纯文字可组合函数,源码定义如下:
@Composable
fun Text(
text: String, //要显示的文本内容
modifier: Modifier = Modifier, //修饰符,
color: Color = Color.Unspecified, //文字颜色
fontSize: TextUnit = TextUnit.Unspecified,//文字大小
fontStyle: FontStyle? = null,//字体样式,如:normal和Italic斜体
fontWeight: FontWeight? = null,//文字线条粗细,
fontFamily: FontFamily? = null,//字体
letterSpacing: TextUnit = TextUnit.Unspecified,//文字间距
textDecoration: TextDecoration? = null,//文字上的装饰,如:None、Underline、LineThrough,默认为None
textAlign: TextAlign? = null,//文字在水平方向的布局
lineHeight: TextUnit = TextUnit.Unspecified,//文本段落的行高
overflow: TextOverflow = TextOverflow.Clip,//文字溢出后的处理方式,方式:Clip、Ellipsis(省略...)和Visible,默认为Clip,即裁剪
softWrap: Boolean = true,//文本是否应在软换行符处换行,默认值为true,即在会自动换行
maxLines: Int = Int.MAX_VALUE,//最大行数
onTextLayout: (TextLayoutResult) -> Unit = {},//新布局更新时的回调,也可理解为文字内容发生变化后的回调
style: TextStyle = LocalTextStyle.current
) {
......
}
各参数的含义,已增加了注释,使用的的时候根据自己的需求给对应的参数传值就OK了,这里不举例了。
输入框
在Material库中有两种,分别为:TextField和OutlineTextField。这两种输入框参数基本相同。这里就选择TextField
详细介绍下其参数,其源码定义:
@Composable
fun TextField(
value: String, //输入的文字内容
onValueChange: (String) -> Unit,//输入框文字内容变化回调函数
modifier: Modifier = Modifier, //修饰符
enabled: Boolean = true, //是否可用,包括但不限于输入等操作
readOnly: Boolean = false, //是否只读,控制是否可输入
textStyle: TextStyle = LocalTextStyle.current,//输入的文字样式
label: @Composable (() -> Unit)? = null,//输入框的标签,可用理解为输入框的标题
placeholder: @Composable (() -> Unit)? = null,//无输入内容时,输入框中的提示信息
leadingIcon: @Composable (() -> Unit)? = null,//输入框起始位置展示一个icon,可选参数
trailingIcon: @Composable (() -> Unit)? = null,//输入框结束位置展示一个icon,可选参数
isError: Boolean = false,//标记输入框中内容是否为错误,如果为true,默认情况下,标签、底部指示器和trailingIcon将以错误颜色显示
visualTransformation: VisualTransformation = VisualTransformation.None,//输入内容的视觉转换,比如:输入密码时用"*"替代
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,//设置输入框可输入类型,支持:Text、Ascii、Number、Phone、Uri、Email、Password、NumberPassword
keyboardActions: KeyboardActions = KeyboardActions(),//响应键盘操作时的回调
singleLine: Boolean = false,//是否单行显示
maxLines: Int = Int.MAX_VALUE,//最大可显示行数
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape =
MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),//输入框形状
colors: TextFieldColors = TextFieldDefaults.textFieldColors()//输入框中各种可设置颜色,包括:输入框文字、label、placeholder、background、leadingIconColor、trailingIconColor、indicatorColor、cursorColor(光标)
) {
......
}
两种输入框对比:
- TextField,默认有带灰色背景,无外边框,获取焦点时其label信息显示在输入框内部;
- OutlineTextField,默认透明背景色,有边框,获取焦点时label信息显示在边框线上;
示例:
Column {
//--------------------TextField 输入框 未设置onValueChange时,无论输入何内容均显示“你好”,因为输入内容未赋值给value,与Android原生EditText不同
TextField(
//输入框中输入的内容
value = "",
onValueChange = {},
//是否单行显示
singleLine = true,
// 输入框提示内容
label = { Text(text = "TextField")},
//输入框中文字为空时占位内容
placeholder = { Text(text = "请输入内容")},
//在输入框左侧显示一个icon
leadingIcon = {Icon(painter = rememberVectorPainter(image = Icons.Default.Star), contentDescription = "placeholder")},
//在输入框最右侧显示一个icon
trailingIcon = {Icon(painter = rememberVectorPainter(image = Icons.Default.Add), contentDescription = "placeholder")},
//设置输入框输入类型
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
//--------------------OutlinedTextField 输入框,可输入内容并显示
val valueState = remember {
mutableStateOf("")
}
OutlinedTextField(
value = valueState.value,
onValueChange = {valueState.value = it},
singleLine = true,
// 输入框提示内容
label = { Text(text = "OutlinedTextField")},
//输入框中文字为空时占位内容
placeholder = { Text(text = "请输入内容") },
//在输入框左侧显示一个icon
leadingIcon = {Icon(painter = rememberVectorPainter(image = Icons.Default.AccountCircle), contentDescription = "placeholder")},
//在输入框最右侧显示一个icon
trailingIcon = {Icon(painter = rememberVectorPainter(image = Icons.Default.Add), contentDescription = "placeholder")},
//设置输入框输入类型
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
)
}
预览效果如下:
图标Icon
类似Image,在Compose库中内置了一些常用的icon,主要在Icons类中。
源码定义:
@Composable
fun Icon(
painter: Painter, //设置资源
contentDescription: String?,//描述此图标所代表内容的文本
modifier: Modifier = Modifier,//修饰符
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)//着色,可以理解为要显示的颜色
) {
......
}
分割线 Divider
分割线,常用于分割页面内容,便于显示,说白了就是画一条线。
源码定义:
@Composable
fun Divider(
modifier: Modifier = Modifier, //修饰符
color: Color = MaterialTheme.colors.onSurface.copy(alpha = DividerAlpha),//线条颜色
thickness: Dp = 1.dp,//线条粗细
startIndent: Dp = 0.dp//线条的起始偏移量
) {
......
}
复选框 CheckBox和切换组件 Switch
这两个可组合函数参数类似,合并到一起介绍
@Composable
fun Checkbox(
checked: Boolean,//是否勾选,或是否打开
onCheckedChange: ((Boolean) -> Unit)?,//状态发生改变时的回调
modifier: Modifier = Modifier,//修饰符
enabled: Boolean = true,//是否可以
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: CheckboxColors = CheckboxDefaults.colors()//颜色相关,包括:checkmarkColor、boxColor、borderColor
) {
......
}
示例:
Row {
//--------------------Switch 组件
val switchState = remember {
mutableStateOf(false)
}
Switch(checked = switchState.value, onCheckedChange = {switchState.value = it})
//--------------------Checkbox 勾选框
val checkState = remember {
mutableStateOf(false)
}
Checkbox(checked = checkState.value, onCheckedChange = {checkState.value = it})
}
效果:
滑块 Slider
同Android View系统的Slider。
分析源码:
@Composable
fun Slider(
value: Float, //值
onValueChange: (Float) -> Unit,//滑动值发生变化时的回调
modifier: Modifier = Modifier,//修饰符
enabled: Boolean = true,//是否可用
valueRange: ClosedFloatingPointRange = 0f..1f,//可滑动的值的取值范围
/*@IntRange(from = 0)*/
steps: Int = 0,//步数,当大于0时,slider会被分为steps+1段,滑动停止时会自动吸附到最近的点上;值为0时,则可连续滑动,不会被分段
onValueChangeFinished: (() -> Unit)? = null,//值改变结束后的回调
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: SliderColors = SliderDefaults.colors()//颜色相关的
) {
......
}
举例:
//--------------------Slider 滑块
val sliderState = remember {
mutableStateOf(0f)
}
Slider(value = sliderState.value, onValueChange = {sliderState.value = it},steps = 2)
进度条
进度条可组合函数,同Android View系统的ProgressBar,分为下面三种:
- ProgressIndicator,基初的进度条;
- CircularProgressIndicator,圆形形状的进度条;
- LinearProgressIndicator,线性形状的进度条;
选取CircularProgressIndicator
进行参数分析:
@Composable
fun CircularProgressIndicator(
/*@FloatRange(from = 0.0, to = 1.0)*/
progress: Float,//当前的进度
modifier: Modifier = Modifier,//修饰符
color: Color = MaterialTheme.colors.primary,//颜色
strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth//进度条粗细
) {
......
}
看下显示效果:
Tab 和TabRow
类似Android View系统中的TabLayout效果。通常Tab会和TabRow搭配使用。
TabRow
为固定不可滑动
的,若多Tab时需要可滑动
,则使用ScrollableTabRow
TabRow参数说明:
@Composable
fun TabRow(
selectedTabIndex: Int, //选中的索引
modifier: Modifier = Modifier,//修饰符
backgroundColor: Color = MaterialTheme.colors.primarySurface,//背景色
contentColor: Color = contentColorFor(backgroundColor),//内容颜色
//指示器
indicator: @Composable (tabPositions: List) -> Unit = @Composable { tabPositions ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
//选项卡行和下面显示的内容之间提供了一层分隔
divider: @Composable () -> Unit = @Composable {
TabRowDefaults.Divider()
},
tabs: @Composable () -> Unit //内容,多个Tab组合函数
) {
......
}
示例:
//--------------------选项卡 TabRow
val state = remember { mutableStateOf(0) }
val titles = listOf("消息", "通讯录", "附近","我")
TabRow(selectedTabIndex = state.value,modifier = Modifier.height(50.dp)) {
titles.forEachIndexed {index,text->
Tab(selected = state.value == index, onClick = { state.value = index }) {
VerticalImageText(text = text)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
//--------------------可滑动选项卡 TabRow
val sTabState = remember { mutableStateOf(0) }
val sTitles = listOf("消息", "通讯录", "附近","我","交友","教育","购物")
ScrollableTabRow(selectedTabIndex = sTabState.value,
modifier = Modifier.height(50.dp),
edgePadding = 0.dp) {
sTitles.forEachIndexed {index,text->
Tab(selected = sTabState.value == index, onClick = { sTabState.value = index }) {
VerticalImageText(text = text)
}
}
}
信息提示组件 SnackBar
类似Android View系统中的Toast,但是SnackBar
可以操作按钮。通常与SnackbarHost
结合使用,通过使用snackbarHostState的showSnackbar方法控制显示内容。值得注意的是showSnackbar为suspend
函数,要在协成
调用。
举一个在槽位布局中的使用示例
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
//floatingActionButton位置
floatingActionButtonPosition = FabPosition.Center,//居中
//添加一个floatingActionButton
floatingActionButton = {
FloatingActionButton(onClick = {
//点击按钮,显示一个SnackBar
scope.launch {
//注意:在协成中调用
scaffoldState.snackbarHostState.showSnackbar("你点击了FloatingActionButton",actionLabel = "Done")
}
}) {
Icon(imageVector = Icons.Default.Add, contentDescription = "")
}
},
//是否与bottomBar区域重叠,true叠加,false 不叠加
isFloatingActionButtonDocked = true,
) {
...
}
Material布局
为了便于开发者开发,Material提供了多种槽位布局,如:Scaffold
、ModalDrawer
、BottomSheetScaffold
、BackdropScaffold
、选项卡
(上面已讲到)。
Scaffold
槽位布局,源码中参数如下:
@Composable
fun Scaffold(
modifier: Modifier = Modifier,//修饰符
scaffoldState: ScaffoldState = rememberScaffoldState(),//scaffold的状态
topBar: @Composable () -> Unit = {},//topBar内容
bottomBar: @Composable () -> Unit = {},//bottomBar的内容
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },//信息提示可组合函数
floatingActionButton: @Composable () -> Unit = {},//floatingActionButton
floatingActionButtonPosition: FabPosition = FabPosition.End,//floatingActionButton的位置
isFloatingActionButtonDocked: Boolean = false,//是否与bottomBar区域重叠,true叠加,false 不叠加
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,//抽屉
drawerGesturesEnabled: Boolean = true,//抽屉手势是否可用
drawerShape: Shape = MaterialTheme.shapes.large,//抽屉形状
drawerElevation: Dp = DrawerDefaults.Elevation,//抽屉阴影
drawerBackgroundColor: Color = MaterialTheme.colors.surface,//抽屉背景色
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),//抽屉内容颜色
drawerScrimColor: Color = DrawerDefaults.scrimColor,//
backgroundColor: Color = MaterialTheme.colors.background,//槽位布局的背景色
contentColor: Color = contentColorFor(backgroundColor),//槽位布局的内容颜色
content: @Composable (PaddingValues) -> Unit//槽位布局的主内容
) {
......
}
示例代码:
/**
* 采用Scaffold槽位
*/
@Composable
fun ScaffoldCompose(){
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
//顶部工具栏
topBar = { TopBar(onMenuClick = {scope.launch {
//控制抽屉显示与关闭
scaffoldState.drawerState.apply {
if (isOpen) close() else open()
}
}
})},
//底部工具栏
bottomBar = {
BottomAppBar(
//当floatActionButton与bottomBar重叠时剪切样式
cutoutShape = MaterialTheme.shapes.small.copy(CornerSize(50.dp)),
) {
VerticalImageText(rememberVectorPainter(Icons.Rounded.Email),"消息")
VerticalImageText(rememberVectorPainter(Icons.Rounded.Menu),"动态")
}
},
//floatingActionButton位置
floatingActionButtonPosition = FabPosition.Center,//居中
//添加一个floatingActionButton
floatingActionButton = {
FloatingActionButton(onClick = {
//点击按钮,显示一个SnackBar
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("你点击了FloatingActionButton",actionLabel = "Done")
}
}) {
Icon(imageVector = Icons.Default.Add, contentDescription = "")
}
},
//是否与bottomBar区域重叠,true叠加,false 不叠加
isFloatingActionButtonDocked = true,
drawerContent = {DrawerContent()}
) {
ShowMaterialComponents()
}
}
效果图
值得注意的是BottomAppBar
支持带有 cutoutShape
参数的 FAB 刘海屏,它接受任何 Shape
。例如,FloatingActionButton 使用 MaterialTheme.shapes.small,并将 50% 的边角大小作为其 shape 参数的默认值:
bottomBar = {
BottomAppBar(
//当floatActionButton与bottomBar重叠时剪切样式
cutoutShape = MaterialTheme.shapes.small.copy(CornerSize(50.dp)),
) {
.....
}
},
模态抽屉式导航栏(ModalDrawer)
上面将了Scaffold
,但是如果您想实现不含 Scaffold
的模态抽屉式导航栏,可以使用 ModalDrawer
可组合项。它接受与 Scaffold
类似的抽屉式导航栏参数,这里就不对每个参数进行说明了,抽屉的显示位置同Scaffold
。
@Composable
@OptIn(ExperimentalMaterialApi::class)
fun ModalDrawer(
drawerContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
gesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
scrimColor: Color = DrawerDefaults.scrimColor,
content: @Composable () -> Unit //屏幕内容
) {
......
}
另外,若想让抽屉放到底部,则可使用BottomDrawer
实现。
val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
BottomDrawer(
drawerState = drawerState,
drawerContent = {
// Drawer content
}
) {
// Screen content
}
底部动作条(BottomSheetScaffold)
用于实现标准的底部动作条,BottomSheetScaffold
同样接受与"Scaffold
类似的参数,例如 topBar
、floatingActionButton
和 snackbarHost
。其中包含额外的参数,这些参数可提供底部动作条的显示方式。
BottomSheetScafold
可接受的参数如下:
@Composable
@ExperimentalMaterialApi
fun BottomSheetScaffold(
sheetContent: @Composable ColumnScope.() -> Unit,//BottomSheetScaffold的内容
modifier: Modifier = Modifier,
scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
topBar: (@Composable () -> Unit)? = null,
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
floatingActionButton: (@Composable () -> Unit)? = null,
floatingActionButtonPosition: FabPosition = FabPosition.End,
sheetGesturesEnabled: Boolean = true,
sheetShape: Shape = MaterialTheme.shapes.large,
sheetElevation: Dp = BottomSheetScaffoldDefaults.SheetElevation,
sheetBackgroundColor: Color = MaterialTheme.colors.surface,
sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
sheetPeekHeight: Dp = BottomSheetScaffoldDefaults.SheetPeekHeight,//可见的内容高度
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
drawerScrimColor: Color = DrawerDefaults.scrimColor,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
content: @Composable (PaddingValues) -> Unit
) {
......
}
如下示例,例子中可以通过点击悬浮按钮/手势展开/收起底部内容:
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun BottomSheetCompose(){
val scaffoldState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = scaffoldState,
sheetContent = { MySheetContent()},
sheetPeekHeight = 120.dp,
floatingActionButton = {
FloatingActionButton(onClick = {
//点击折叠和收起
scope.launch {
scaffoldState.bottomSheetState.apply {
if (isCollapsed) expand() else collapse()
}
}
},
//设置悬浮窗背景色
backgroundColor = Color.White
) {
Image(painter = painterResource(id = R.drawable.ic_baseline_directions_bike_24), contentDescription = "",
//过滤颜色
colorFilter = ColorFilter.tint(MaterialTheme.colors.primary)
)
}
}
) {
Text(text = "应用主内容")
}
}
背景幕(BackdropScaffold)
背景幕,即有两层布局,可以通过手势滑动上次,来展示下层的内容。
BackdropScaffold 接受一些额外的背景幕参数。例如,您可以使用 peekHeight 和 headerHeight 参数来设置后层的可视高度和前层的最小非活动高度。此外,您还可以使用 gesturesEnabled 参数来切换背景幕是否响应拖动。
其源码中详细参数如下:
@Composable
@ExperimentalMaterialApi
fun BackdropScaffold(
appBar: @Composable () -> Unit,//topBar
backLayerContent: @Composable () -> Unit,//背景层内容
frontLayerContent: @Composable () -> Unit,//前层内容
modifier: Modifier = Modifier,
scaffoldState: BackdropScaffoldState = rememberBackdropScaffoldState(Concealed),
gesturesEnabled: Boolean = true,//前层可滑动
peekHeight: Dp = BackdropScaffoldDefaults.PeekHeight,//背景层可视高度
headerHeight: Dp = BackdropScaffoldDefaults.HeaderHeight,//前层header的最小非活动高度
persistentAppBar: Boolean = true,
stickyFrontLayer: Boolean = true,
backLayerBackgroundColor: Color = MaterialTheme.colors.primary,//背景层颜色
backLayerContentColor: Color = contentColorFor(backLayerBackgroundColor),//背景层内容颜色
frontLayerShape: Shape = BackdropScaffoldDefaults.frontLayerShape,//前景层Shape
frontLayerElevation: Dp = BackdropScaffoldDefaults.FrontLayerElevation,
frontLayerBackgroundColor: Color = MaterialTheme.colors.surface,
frontLayerContentColor: Color = contentColorFor(frontLayerBackgroundColor),
frontLayerScrimColor: Color = BackdropScaffoldDefaults.frontLayerScrimColor,
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }
) {
......
}
示例代码就不贴出来了,太多了,贴一张效果图吧。
好了,到目前关于Material的常用组件和布局已经讲完了,内容有点多,慢慢看吧。
欢迎留言,一起学习,共同进步!
github - 示例源码
gitee - 示例源码