原文地址:https://www.raywenderlich.com/9475-constraintlayout-tutorial-for-android-complex-layouts
- 转换其他类型的布局到ConstraintLayout
- 相对于在屏幕上的其他元素动态定位UI元素
- 让你的Views实现动画
在这篇教程中,会构建一个星际旅行的app,火箭将会绕着行星不停的转动。
在这个app中有很多元素,你能够学到如何使用复杂的ConstraintLayout来正常的显示他们。
转换一个布局到ConstraintLayout
在Component Tree面板中,在最顶层的布局中右键并且选择Convert LinearLayout to ConstraintLayout:
接着会看到一个弹出框并且有一些选项:
阅读完所有内容后不改变默认的选中状态,然后点击ok关闭对话框,AndroidStudio接下来就会把你的布局转换成ConstraintLayout。
转换过后,你的布局就变成下面的这个样子:
如果此时所有的视图都跑到了左上角,请不要惊慌,
请确保关闭AutoConnect
移除推断约束
执行转换的过程中,AndroidStudio会执行好几个步骤,最后一步就是进行推断约束,但是结果并不是你想要的,此时,你只需要转到编辑菜单并选择撤销推断约束。
或者,执行cmd+Z,现在你的界面看起来如下:
你可以稍微拖动视图,让他看起来更像原始的样子:
如果在拖动过程中Android Studio自动添加了任何的约束,可以点击Clear All Constraints按钮去清除他们。
调整Image
通过点击顶部的每个图标,spaceStationIcon,flightIcon和roverIcon来修复图像的大小,接着在属性面板,将layout_width和layout_heigth从wrap_content改为30dp。
此时你将会在Comoonent Tree中看到一堆错误,这是因为没有任何约束信息告诉它在哪里定位,现在开始解决这个问题。
添加约束:找出对齐方式
使用自上而下的方法设置约束,从屏幕顶部的元素开始,一直向下设置。
希望顶部三个图标水平分布排列,然后标签置于图标下方
约束第一个图标
单击第一个图标并且显示约束锚点,点击上面的锚点并且拖动到顶部的view,该图标就会自动滑动到顶部。先不要连接左侧的约束、
接着,切换到Code界面检查第一个图标xml的更新,发现添加了一个新的约束app:layout_constraintTop_toTopOf="parent",XML看起来如下:
你也可以在设计视图中调整边距,切换到设计视图点击Attributestab。
接着点击第一个图标并且查看属性,你会看到margins的图形表示。
你可以通过从下拉菜单中选择边距或单击数字并输入新值来为边距选择新值。
水平对齐顶部的三个图标:使用Chains
接着,希望顶部的三个图标在同一条水平线上并且平均分布,需要为每一个图标添加一系列的约束,这里有个更快速的方法就是使用chains。
Chains
如果有双向约束,就会出现链条,当你使用了菜单中的对齐约束时,Android Studio实际上就使用了链条,你可以将不同的样式、权重、边距应用到链条.
接着切换到设计面板,同时选中顶部的三个图标,右键然后选择Center -> Horizontally,此时会自动创建一个链条并且生成约束。
在设计面板你就能看到链与其他约束的不同,其他的是波浪线表示,而链条是一条链。
探索链条
要探索某些链的模式,选择一个元素,单击图标底部显示的循环链模式按钮。
模式有:
- Packed:元素会被压缩到一起
- Spread:如上所示,元素被分布到可用空间上
- Spread inside: 与spread类似,但链的端点不会分散。
确保以spread为链的模式,修改方法有两种:
- 视图将以实力屏幕截图中的图标间隔显示
- 在其中一个图标的xml中将会出现app:layout_constraintHorizontal_chainStyle="spread",更新该属性,可以将链的模式改变成其他的。
对齐Views
接着,再次选中三个图标,从tool bar中,选择Align -> Vertical Centers.Android Studio会添加约束用来使每一个view的底部和顶部与相邻的view对齐。
布局看起来如下:
此时三个图标的xml如下所示:
如果你的界面跟图片不匹配,检查Text和Design面板,就重新再做一次。
对齐每个图标的文本
接着设置图标下面的文字,将第一个Text的左侧的约束连接到第一个图标的左侧,右侧的约束连接到右侧,上面的约束添加到上面,其他三个Text做同样的操作。
然后将工具栏中的默认边距更改为15dp,只需要将顶部锚点拖动到图标的底部锚点,即可在一个步骤中设置约束和边距。
现在上面两行的约束错误已经消失了,XML中图标和标签的代码如下:
Using Guidelines
在最终的布局中,双箭头图像应居中并且与两个绿色视图重叠
设置水平和垂直的Guidelines
选中双箭头图标并且设置宽高为60dp,然后在该图标上点击右键,选择Center -> Horizontally in Parent.
为每一个绿色的TextView设置宽为124dp,高为98dp。
确保双箭头图片在两个绿色的TextView之上,将左侧TextView的右侧约束到双箭头图片的右侧,并将右边距设置为40dp。
同样的将右侧绿色的TextView的左侧约束到双箭头图标的左侧,并将左侧边距设置为40dp。
最后,将两个TextView的上下分别约束到双箭头图标的上下。
[图片上传失败...(image-5a6638-1550557283059)]
最后,点击Guidelines,选择Add Horizontal Guideline
将会添加一条水平的虚线到布局中。
在Conponent Tree中选中水平guideline,在Attributes 检查器中,改变ID为guideline1,注意guideline属性:layout_constraintGuide_begin 和 layout_constraintGuide_percent
对于水平的guideline1,设置layout_constraintGuide_begin为200dp
最后,添加一个竖直的guideline,设置id为guideline2并且设置layout_constraintGuide_percent为0.05,这将guideline2定位到距离屏幕左侧为屏幕宽度5%的位置。
定位Guidelines
定位guideline使用下面三个属性:
- layout_constraintGuide_begin: 从左侧或其父级的顶部定位具有指定dp的guideline
- layout_constraintGuide_end: 从右侧或其父级底部定位指定dp的guideline
- layout_constraintGuide_percent: 使用百分比来定位guideline
添加约束到Guidelines
现在guidelines已经设置了,可以给他们添加一些约束。
首先,对于双箭头图标:
- 将底部约束到水平guideline
- 设置底部边距为40dp
对于开关:
- 设置宽度为160dp
- 设置左侧约束到垂直guideline
- 设置top约束到父布局
- 设置top margin为200dp
对于开关下面的标签:
- 设置左边的约束到垂直guideline
- 设置顶部约束到开关的底部
对于galaxy icon
- 设置宽高为90dp
- 设置top约束到水平guideline
- 约束bottom到父布局的底部,这样就会在水平guideline和底部之间居中
- 在父视图中将其水平居中
对于rocket icon:
- 设置宽和高为30dp
- 约束rocket icon的上下和右到galaxy图标的上下和左边
最后,对于DEPART按钮:
- 将宽度从wrap_content修改为match_parent
- 将底部约束到parent的底部
此时,你已经设置完了所有的约束,在Component Tree中也没有任何错误了,布局此时看起来如下所示:
圆形位置约束
除了上面的,还可以使用距离和角度来约束UI元素。允许你将他们防止到一个圆上,其中一个元素位于圆的中心,另一个元素位于圆周上。
选择rocket icon,并在代码视图中更新其代码,代码如下:
第一个约束属性layout_constraintCircle指示将位于圆心的UI元素的ID,另外两个属性表示角度和半径。
让UI元素在屏幕上动起来
约束集
使用ConstraintLayout,你可以设置帧动画从而是你的views动起来,为此,你需要提供布局文件的副本,称为ConstraintSet,ConstraintSet只需要包含给定ConstraintLayout中元素的约束,边距以及填充。
如果你使用的是kotlin代码,那么你可以直接将ConstraintSet应用到你的ConstraintLayout。
要构建动画,你需要指定单个布局文件和ConstraintSet作为起始和结束关键帧,你也可以应用过渡是动画更有趣。
设置动画的起始布局
在项目中复制布局文件并命名为keyframe1.xml,并将此布局设置为应用程序的起始布局。
打开keyframe1.xml,将guideline1的layout_constraintGuide_begin属性值从200dp改为0dp,这样会移动guideline,限制在guideline中的元素,将会移除屏幕
接着将guideline2的layout_constraintGuide_percent属性值从0.05修改成1,这会将指南移动到屏幕最右侧,从而受其约束的元素被移动到屏幕外。
接着修改MainActivity中的setContentView中的R.layout.activity_main为R.layout.keyframe1
动画视图
将MainActivity中的:
import kotlinx.android.synthetic.main.activity_main.*
改为:
import kotlinx.android.synthetic.main.keyframe1.*
可以让你直接饮用UI中的id,而不用使用findViewById(),
接着,添加如下的代码:
private val constraintSet1 = ConstraintSet()
private val constraintSet2 = ConstraintSet()
private var isOffscreen = true
Transition Manager
可以使用Transition Manager类来处理从一个keyframe到另一个的过渡,创建一个布局动画,你只需要向Transition Manager提供要设置动画的ConstraintSet,他将会处理其余的部分。
将如下的代码添加到onCreate方法中:
constraintSet1.clone(constraintLayout) //1
constraintSet2.clone(this, R.layout.activity_main) //2
departButton.setOnClickListener { //3
//apply the transition
TransitionManager.beginDelayedTransition(constraintLayout) //4
val constraint = if (!isOffscreen) constraintSet1 else constraintSet2
isOffscreen = !isOffscreen
constraint.applyTo(constraintLayout) //5
}
动画视图的界限
不仅可以通过影响其约束来更改屏幕上元素的位置,还可以改变其大小。
打开keyframe1.xml选择galaxy icon,id为galaxyIcon,将高度从90dp改为10dp。
接着运行app可以看到大小的改变。
使用自定义过渡使动画更简单
创建一个自定义动画来替代默认的动画,可以自定义动画的时长。
添加如下方法到MainActivity中。
override fun onEnterAnimationComplete() {
super.onEnterAnimationComplete()
constraintSet2.clone(this, R.layout.activity_main)
val transition = AutoTransition()
transition.duration = 1000
TransitionManager.beginDelayedTransition(constraintLayout,transition)
constraintSet2.applyTo(constraintLayout)
}
- 动画执行过程中,Activity无法绘制任何内容,onEnterAnimationComplete()方法表示动画执行完成,可以调用绘制代码。
- 会将布局信息从最终布局拉入constraintSet2
- 创建一个自定义过渡,使用AutoTransition,首先淡出要消失的目标,然后移动并调整现有目标的大小,最后淡出出现的目标。
- 动画执行时长为1000毫秒
- 调用Transition Manager的beginDelayedTransition方法,但这次提供的是自定义过渡
- 应用一个新的ConstraintSet到当前消失的ConstraintLayout上。
效果如下:
使圆形约束动起来
要在火星周围制作火箭动画,必须改变两个属性:圆形约束的角度,他将火箭的位置移动到圆周,以及火箭的旋转来完成动画,你还可以检查单向/往返开关值以确定火箭是飞行半圈还是一整圈。
替换DEPART button的点击事件的代码为如下的代码:
departButton.setOnClickListener {
// TransitionManager.beginDelayedTransition(constraintLayout)
// val constraint = if (!isOffscreen) constraintSet1 else constraintSet2
// isOffscreen = !isOffscreen
// constraint.applyTo(constraintLayout)
val layoutParams = rocketIcon.layoutParams as ConstraintLayout.LayoutParams
val startAngle = layoutParams.circleAngle
val endAngle = startAngle + (if (switch1.isChecked) 360 else 180)
val anim = ValueAnimator.ofFloat(startAngle,endAngle)
anim.addUpdateListener { valueAnimator ->
val animatedValue = valueAnimator.animatedValue as Float
val layoutParams = rocketIcon.layoutParams as ConstraintLayout.LayoutParams
layoutParams.circleAngle = animatedValue
rocketIcon.layoutParams = layoutParams
rocketIcon.rotation = (animatedValue % 360 - 270)
}
anim.duration = if(switch1.isChecked) 2000 else 1000
anim.interpolator = LinearInterpolator()
anim.start()
}
- 在动画开始之前,将火箭的startAngle设置为火箭的当前角度,依赖one way/ Round Trip切换,endAngle在startAngle的之上添加180或360
- 使用startAngle和endAngle创建一个ValueAnimator
- 在动画监听器中,获取动画值并将其设置给rocket的layoutParams中的circleAngle属性
- 用动画值旋转火箭,将使火箭飞的更加自然。
- 单向动画需要一秒,而往返动画需要2秒
- 使用LinearInterpolator,可以试试AnticipateOvershootInterpolator看看会发生什么!
最后
如果你比较喜欢view动画,那么可以使用MotionEvent尝试更多的动画,https://youtu.be/S3FeIRKu_Z8?t=1275
这篇文章内容真的挺丰富的,写的很不错,所以就简单翻译了一下,如果有朋友觉得哪些翻译的不够好的,欢迎给我评论,我会及时纠正。