最近在从根底开端学习Compose,刚学到布局这一部分,才发现本来Compose里边也有ConstraintLayout,不愧是谷歌认可的布局,否则也不会在Compose这个新的UI系统里边,也弄出来一个束缚布局,甚至连姓名也不变,那么咱们也有必要去学习一下Compose的束缚布局是怎样运用的,看看跟传统Android里边的束缚布局比较,这个束缚布局有什么不相同的当地。

准备工作

Compose的束缚布局跟传统视图的束缚布局相同,要运用它首要必须导入进来,咱们在gradle文件里边加入它的依靠,留意的是Compose的束缚布局版别跟Compose不太相同,有指定当地去检查它的 最新版别

implementation 'androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha07'

完成了配置,接下去就如标题所示,咱们用束缚布局来制作一个版的“我的”页面

全体布局

“我的”页面的布局不杂乱,全体下来分七个部分,分别是标题栏,个人信息,数据信息,vip,活动区,创作者中心和更多功用,笔直线性布局的,第一反应便是简略,用Column就好,但咱今天是要练习束缚布局的,所以运用束缚布局的话第一步就要树立好这七个区域的束缚联系

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

首要是创立个ConstraintSet目标,然后在ConstraintSetScope里边运用createRefFor创立七个ConstrainedLayoutReference引证目标,随后咱们给创立好的引证目标树立束缚联系,记住createRefFor传入的字符串,这个在后边用来绑定视图用的,引证创立好了,接下来是根视图,根视图的布景色彩有点小灰,然后还有点内边距,代码完成如下

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

这儿咱们留意到ConstraintLayout的第一个参数便是咱们刚刚创立的ConstraintSet目标,这个不要忘记了,否则没法将上述树立好的引证和视图树立起绑定联系,可是假如不传入ConstraintSet目标当然也能够创立ConstraintLayout目标,可是视图与引证之间的绑定联系就要运用另一种方法了。

标题栏

第一步开端制作标题栏部分,它是由四个icon组成,一个在左,别的三个靠右水平布局,这四个元素跟刚刚根视图那边就不相同了,要在组件内部树立起束缚联系,代码完成如下

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

咱们看到标题栏的根视图,运用了Modifier.layoutId操作符绑定了咱们上面树立的标题引证titleRef,像极了传统视图里边的findViewById是不,再看标题栏里边,这儿创立四个icon的引证的方法是运用createRefs()函数,这个函数是用来树立多个引证的,假如你一次只想树立一个,就能够运用另一个函数createRef(),很好区分,一个有s一个没s,那么引证的束缚联系怎样完成呢?咱们看代码里边,分别是在各个Image组件里边运用Modifier的另一个操作符constrainAs去绑定,这个函数有两个参数,一个是咱们上面树立好的引证,另一个便是ConstraintScope为接受者的lambda表达式,表达式里边便是这个组件的束缚代码,写过传统视图的束缚布局的小伙伴必定一眼就看理解是啥意思了,这个时分咱们现已完成标题栏的制作了,看下作用

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

图片是我从截图上扣下来的,所以看起来有点怪,不过大致作用有了,咱们接下去开端个人信息部分

个人信息

首要看看个人信息部分有哪些个元素,它们分别是头像,昵称,个人主页入口,写作等级,生长等级以及徽章,要制作这些元素的话,第一步便是声明需要用的引证

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

然后这边略微做个优化,因为我的昵称由八个字母组成,但在“我的”tab页面这儿只显示了七个,可能是做了宽度上的束缚,可是这个给体验带来了影响,分明右边还有很大一片区域能展现,为什么不利用呢?所以这边的做法是充分利用头像与个人主页入口之间的空间,代码如下

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

咱们看到这边是将昵称左右两边的束缚都束缚在头像与个人主页入口之间,并且给ConstraintScopewidth设置了Dimension.fillToConstraints,表明的意思是将Text的实践宽度拉伸至束缚信息规则的宽度,Dimension有如下几个可选值,可依据实践场景做挑选

  • wrapContent:实践尺度为依据内容自适应的尺度
  • matchParent:实践尺度为铺满整个父组件的尺度
  • fillToConstraints:实践尺度为依据束缚信息拉伸后的尺度
  • preferredWrapContent:假如剩下空间大于依据内容自适应的尺度,实践尺度为自适应的尺度,假如剩下空间小于内容自适应的尺度时,实践尺度为剩下空间的尺度
  • ratio(String):依据字符串核算实践尺度所占比例
  • percent(Float):依据浮点数核算实践尺度所占比例
  • value(Dp):依据尺度设定为固定值
  • preferredValue(Dp):假如剩下空间大于固定值,实践尺度为固定值,假如剩下空间小于固定值,实践尺度为剩下空间的尺度

