基于 ConstraintLayout + CircularPositioning(圆形定位) 的 FloatingActionButton(浮动按钮) 点赞+编辑+返回顶部的弹窗效果

前言

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 属性进行比较:
ConstraintLayout vs 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宽度类型设置,可以设置 percentspreadwrap
layout_constraintHeight_default高度类型设置,同上
layout_constraintWidth_percent如果 layout_constraintWidth_percent 设置的百分比,这里设置小数,为占父布局宽度的多少
layout_constraintHeight_percent设置高度的大小,同上

4、Ratio(比例)

控件的宽和高设置一定比例

属性介绍
layout_constraintDimensionRatio宽高比

5、Chain Style(约束链类型)

设置约束链类型,约束链类型包括:spreadspread_insidepacked

属性介绍
layout_constraintHorizontal_chainStyle横向约束链
layout_constraintVertical_chainStyle纵向约束链

三 实战

实战部分主要讲解一下 ConstraintLayoutCircular positioning(圆形定位)功能。

1、什么是Circular positioning呢?
之所以称之为圆形定位,它就是以目标控件为圆心,通过设置角度和半径确定我们当前控件的位置,如官方图:
Circular Positioning
2、目标

我们先来看一下效果:
效果图

3、设置布局

布局的xml文件比较长,内容其实很简单,主要是四个 FloatingActionButton 和三个 GroupGroup 在的 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 (传送门)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值