基于 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 (传送门)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354