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

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

4. ConstraintLayout 约束布局


众所周知,Android View 体系中官方最推荐的布局是约束布局 —— ConstraintLayout,以致于在默认新建布局时就给你初始化成 ConstraintLayout。当然,ConstraintLayout 确实可以解决 View 体系中多层嵌套的问题,那么在 Compose 中也可以使用吗?

答案是肯定的。Compose 中也可以使用 ConstraintLayout,是使用 Row、Column、Box 布局的另一种解决方案。在实现更大的布局以及有许多复杂对齐要求以及布局嵌套过深的场景下,ConstraintLayout 用起来更加顺手。使用前,得引入 Compose 中的 ConstraintLayout 依赖库:

// build.gradle
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha07"

在 Compose 中使用 ConstraintLayout 有几点需要注意的:

  1. ConstraintLayout 中的子元素是通过 createRefs() 或 createRef() 方法初始化声明的,并且每个子元素都会关联一个ConstraintLayout 中的 Composable 组件;
  2. 子元素之间的约束关系是通过 Modifier.constrainAs() 的 Lambda 表达式来实现的,具体的可以看下面的 code 9;
  3. 约束关系可以使用 linkTo 或其他约束方法实现;
  4. parent 是一个默认存在的引用,代表 ConstraintLayout 父布局本身,也是用于子元素的约束关联。

下面是一个简单的例子:

// code 9
@Composable
fun ConstraintLayoutDemo() {
    ConstraintLayout {
        // 初始化声明两个元素,如果只声明一个,则可用 createRef() 方法
        // 这里声明的类似于 View 的 id
        val (button, text) = createRefs()
        Button(
            onClick = {},
            // constrainAs() 将 Composable 组件与初始化的引用关联起来
            // 关联之后就可以在其他组件中使用并添加约束条件了
            modifier = Modifier.constrainAs(button) {
                // 熟悉 ConstraintLayout 约束写法的一眼就懂
                // parent 引用可以直接用,跟 View 体系一样
                top.linkTo(parent.top, margin = 20.dp)
                start.linkTo(parent.start, margin = 10.dp)
            }
        ){
            Text("Button")
        }
        Text(text = "Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
            start.linkTo(button.start)
            centerHorizontallyTo(parent)  // 摆放在 ConstraintLayout 水平中间
        })
    }
}

image.png

细心的同学可能会有疑问,为啥下面的 Text 设置了父布局水平居中,但是好像是在 Button 宽度的中间呢?这是因为父布局的 ConstraintLayout 的大小默认是尽量小的容纳它的子元素,这跟 wrap_content 一样。可以将开发者选项中的显示布局边界打开看看:

image.png

这样就直观多了。要把 Text 放在整个屏幕的水平居中的位置,需要在 ConstraintLayout 中设置 Modifier.fillMaxWidth() 即可。

当然,Compose 版本的 ConstraintLayout 也支持设置使用 guideline、barrier、chain 等。


4.1 Barrier 的用法


先来看看 Barrier 的用法,就是字面意思,给一些子元素设置栅栏,将栅栏两侧的子元素分隔开的作用:

// code 10
@Composable
fun ConstraintLayoutDemo1() {
    ConstraintLayout {
        val (button1, button2, text) = createRefs()
        Button(
            onClick = {},
            modifier = Modifier.constrainAs(button1) {
                top.linkTo(parent.top, margin = 20.dp)
                start.linkTo(parent.start, margin = 10.dp)
            }
        ){
            Text("Button1")
        }
        Text(text = "Text文本", Modifier.constrainAs(text) {
            top.linkTo(button1.bottom)
            // 将 Text 的中心摆放在 button1 右边界的位置
            centerAround(button1.end)
        })
        // 设置一个 button1 和 text 右边的一个栅栏,将两者放在栅栏的左侧
        val barrier = createEndBarrier(button1, text)
        Button(
            onClick = {},
            modifier = Modifier.constrainAs(button2) {
                top.linkTo(parent.top, margin = 20.dp)
                // 将 button2 放在栅栏的右侧
                start.linkTo(barrier)
            }
        ) {
            Text(text = "button2")
        }
    }
}

image.png

创建栅栏的函数不仅有 createEndBarrier() 方法,类似用法的总结起来有:

  • createTopBarrier()、createBottomBarrier() : 创建分隔上下组件的栅栏;
  • createStartBarrier()、createEndBarrier() : 创建分隔左右组件的栅栏;
  • createAbsoluteLeftBarrier()、createAbsoluteRightBarrier() : 创建分隔左右组件的栅栏,满足国际化的需求。