现在咱们看下实践作用

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

八个字符的昵称全部展现出来了,咱们再假设有一天我改名了,姓名特别长,那么这儿应该超过束缚宽度的话会换行展现,咱们测验下是不是这样

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

再看下实践作用

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

的确是超过束缚信息宽度后换行展现了,所以理解并获用Dimension能够让布局变得愈加灵活,咱们把个人信息剩下的几个等级徽章也弥补上去

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

咱们看到这儿运用了束缚布局另一个特性分界线Barrier,意图是为了昵称不管是一行展现仍是多行展现,等级与徽章永远都能够与昵称坚持相同的距离,咱们再看下作用

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲
只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

能看到昵称一行跟多行与下面的等级徽章距离是相同的

数据信息

数据信息部分其实是三个相同的布局水平排列,咱们看到这三个布局根本便是把横向布局进行了三等分,这边咱们就要运用到束缚布局的另一个特性,chain束缚,咱们同样是先给三个布局创立引证

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

咱们看到这边创立好引证之后调用了一个createHorizontalChain函数,这个函数的含义便是将三个布局水平绑定在一个链条上,并且设置了SpreadInsideChainStyle,ChainStyle总共有三个值,跟传统视图里的Chain根本相同

  • Spread:链条中每个元素平分空间
  • SpreadInside:链条中首尾元素紧贴鸿沟,剩下的平分剩下空间
  • Packed:链条中的所有元素集合在中间 设置好链条联系之后,咱们将ConstraintSet带入到ConstraintLayout函数中
只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

其中itemData是一个创立单个视图的函数

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

数据信息部分就完成了,咱们看下作用图

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

会员信息

会员信息部分就没那么杂乱了,我想仅有的难点便是确认文字以及布景的色值,没办法咱也不是视觉设计师,只能找几个差不多的色值代替下,做不到完全还原。

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

logo与标题水平布局,黑色布景的圆角咱们运用了Modifier修饰符里边的clip函数完成,这儿留意一定要在设置完巨细今后再设置圆角,否则圆角作用是没有的,咱们运行一下能够看到作用现已出来了

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

还有便是右边的按钮,也是个圆角视图,能完成的方法有很多种,这儿也是挑选用束缚布局来做这个按钮,代码如下

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

运行一下,一个完好的会员信息公告栏就出来了

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

活动功用区

活动功用区域跟数据区域有个共同的当地,那便是也相同等分整个容器,不同的是它的每一项是图片与文字的组合,所以不能复用之前运用过的itemData函数,不过能够拿过来改一下,把其中一个Text组件改成Image组件就能够了,咱们把这个函数起名为itemImageData

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

这样咱们就能够把刚刚数据区域的代码拿过来改一下就变成咱们想要的活动专栏的代码了

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

这样咱们的活动区域也开发完成了,作用图如下

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

创作者中心

创作者中心与活动区域仅有的区别便是多了一个副标题区域,不过也不杂乱,无非便是在活动区域的代码中加两个Text的组件就能够了

@Composable
private fun makeAuthorCenter() {
    val constraint = ConstraintSet {
        val contentRef = createRefFor("contentRef")
        val fansRef = createRefFor("fansRef")
        val createRef = createRefFor("createRef")
        val boxRef = createRefFor("boxRef")
        createHorizontalChain(contentRef,fansRef,createRef,boxRef,
            chainStyle = ChainStyle.SpreadInside
        )
    }
    ConstraintLayout(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(12.dp))
            .background(colorResource(id = R.color.white))
            .padding(20.dp)
            .layoutId("authorRef")
    ) {
        val (titleText, subText, tabRef) = createRefs()
        Text(
            text = "创作者中心",
            modifier = Modifier.constrainAs(titleText) {
                start.linkTo(parent.start)
                top.linkTo(parent.top)
            },
            colorResource(id = R.color.black),
            fontSize = 15.sp,
            fontWeight = FontWeight(800)
        )
        Text(
            text = "进入主页 >",
            modifier = Modifier.constrainAs(subText) {
                end.linkTo(parent.end)
                top.linkTo(parent.top)
            },
            colorResource(id = R.color.color_999999),
            fontSize = 15.sp,
        )
        ConstraintLayout(constraint, modifier = Modifier
            .fillMaxWidth()
            .constrainAs(tabRef) {
                top.linkTo(titleText.bottom, 15.dp)
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }) {
            itemImageData(id = R.mipmap.img_content, label = "内容数据", ref = "contentRef")
            itemImageData(id = R.mipmap.img_fans, label = "粉丝数据", ref = "fansRef")
            itemImageData(id = R.mipmap.img_create, label = "创作活动", ref = "createRef")
            itemImageData(id = R.mipmap.img_box, label = "草稿箱", ref = "boxRef")
        }
    }
}

