Compose: 怎么能少了 ConstraintLayout

1,197 阅读5分钟

我正在参加「掘金·启航计划」

作为新时代的 Android 开发者,大概没有不知道 ConstraintLayout 约束布局的吧?(如果有,而且是你,还不赶快去学?)它直接继承自 ViewGroup,用「相对布局」的概念,让布局更灵活、简单。相较于传统的 RelativeLayoutLinearLayout 等,ConstraintLayout 能做到的扁平化布局的同时,功能还强大很多。对于复杂的布局,它的优势就更明显了。

ConstraintLayout 依托于 Android Studio 的布局工具,布局变得可视化,不用苦哈哈的写 xml 代码了,在 IDE 上一阵拖拽就搞定。

如此一来,对于 Compose 开发来说,没有 xml 布局了,是不是也没有 ConstraintLayout 了呢?毕竟,官方教程里也只见到 Box, Column(相当于 FrameLayoutLinearLayout)这样的简单布局。本人刚开始学 Compose 的时候,的确是这么想的。实际上当然不是啦!要不然接下来也没的讲了。

ConstraintLayoutCompose 中也是等同于 Box 一样的官方支持组件,只是同样的,它在复杂布局中,才能发挥出威力,普通的布局,BoxColumn 或者 Row 随便写写就搞定了,所以官方介绍 Compose 的时候,完全犯不着一来就上「约束布局」。

自然地,xml 中的约束属性,或者说 IDE 的那些拖拽式的约束属性生成,在Compose 中,就都变到了纯 Kotlin 代码了。

案例实践下

关于 ConstraintLayout,网上有一堆资料可以学习,在此不细讲了,咱直接上案例,并分别用 View 系统和 Compose 系统来实现。对比之下,对于 ComposeConstraintLayout 用法,大概心里就有谱了。

案例如下:

image.png

这是一款英语阅读 APP「百词斩爱阅读」中的一个界面,是一个专辑的详情页

我们就实现顶部那块详情信息部分吧。

View

xml 布局打天下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/cover"
        android:layout_width="90dp"
        android:layout_height="120dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="24dp"
        android:scaleType="centerCrop"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:srcCompat="@tools:sample/avatars" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="6dp"
        android:text="迷你人物传"
        android:textColor="#000"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@+id/cover"
        app:layout_constraintTop_toTopOf="@+id/cover" />

    <TextView
        android:id="@+id/title_en"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:text="Mini Biography"
        android:textColor="#000"
        android:textSize="14sp"
        app:layout_constraintStart_toStartOf="@+id/title"
        app:layout_constraintTop_toBottomOf="@+id/title" />

    <TextView
        android:id="@+id/require"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:text="词汇量要求"
        android:textColor="#8000"
        android:textSize="14sp"
        app:layout_constraintStart_toStartOf="@+id/title"
        app:layout_constraintTop_toBottomOf="@+id/title_en" />

    <TextView
        android:id="@+id/require_level"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:fontFamily="sans-serif-medium"
        android:text="大学+"
        android:textColor="#000"
        android:textSize="14sp"
        app:layout_constraintStart_toEndOf="@+id/require"
        app:layout_constraintTop_toTopOf="@+id/require" />

    <TextView
        android:id="@+id/summary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:fontFamily="sans-serif-medium"
        android:text="共 7 篇短文"
        android:textColor="#8000"
        android:textSize="14sp"
        app:layout_constraintStart_toStartOf="@+id/title"
        app:layout_constraintTop_toBottomOf="@+id/require" />

    <TextView
        android:id="@+id/profile_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:fontFamily="sans-serif-medium"
        android:text="简介"
        android:textColor="#000"
        android:textSize="16sp"
        app:layout_constraintStart_toStartOf="@+id/cover"
        app:layout_constraintTop_toBottomOf="@+id/cover" />

    <TextView
        android:id="@+id/profile_content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:layout_marginEnd="24dp"
        android:text="迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传"
        android:textColor="#000"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/profile_title"
        app:layout_constraintTop_toBottomOf="@+id/profile_title" />

</androidx.constraintlayout.widget.ConstraintLayout>

效果基本达成:

image.png

基本要点如下:

  • cover 封面定下了基础位置
  • 标题依赖 cover 放好,下面的英文标题、等级信息等,就跟着它排好就行
  • 简介也依赖 cover,起始对齐,间隔一定距离

要是用线性布局或者其他的,不知道要写多久,嵌套多少层,而上面的代码,纯是在 Android Studio 的「Design」模式下,拖拽组件库生成的,快捷又简单,这就是 ConstraintLayout 的强大。

Compose

Compose 里使用 ConstraintLayout,需要引入如下包,基础库里是没有的:

implementation "androidx.constraintlayout:constraintlayout-compose:$version"  

好,准备就绪,开干!

import androidx.constraintlayout.compose.ConstraintLayout  
  
@Composable  
    private fun DetailInfo() {  
    ConstraintLayout {  

    }  
}  

Box 的使用没什么两样,而这个 androidx.constraintlayout.compose.ConstraintLayout 是这样的:

@Composable
inline fun ConstraintLayout(
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    crossinline content: @Composable ConstraintLayoutScope.() -> Unit
) {
//...
}

参数真少,怎么实现约束布局的复杂功能的?答案就在这个 ConstraintLayoutScope 里。继续边做边讲吧。

