FloatingActionButton浮动按钮动画效果实现

我们想实现一个跟知呼功能类似的悬浮按钮,点击后会展开相应的菜单。

1.FloatingActionButton

我们先来看一张图片,认识一下什么是 FloatingActionButton :

FloatingActionButton浮动按钮动画效果实现 - 阿里云

FloatingActionButton 一般浮现在右下角,是 Material Design 的一个控件。

使用 Android Studio 创建的新工程中,可以引入该控件:

compile ‘com.android.support:design:26.+’

然后在布局文件中使用该控件:

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@android:drawable/ic_dialog_email" />

该控件是自带阴影效果的。

我们想实现下面的这样一个功能:

FloatingActionButton浮动按钮动画效果实现 - 阿里云

点击按钮,然后展开菜单。

2.展开菜单实现思路:

1.将要显示的菜单按钮都放在与Fab同一位置,然后设置为 INVISIBLE 不可见。

2.点击 Fab 的时候,将菜单按钮设置为可见,并且动过动画平移到各个位置。

3.在此点击 Fab 或者点击菜单之后,将菜单折叠回来,并设置为不可见。

3.展开菜单实现代码:

1.新建一个类: FloatingActionButtonContainerView ,继承 FrameLayout

我们先定义一些成员变量(下面的代码遇到不懂的再回来看看):

private final static int INIT_SIZE = 5; /*默认的容器中的FloatingActionButton的数量*/
private static final int DO_ROTATE = 1;//旋转动画
private static final int RECOVER_ROTATE = -1;//恢复旋转之前的状态
private static final int UNFOLDING = 2;//菜单展开状态
private static final int FOLDING = 3;//菜单折叠状态
private int mWidth = 400;//viewGroup的宽
private int mHeight = 620;//ViewGroup的高
private int length = 200;//子view展开的距离
private int flag = FOLDING;//菜单展开与折叠的状态
private float mScale = 0.8f;//展开之后的缩放比例
private int mDuration = 400;//动画时长
private FloatingActionButton ctrlButton;//在Activity中显示的button

重写 onMeasure ( ) 方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//测量子view的宽高 这是必不可少的不然子view会没有宽高
measureChildren(widthMeasureSpec,heightMeasureSpec);
//设置该viewGroup的宽高
setMeasuredDimension(mWidth,mHeight);
}

重写 onLayout 方法

在这个方法中,我们要做的是为子 view 设置布局位置:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutCtrlButton();
layoutExpandChildButton();
}
private void layoutCtrlButton(){
//获取宽高
int width = ctrlButton.getMeasuredWidth();
int height = ctrlButton.getMeasuredHeight();
//1:相对于父布局控件的left
//2:控件的top
//3:右边缘的left
//4:底部的top
//所以后两个直接用left加上宽 以及 top加上height就好
ctrlButton.layout(mWidth – width, (mHeight – height) / 2, mWidth, (mHeight – height) / 2 + height);
}
private void layoutExpandChildButton(){
final int cCount = getChildCount();
final int width = ctrlButton.getMeasuredWidth();
final int height = ctrlButton.getMeasuredHeight();
//设置子view的初始位置与mainButton重合并且设置为不可见
for (int i = 1; i < cCount; i++) {
final View view = getChildAt(i);
view.layout(mWidth – width, (mHeight – height) / 2, mWidth, (mHeight – height) / 2 + height);
view.setVisibility(INVISIBLE);
}
}

在 onLayout ( ) 方法中,我们布置了 ctrlButton 的显示位置,设置在右边缘的中部。

ctrlButton 的点击事件:

/**
* 设置控制按钮的点击事件
*
* @param view
*/
private void setCtrlButtonListener(final View view) {
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (flag == FOLDING) {//折叠状态
final int cCount = FloatingActionButtonContainerView.this.getChildCount();
for (int i = 1; i < cCount; i++) {
View view = getChildAt(i);
view.setVisibility(VISIBLE);
//开始平移第一个参数是view 第二个是角度
setTranslation(view, 180 / (cCount – 2) * (i – 1));
}
flag = UNFOLDING;//展开状态
//开始旋转
setRotateAnimation(view, DO_ROTATE);
} else {
setBackTranslation();
flag = FOLDING;
//开始反向旋转 恢复原来的样子
setRotateAnimation(view, RECOVER_ROTATE);
}
}
});
}

