JetpackCompose之ConstraintLayout

Jetpack Compose系列(10) - ConstraintLayout

ConstraintLayout

在View体系中,ConstraintLayout就已经展现出其关于布局构建功能的强大性,能够避免过多的布局嵌套导致页面过多的渲染和代码维护性,这么方便快捷且强大的组件当然要保留到Compose中啦。

通过对子项之间进行约束条件,从而定位子项的布局。

虽说作用都一致,但在用法上也会有些许差异,尤其需要注意下API是否已经变更。

依赖

在app的build.gradle中,引入

implementation("androidx.constraintlayout:constraintlayout-compose:1.0.0")

(Compose和Constraintlayout都还没有到稳定版本,所以对相关库的依赖一定都要更新到最新适配版本,不然可能存在不兼容问题。另外注意这里的compose,引用错误会导致导入原View体系的Comstrainlayout)

构造函数

ConstraintLayout有两个构造函数:

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

@OptIn(ExperimentalMotionApi::class)
@Suppress("NOTHING_TO_INLINE")
@Composable
inline fun ConstraintLayout(
    constraintSet: ConstraintSet,
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    animateChanges: Boolean = false,
    animationSpec: AnimationSpec = tween(),
    noinline finishedAnimationListener: (() -> Unit)? = null,
    noinline content: @Composable () -> Unit
)

各主要参数含义如下:

· modifier: Modifier = Modifier :修饰符

· content: ConstraintLayoutScope.() -> Unit :子视图内容,可以添加任意数量。

· constraintSet: ConstraintSet :对子View约束相关的描述

· content: () -> Unit :使用ConstraintSet参数定义的子级内容

· optimizationLevel: Int : 适用于在管理约束时设置优化级别,默认选项是 Optimizer.OPTIMIZATION_STANDARD。

约束ID

既然是约束布局,那么肯定要有约束条件,而使用约束条件的前提就是不同控件间有id,通过相应的id去约束各个组件间的关系,Compose中的ConstraintLayout支持DSL,有两种方式来创建id:

· 引用是使用 createRefs()(或 createRef())创建的,ConstraintLayout 中的每个可组合项都需要有与之关联的引用。
· 约束条件是使用 constrainAs 修饰符提供的,该修饰符将引用作为参数,可让您在主体 lambda 中指定其约束条件。

在ConstraintLayout中,约束条件是使用 linkTo 或其他有用的函数指定。parent 是一个现有的引用,可用于指定对ConstraintLayout 可组合项本身的约束条件。

例如,如下代码:

ConstraintLayout {

    // Create references for the composables to constrain
    val (button, text) = createRefs()

    Button(
        onClick = { /* Do something */ },
        modifier = Modifier.constrainAs(button) {
            top.linkTo(parent.top, margin = 16.dp)
        }
    ) {
        Text("Button")
    }
    
    Text("Text", Modifier.constrainAs(text) {
        top.linkTo(button.bottom, margin = 16.dp)
    })
}

看到constrainAs()和constrain()不明白什么意思?别急,我们看到最后你就都明白了。

这里使用 16.dp 的外边距来约束 Button 顶部到父元素的距离,同样使用 16.dp 的外边距来约束 Text 到 Button 底部的距离。对应的生成效果为:

JetpackCompose之ConstraintLayout_第1张图片
如果希望文本相对Button水平居中,可以使用 centerHorizontallyTo 函数将 Text 的 start 和 end 均设置为 parent 的边缘。例如,修改原代码中的Text():

Text("Text", Modifier.constrainAs(text) {
    top.linkTo(button.bottom, margin = 16.dp)
    // 添加下面代码:
    centerHorizontallyTo(parent)
})

对应效果为:

JetpackCompose之ConstraintLayout_第2张图片
ConstraintLayout 会尽可能占用小布局,以封装其内容。这就是 这里的Text 似乎以 Button 而非父元素为中心的原因所在。如果需要其他大小调整行为,应将大小调整修饰符(例如 fillMaxSize、size)应用于 ConstraintLayout 可组合项,就像 Compose 中的任何其他布局一样。

DSL 还支持创建准则、限制和链。

例如如下代码:

ConstraintLayout {
    val (button1, button2, text) = createRefs()

    Button(
        onClick = { /* Do something */ },
        modifier = Modifier.constrainAs(button1) {
            top.linkTo(parent.top, margin = 16.dp)
        }
    ) {
        Text("Button 1")
    }

    Text("Text", Modifier.constrainAs(text) {
        top.linkTo(button1.bottom, margin = 16.dp)
        centerAround(button1.end)
    })

    val barrier = createEndBarrier(button1, text)
    Button(
        onClick = { /* Do something */ },
        modifier = Modifier.constrainAs(button2) {
            top.linkTo(parent.top, margin = 16.dp)
            start.linkTo(barrier)
        }
    ) {
        Text("Button 2")
    }
}

