Jetpack-Compose 学习笔记(二)—— Compose 布局你学会了么?(上)

简介: Jetpack-Compose 学习笔记(二)—— Compose 布局你学会了么?(上)

这是我参与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)
        }
    }
}

image.png

注意到在展示第二行文本的时候,外面包了一层 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
)

image.png

再说回官方栗子,官方栗子是使用 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)
            }
        }
    }
}

image.png


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~")
        }
    }
}

image.png

在这里实现了一个带边框圆角和阴影的按钮。Surface 的功能主要有:

  1. 裁剪,根据 shape 属性描述的形状进行裁剪;
  2. 高度,根据 elevation 属性设置容器平面的高度,让人看起来有阴影的效果;
  3. 边框,根据 border 属性设置边框的粗细以及色值;
  4. 背景,Surface 在 shape 指定的形状上填充颜色。这里会比较复杂一点,如果颜色是 Colors.surface,则会将 LocalElevationOverlay 中设置的 ElevationOverlay 进行叠加,默认情况下只会发生在深色主题中。覆盖的颜色取决于这个 Surface 的高度,以及任何父级 Surface 设置的 LocalAbsoluteElevation。这可以确保一个 Surface 的叠加高度永远不会比它的祖先低,因为它是所有先前 Surface 的高度总和。
  5. 内容颜色,根据 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)
            }
        }
    }
}

image.png

细心的同学发现了,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")
    }
}

image.png

可以看出,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 放在之前的布局中展示出来
    }
}

image.png

这种实现方法最简单,但是会在页面开始展示时,将列表中所有的 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)
        }
    }
}

image.png

列表项比较简单,就一张图一个文案,这里图片加载库使用的是 Coil,使用 Kotlin 协程写的一个图片加载库,感兴趣的可以看看。需要引入 Coil 的依赖:

// build.gradle
implementation 'io.coil-kt:coil-compose:1.3.0'

引入之后就可以使用 code 8 中的 rememberImagePainter 直接将图片链接传给 data 即可。还要记得获取一下网络权限。还可以看到这里图片与文案之间的间隔是用 Spacer 来实现的,当然也可以在 Text 中的 Modifier 属性设置 padding 来实现。

目录
相关文章
|
5月前
|
存储 缓存 Android开发
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【远程url】音频,搭配Okhttp库进行下载和缓存,播放完随机播放下一首
这是一个Kotlin项目,使用Jetpack Compose和ExoPlayer框架开发Android应用,功能是播放远程URL音频列表。应用会检查本地缓存,如果文件存在且大小与远程文件一致则使用缓存,否则下载文件并播放。播放完成后或遇到异常,会随机播放下一首音频,并在播放前随机设置播放速度(0.9到1.2倍速)。代码包括ViewModel,负责音频管理和播放逻辑,以及UI层,包含播放和停止按钮。
|
5月前
|
存储 数据库 Android开发
安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐
为了在UI界面添加用于添加和删除本地音乐文件的按钮,以及相关的播放功能,你需要实现以下几个步骤: 1. **集成用户选择本地音乐**:允许用户从设备中选择音乐文件。 2. **创建UI按钮**:在界面中创建添加和删除按钮。 3. **数据库功能**:使用Room数据库来存储音频文件信息。 4. **更新ViewModel**:处理添加、删除和播放音频文件的逻辑。 5. **UI实现**:在UI层支持添加、删除音乐以及播放功能。
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
54 4
|
4月前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
5月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
5月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
5月前
|
缓存 Android开发 Kotlin
【安卓app开发】kotlin Jetpack Compose框架 | 先用OKhttp下载远程音频文件再使用ExoPlayer播放
使用 Kotlin 的 Jetpack Compose 开发安卓应用时,可以结合 OkHttp 下载远程音频文件和 ExoPlayer 进行播放。在 `build.gradle` 添加相关依赖后,示例代码展示了如何下载音频并用 ExoPlayer 播放。代码包括添加依赖、下载文件、播放文件及简单的 Compose UI。注意,示例未包含完整错误处理和资源释放,实际应用需补充这些内容。
|
5月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
5月前
|
存储 Android开发 Kotlin
开发安卓app OKhttp下载后使用MediaPlayer播放
在Android Jetpack Compose应用程序中,要使用OkHttp下载远程音频文件并在本地播放,你需要完成以下几个步骤: 1. **添加依赖**:确保`build.gradle`文件包含OkHttp和Jetpack Compose的相关依赖。 2. **下载逻辑**:创建一个`suspend`函数,使用OkHttp发起网络请求下载音频文件到本地。 3. **播放逻辑**:利用`MediaPlayer`管理音频播放状态。 4. **Compose UI**:构建用户界面,包含下载和播放音频的按钮。
|
5月前
|
存储 Android开发
安卓app,MediaPlayer播放本地音频 | 按钮控制播放和停止
在Jetpack Compose中,不直接操作原生Android组件如`Button`和`MediaPlayer`,而是使用Compose UI构建器定义界面并结合ViewModel管理音频播放逻辑。以下示例展示如何播放本地音频并用按钮控制播放/停止:创建一个`AudioPlayerViewModel`管理`MediaPlayer`实例和播放状态,然后在Compose UI中使用`Button`根据`isPlaying`状态控制播放。记得在`MainActivity`设置Compose UI,并处理相关依赖和权限。