最后两个是用于国际化适配,因为有些语言是从右到左排列的,如阿拉伯语,所以如果要严格按照左右来区分的话,使用带 Absolute 的方法,这个跟 marginStart 和 marginLeft 概念差不多。此外,创建 Barrier 并设置组件在 Barrier 的相对位置时,需要满足客观逻辑的。不能创建一个分隔左右组件的栅栏,但是我又设置 top.linkTo(barrier) 或 bottom.linkTo(barrier)。这在客观逻辑上就不成立,当然代码也会报错。


4.2 Guideline 的用法


Compose 版本中的 Guideline 用法大同小异,还是先从例子说起:

// code 11
@Composable
fun LargeConstraintLayout() {
    ConstraintLayout(Modifier.fillMaxHeight()) {
        val text = createRef()
        val guideline1 = createGuidelineFromStart(fraction = 0.5f)
        Text(text = "This text is very long",
            modifier = Modifier.constrainAs(text) {
                linkTo(start = guideline1, end = parent.end)
            }
        )
        val text1 = createRef()
        val guideline2 = createGuidelineFromTop(fraction = 0.333f)
        Text(text = "我距离屏幕上方约三分之一处~",
            modifier = Modifier.constrainAs(text1) {
                top.linkTo(guideline2)
            }
        )
    }
}

image.png

guideline1 设置的是在父布局水平位置 50% 的地方,这里由于 ConstraintLayout 默认尺寸是 wrap_content,所以父布局的宽度会设置为 text 的两倍的宽度,这样就满足了 text 起始位置在父布局的中间,根据图中的布局分界线也可以看出。而 guideline2 是在竖直方向上距离屏幕高度三分之一的位置,需要把父布局的高度设置为屏幕高度才可以实现。

上面的例子只列举了 guideline 根据百分比来设置它的位置,其实也可以根据偏移量来设置。用法总结起来如下列所示:

  • createGuidelineFromStart(offset: Dp):根据左侧距离父布局偏移量来设置 guideline 位置
  • createGuidelineFromStart(fraction: Float):根据左侧距离父布局的百分比来设置 guideline 位置
  • createGuidelineFromAbsoluteLeft(offset: Dp):国际化才使用
  • createGuidelineFromAbsoluteLeft(fraction: Float)
  • createGuidelineFromEnd(offset: Dp)
  • createGuidelineFromEnd(fraction: Float)
  • createGuidelineFromAbsoluteRight(offset: Dp)
  • createGuidelineFromAbsoluteRight(fraction: Float)
  • createGuidelineFromTop(offset: Dp)
  • createGuidelineFromTop(fraction: Float)
  • createGuidelineFromBottom(offset: Dp)
  • createGuidelineFromBottom(fraction: Float)

看着挺多,其实就是上下左右加上国际化的情况。

ConstraintLayout 还有一个特性,就是当它的子元素过大时,ConstraintLayout 默认是可以允许子元素超出屏幕范围的,以上面的例子继续说,当横向的 Text 内容很多时,就会出现 Text 部分内容超出屏幕。。。。上代码:

// code 12
@Composable
fun LargeConstraintLayout() {
    ConstraintLayout(Modifier.fillMaxHeight()) {
        val text = createRef()
        val guideline1 = createGuidelineFromStart(fraction = 0.5f)
        // very 单词有 10 个
        Text(text = "This text is very very very very very very very very very very long",
            modifier = Modifier.constrainAs(text) {
                linkTo(start = guideline1, end = parent.end)
            }
        )
    }
}

image.png

注意看,Text 文本有 10 个 very,但是只展示出来 8个,而且明显 Text 左边界不是位于屏幕中间位置,所以在默认情况下,ConstraintLayout 允许子元素超出屏幕。怎么做才能达到我们想要的效果?在这里需要设置一下 Text 的 width 宽度的属性为 Dimension.preferredWrapContent。

// code 13
Text(text = "This text is very very very very very very very very very very long",
        modifier = Modifier.constrainAs(text) {
            linkTo(start = guideline1, end = parent.end)
            width = Dimension.preferredWrapContent
        }
)

image.png

OK, 这个 Dimension 的属性一共有五种:

  1. preferredWrapContent:布局大小是根据内容所设置,并受布局约束的影响。这个例子中对 Text 右边界做了限制,所以使用这个属性可以控制 Text 右边界只能到达父布局右边界,不能超出屏幕;
  2. wrapContent:Dimension 的默认值,即布局大小只根据内容所设置,不受约束;
  3. fillToConstraints:布局大小将展开填充由布局约束所限制的空间。也就是说,这个属性是先看看布局约束所限制的空间有多大,然后再将该子元素填充到这个有约束的空间中;
  4. preferredValue:布局大小是一个固定值,并受布局约束的影响;
  5. value:布局大小是一个固定值,不受约束。