我们设置一个 flag 来表示菜单的折叠状态,然后点击 ctrlButton 的时候做出相应的动画(展开菜单或者折叠菜单)。

平移动画:

我们这里使用的属性动画,也比较简单,大家可以学习学习属性动画。

public void setTranslation(View view,int angle){
int x= (int) (length*Math.sin(Math.toRadians(angle)));
int y = (int) (length*Math.cos(Math.toRadians(angle)));
ObjectAnimator tX = ObjectAnimator.ofFloat(view,"translationX",-x);
ObjectAnimator tY = ObjectAnimator.ofFloat(view,"translationY",-y);
ObjectAnimator alpha= ObjectAnimator.ofFloat(view,"alpha",1);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view,"scaleX",mScale);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view,"scaleY",mScale);
AnimatorSet set = new AnimatorSet();
set.play(tX).with(tY).with(alpha);
set.play(scaleX).with(scaleY).with(tX);
set.setDuration(mDuration);
set.setInterpolator(new AccelerateDecelerateInterpolator());
set.start();
}

为了理解这个动画,我们要结合一张图: FloatingActionButton浮动按钮动画效果实现 - 阿里云

通过 length 与 angle 计算出子 view 的位置,然后通过动画属性进行设置 x 与 y 的偏移量就好。

这样就可以实现点击 ctrlButton 然后展开菜单了。

折叠动画:
private void setBackTranslation(){
int cCount =getChildCount();
for (int i = 1; i < cCount; i++) {
final View view = getChildAt(i);
ObjectAnimator tX = ObjectAnimator.ofFloat(view,"translationX",0);
ObjectAnimator tY = ObjectAnimator.ofFloat(view,"translationY",0);
ObjectAnimator alpha= ObjectAnimator.ofFloat(view,"alpha",0);//透明度 0为完全透明
AnimatorSet set = new AnimatorSet(); //动画集合
set.play(tX).with(tY).with(alpha);
set.setDuration(mDuration); //持续时间
set.setInterpolator(new AccelerateDecelerateInterpolator());
set.start();
//动画完成后 设置为不可见
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(INVISIBLE);
}
});
}
}

旋转动画:
public void setRotateAnimation(View view,int flag){
ObjectAnimator rotate = null;
if(flag==DO_ROTATE)
rotate = ObjectAnimator.ofFloat(view,"rotation",135);
else rotate = ObjectAnimator.ofFloat(view,"rotation",0);
rotate.setDuration(mDuration);
rotate.start();
}

缩放动画:
/**
* 展开动画
* @param view
* @param angle
*/
public void setTranslation(View view,int angle){
int x= (int) (length*Math.sin(Math.toRadians(angle)));
int y = (int) (length*Math.cos(Math.toRadians(angle)));
Log.d("ICE","angle"+angle +"y:"+y);
ObjectAnimator tX = ObjectAnimator.ofFloat(view,"translationX",-x);
ObjectAnimator tY = ObjectAnimator.ofFloat(view,"translationY",-y);
ObjectAnimator alpha= ObjectAnimator.ofFloat(view,"alpha",1);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view,"scaleX",mScale);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view,"scaleY",mScale);
AnimatorSet set = new AnimatorSet();
set.play(tX).with(tY).with(alpha);
set.play(scaleX).with(scaleY).with(tX);
set.setDuration(mDuration);
set.setInterpolator(new AccelerateDecelerateInterpolator());
set.start();
}

点击子 view 执行子 view 的点击事件,并且折叠菜单。