封面

首先,还是先摆好封面:

@Composable
@Preview(showBackground = true, backgroundColor = 0xffffff)
private fun DetailInfo() {
    ConstraintLayout {
        val (cover) = createRefs()
        Box(modifier = Modifier
            .size(90.dp, 120.dp)
            .background(Color.Gray)
            .constrainAs(cover) {
                start.linkTo(parent.start, 24.dp) // 相当于 layout_constraintStart_toStartOf="parent" + layout_marginStart="24dp"
                top.linkTo(parent.top, 24.dp) // 相当于 layout_constraintTop_toTopOf="parent" + layout_marginTop="24dp"
            })
    }
}

image.png

因为没有资源图,封面就拿一个颜色充当了。调用 createRefs(),是为了创建「引用索引」,其实就是类似 id,这是创建约束关联的钥匙 —— 后面的 constrainAs 方法需要这个参数。

前面提到的 ConstraintLayoutScope 该上场了:

@LayoutScopeMarker
class ConstraintLayoutScope @PublishedApi internal constructor() : ConstraintLayoutBaseScope() {
    // ...

    // 用于创建多个 ConstrainedLayoutReferences
    @Stable
    fun createRefs() =
            referencesObject ?: ConstrainedLayoutReferences().also { referencesObject = it }

    //...

    @Stable
    fun Modifier.constrainAs(
        ref: ConstrainedLayoutReference,
        constrainBlock: ConstrainScope.() -> Unit
    ) = this.then(ConstrainAsModifier(ref, constrainBlock))

    //...
}

createRefs()Modifier.constrainAs() 都是它内部定义的方法。

而真正的约束关联的建立,就是在 createRefs() 方法尾随的 ConstrainScope 域块了,详见注释。

信息列

信息列,就是封面右侧那一列文本信息。

封面的布局搞明白了,其他的也就顺理成章:

@Composable
@Preview(showBackground = true, backgroundColor = 0xffffff)
private fun DetailInfo() {
    ConstraintLayout(Modifier.fillMaxSize()) {
        val (cover, title, titleEn, require, requireLevel, summary) = createRefs()
        Box(modifier = Modifier
            .size(90.dp, 120.dp)
            .background(Color.Gray)
            .constrainAs(cover) {
                start.linkTo(parent.start, 24.dp)
                top.linkTo(parent.top, 24.dp)
            })

        Text(text = "迷你人物传", fontWeight = FontWeight.Bold, fontSize = 18.sp, color = Color.Black, modifier = Modifier.constrainAs(title) {
            top.linkTo(cover.top, 6.dp)
            start.linkTo(cover.end, 16.dp)
        })

        Text(text = "Mini Biography", fontSize = 14.sp, color = Color.Black, modifier = Modifier.constrainAs(titleEn) {
            top.linkTo(title.bottom, 6.dp)
            start.linkTo(title.start)
        })

        Text(text = "词汇量要求", fontSize = 14.sp, color = Color.Black.copy(.5f), modifier = Modifier.constrainAs(require) {
            top.linkTo(titleEn.bottom, 6.dp)
            start.linkTo(title.start)
        })

        Text(text = "大学+", fontSize = 14.sp, color = Color.Black, fontWeight = FontWeight.Medium, modifier = Modifier.constrainAs(requireLevel) {
            top.linkTo(require.top)
            start.linkTo(require.end, 12.dp)
        })

        Text(text = "共 7 篇短文", fontSize = 14.sp, color = Color.Black.copy(.5f), modifier = Modifier.constrainAs(summary) {
            top.linkTo(require.bottom, 6.dp)
            start.linkTo(title.start)
        })
    }
} 

createRefs() 的结果里,又加了多个「id」,分别是对应各个文本项。依葫芦画瓢,各个文本组件完全复制了 xml 里面对应的尺寸和样式,十分简单。

效果:

image.png

简介

简介就更简单了,就一个标题、一个介绍,共两个文本:

// ...
val (cover, title, titleEn, require, requireLevel, summary, profileTitle, profileContent) = createRefs()

// ...
t = "简介", fontSize = 16.sp, color = Color.Black, modifier = Modifier.constrainAs(profileTitle) {
    top.linkTo(cover.bottom, 32.dp)
    start.linkTo(cover.start)
})

Text(text = "迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传迷你人物传", fontSize = 16.sp, color = Color.Black, fontWeight = FontWeight.Medium, modifier = Modifier.constrainAs(profileContent) {
    top.linkTo(profileTitle.bottom, 12.dp)
    start.linkTo(cover.start)
    end.linkTo(parent.end, 24.dp)
    width = Dimension.fillToConstraints
})

值得注意的是,介绍文本的约束里,有一个 width 的设定,值为 Dimension.fillToConstraints。这其实就是 View 版本的 MATCH_CONSTRAINT,意思是「文本占满容器宽度」。

至此,完成。来看看最终效果:

image.png

不说和 View 的版本一模一样,起码也能说是如出一辙吧?

小结

通过这个直观的案例,相信 Compose 下的 ConstraintLayout 可以放心地放入你的工程了。一路使用下来,是不是很简单?当然这里只是一个简单的例子,当布局更加复杂时,可能还会引入更多的知识点,比如说 GuideBarrier 这些东西,有机会再继续聊吧。