这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
在前一篇笔记中,我们知道了 Compose 布局的一些基本知识,这篇笔记就来详细看看 Compose 布局吧!还有些 Compose 其他的知识,根据官方的实例,我们边看边说。
1. Compose 布局方式
Android 目前的布局 Layout 有许多:LinearLayout 线性布局、RelativeLayout 相对布局、ConstraintLayout 约束布局、FrameLayout 帧布局、TableLayout 表格布局、AbsoluteLayout 绝对布局、GridLayout 网格布局 7 种。后面的几种基本上用的很少了,而 Compose 的布局方式总共有三种:Column 纵向排列布局、Row 横向排列布局、Box 堆叠排列布局。先来个简单的例子:
// code 1 @Composable fun PhotographerCard() { Column { Text("小明", fontWeight = FontWeight.Bold) CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text("3 分钟前", style = MaterialTheme.typography.body2) } } }
注意到在展示第二行文本的时候,外面包了一层 CompositionLocalProvider 方法,这个是干嘛用的?要想知道这个,就必须先知道 CompositionLocal 是什么了。
1.1 CompositionLocal 用法简介
CompositionLocal 类位于 androidx.compose.runtime 包下,总的来说是用于在 composition 树中共享变量的值。在 Compose 构建的 composition 树中,如果需要将顶层的 Composable 函数中的某个变量传递到最底层的 Composable 函数,通常最简单有效的方法就是:1)定义一个全局变量,通过全局变量传值;2)中间层的 Composable 函数添加一个形参,层层传递。
但是这两种方式都不太优雅,尤其是嵌套过深,或者数据比较敏感,不想暴露给中间层的函数时,这种情况下,就可以使用 CompositionLocal 来隐式的将数据传递给所需的 composition 树节点。
CompositionLocal 在本质上就是分层的,它可以将数据限定在以某个 Composable 作为根结点的子树中,而且数据默认会向下传递,当然,当前子树中的某个 Composable 函数可以对该 CompositionLocal 的数据进行覆盖,从而使得新值会在这个 Composable 层级中继续向下传递。举个栗子:
// code 2 // compositionLocalOf 方法可以创建一个 CompositionLocal 实例 val ActiveUser = compositionLocalOf { // 设置默认值 User("小明","3分钟") // 如果无须默认值,也可设置错误信息 // error("No active user found!") } @Composable fun PhotographerCard() { Column { val user = ActiveUser.current // 通过 current 方法取出当前值 Text(user.name, fontWeight = FontWeight.Bold) CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text(user.lastActiveTime, style = MaterialTheme.typography.body2) } // 通过 providers 中缀表达式可以重新对 CompositionLocal 实例赋值 CompositionLocalProvider(ActiveUser provides User("小红", "5分钟前")) { val newUser = ActiveUser.current Text(newUser.name, fontWeight = FontWeight.Bold) CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text(newUser.lastActiveTime, style = MaterialTheme.typography.body2) } } } } data class User( val name: String, val lastActiveTime: String )
再说回官方栗子,官方栗子是使用 CompositionLocalProvider 对 LocalContentAlpha 进行了重新赋值,对色值的透明度做了调整。查看源码会发现,在 ContentAlpha.kt 中将 LocalContentAlpha 同样使用了 compositionLocalOf 方法设置了它的默认值为 1f,而在这里就重新赋值为 0.74f(ContentAlpha.medium)了,感兴趣的同学可以自己看下~
再说回布局,上面只用到 Column,可以将元素纵向排列;Row 则可以将元素横向进行排列。在官方栗子中还用到了 Surface。
// code 3 @Composable fun PhotographerCard() { Row { Surface( modifier = Modifier.size(50.dp), // 设置大小 shape = CircleShape, // 设置形状 color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f) // 设置色值 ) { // 加载网络图片逻辑 } Column { val user = ActiveUser.current // 通过 current 方法取出当前值 Text(user.name, fontWeight = FontWeight.Bold) CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text(user.lastActiveTime, style = MaterialTheme.typography.body2) } } } }
1.2 Surface 用法
Surface 位于 androidx.compose.material 包中,很显然它是 Material Design 风格的,可以将它理解为一个容器,我们可以设置容器的高度(带阴影效果)、Shape形状、Background背景等。举个栗子说明会更直观:
// code 4 @Composable fun SurfaceShow() { Surface( shape = RoundedCornerShape(6.dp), border = BorderStroke(0.5.dp, Color.Green), // 边框 elevation = 10.dp, // 高度 modifier = Modifier .padding(10.dp), // 外边距 // color = Color.Black, // 背景色 contentColor = Color.Blue, ) { Surface( modifier = Modifier .clickable { } // 点击事件在 padding 前,则此padding为内边距 .padding(10.dp), contentColor = Color.Magenta // 会覆盖之前 Surface 设置的 contentColor ) { Text(text = "This is a SurfaceDemo~") } } }
在这里实现了一个带边框圆角和阴影的按钮。Surface 的功能主要有:
- 裁剪,根据 shape 属性描述的形状进行裁剪;
- 高度,根据 elevation 属性设置容器平面的高度,让人看起来有阴影的效果;
- 边框,根据 border 属性设置边框的粗细以及色值;
- 背景,Surface 在 shape 指定的形状上填充颜色。这里会比较复杂一点,如果颜色是 Colors.surface,则会将 LocalElevationOverlay 中设置的 ElevationOverlay 进行叠加,默认情况下只会发生在深色主题中。覆盖的颜色取决于这个 Surface 的高度,以及任何父级 Surface 设置的 LocalAbsoluteElevation。这可以确保一个 Surface 的叠加高度永远不会比它的祖先低,因为它是所有先前 Surface 的高度总和。
- 内容颜色,根据 contentColor 属性给这个平面的内容指定一个首选色值,这个色值会被文本和图标组件以及点击态作为默认色值使用。当然可以被子节点设置的色值覆盖。
1.3 Modifier 简单用法
Modifier 属性用法太多了,设置 padding、click 等等,布局排版的许多工作都是由它来完成的。
// code 5 @Composable fun PhotographerCard() { Row ( modifier = Modifier.fillMaxWidth() // 相当于 width = match_parent .padding(10.dp) // 外边距为 10dp .clip(RoundedCornerShape(6.dp)) // 设置圆角 .clickable { } // 点击事件 .padding(16.dp) // 内边距为 16dp ){ Surface( modifier = Modifier.size(50.dp), // 设置大小 shape = CircleShape, // 设置形状 color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f) // 设置色值 ) { // 加载网络图片逻辑 } Column( modifier = Modifier.padding(start = 8.dp) // 单独设置 左边距 .align(Alignment.CenterVertically) // 设置里面的子元素竖直方向上居中分布 ) { val user = ActiveUser.current // 通过 current 方法取出当前值 Text(user.name, fontWeight = FontWeight.Bold) CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text(user.lastActiveTime, style = MaterialTheme.typography.body2) } } } }
细心的同学发现了,modifier 只能设置 padding,没有 margin 属性。在 clickable 前后各有一个 padding,前者就是设置的外边距,后者就是内边距。所以,在 Modifier 中设置 padding 的次序很重要。
2. Scaffold 脚手架用法
Compose 自带 Material 组件用于快速开发一个符合 Material Design 标准的 APP,最顶端的组件是 Scaffold,咦?是不是又看到了 Flutter 的影子?
不得不说,Google 的工程师真的很了解建筑学,连起名都借用了建筑学的概念,这个 Scaffold 组件的功能就跟它的翻译一样,用于构建一个基本的 Material Design 布局框架。它提供了诸如 TopAppBar、BottomAppBar、FloatingActionButton 和 Drawer 等常见的组件。
// code 6 @Composable fun LayoutInCompose() { var selectedItem by remember { mutableStateOf(0) } val navItems = listOf("Songs", "Artists", "Playlists") Scaffold( topBar = { // topBar 属性用于设置 AppBar TopAppBar( title = { // 可设置标题 Text(text = "LayoutInCompose") }, actions = { // 设置 AppBar 上的按钮 Button IconButton(onClick = { /*TODO*/ }) { // Icon 系统为我们提供了许多常见的 Icon Icon(Icons.Filled.Favorite, contentDescription = null) } } ) }, bottomBar = { // bottomBar 可用于设置 BottomNavigation BottomNavigation() { navItems.forEachIndexed { index, item -> BottomNavigationItem( icon = {Icon(Icons.Filled.Face, contentDescription = null)}, label = {Text(item)}, selected = selectedItem == index, onClick = { selectedItem = index } ) } } } ) { BodyContent(modifier = Modifier .padding(it) .padding(8.dp)) } } @Composable fun BodyContent(modifier: Modifier) { Column(modifier = modifier) { Text(text = "Hi there!") Text(text = "Thanks for watching this") } }
可以看出,Scaffold 真的为我们提供了好多组件,这里仅仅举了 TopAppBar 和 BottomNavigation 两个。但在实际中,我们用到的并不多,除非是需要快速上线,没有 UI 设计等等。所以我个人感觉,Scaffold 并不是我们应该掌握的重点,了解即可。
3. List 中布局的使用
在笔记一中,我们见识到了 Compose 使用 LazyColumn 来实现一个可滑动的 List,其实实现一个可滑动的 List 并不需要用到 LazyColumn,只需要用 Column 中的 Modifier.verticalScroll 属性就可以了。看代码:
// code 7 @Composable fun SimpleList() { // 使用 rememberScrollState 保存滚动的位置信息 val scrollState = rememberScrollState() // Modifier.verticalScroll 可添加竖直方向上的滚动属性 // 使用 Column 的 Modifier.verticalScroll 方法确实可以创建一个可滑动的 // List,但是这种方法在开始时就会将所有 item 全部加载,类似于 ScrollView Column(Modifier.verticalScroll(scrollState)) { repeat(100) { Text(text = "Item #$it") Divider(color = Color.Blue, thickness = 1.5.dp, startIndent = 10.dp) } } } @Composable fun BodyContent(modifier: Modifier) { Column(modifier = modifier) { Text(text = "Hi there!") Text(text = "Thanks for watching this") SimpleList() // 将 List 放在之前的布局中展示出来 } }
这种实现方法最简单,但是会在页面开始展示时,将列表中所有的 item 加载到内存中,虽然很多 item 都没有显示在屏幕上,这种方法当列表内容很多时,会出现内存占用大的问题。
所以一般是使用 LazyColumn 来展示列表数据,LazyColumn 开始时并不会把所有的列表数据都加载进内存,它会先将展示在屏幕上的列表数据加载进内存,当滑动查看更多列表数据时,才会将这些数据加载到内存中。而且,LazyColumn 在内部已经实现了滑动的逻辑,不需要用 Modifier.verticalScroll 来实现。来看一下例子:
// code 8 @Composable fun ImageListItem(index: Int) { // 列表 item 布局 // Row 可设置竖直方向上的对齐方式 Row(verticalAlignment = Alignment.CenterVertically) { Image( painter = rememberImagePainter( data = "https://pic.ntimg.cn/20140810/3822951_180850680000_2.jpg" ), contentDescription = "Test Img", modifier = Modifier.size(50.dp) ) Spacer(modifier = Modifier.width(10.dp)) // Spacer 也可设置边距 Text(text = "Item #$index", style = MaterialTheme.typography.subtitle1) } } @Composable fun ScrollingList() { val listSize = 100 // 使用 rememberLazyListState 保存滚动的位置 val scrollState = rememberLazyListState() LazyColumn(state = scrollState) { items(listSize) { ImageListItem(index = it) Divider(color = Color.Blue, thickness = 1.5.dp, startIndent = 10.dp) } } }
列表项比较简单,就一张图一个文案,这里图片加载库使用的是 Coil,使用 Kotlin 协程写的一个图片加载库,感兴趣的可以看看。需要引入 Coil 的依赖:
// build.gradle implementation 'io.coil-kt:coil-compose:1.3.0'
引入之后就可以使用 code 8 中的 rememberImagePainter 直接将图片链接传给 data 即可。还要记得获取一下网络权限。还可以看到这里图片与文案之间的间隔是用 Spacer 来实现的,当然也可以在 Text 中的 Modifier 属性设置 padding 来实现。