创作者中心也开发完成了,作用图如下

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

更多功用

目前来讲,开发还算比较简单的,靠着传统Android里边的束缚布局打下的根底以及一个Chain的特性根本能将整个页面开发出来,不过到了最后一个板块就有点板滞了,为啥板滞呢?因为更多功用这个板块是能够左右滑动的,有点像ViewPager,可是Compose里边偏偏就没有ViewPager这个组件,那该怎样整呢?谷歌现已帮咱们想到这一点了,告诉咱们能够运用accompanist-pager这个组件,首要得将这个库导入进来

implementation 'com.google.accompanist:accompanist-pager:0.24.2-alpha'

运用起来也很简单,比如要完成咱们这样能够左右滑动的功用,只需要运用这个库供给的HorizontalPager就能够了,咱们先去看下源码看看它都有哪些参数

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲
  • count:item项的个数
  • modifier:操作符,很常见,不多说了
  • state:Pager滑动的状态
  • reverselayout:是否倒转item的次序
  • itemSpacing:item与item之间的距离
  • contentPadding:item的内边距
  • verticalAlignment:笔直方向的排列方法,默认为笔直居中
  • flingBehavior:记载触屏操作的状态行为
  • key:item的下标值
  • userScrollEnabled:滑动操作是否按照用户行为或许是辅助行为
  • content : item的具体内容

参数有点多,目不暇接了是不,其实真实运用起来只需要这样就能够了

HorizontalPager(count = {item的个数}){
}

是不是比Viewpager简略多了,现在咱们就用来制作咱们的更多功用模块,首要全体结构跟创作者中心比较类似的,也有副标题,副标题的下面才是HorizontalPager

@OptIn(ExperimentalPagerApi::class)
@Composable
private fun makeFunction() {
    ConstraintLayout(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(12.dp))
            .background(colorResource(id = R.color.white))
            .padding(20.dp)
            .layoutId("functionRef")
    ) {
        val (titleRef, pagerRef) = createRefs()
        Text(
            text = "更多功用",
            modifier = Modifier.constrainAs(titleRef) {
                start.linkTo(parent.start)
                top.linkTo(parent.top)
            },
            colorResource(id = R.color.black),
            fontSize = 15.sp,
            fontWeight = FontWeight(800)
        )
        HorizontalPager(count = 2, modifier = Modifier.constrainAs(pagerRef) {
            start.linkTo(parent.start)
            top.linkTo(titleRef.bottom, 15.dp)
            end.linkTo(parent.end)
        }) {
            when (it) {
                0 -> {
                    createFirstPage()
                }
                else -> {
                    createSecondPage()
                }
            }
        }
    }
}

当HorizontalPager的下标值为0的时分,就展现createFirstPage的内容,当下标值为1的时分,就展现createSecondPage的内容,首要是因为咱们得运用束缚布局,否则只需要一个网格布局就能够了,然后依据一个List的参数来决定具体显示多少个item,下面咱们看下createFirstPagecreateSecondPage两个函数的代码

