Android ConstraintLayout使用进阶
前言
曾经Android有五大布局,LinearLayout、FrameLayout、TableLayout、RelativeLayout和AbsoluteLayout,那会我们比较常用的布局就两三个,写xml的时候根据界面灵活选择布局,但是往往会面临布局嵌套过深的问题,阅读也不方便。随着Android生态的发展,Google后来推出了新的布局——ConstraintLayout(约束布局)。
我很快去学习并将其用在项目中,刚开始的时候觉得比较抽象难懂,各种不适应;一段时间过后,这玩意儿真香!
本文不讲ConstraintLayout基本使用(网上资料很多),而是关于使用ConstraintLayout的进阶。
导入依赖:(2.x版本)
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
进阶1
在开发中可能需要实现如下效果:
文本外层有背景,短文本的时候宽度自适应,长文本超过屏幕的时候,背景贴右边,文字显示...,这样的UI需求很常见,我们来一步步拆解。
1、文本背景需要占满屏幕,并且文本显示...
<TextView
android:layout_width="0dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:background="@drawable/xxx"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
2、这时候TextView会水平居中,我们需要添加
app:layout_constraintHorizontal_bias="0"
layout_constraintHorizontal_bias表示水平偏移,即“当组件左侧和右侧 ( 或者 开始 和 结束 ) 两边被约束后, 两个联系之间的比例”,取值为0-1,具体看ConstraintLayout 偏移 ( Bias ) 计算方式详解,我们只需要将水平偏移量设置为0,控件就会被约束在左侧了。
3、最后一步,短文本的时候宽度自适应,长文本的时候占满屏幕,需要添加
app:layout_constraintWidth_max="wrap"
layout_constraintWidth_max表示指定视图的最大宽度,取值为“wrap”,它和“wrap_content”不同,虽然都是适应内容,但仍然允许视图比约束要求的视图更小。
最终代码:
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/xxx"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_max="wrap"
tools:text="这是一个测试文案"
/>
进阶2
再来看个效果图:
还是文本适配的问题,短昵称的时候自适应,长昵称的时候,性别图标跟随文本长度移动,但是图标必须在“聊天”按钮左侧,文本显示...
我们再来一步步拆解(仅针对昵称Textview):
一、重复上面的步骤1和步骤2,代码如下(注意layout_width="wrap_content",上面的是0dp)
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是昵称"
android:singleLine="true"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/iv_head"
app:layout_constraintEnd_toStartOf="@id/iv_gender"
/>
二、这时候我们会发现布局是居中的,而且昵称TextView都需要收尾元素相连,我们可以使用layout_constraintHorizontal_chainStyle改变整条链的约束状态,它有三个值,分别是spread、spread_inside和packed,其中packed表示将所有 Views 打包到一起不分配多余的间隙(当然不包括通过 margin 设置多个 Views 之间的间隙),然后将整个组件组在可用的剩余位置居中(可以查看Chains链布局),同时由于layout_constraintHorizontal_bias="0"的作用,布局将会向左侧偏移。
app:layout_constraintHorizontal_chainStyle="packed"
三、最后,当我们输入文本时,发现文本并没有约束到“聊天”按钮左侧,因为layout_width="wrap_content",添加的约束是不起作用的,所以需要强制约束
app:layout_constrainedWidth="true"
代码动态改变约束
初始约束:
修改后的约束:
如上图,初始状态,中间按钮约束在按钮1右侧,某个条件下需要将中间按钮约束在按钮2左侧,这种时候,我们就需要在代码动态设置约束了。
具体代码:
constraintLayout?.let {
//初始化一个ConstraintSet
val set = ConstraintSet()
//将原布局复制一份
set.clone(it)
//分别将“中间按钮”START方向和BOTTOM方向的约束清除
set.clear(“中间按钮”, ConstraintSet.START)
set.clear(“中间按钮”, ConstraintSet.BOTTOM)
//重新建立新的约束
//“中间按钮”的END约束“按钮2”控件的START
//相当于 app:layout_constraintEnd_toStartOf="@id/按钮2"
set.connect(
“中间按钮”,
ConstraintSet.END,
“按钮2”,
ConstraintSet.START,
resources.getDimensionPixelSize(R.dimen.dp_9)
)
//以及底部方向的约束
...
//最后将更新的约束应用到布局
set.applyTo(it)
}
MotionLayout
接下来是今天重头戏——MotionLayout。
MotionLayout继承自ConstraintLayout,能够通过约束关系构建丰富的view动画,动画状态分为start与end两个状态,它还能作为支持库,兼容到api 14。
来看下效果图,这是我司App某个页面的动画效果,就是用MotionLayout实现。
我们可以写个简单的demo实现上面一部分动画效果,如下图
首先我们需要在资源文件夹 res 下新建一个名为 xml 的资源文件夹,然后在 文件夹内新建一个根节点是 MotionScene 的 xml 文件,文件名为 test_motion_scene.xml,如下:
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
</MotionScene>
activity的xml根布局改为MotionLayout,使用app:layoutDescription与之关联
再编写视图,定义视图具体的view和对应id
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:layoutDescription="@xml/test_motion_scene"
...
>
<ImageView
android:id="@+id/iv_head"
...
/>
<TextView
android:id="@+id/tv1"
...
/>
然后切换到test_motion_scene.xml,我们需要明确动画布局的两个状态,start和end。
在MotionScene标签下定义Transition标签,指定动画的start和end状态
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="500">
</Transition>
之后,在Transition同级下再定义ConstrainSet标签,它表示用于指定所有视图在动画序列中某一点上的位置和属性,你可以把它理解成一个集合,集合了所有参与动画的view相关位置和属性,如下:
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition>
...
</Transition>
<ConstraintSet android:id="@+id/start">
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
</ConstraintSet>
</MotionScene>
大体的框架搭建好了,最后就是填充约束view状态的代码了。这时候我们需要明确动画的start状态和end状态,即
(start状态)↓
(end状态)↓
前面提到,ConstraintSet是存放一些view 约束和属性的的集合,而具体描述View约束和属性是通过Constraint 标签。我们声明Constraint标签,它支持一组标准 ConstraintLayout 属性,用于添加每个view start状态的约束。
<ConstraintSet android:id="@+id/start">
<Constraint
<!-- "android:id"表示activity的xml对应的view id
android:id="@id/iv_head"
android:layout_width="90dp"
android:layout_height="90dp"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintEnd_toEndOf="parent"/>
<Constraint
android:id="@id/iv1"
.../>
<Constraint
android:id="@id/iv2"
.../>
...
</ConstraintSet>
接下来以同样的方式添加end状态的view约束
<ConstraintSet android:id="@+id/end">
...
</ConstraintSet>
最后,我们需要让它动起来,在Transition标签写添加一个OnClick标签,run,就能让动画动起来
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<!-- 点击-->
<OnClick
motion:clickAction="toggle"
motion:targetId="@id/search_go_btn"/>
</Transition>
OnClick:表示由用户点击触发
属性:
motion:targetId="@id/target_view" (目标View的id)
如果不指定次属性,就是点击整个屏幕触发如果写了这个属性,就是点击对应id的View 触发转场动画
motion:clickAction=“action” 点击后要进行的行为 ,此属性可以设置以下几个值:
transitionToStart
过渡到 元素 motion::constraintSetStart 属性指定的状态,有过度动画效果。
transitionToEnd
过渡到 元素motion:constraintSetEnd 属性指定的状态,有过度动画效果。
jumpToStart
直接跳转到 元素 motion::constraintSetStart 属性指定的状态,没有动画效果。
jumpToEnd
直接跳转到 元素 motion:constraintSetEnd 属性指定的状态。
toggle
默认值就是这个,在 元素motion:constraintSetStart和 motion:constraintSetEnd 指定的布局之间切换,如果处于start状态就过度到end状态,如果处于end状态就过度到start状态,有过度动画。
除了OnClick之外,还有OnSwipe,它是根据用户滑动行为调整动画的进度,具体可查看文末资料。
改变动画运动过程(关键帧KeyFrameSet)
上面讲解了动画的start与end状态,但是如果我们想在动画运动过程去改变一些属性,比如设置view的透明度、旋转,又或者是改变动画运动过程的轨迹等,这时候可以用到关键帧。
KeyFrameSet是Transition的子元素,与OnClick、OnSwipe同级。KeyFrameSet中可以包含KeyPosition、KeyAttribute、KeyCycle、KeyTimeCycle、KeyTrigger,它们都可以用来改变动画过程。
此外还有与KeyFrameSet同级的KeyPosition、KeyAttribute,具体大家根据需要自行了解即可。
最后再提一下MotionLayout一些常用的java api:
loadLayoutDescription() ——对应xml"app:layoutDescription",通过代码加载MotionScene;
transitionToStart() ——表示切换到动画start状态;
transitionToEnd() ——表示切换到动画end状态;
它们都默认有过渡效果,如果不需要过渡效果,可以通过**setProgress(float pos)**处理过渡进度,取值0-1;
transitionToState(int id) ——表示切换到动画某个状态,可以是start也可以是end,参数id指的是ConstraintSet标签定义的id;
setTransitionListener(MotionLayout.TransitionListener listener) ——监听MotionLayout动画执行过程,接口有四个方法,onTransitionStarted、onTransitionChange、onTransitionCompleted、onTransitionTrigger。
OK,最最后,ConstraintLayout能有效提升日常的开发效率,通过这篇文章的介绍,此刻你学废了嘛~
参考
ConstraintLayout / MotionLayout GitHub示例
来源:juejin.cn/post/6886337167279259661