关于Jetpack Compose 中 ConstraintLayout布局探索

889 阅读4分钟

ConstraintLayout 有助于依据可组合项的相对位置将它们放置到界面上,如果复杂界面需要多个 RowColumnBox 元素,在实现对齐要求比较复杂的较大布局时,ConstraintLayout 很有用。

注意:在 View 系统中,建议使用 ConstraintLayout 创建复杂的大型布局,因为扁平视图的层次结构性能更好。不过,这在 Compose 中不存在这个问题,与性能无关,因为它能够高效地处理较深的布局层次结构。

首先在项目的 build.gradle 文件中找到 Compose Constraint Layout 依赖项:

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

Compose 中的 ConstraintLayout 支持 DSL

  • 引用是使用 createRefs()(或 createRef())创建的,ConstraintLayout 中的每个可组合项都需要有与之关联的引用。
  • 约束条件由 constrainAs 修饰符提供,该修饰符将引用作为参数,可在主体 lambda 中指定其约束条件。
  • 约束条件是使用 linkTo 或其他有用的方法指定的。
  • parent 是一个现有的引用,可用于指定对 ConstraintLayout 可组合项本身的约束条件。
1. 我们先从一个简单的例子开始xxx.linkTo(xxx)
@Composable
fun ConstraintLayoutDemo1() {
    ConstraintLayout() {
        val (loginBtn, userName) = createRefs()
​
        Button(
            onClick = { /*TODO*/ },
            modifier = Modifier
                .fillMaxWidth()
                .constrainAs(loginBtn) {
                    top.linkTo(parent.top, margin = 10.dp)
                    start.linkTo(parent.start, margin = 20.dp)
                }) {
            Text(text = "登录")
        }
​
        Text(
            text = "xxx@162.com",
            modifier = Modifier
                .wrapContentSize()
                .constrainAs(userName) {
                    top.linkTo(loginBtn.bottom, margin = 30.dp)
​
                    // 下面两行等价于:  centerHorizontallyTo(parent)
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                }
        )
    }
}

上面案例只是约束了上下左右间距

8075217.jpg

2. barrierLine约束
  • 先看 centerAround()以及createEndBarrier()
@Composable
fun ConstraintLayoutDemo2() {
    ConstraintLayout() {
        val (loginBtn, userName,password) = createRefs()
​
        Button(
            onClick = { /*TODO*/ },
            modifier = Modifier
                .wrapContentSize()
                .constrainAs(loginBtn) {
                    top.linkTo(userName.bottom, margin = 10.dp)
                    // 将该控件中心点约束在userName的结束处
                    centerAround(userName.end)
                }) {
            Text(text = "登录")
        }
​
        Text(
            text = "xxx@162.com",
            modifier = Modifier
                .wrapContentSize()
                .constrainAs(userName) {
                    top.linkTo(parent.top, margin = 30.dp)
                }
        )
​
        // 定义一个栅栏,在userName与loginBtn右边
        val barrier = createEndBarrier(userName,loginBtn)
​
        Text(
            text = "*********",
            modifier = Modifier
                .wrapContentSize()
                .constrainAs(password) {
                    top.linkTo(parent.top, margin = 20.dp)
                    start.linkTo(barrier)
                }
        )
    }
}

这里定义了一个barrierLine, 而centerAround从效果来看是将第一个控件"登录"的中心约束在了第二个控件“@162.com”结尾

8082146.jpg

  • 再看看如何便捷约束到父节点中心, centerTo,centerHorizontallyTo
@Composable
fun DemoInlineDSL() {
    ConstraintLayout(
        modifier = Modifier
            .wrapContentSize()
            .background(color = Color.Gray.copy(0.3f))
    ) {
        val (textA, textB, textC) = createRefs()

        Text("AAA",
            Modifier
                .wrapContentSize()
                .background(color = Color.Yellow.copy(alpha = 0.5f))
                .constrainAs(textA) {
                    start.linkTo(textB.end, margin = 10.dp)
                })
        Text("BBB",
            Modifier
                .wrapContentSize()
                .background(color = Color.Green.copy(alpha = 0.4f))
                .constrainAs(textB) {
                    centerTo(parent)
                })

        val barrier = createBottomBarrier(textA, textB)
        Text("This is a very long CCCC",
            Modifier
                .wrapContentSize()
                .background(color = Color.Blue.copy(alpha = 0.4f))
                .constrainAs(textC) {
                    top.linkTo(barrier, margin = 10.dp)
                    centerHorizontallyTo(parent)
                    width = Dimension.preferredWrapContent.atMost(20.dp)
                })
    }
}