@Composable
private fun createFirstPage() {
    ConstraintLayout(
        modifier = Modifier
            .fillMaxWidth()
            .height(180.dp)
    ) {
        val (firstRow, secondRow) = createRefs()
        val firstConstraint = ConstraintSet {
            val lessonRef = createRefFor("lessonRef")
            val centerRef = createRefFor("centerRef")
            val noteRef = createRefFor("noteRef")
            val voucherRef = createRefFor("voucherRef")
            createHorizontalChain(
                lessonRef, centerRef, noteRef, voucherRef, chainStyle = ChainStyle.SpreadInside
            )
        }
        val secondConstraint = ConstraintSet {
            val ringRef = createRefFor("ringRef")
            val recordRef = createRefFor("recordRef")
            val inviteRef = createRefFor("inviteRef")
            val feedbackRef = createRefFor("feedbackRef")
            createHorizontalChain(
                ringRef, recordRef, inviteRef, feedbackRef, chainStyle = ChainStyle.SpreadInside
            )
        }
        ConstraintLayout(firstConstraint,
            modifier = Modifier
                .fillMaxWidth()
                .constrainAs(firstRow) {
                    top.linkTo(parent.top)
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                }) {
            itemImageData(id = R.mipmap.img_lesson, label = "课程中心", ref = "lessonRef")
            itemImageData(id = R.mipmap.img_center, label = "推行中心", ref = "centerRef")
            itemImageData(id = R.mipmap.img_note, label = "闪念笔记", ref = "noteRef")
            itemImageData(id = R.mipmap.img_voucher, label = "我的优惠券", ref = "voucherRef")
        }
        ConstraintLayout(secondConstraint,
            modifier = Modifier
                .fillMaxWidth()
                .constrainAs(secondRow) {
                    top.linkTo(firstRow.bottom, 15.dp)
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                }) {
            itemImageData(id = R.mipmap.img_ring, label = "我的圈子", ref = "ringRef")
            itemImageData(id = R.mipmap.img_record, label = "阅读记载", ref = "recordRef")
            itemImageData(id = R.mipmap.img_invite, label = "邀请有礼", ref = "inviteRef")
            itemImageData(id = R.mipmap.img_feedback, label = "意见反馈", ref = "feedbackRef")
        }
    }
}
@Composable
private fun createSecondPage() {
    ConstraintLayout(
        modifier = Modifier
            .fillMaxWidth()
            .height(180.dp)
    ) {
        val singleRef = createRef()
        val constraint = ConstraintSet {
            val manageRef = createRefFor("manageRef")
            val myRef = createRefFor("myRef")
            val previewRef = createRefFor("previewRef")
            val emptyRef = createRefFor("emptyRef")
            createHorizontalChain(
                manageRef, myRef, previewRef, emptyRef, chainStyle = ChainStyle.SpreadInside
            )
        }
        ConstraintLayout(constraint, modifier = Modifier
            .fillMaxWidth()
            .constrainAs(singleRef) {
                start.linkTo(parent.start)
                top.linkTo(parent.top)
                end.linkTo(parent.end)
            }) {
            itemImageData(id = R.mipmap.img_tag, label = "标签管理", ref = "manageRef")
            itemImageData(id = R.mipmap.img_my, label = "我的报名", ref = "myRef")
            itemImageData(id = R.mipmap.img_preview, label = "简历管理", ref = "previewRef")
            Spacer(modifier = Modifier.layoutId("emptyRef"))
        }
    }
}

这样咱们更多功用的可滑动区域也完成了,咱们看下作用怎么

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

滑动的作用完成了,最后还剩下一个indicator,好像Compose也不供给这样的组件,又开端板滞了,不过谷歌也帮咱们想好了,想想也是,Pager跟indicator通常都是配套出现的,怎样可能做了Pager不做indicator呢,连依靠的链接都长的差不多

implementation 'com.google.accompanist:accompanist-pager-indicators:0.24.2-alpha'

运用起来也很简单,水平方向的indicator只需要运用HorizontalPagerIndicator这个组件就能够了,代码如下

