前言
ConstraintLayout
是2016年 Google 的I/O大会推出的新型布局----约束布局,话说,今年都2019了,作为一名 Android 开发者还没真正了解过ConstraintLayout
实属惭愧。因此今天就来尝试一下吧。
一 定义
关于这点,我们先看谷歌的官方文档吧:
A
ConstraintLayout
is a ViewGroup which allows you to position and size widgets in a flexible way.
换成中文就是 ConstraintLayout
可以灵活的设置其他控件的大小和位置。为什么说灵活呢?因为它可以不用写代码,使用鼠标操控就可以直接实现我们的界面,关于直接使用鼠标操作界面的方式,这里我就不再赘述了,请移步郭神的 Android新特性介绍,ConstraintLayout 完全解析。
导入库
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
二 属性介绍
这里我们先学习一下属性的使用,从基础属性开始吧。如果我直接讲这个属性有什么用可能不是特别清楚,这里我会将这些基础的属性和同类型的 RelativeLayout
属性进行比较:
对于和 left、right 相似的 start、end 基础的属性,这里不再赘述,大家可以自行查阅。当然了,除了一些基础的属性,
ConstraintLayout
也有自己特有的属性,这里向大家介绍一下常用的属性:
1、bias(偏移量)
长度和高度的偏移量
属性 | 介绍 |
---|---|
layout_constraintHorizontal_bias |
水平方向的偏移量(小数) |
layout_constraintVertical_bias |
竖直方向的偏移量(小数) |
2、 Circular positioning(圆形定位)
以一个控件为圆心设置角度和半径定位
属性 | 介绍 |
---|---|
layout_constraintCircle |
关联另一个控件,将另一个控件放置在自己圆的半径上,会和下面两个属性一起使用 |
layout_constraintCircleRadius |
圆的半径 |
layout_constraintCircleAngle |
圆的角度 |
3、 Percent dimension(百分比布局)
宽高设置百分比长度
属性 | 介绍 |
---|---|
layout_constraintWidth_default |
宽度类型设置,可以设置 percent 、spread 和wrap
|
layout_constraintHeight_default |
高度类型设置,同上 |
layout_constraintWidth_percent |
如果 layout_constraintWidth_percent 设置的百分比,这里设置小数,为占父布局宽度的多少 |
layout_constraintHeight_percent |
设置高度的大小,同上 |
4、Ratio(比例)
控件的宽和高设置一定比例
属性 | 介绍 |
---|---|
layout_constraintDimensionRatio |
宽高比 |
5、Chain Style(约束链类型)
设置约束链类型,约束链类型包括:spread
, spread_inside
和 packed
属性 | 介绍 |
---|---|
layout_constraintHorizontal_chainStyle |
横向约束链 |
layout_constraintVertical_chainStyle |
纵向约束链 |
三 实战
实战部分主要讲解一下 ConstraintLayout
的 Circular positioning(圆形定位)
功能。
1、什么是Circular positioning呢?
之所以称之为圆形定位,它就是以目标控件为圆心,通过设置角度和半径确定我们当前控件的位置,如官方图:
2、目标
我们先来看一下效果:
3、设置布局
布局的xml文件比较长,内容其实很简单,主要是四个 FloatingActionButton
和三个 Group
,Group
在的 ConstraintLayout
中用来统一的控制视图的显示和隐藏,如果只用一个 Group
并不能让我们的控件有序的显示和隐藏,而 FloatingActionButton
由于不能使用 setVisibility
方法,只能使用 Group
管理 FloatingActionButton
的显示和隐藏,因此使用三个 Group
来实现上图三个 FloatingActionButton
有序的显示和隐藏(本来打算使用 FloatingActionButton
代替 ImageView
减少工作量的, FloatingActionButton
导致的问题反而使工作量增加了,哈哈~), activity_main.xml
如下:
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:layout_marginEnd="32dp"
android:backgroundTint="@color/colorAccent"
android:padding="10dp"
android:src="@drawable/ic_fb_add"
app:fabSize="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:pressedTranslationZ="20dp"
app:rippleColor="#1f000000" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_like"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:layout_marginEnd="32dp"
android:visibility="gone"
android:backgroundTint="@color/colorAccent"
android:padding="10dp"
android:src="@drawable/ic_fb_like"
app:fabSize="normal"
app:layout_constraintCircle="@+id/fab_add"
app:layout_constraintCircleRadius="80dp"
app:layout_constraintCircleAngle="270"
app:pressedTranslationZ="20dp"
app:rippleColor="#1f000000" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_write"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:layout_marginEnd="32dp"
android:backgroundTint="@color/colorAccent"
android:padding="10dp"
android:src="@drawable/ic_fb_write"
app:fabSize="normal"
app:layout_constraintCircle="@+id/fab_add"
app:layout_constraintCircleRadius="80dp"
app:layout_constraintCircleAngle="315"
app:pressedTranslationZ="20dp"
app:rippleColor="#1f000000" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:layout_marginEnd="32dp"
android:backgroundTint="@color/colorAccent"
android:padding="10dp"
android:src="@drawable/ic_fb_top"
app:fabSize="normal"
app:layout_constraintCircle="@+id/fab_add"
app:layout_constraintCircleRadius="80dp"
app:layout_constraintCircleAngle="360"
app:pressedTranslationZ="20dp"
app:rippleColor="#1f000000" />
<android.support.constraint.Group
android:id="@+id/gp_like"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="fab_like"/>
<android.support.constraint.Group
android:id="@+id/gp_write"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="fab_write"/>
<android.support.constraint.Group
android:id="@+id/gp_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="fab_top"/>
</android.support.constraint.ConstraintLayout>
4、业务逻辑
首先确定我们需要使用的实例:
private FloatingActionButton mAdd;
private FloatingActionButton mLike;
private FloatingActionButton mWrite;
private FloatingActionButton mTop;
private Group likeGroup;
private Group writeGroup;
private Group topGroup;
// 动画集合,用来控制动画的有序播放
private AnimatorSet animatorSet;
// 圆的半径
private int radius;
// FloatingActionButton宽度和高度,宽高一样
private int width;
接着初始化我们的控件,这里的代码比较简单,initListener()
我们放在后面介绍:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_constraint);
initWidget();
initListener();
}
@Override
protected void onResume() {
super.onResume();
// 动态获取FloatingActionButton的宽
mAdd.post(new Runnable() {
@Override
public void run() {
width = mAdd.getMeasuredWidth();
}
});
// 在xml文件里设置的半径
radius = UiUtils.dp2px(this, 80);
}
private void initWidget() {
mAdd = findViewById(R.id.fab_add);
mLike = findViewById(R.id.fab_like);
mTop = findViewById(R.id.fab_top);
mWrite = findViewById(R.id.fab_write);
likeGroup = findViewById(R.id.gp_like);
writeGroup = findViewById(R.id.gp_write);
topGroup = findViewById(R.id.gp_top);
// 将三个弹出的FloatingActionButton隐藏
setViewVisible(false);
}
private void setViewVisible(boolean isShow) {
likeGroup.setVisibility(isShow?View.VISIBLE:View.GONE);
writeGroup.setVisibility(isShow?View.VISIBLE:View.GONE);
topGroup.setVisibility(isShow?View.VISIBLE:View.GONE);
}
我们的重点就在 initListener()
里面,思路就是利用属性动画控制 ConstraintLayout.LayoutParams
,从而控制 Circular positioning
的角度和半径:
private void initListener() {
mAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 播放动画的时候不可以点击
if(animatorSet != null && animatorSet.isRunning())
return;
// 判断播放显示还是隐藏动画
if(likeGroup.getVisibility() != View.VISIBLE) {
animatorSet = new AnimatorSet();
ValueAnimator likeAnimator = getValueAnimator(mLike, false, likeGroup,0);
ValueAnimator writeAnimator = getValueAnimator(mWrite, false, writeGroup,45);
ValueAnimator topAnimator = getValueAnimator(mTop, false, topGroup,90);
animatorSet.playSequentially(likeAnimator, writeAnimator, topAnimator);
animatorSet.start();
}else {
animatorSet = new AnimatorSet();
ValueAnimator likeAnimator = getValueAnimator(mLike, true, likeGroup,0);
ValueAnimator writeAnimator = getValueAnimator(mWrite, true, writeGroup,45);
ValueAnimator topAnimator = getValueAnimator(mTop, true, topGroup,90);
animatorSet.playSequentially(topAnimator, writeAnimator, likeAnimator);
animatorSet.start();
}
}
});
}
/**
* 获取ValueAnimator
*
* @param button FloatingActionButton
* @param reverse 开始还是隐藏
* @param group Group
* @param angle angle 转动的角度
* @return ValueAnimator
*/
private ValueAnimator getValueAnimator(final FloatingActionButton button,
final boolean reverse, final Group group, final int angle) {
ValueAnimator animator;
if (reverse)
animator = ValueAnimator.ofFloat(1, 0);
else
animator = ValueAnimator.ofFloat(0, 1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float v = (float) animation.getAnimatedValue();
ConstraintLayout.LayoutParams params
= (ConstraintLayout.LayoutParams) button.getLayoutParams();
params.circleRadius = (int) (radius * v);
//params.circleAngle = 270f + angle * v;
params.width = (int) (width * v);
params.height = (int) (width * v);
button.setLayoutParams(params);
}
});
animator.addListener(new SimpleAnimation() {
@Override
public void onAnimationStart(Animator animation) {
group.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
if(group == likeGroup && reverse){
setViewVisible(false);
}
}
});
animator.setDuration(300);
animator.setInterpolator(new DecelerateInterpolator());
return animator;
}
abstract class SimpleAnimation implements Animator.AnimatorListener{
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
}
屏幕工具类
/**
* @description: 屏幕的工具类
* @author: HuaiAngg
* @create: 2019-04-15 8:49
*/
public class UiUtils {
public static int dp2px(Context context, float dpValue) {
float scale = context.getResources()
.getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
public static int sp2px(Context context, float spValue) {
float fontScale = context.getResources()
.getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
这样写完效果就出来了:
如果你觉得弹出的曲线不够圆滑,你可以在
getValueAnimator
方法中取消对 //params.circleAngle = 270f + angle * v;
这行的注释,效果就如本章一开始的效果。
总结
本文的思路就是利用属性动画控制ConstraintLayout.LayoutParams,从而控制Circular positioning的角度和半径,内容比较简单,前提是你得掌握属性动画和ConstraintLayout的使用。本人水平有限,难免有误,如有错误,欢迎提出。
代码已上传到 GitHub (传送门)