我们想实现一个跟知呼功能类似的悬浮按钮,点击后会展开相应的菜单。
1.FloatingActionButton
我们先来看一张图片,认识一下什么是FloatingActionButton
:
FloatingActionButton
一般浮现在右下角,是Material Design
的一个控件。
使用Android Studio
创建的新工程中,可以引入该控件:
1 |
compile 'com.android.support:design:26.+' |
然后在布局文件中使用该控件:
1 2 3 4 5 6 7 |
<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
我们先定义一些成员变量(下面的代码遇到不懂的再回来看看):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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()
方法:
1 2 3 4 5 6 7 |
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //测量子view的宽高 这是必不可少的 不然子view会没有宽高 measureChildren(widthMeasureSpec,heightMeasureSpec); //设置该viewGroup的宽高 setMeasuredDimension(mWidth,mHeight); } |
重写onLayout
方法
在这个方法中,我们要做的是为子view
设置布局位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@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
的点击事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** * 设置控制按钮的点击事件 * * @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
的时候做出相应的动画(展开菜单或者折叠菜单)。
平移动画:
我们这里使用的属性动画,也比较简单,大家可以学习学习属性动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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
然后展开菜单了。
折叠动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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); } }); } } |
旋转动画:
1 2 3 4 5 6 7 8 |
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(); } |
缩放动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/** * 展开动画 * @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
的点击事件,并且折叠菜单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/** * 执行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
中写点击事件,如果在这里写了,就会覆盖点击事件了。所以我们用触摸监听来间接实现。
注意,这部分并不是必须这样写,有很多变通的方式,不必拘泥在这样的实现上。
完整的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
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; } }); } } |
参考链接