最近在从基础开始学习Compose,刚学到布局这一部分,才发现原来Compose里面也有ConstraintLayout,不愧是谷歌认可的布局,不然也不会在Compose这个新的UI体系里面,也弄出来一个约束布局,甚至连名字也不变,那么我们也有必要去学习一下Compose的约束布局是怎么使用的,看看跟传统Android里面的约束布局相比,这个约束布局有什么不一样的地方。
准备工作
Compose的约束布局跟传统视图的约束布局一样,要使用它首先必须导入进来,我们在gradle文件里面加入它的依赖,注意的是Compose的约束布局版本跟Compose不太一样,有指定地方去查看它的 最新版本
implementation 'androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha07'
完成了配置,接下去就如标题所示,我们用约束布局来绘制一个掘金版的“我的”页面
整体布局
"我的"页面的布局不复杂,整体下来分七个部分,分别是标题栏,个人信息,数据信息,vip,活动区,创作者中心和更多功能,垂直线性布局的,第一反应就是简单,用Column
就好,但咱今天是要练习约束布局的,所以使用约束布局的话第一步就要建立好这七个区域的约束关系
首先是创建个ConstraintSet
对象,然后在ConstraintSetScope
里面使用createRefFor
创建七个ConstrainedLayoutReference
引用对象,随后我们给创建好的引用对象建立约束关系,记住createRefFor
传入的字符串,这个在后面用来绑定视图用的,引用创建好了,接下来是根视图,根视图的背景颜色有点小灰,然后还有点内边距,代码实现如下
这里我们注意到ConstraintLayout的第一个参数就是我们刚刚创建的ConstraintSet对象,这个不要忘记了,不然没法将上述建立好的引用和视图建立起绑定关系,但是如果不传入ConstraintSet对象当然也可以创建ConstraintLayout对象,但是视图与引用之间的绑定关系就要使用另一种方式了。
标题栏
第一步开始绘制标题栏部分,它是由四个icon组成,一个在左,另外三个靠右水平布局,这四个元素跟刚刚根视图那边就不一样了,要在组件内部建立起约束关系,代码实现如下
我们看到标题栏的根视图,使用了Modifier.layoutId
操作符绑定了我们上面建立的标题引用titleRef
,像极了传统视图里面的findViewById
是不,再看标题栏里面,这里创建四个icon的引用的方式是使用createRefs()
函数,这个函数是用来建立多个引用的,如果你一次只想建立一个,就可以使用另一个函数createRef()
,很好区分,一个有s一个没s,那么引用的约束关系怎么实现呢?我们看代码里面,分别是在各个Image组件里面使用Modifier的另一个操作符constrainAs去绑定,这个函数有两个参数,一个是我们上面建立好的引用,另一个就是ConstraintScope为接受者的lambda表达式,表达式里面就是这个组件的约束代码,写过传统视图的约束布局的小伙伴肯定一眼就看明白是啥意思了,这个时候我们已经完成标题栏的绘制了,看下效果
图片是我从截图上扣下来的,所以看起来有点怪,不过大致效果有了,我们接下去开始个人信息部分
个人信息
首先看看个人信息部分有哪些个元素,它们分别是头像,昵称,个人主页入口,写作等级,成长等级以及徽章,要绘制这些元素的话,第一步就是声明需要用的引用
然后这边稍微做个优化,因为我的昵称由八个字母组成,但在掘金“我的”tab页面这里只显示了七个,可能是做了宽度上的限制,但是这个给体验带来了影响,明明右边还有很大一片区域能展示,为什么不利用呢?所以这边的做法是充分利用头像与个人主页入口之间的空间,代码如下
我们看到这边是将昵称左右两边的约束都限制在头像与个人主页入口之间,并且给ConstraintScope
的width
设置了Dimension.fillToConstraints
,表示的意思是将Text的实际宽度拉伸至约束信息规定的宽度,Dimension
有如下几个可选值,可根据实际场景做选择
- wrapContent:实际尺寸为根据内容自适应的尺寸
- matchParent:实际尺寸为铺满整个父组件的尺寸
- fillToConstraints:实际尺寸为根据约束信息拉伸后的尺寸
- preferredWrapContent:如果剩余空间大于根据内容自适应的尺寸,实际尺寸为自适应的尺寸,如果剩余空间小于内容自适应的尺寸时,实际尺寸为剩余空间的尺寸
- ratio(String):根据字符串计算实际尺寸所占比例
- percent(Float):根据浮点数计算实际尺寸所占比例
- value(Dp):根据尺寸设定为固定值
- preferredValue(Dp):如果剩余空间大于固定值,实际尺寸为固定值,如果剩余空间小于固定值,实际尺寸为剩余空间的尺寸
现在我们看下实际效果
八个字符的昵称全部展示出来了,我们再假设有一天我改名了,名字特别长,那么这里应该超过约束宽度的话会换行展示,我们测试下是不是这样
再看下实际效果
的确是超过约束信息宽度后换行展示了,所以理解并获用Dimension可以让布局变得更加灵活,我们把个人信息剩下的几个等级徽章也补充上去
我们看到这里使用了约束布局另一个特性分界线Barrier,目的是为了昵称不管是一行展示还是多行展示,等级与徽章永远都可以与昵称保持相同的间距,我们再看下效果
能看到昵称一行跟多行与下面的等级徽章间距是一样的
数据信息
数据信息部分其实是三个相同的布局水平排列,我们看到这三个布局基本就是把横向布局进行了三等分,这边我们就要使用到约束布局的另一个特性,chain约束,我们同样是先给三个布局创建引用
我们看到这边创建好引用之后调用了一个createHorizontalChain
函数,这个函数的意义就是将三个布局水平绑定在一个链条上,并且设置了SpreadInside
的ChainStyle
,ChainStyle
总共有三个值,跟传统视图里的Chain基本相同
- Spread:链条中每个元素平分空间
- SpreadInside:链条中首尾元素紧贴边界,剩下的平分剩余空间
- Packed:链条中的所有元素聚集在中间
设置好链条关系之后,我们将
ConstraintSet
带入到ConstraintLayout
函数中
其中itemData是一个创建单个视图的函数
数据信息部分就完成了,我们看下效果图
会员信息
会员信息部分就没那么复杂了,我想唯一的难点就是确定文字以及背景的色值,没办法咱也不是视觉设计师,只能找几个差不多的色值代替下,做不到完全还原。
logo与标题水平布局,黑色背景的圆角我们使用了Modifier修饰符里面的clip函数实现,这里注意一定要在设置完大小以后再设置圆角,不然圆角效果是没有的,我们运行一下可以看到效果已经出来了
还有就是右边的按钮,也是个圆角视图,能实现的方式有很多种,这里也是选择用约束布局来做这个按钮,代码如下
运行一下,一个完整的会员信息公告栏就出来了
活动功能区
活动功能区域跟数据区域有个共同的地方,那就是也一样等分整个容器,不同的是它的每一项是图片与文字的组合,所以不能复用之前使用过的itemData
函数,不过可以拿过来改一下,把其中一个Text组件改成Image组件就可以了,我们把这个函数起名为itemImageData
这样我们就可以把刚刚数据区域的代码拿过来改一下就变成我们想要的活动专栏的代码了
这样我们的活动区域也开发完成了,效果图如下
创作者中心
创作者中心与活动区域唯一的区别就是多了一个副标题区域,不过也不复杂,无非就是在活动区域的代码中加两个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")
}
}
}
创作者中心也开发完成了,效果图如下
更多功能
目前来讲,开发还算比较容易的,靠着传统Android里面的约束布局打下的基础以及一个Chain的特性基本能将整个页面开发出来,不过到了最后一个板块就有点呆滞了,为啥呆滞呢?因为更多功能这个板块是可以左右滑动的,有点像ViewPager
,但是Compose里面偏偏就没有ViewPager
这个组件,那该怎么整呢?谷歌已经帮我们想到这一点了,告诉我们可以使用accompanist-pager
这个组件,首先得将这个库导入进来
implementation 'com.google.accompanist:accompanist-pager:0.24.2-alpha'
使用起来也很容易,比如要实现我们这样可以左右滑动的功能,只需要使用这个库提供的HorizontalPager就可以了,我们先去看下源码看看它都有哪些参数
- 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,下面我们看下createFirstPage
与createSecondPage
两个函数的代码
@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"))
}
}
}
这样我们更多功能的可滑动区域也完成了,我们看下效果如何
滑动的效果完成了,最后还剩下一个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的效果也有了
虽然效果是有了,但跟实际效果还是有点出入,实际效果选中indicator的width要比未选中的要宽,但是我们发现HorizontalPagerIndicator
的函数里面并没有提供未选中indicator宽度的属性设置,那怎么办呢?难道就这么算了?那不行,得有点追求是不,既然不提供我们就现做一个,把源代码拿过来二次开发一下
定义了一个新的函数叫CoffeeHorizontalPagerIndicator
的组件,将HorizontalPagerIndicator
组件里面的属性都复制过来,另外新增一个inactiveIndicatorWidth
的参数,用来给上层设置,另外,在位移计算的部分,原来计算的代码是下面这样
横向位移距离差不多是indicator之间的间距加上一个indicator的宽度,但是我们现在位移的距离小了,差不多只是两个间距的大小,所以在位移计算的代码上也做了如下修改
最后再将上面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)
)
}
}
最终效果图如下所示
总结
整个开发过程刚开始还是比较纠结的,毕竟好多地方使用Column或者Row布局就可以轻松实现的,非得创建一个个引用,然后建立约束关系才可以实现,但人总是被逼出来的,如果不多尝试几次,就没法彻底掌握一个知识点,后面也会时不时的来几个这样针对性的小demo,分享一下自己学Compose的过程与经验。