效果如下,灰色背景代表整个父布局:

  • ConstraintSet 的使用,如何给控件设置id
@Composable
fun DemoConstraintSet() {
    ConstraintLayout(
        ConstraintSet {
        val textA = createRefFor("id_a")
        val textB = createRefFor("id_b")
        val textC = createRefFor("id_c")

        constrain(textA) {
            start.linkTo(textB.end, margin = 20.dp)
        }
        constrain(textB) {
            centerTo(parent)
        }

        val barrier = createBottomBarrier(textA, textB)
        constrain(textC) {
            top.linkTo(barrier, margin = 20.dp)
            centerHorizontallyTo(parent)
            width = Dimension.preferredWrapContent.atMost(20.dp)
        }
    },Modifier.background(color = Color.Gray.copy(0.5f))
    ) {
        Text("Text1", Modifier.layoutId("id_a"))
        Text("Text2", Modifier.layoutId("id_b"))
        Text("This is a very long CCCC", Modifier.layoutId("id_c"))
    }
}

效果展示:

3. guideline约束
@Composable
fun LargeConstraintLayoutDemo3() {
    ConstraintLayout (){
        val text = createRef()
​
        val guideline = createGuidelineFromStart(0.5f)
        Text(
            "什么是JSON JSON的用法 阿里云双11最高立减1111 恒创科技_海外CN2服务器26元/月起",
            Modifier.constrainAs(text) {
                linkTo(guideline, parent.end)
            },
        )
    }
}

垂直父节点0.5处建了一条guideLine, Text左临guideLine,右贴parent.end, 这样的话正常应该是占满guideLine右边。

实际效果是:

wrapContent.jpg

因为默认宽度width = Dimension.wrapContent是不允许布局大小是包裹内容大小的,所以出现了左边越界。

需要添加: width = Dimension.preferredWrapContent,即在约束条件下,指定布局大小为包裹内容的大小:

@Composable
fun LargeConstraintLayoutDemo3() {
    ConstraintLayout (){
        val text = createRef()
​
        val guideline = createGuidelineFromStart(0.5f)
        Text(
            "什么是JSON JSON的用法 阿里云双11最高立减1111 恒创科技_海外CN2服务器26元/月起",
            Modifier.constrainAs(text) {
                linkTo(guideline, parent.end)
                width = Dimension.preferredWrapContent
            },
        )
    }
}

效果:

preferredWrapContent.jpg width的取值还有其他几种情况:

 fillToConstraints - 布局将展开,以填充由该维度的约束条件定义的空间。使用
preferredValue - 布局是固定的 dp 值,受限于该维度的约束条件。
value - 布局是固定的 dp 值,无论该维度中的约束条件如何

4. Chains

链允许我们垂直或水平对齐视图,也可以控制它们之间的空间,以及视图如何使用空间。
链的创建:
>createHorizontalChain(可变参数元素:ConstrainedLayoutReference,chainStyle: chainStyle = chainStyle . spread) -创建水平链。

createVerticalChain(可变参数元素:ConstrainedLayoutReference,chainStyle: chainStyle = chainStyle . spread) -创建垂直链。

  • ChainStyle 有三种不同类型的连锁风格:
    • Spread: 子View分布均匀。
    • Spread inside: 第一个和最后一个视图与链两端的约束对齐,其余的均匀分布。
    • Packed: 子View挤在一起。

看下案例:

@Composable
fun ChainExample() {
    ConstraintLayout(modifier = Modifier
        .fillMaxWidth()
        .padding(12.dp)
        .background(color = Color.Gray.copy(0.5f))
    ) {
        val (button1, button2, button3) = createRefs()

        createHorizontalChain(button1, button2, button3, chainStyle = ChainStyle.Packed)

        Button(onClick = { }, modifier = Modifier.constrainAs(button1) {
        }) {
            Text(text = "Button 1")
        }

        Button(onClick = { }, modifier = Modifier.constrainAs(button2) {
        }) {
            Text(text = "Button 2")
        }

        Button(onClick = { }, modifier = Modifier.constrainAs(button3) {
        }) {
            Text(text = "Button 3")
        }
    }
}

4_093811.jpg