/**
* 执行child的点击事件
*/
private void setChildButtonListener(final View view) {
//设置点击时候执行点击事件并且缩回原来的位置
view.setOnTouchListener(new OnTouchListener() {
int x,y;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
x = (int) event.getX();
y = (int) event.getY();
break;
case MotionEvent.ACTION_UP:
if((int)event.getX() == x &;&; (int)event.getY()==y){
//如果手指点击时 与抬起时的x y 坐标相等 那么我们认为手指点了该view
setBackTranslation();//折叠菜单
setRotateAnimation(ctrlButton,RECOVER_ROTATE); //旋转mainButton
flag = UNFOLDING;//设置为展开状态
//执行该view的点击事件
view.callOnClick();
}
break;
}
return true;
}
});
}

我们不能够重写子 view 的 onClickListener ( ) ,因为我们可以在 Activity 中写点击事件,如果在这里写了,就会覆盖点击事件了。所以我们用触摸监听来间接实现。

注意,这部分并不是必须这样写,有很多变通的方式,不必拘泥在这样的实现上。

完整的代码如下:

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
/**
* Created by longsky on 17-8-7.
*/
public class FloatingActionButtonContainerView extends FrameLayout {
private final static int INIT_SIZE = 5; /*默认的容器中的FloatingActionButton的数量*/
private static final int DO_ROTATE = 1;//旋转动画
private static final int RECOVER_ROTATE = -1;//恢复旋转之前的状态
private static final int UNFOLDING = 2;//菜单展开状态
private static final int FOLDING = 3;//菜单折叠状态
private int mWidth = 400;//viewGroup的宽
private int mHeight = 620;//ViewGroup的高
private int length = 200;//子view展开的距离
private int flag = FOLDING;//菜单展开与折叠的状态
private float mScale = 0.8f;//展开之后的缩放比例
private int mDuration = 400;//动画时长
private FloatingActionButton ctrlButton;//在Activity中显示的button
public FloatingActionButtonContainerView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.initContainerView();
}
public FloatingActionButtonContainerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.initContainerView();
}
private void initContainerView(){
this.removeAllViews();
setupCtrlButton();
setContainerSize(INIT_SIZE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//测量子view的宽高 这是必不可少的 不然子view会没有宽高
measureChildren(widthMeasureSpec, heightMeasureSpec);
//设置该viewGroup的宽高
setMeasuredDimension(mWidth, mHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutCtrlButton();
layoutExpandChildButton();
}
private void layoutCtrlButton(){
//获取宽高
int width = ctrlButton.getMeasuredWidth();
int height = ctrlButton.getMeasuredHeight();
//1:相对于父布局控件的left
//2:控件的top
//3:右边缘的left
//4:底部的top
//所以后两个直接用left加上宽 以及 top加上height就好
ctrlButton.layout(mWidth – width, (mHeight – height) / 2, mWidth, (mHeight – height) / 2 + height);
}
private void layoutExpandChildButton(){
final int cCount = getChildCount();
final int width = ctrlButton.getMeasuredWidth();
final int height = ctrlButton.getMeasuredHeight();
//设置子view的初始位置与mainButton重合并且设置为不可见
for (int i = 1; i < cCount; i++) {
final View view = getChildAt(i);
view.layout(mWidth – width, (mHeight – height) / 2, mWidth, (mHeight – height) / 2 + height);
view.setVisibility(INVISIBLE);
}
}
private void setupCtrlButton(){
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
ctrlButton = new FloatingActionButton(this.getContext());
ctrlButton.setImageResource(android.R.drawable.ic_input_add);
//设置主按钮的点击事件
setCtrlButtonListener(ctrlButton);
this.addView(ctrlButton, lp);
}
public void setContainerSize(int size) {
boolean reqLayout = false;
final int childCount = this.getChildCount();
final int expandChild = childCount – 1;
if (size > expandChild) {
for (int i = 0; i < (size – expandChild); i++) {
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
FloatingActionButton btnFloatingAction = new FloatingActionButton(this.getContext());
setChildButtonListener(btnFloatingAction);
btnFloatingAction.setVisibility(View.INVISIBLE);
btnFloatingAction.setImageResource(android.R.drawable.ic_delete);
this.addView(btnFloatingAction, lp);
reqLayout = true;
}
} else if (size < expandChild) {
if (size < 0) {
size = 0;
}
for (int i = 0; i < (expandChild – size); i++) {
this.removeViewAt(this.getChildCount() – 1);
reqLayout = true;
}
}
if (reqLayout) {
this.requestLayout();
}
}
/**
* 设置主按钮的点击事件
*
* @param view
*/
private void setCtrlButtonListener(final View view) {
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (flag == FOLDING) {//折叠状态
final int cCount = FloatingActionButtonContainerView.this.getChildCount();
for (int i = 1; i < cCount; i++) {
View view = getChildAt(i);
view.setVisibility(VISIBLE);
//开始平移第一个参数是view 第二个是角度
setTranslation(view, 180 / (cCount – 2) * (i – 1));
}
flag = UNFOLDING;//展开状态
//开始旋转
setRotateAnimation(view, DO_ROTATE);
} else {
setBackTranslation();
flag = FOLDING;
//开始反向旋转 恢复原来的样子
setRotateAnimation(view, RECOVER_ROTATE);
}
}
});
}
public void setTranslation(View view,int angle){
int x= (int) (length*Math.sin(Math.toRadians(angle)));
int y = (int) (length*Math.cos(Math.toRadians(angle)));
ObjectAnimator tX = ObjectAnimator.ofFloat(view,"translationX",-x);
ObjectAnimator tY = ObjectAnimator.ofFloat(view,"translationY",-y);
ObjectAnimator alpha= ObjectAnimator.ofFloat(view,"alpha",1);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view,"scaleX",mScale);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view,"scaleY",mScale);
AnimatorSet set = new AnimatorSet();
set.play(tX).with(tY).with(alpha);
set.play(scaleX).with(scaleY).with(tX);
set.setDuration(mDuration);
set.setInterpolator(new AccelerateDecelerateInterpolator());
set.start();
}
private void setBackTranslation(){
int cCount =getChildCount();
for (int i = 1; i < cCount; i++) {
final View view = getChildAt(i);
ObjectAnimator tX = ObjectAnimator.ofFloat(view,"translationX",0);
ObjectAnimator tY = ObjectAnimator.ofFloat(view,"translationY",0);
ObjectAnimator alpha= ObjectAnimator.ofFloat(view,"alpha",0);//透明度 0为完全透明
AnimatorSet set = new AnimatorSet(); //动画集合
set.play(tX).with(tY).with(alpha);
set.setDuration(mDuration); //持续时间
set.setInterpolator(new AccelerateDecelerateInterpolator());
set.start();
//动画完成后 设置为不可见
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(INVISIBLE);
}
});
}
}
public void setRotateAnimation(View view,int flag){
ObjectAnimator rotate = null;
if(flag==DO_ROTATE)
rotate = ObjectAnimator.ofFloat(view,"rotation",135);
else rotate = ObjectAnimator.ofFloat(view,"rotation",0);
rotate.setDuration(mDuration);
rotate.start();
}
/**
* 执行child的点击事件
*/
private void setChildButtonListener(final View view) {
//设置点击时候执行点击事件并且缩回原来的位置
view.setOnTouchListener(new OnTouchListener() {
int x,y;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
x = (int) event.getX();
y = (int) event.getY();
break;
case MotionEvent.ACTION_UP:
if((int)event.getX() == x &;&; (int)event.getY()==y){
//如果手指点击时 与抬起时的x y 坐标相等 那么我们认为手指点了该view
setBackTranslation();//折叠菜单
setRotateAnimation(ctrlButton,RECOVER_ROTATE); //旋转mainButton
flag = UNFOLDING;//设置为展开状态
//执行该view的点击事件
view.callOnClick();
}
break;
}
return true;
}
});
}
}

参考链接
仿知乎FloatingActionButton浮动按钮动画效果实现(一)
FloatingActionButton展开按钮