我们想实现一个跟知呼功能类似的悬浮按钮,点击后会展开相应的菜单。
1.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" />
该控件是自带阴影效果的。
我们想实现下面的这样一个功能:
点击按钮,然后展开菜单。
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(); }
为了理解这个动画,我们要结合一张图:
通过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; } }); } }
参考链接