对应的效果为:
JetpackCompose之ConstraintLayout_第3张图片
注意,在这里:

· 限制(以及所有其他DSL)可以在 ConstraintLayout 的正文中创建,但不能在 constrainAs 内部创建。

· linkTo 可用于约束准则和限制,就像它运用于布局边缘的工作原理一样。

Guideline

即引导线,可以从特定的位置(某一方向上的偏移量或者某一方向上的比例)创建一条实际并不可见的参考线,提供给各个控件约束条件,可以给出占屏幕百分比。其种类共有以下几种:

createGuidelineFromStart(offset: Dp)
createGuidelineFromAbsoluteLeft(offset: Dp)
createGuidelineFromStart(fraction: Float)
createGuidelineFromAbsoluteLeft(fraction: Float)
createGuidelineFromEnd(offset: Dp)
createGuidelineFromAbsoluteRight(offset: Dp)
createGuidelineFromEnd(fraction: Float)
createGuidelineFromAbsoluteRight(fraction: Float)
createGuidelineFromTop(offset: Dp)
createGuidelineFromTop(fraction: Float)
createGuidelineFromBottom(offset: Dp)
createGuidelineFromBottom(fraction: Float)

看名知意,这些方法都是在上、下、左、右方向分别支持某一偏移量,或某比例进行创建引导线。而…FromAbsolute…的表示绝对的左右偏移方向。

例如,我们使用ConstraintLayoutScope()来进行示例:

ConstraintLayout(
    modifier = Modifier.fillMaxSize()
) {
    val guideline = createGuidelineFromStart(0.5f)
    val (box1, box2) = createRefs()
    Box(
        modifier = Modifier.fillMaxSize()
            .background(color = Color.Red)
            .constrainAs(box1) {
                end.linkTo(guideline)
            }
    )
    Box(
        modifier = Modifier.fillMaxSize()
            .background(color = Color.Blue)
            .constrainAs(box2) {
                start.linkTo(guideline)
            }
    )
}

代码中可以看出,引导线在中间,路边是不同的Box控件,对应的显示为:

JetpackCompose之ConstraintLayout_第4张图片

自定义维度

默认情况下,系统允许 ConstraintLayout 的子项选择封装其内容所需的大小。例如,这意味着当文本过长时,可以超出界面边界:例如如下示例:

ConstraintLayout {
    val text = createRef()
    val guideline = createGuidelineFromStart(fraction = 0.5f)
    Text(
        "This is a text1 text2 text3 text4 text5 text6 text7 text8 text9 text10",
        Modifier.constrainAs(text) {
            linkTo(start = guideline, end = parent.end)
        }
    )
}

JetpackCompose之ConstraintLayout_第5张图片
可见文本在text8时就超出了屏幕宽度限制,这个时候就应该换行了。换行可以使用with:

原代码中的Text()修改为:

Text(
    "This is a text1 text2 text3 text4 text5 text6 text7 text8 text9 text10",
    Modifier.constrainAs(text) {
        linkTo(start = guideline, end = parent.end)
        width = Dimension.preferredWrapContent
    }
)

所对应效果为:

JetpackCompose之ConstraintLayout_第6张图片
这里引申出一个新的函数Dimension,其各参数如下:

· preferredWrapContent - 布局是封装内容,受限于该维度的约束条件;

· wrapContent - 布局是封装内容,即使约束条件不允许该内容;

· fillToConstraints - 布局将展开,以填充由该维度的约束条件定义的空间;

· preferredValue - 布局是固定的 dp 值,受限于该维度的约束条件;

· value - 布局是固定的 dp 值,无论该维度中的约束条件如何。

此外,某些情况下Dimension 可以强制转换,例如:

width = Dimension.preferredWrapContent.atLeast(100.dp)

Barrier

即屏障,其作用见名知意。同样的创建屏障的参数也有如下几个:

·createStartBarrier()
·createAbsoluteLeftBarrier()
·createTopBarrier()
·createEndBarrier()
·createAbsoluteRightBarrier()
·createBottomBarrier()

同样的是四个方向,加上两个绝对方向。

使用场景

如何理解这个函数呢?例如我们现在有个这个场景,有个长文本、一个短文本,同时还有一个按钮,这时我们要限制按钮在长文本的右边,但长文本和短文本是可以相互变化的,即不固定的,此时Barrier就应运而生来表达这种关系:

JetpackCompose之ConstraintLayout_第7张图片
例如以下代码:

ConstraintLayout(
    ConstraintSet {
        val Text1 = createRefFor("Text1")
        val Text2 = createRefFor("Text2")
        val Button3 = createRefFor("Button3")
        constrain(Text1) {
            top.linkTo(parent.top)
            start.linkTo(parent.start)
        }
        constrain(Text2) {
            top.linkTo(Text1.bottom)
            start.linkTo(parent.start)
        }
        val barrier = createEndBarrier(Text1, Text2)
        constrain(Button3) {
            start.linkTo(barrier)
            top.linkTo(Text1.top)
            bottom.linkTo(Text2.bottom)
        }
    }
) {
    Text(text = "长文本卡啦啦啦啦啦啦啦",
        modifier = Modifier
            .layoutId("Text1")
            .background(color = Color.Red)
            .width(180.dp)
            .height(50.dp)
    )
    Text(text = "短文本",
        modifier = Modifier
            .layoutId("Text2")
            .background(color = Color.Yellow)
            .width(110.dp)
            .height(50.dp)
    )
    Button(onClick = { /*TODO*/ },
        modifier = Modifier
            .layoutId("Button3")
            .background(color = Color.Blue)
            .width(200.dp)
            .height(100.dp)
    ){
        Text(text = "Button")
    }
}

对应的效果为:

JetpackCompose之ConstraintLayout_第8张图片
无论后续修改两个文本哪个是长文本、哪个为段文本,Button都在右边。

Chain

即,链。类似于View体系中xml文件里的chain。作用类似于将一系列组件按顺序打包成一行或一列。此API目前被官方标记为可改进状态,后续可能有所更改。

其创建方式如下:

createHorizontalChain()     //创建横向的链
createVerticalChain()       //创建竖向的链

这俩构造函数为:

fun createHorizontalChain(
    vararg elements: ConstrainedLayoutReference,
    chainStyle: ChainStyle = ChainStyle.Spread
)
fun createVerticalChain(
    vararg elements: ConstrainedLayoutReference,
    chainStyle: ChainStyle = ChainStyle.Spread
)

可以看出,第一个参数是控件的编号,第二个参数是链的类型。而其类型又分为三种:

·Spread:默认类型,所有控件平均分布在父布局中;

·SpreadInside:第一个和最后一个分布在链条的两端,其余的控件平均分布剩下的空间;

·Packed:所有控件包在一起,并放置在链条的中间。

例如如下代码:

ConstraintLayout(modifier = Modifier.fillMaxSize()) {
    val (box1, box2, box3) = createRefs()
    createVerticalChain(box1, box2, box3)
    Box(modifier = Modifier.size(100.dp).background(Color.Black).constrainAs(box1) {})
    Box(modifier = Modifier.size(100.dp).background(Color.Red).constrainAs(box2) {})
    Box(modifier = Modifier.size(100.dp).background(Color.Blue).constrainAs(box3) {})
}

对应的预览效果为:

JetpackCompose之ConstraintLayout_第9张图片

constrainAs () 和constrain ()

看到这里你应该就明白了这两个函数的主要用法和意义,除了定义id和编号外,实现约束位置的主要逻辑都在这两个函数里。

其函数参数属性如下:

· parent
· start
· absolutLeft
· top
· end
· absoluteRight
· bottom
· baseline

见名知意,各参数含义作用看名称就能明白。使用linkTo()函数链接到另一个控件的相应属性上即可。当然,不能让一个控件左侧对齐另一个控件的上侧,这样会报错。

Decoupled API

另一种使用方式:除了以内嵌方式指定约束条件,在某些情况下,还可以使约束条件与它们所应用到的布局分离:常见的为根据界面配置轻松更改约束条件,或在 2 个约束条件集之间添加动画效果。这个功能能大大的改善代码的耦合度。

这些情况下,可以通过不同的方式使用 ConstraintLayout:

· 将 ConstraintSet 作为参数传递给 ConstraintLayout。

· 使用 layoutId 修饰符将在 ConstraintSet 中创建的引用分配给可组合项。

此 API 形状适用于上面显示的第一个 ConstraintLayout 示例,它针对界面宽度进行了优化。

@Composable
fun CustomView() {
    BoxWithConstraints {
        val constraints = if (maxWidth < maxHeight) {
            decoupledConstraints(margin = 16.dp) // Portrait constraints
        } else {
            decoupledConstraints(margin = 32.dp) // Landscape constraints
        }

        ConstraintLayout(constraints) {
            Button(
                onClick = { /* Do something */ },
                modifier = Modifier.layoutId("button")
            ) {
                Text("Button")
            }

            Text("Text", Modifier.layoutId("text"))
        }
    }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
    return ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        constrain(button) {
            top.linkTo(parent.top, margin= margin)
        }
        constrain(text) {
            top.linkTo(button.bottom, margin)
        }
    }
}

对应的显示情况也在预料之中:

JetpackCompose之ConstraintLayout_第10张图片

目前为止,ConstraintLayout所有知识点几乎都在这,当然,也肯定会有遗漏,欢迎留言交流。

你可能感兴趣的:(Jetpack,Compose,Android进阶,android,jetpack,android-jetpack,android,kotlin)