@OptIn(ExperimentalPagerApi::class)
@Composable
private fun makeFunction() {
    ConstraintLayout(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(12.dp))
            .background(colorResource(id = R.color.white))
            .padding(20.dp)
            .layoutId("functionRef")
    ) {
        val (titleRef, pagerRef, indicator) = createRefs()
        val pState = rememberPagerState()
        Text(
            text = "更多功用",
            modifier = Modifier.constrainAs(titleRef) {
                start.linkTo(parent.start)
                top.linkTo(parent.top)
            },
            colorResource(id = R.color.black),
            fontSize = 15.sp,
            fontWeight = FontWeight(800)
        )
        HorizontalPager(count = 2, modifier = Modifier.constrainAs(pagerRef) {
            start.linkTo(parent.start)
            top.linkTo(titleRef.bottom, 15.dp)
            end.linkTo(parent.end)
        }, state = pState) {
            when (it) {
                0 -> {
                    createFirstPage()
                }
                else -> {
                    createSecondPage()
                }
            }
        }
        HorizontalPagerIndicator(
            pagerState = pState, modifier = Modifier.constrainAs(indicator) {
                start.linkTo(parent.start)
                end.linkTo(parent.end)
                top.linkTo(pagerRef.bottom, 8.dp)
            }, activeColor = colorResource(id = R.color.color_999999),
            inactiveColor = colorResource(id = R.color.color_CCCCCC),
            indicatorWidth = 20.dp,
            indicatorHeight = 2.dp,
            spacing = 5.dp,
            indicatorShape = RoundedCornerShape(2.dp)
        )
    }
}

咱们看到HorizontalPagerIndicator需要一个pagerState的参数,这个参数是从上面HorizontalPager里边取得的,除此之外,它还支撑定义选中下标的色彩,未选中下标的色彩,下标的宽度,下标的高度,下标之间的距离以及下标的形状,咱们分别给这些参数带入对应的值今后,indicator的作用也有了

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

虽然作用是有了,但跟实践作用仍是有点收支,实践作用选中indicator的width要比未选中的要宽,可是咱们发现HorizontalPagerIndicator的函数里边并没有供给未选中indicator宽度的特点设置,那怎样办呢?莫非就这么算了?那不可,得有点追求是不,已然不供给咱们就现做一个,把源代码拿过来二次开发一下

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

定义了一个新的函数叫CoffeeHorizontalPagerIndicator的组件,将HorizontalPagerIndicator组件里边的特点都复制过来,别的新增一个inactiveIndicatorWidth的参数,用来给上层设置,别的,在位移核算的部分,本来核算的代码是下面这样

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

横向位移距离差不多是indicator之间的距离加上一个indicator的宽度,可是咱们现在位移的距离小了,差不多只是两个距离的巨细,所以在位移核算的代码上也做了如下修改

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

最后再将上面indicator的代码换成咱们新生成的CoffeeHorizontalPagerIndicator

@OptIn(ExperimentalPagerApi::class)
@Composable
private fun makeFunction() {
    ConstraintLayout(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(12.dp))
            .background(colorResource(id = R.color.white))
            .padding(20.dp)
            .layoutId("functionRef")
    ) {
        val (titleRef, pagerRef, indicator) = createRefs()
        val pState = rememberPagerState()
        Text(
            text = "更多功用",
            modifier = Modifier.constrainAs(titleRef) {
                start.linkTo(parent.start)
                top.linkTo(parent.top)
            },
            colorResource(id = R.color.black),
            fontSize = 15.sp,
            fontWeight = FontWeight(800)
        )
        HorizontalPager(count = 2, modifier = Modifier.constrainAs(pagerRef) {
            start.linkTo(parent.start)
            top.linkTo(titleRef.bottom, 15.dp)
            end.linkTo(parent.end)
        }, state = pState) {
            when (it) {
                0 -> {
                    createFirstPage()
                }
                else -> {
                    createSecondPage()
                }
            }
        }
        CoffeeHorizontalPagerIndicator(
            pagerState = pState, modifier = Modifier.constrainAs(indicator) {
                start.linkTo(parent.start)
                end.linkTo(parent.end)
                top.linkTo(pagerRef.bottom, 8.dp)
            }, activeColor = colorResource(id = R.color.color_999999),
            inactiveColor = colorResource(id = R.color.color_CCCCCC),
            indicatorWidth = 20.dp,
            inactiveIndicatorWidth = 5.dp,
            indicatorHeight = 2.dp,
            spacing = 20.dp,
            indicatorShape = RoundedCornerShape(2.dp)
        )
    }
}

最终作用图如下所示

只用Compose的约束布局做个掘金“我的”tab页,好像也没那么费劲

总结

整个开发进程刚开端仍是比较纠结的,毕竟好多当地运用Column或许Row布局就能够轻松完成的,非得创立一个个引证,然后树立束缚联系才能够完成,但人总是被逼出来的,假如不多测验几回,就没法完全把握一个知识点,后边也会时不时的来几个这样针对性的小demo,分享一下自己学Compose的进程与经验。