此外,Dimension 还可组合设置布局大小,例如:width = Dimension.preferredWrapContent.atLeast(100.dp) 可设置最小布局大小,同样还有 atMost() 可设置最大布局大小等等。


4.3 Chain 的用法


Chain 链,与 xml 中的用法一样,就是将一系列子元素按顺序打包成一行或一列。官方将这个 api 标记为可以改进的状态,可能后续会发生变化。api 只有两个,创建横向和纵向的链:

  • createHorizontalChain()
  • createVerticalChain()

第一个参数是需要打包在一起的所有子元素的id,第二个参数是链的类型,目前有三种类型:

  1. Spread:所有子元素平均分布在父布局空间中,是默认类型;
  2. SpreadInside:第一个和最后一个分布在链条的两端,其余子元素平均分布剩下的空间;
  3. Packed:所有子元素打包在一起,并放在链条的中间。

代码及效果如下:

// code 14
@Composable
fun ConstraintLayoutChainDemo() {
    ConstraintLayout(modifier = Modifier.fillMaxSize()) {
        val (box1, box2, box3) = createRefs()
        createHorizontalChain(box1,box2,box3, chainStyle = ChainStyle.Spread)
        Box(modifier = Modifier.size(100.dp).background(Color.Red).constrainAs(box1){})
        Box(modifier = Modifier.size(100.dp).background(Color.Green).constrainAs(box2){})
        Box(modifier = Modifier.size(100.dp).background(Color.Blue).constrainAs(box3){})
    }
}

chainStyle 设置为 ChainStyle.Spread 的效果:

image.png

chainStyle 设置为 ChainStyle.SpreadInside的效果:

image.png

chainStyle 设置为 ChainStyle.SpreadInside的效果:

image.png


4.4 ConstraintSet 实现动态适配


上面谈论的都是静态设置各种约束布局的情况,没有考虑到横竖屏切换可能导致的布局适配问题。其实 ConstraintLayout 可以传入一个 ConstraintSet 类型的参数,根据这个参数可以设置不同的约束条件,可以进行灵活设置。

// code 15
@Composable
fun DecoupledConstraintLayout() {
    BoxWithConstraints() {
        val constraints = if (maxWidth < maxHeight) {
            // 竖屏
            decoupledConstraints(false)
        } else {
            // 横屏
            decoupledConstraints(true)
        }
        ConstraintLayout(constraints) {
            Button(
                onClick = { /*TODO*/ },
                // layoutId 必须与 ConstraintSet 中的一致
                Modifier.layoutId("button")
            ) {
                Text(text = "Button")
            }
            Text(
                text = "Text",
                Modifier.layoutId("text")
            )
        }
    }
}
private fun decoupledConstraints(isPad: Boolean): ConstraintSet {
    return ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")
        if (isPad) {
            // 横屏模式
            constrain(button) {
                top.linkTo(parent.top, 15.dp)
                start.linkTo(parent.start, 30.dp)
            }
            constrain(text) {
                top.linkTo(parent.top, 15.dp)
                start.linkTo(button.end, 20.dp)
            }
        } else {
            // 竖屏模式
            constrain(button) {
                top.linkTo(parent.top, 30.dp)
                start.linkTo(parent.start, 15.dp)
            }
            constrain(text) {
                top.linkTo(button.bottom, 20.dp)
                start.linkTo(parent.start, 15.dp)
            }
        }
    }
}

这里横竖屏的布局有所不同,就是通过设置不同的 ConstraintSet 来实现的,如果布局元素很多,可以分为两个 ConstraintSet 对象来分别设置。需要注意的是,ConstraintLayout 中子元素的 layoutId 是通过 Modifier 设置的,需要与 ConstraintSet 的 createRefFor 的参数保持一致。下面是横竖屏的显示效果:

竖屏:

image.png

横屏:

image.png

第二篇 Compose 学习笔记终于完成,Compose 的布局你学会了么?欢迎留言交流~也可关注 公众号:修之竹


参考文献


  1. developer.android.google.cn/codelabs/je…
  2. developer.android.google.cn/reference/k…
  3. compose.net.cn/design/them…
  4. compose.net.cn/elements/su…
  5. developer.android.google.cn/reference/k…
  6. 乐翁龙. 《Jetpack Compose - ConstraintLayout》blog.csdn.net/u010976213/…

ps. 赠人玫瑰,手留余香。欢迎转发分享加关注,你的认可是我继续创作的精神源泉。

目录
相关文章
|
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的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
67 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,并处理相关依赖和权限。