最近手头有一个需求,要实现一个悬浮的按钮位于界面的右下角,但是距边框还有点距离,点击这个悬浮按钮可以打开扇形菜单,并且悬浮按钮也要更换成另一个图片,先看一下效果图吧
最后实现的效果大概就是这个样子的,下面不再解说这个界面的整体实现,只说一下这个点击悬浮按钮弹出来这个卫星扇形菜单按钮的实现,下面说几个思路:
1、看到这样的效果,手动入手肯定是不划算的,时间划不来,首先就是上www.baidu.com、github找类似效果的源码
2、找到这种类似的效果图以后可能不是自己需要的最终效果,可能还需要修改其中的某些动画等等,这些都需要自己去手动实现更改,满足当前项目的需求
那么下面步入正题,当我看到这个效果的时候,在脑海里分为几步走:
1、主菜单按钮需要悬浮
2、主菜单按钮需要触发点击事件,并且需要旋转切换成另一张图片
3、三个子按钮需要同时向外弹出、并且附加点击事件
4、再次点击主按钮、子按钮需要同时收回
5、子菜单弹出来以后整个界面的背景需要半透明,且禁止操作
6、主按钮和子按钮在一个group组内,主按钮永远显示,子按钮在弹出的时候显示,收回的时候隐藏
好了,需求也就这么多,开始撸代码了,先来瞄一下xml布局,上面这个RelativiLayout是一个自定义的layout,为的是实现弹出子菜单后整个节面只能子菜单和主菜单能操作,禁止触摸这个layout以下的控件,默认是gone状态,主菜单和子菜单是在下面那个SrcMenu布局,这个是核心,自定义的卫星菜单栏是主要部分,下面可以看下代码,继承了ViewGroup
<com.android.infantschool.ui.view.ForbidRelativeLayout
android:id="@+id/rl_shan"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/title"
android:background="@drawable/bg_rl_shan_open"//自定义的shape,下面给出
android:visibility="gone" />
<com.android.infantschool.ui.view.SrcMenu xmlns:srcmenu="http://schemas.android.com/apk/res-auto"
android:id="@+id/src_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
srcmenu:position="right_bottom"
srcmenu:radius="150dp">
<ImageView
android:layout_width="55dp"
android:layout_height="55dp"
android:src="@mipmap/icon_dynamic_video"//子菜单1图片
android:tag="Video" />
<ImageView
android:layout_width="55dp"
android:layout_height="55dp"
android:src="@mipmap/icon_dynamic_camera"//子菜单2图片
android:tag="Camera" />
<ImageView
android:layout_width="55dp"
android:layout_height="55dp"
android:src="@mipmap/icon_dynamic_text"//子菜单3图片
android:tag="Text" />
<ImageView
android:id="@+id/btn_plus"
android:layout_width="55dp"
android:layout_height="55dp"
android:src="@mipmap/icon_dynamic_edit"//主菜单图片
android:stateListAnimator="@null" />
</com.android.infantschool.ui.view.SrcMenu>
自定义RelativiLayout
public class ForbidRelativeLayout extends RelativeLayout {
public ForbidRelativeLayout(Context context) {
super(context);
}
public ForbidRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
/**
* 添加事件拦截,以免用户在banner广告中随意切换广告
*
* @param event
* @return true:拦截,false:不拦截
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
return true;
}
/**
* 添加事件拦截,以免用户在banner广告中随意切换广告
*
* @param event
* @return true:拦截,false:不拦截
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
return true;
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
}
}
自定义菜单栏
public class SrcMenu extends ViewGroup implements View.OnClickListener {
private static final int POS_LEFT_TOP = 0;
private static final int POS_LEFT_BOTTOM = 1;
private static final int POS_RIGHT_TOP = 2;
private static final int POS_RIGHT_BOTTOM = 3;
private Position mPosition = Position.RIGHT_BOTTOM;//菜单位置
private int mRadius;//菜单半径
public Status mStatus = Status.CLOSE;//菜单开闭状态
public View mCButton;//菜单主按钮
private OnMenuItemClickListener mOnMenuItemClickListener;//菜单点击事件的成员变量
private OnMainClickListener mainClickListener;//主菜单事件
/**
* 菜单状态枚举类
*/
public enum Status {
OPEN, CLOSE
}
/**
* 菜单位置枚举类
*/
private enum Position {
LEFT_TOP, LEFT_BOTTOM, RIGHT_TOP, RIGHT_BOTTOM
}
/**
* 菜单按钮点击事件回调接口
*/
public interface OnMenuItemClickListener {
void onClick(View view, int position);
}
/**
* 菜单按钮点击事件回调接口
*/
public interface OnMainClickListener {
void onClick();
}
public SrcMenu(Context context) {
this(context, null);
}
public SrcMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SrcMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_FRACTION, 60,
getResources().getDisplayMetrics());//设置菜单半径默认值
//获取自定义属性值
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SrcMenu, defStyleAttr, 0);
//获取位置值
int pos = array.getInt(R.styleable.SrcMenu_position, POS_RIGHT_BOTTOM);
switch (pos) {
case POS_LEFT_TOP:
mPosition = Position.LEFT_TOP;
break;
case POS_LEFT_BOTTOM:
mPosition = Position.LEFT_BOTTOM;
break;
case POS_RIGHT_TOP:
mPosition = Position.RIGHT_TOP;
break;
case POS_RIGHT_BOTTOM:
mPosition = Position.RIGHT_BOTTOM;
break;
default:
break;
}
//获取菜单半径值
mRadius = (int) array.getDimension(R.styleable.SrcMenu_radius, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_FRACTION, 60, getResources().getDisplayMetrics())) / 5 * 4;
array.recycle();
}
/**
* 设置菜单点击事件
*/
public void setOnMenuItemClickListener(OnMenuItemClickListener onMenuItemClickListener) {
mOnMenuItemClickListener = onMenuItemClickListener;
}
private int type;
public void setMainOnClickLister(OnMainClickListener mainOnClickLister) {
this.mainClickListener = mainOnClickLister;
}
/**
* 测量模式+测量值
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
//测量 child
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
if (b) {
layoutCButton();
int count = getChildCount();
for (int j = 0; j < count - 1; j++) {
View child = getChildAt(j);
//开始时设置子菜单不可见
child.setVisibility(GONE);
//默认按钮位于左上时
int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * j));
int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * j));
int cWidth = child.getMeasuredWidth();
int cHeight = child.getMeasuredHeight();
//按钮位于左下、右下时
if (mPosition == Position.LEFT_BOTTOM || mPosition == Position.RIGHT_BOTTOM) {
ct = getMeasuredHeight() - cHeight - ct;
}
//按钮位于右上、右下时
if (mPosition == Position.RIGHT_TOP || mPosition == Position.RIGHT_BOTTOM) {
cl = getMeasuredWidth() - cWidth - cl;
}
child.layout(cl - 30, ct - 50, cl + cWidth - 30, ct + cHeight - 50);
}
}
}
/**
* 定位主菜单按钮
*/
private void layoutCButton() {
mCButton = getChildAt(3);
/*mCButton.bringToFront();*/
mCButton.setOnClickListener(this);
int l = 0;
int t = 0;
int width = mCButton.getMeasuredWidth();
int height = mCButton.getMeasuredHeight();
//设置按钮显示的位置
switch (mPosition) {
case LEFT_BOTTOM:
l = 0;
t = getMeasuredHeight() - height;
break;
case LEFT_TOP:
l = 0;
t = 0;
break;
case RIGHT_TOP:
l = getMeasuredWidth() - width;
t = 0;
break;
case RIGHT_BOTTOM:
l = getMeasuredWidth() - width;
t = getMeasuredHeight() - height;
break;
default:
break;
}
mCButton.layout(l - 30, t - 50, l + width - 30, t + height - 50);
}
@Override
public void onClick(View view) {//主按钮点击事件
if (isOpen()) {//打开
rotateCButton(view, -90, 0, 200);
} else {
rotateCButton(view, 0, -90, 200);
}
toggleMenu();
if (mainClickListener != null) {
mainClickListener.onClick();
}
}
/**
* 切换菜单
*/
public void toggleMenu() {
int count = getChildCount();
for (int i = 0; i < count - 1; i++) {
final View childView = getChildAt(i);
childView.setVisibility(VISIBLE);
int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));
int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));
//根据菜单所处位置的不同,设置不同的参数值
int xFlag = 1;
int yFlag = 1;
if (mPosition == Position.LEFT_TOP || mPosition == Position.LEFT_BOTTOM) {
xFlag = -1;
}
if (mPosition == Position.LEFT_TOP || mPosition == Position.RIGHT_TOP) {
yFlag = -1;
}
//平移动画
AnimationSet animationSet = new AnimationSet(true);
Animation animation;
if (mStatus == Status.CLOSE) {
//打开按钮的动画
animation = new TranslateAnimation(xFlag * cl - 00, -00, yFlag * ct - 00, -00);
childView.setFocusable(true);
childView.setClickable(true);
animation.setDuration(300);
} else {
//关闭按钮的动画 xFlag * cl yFlag * ct
animation = new TranslateAnimation(-00, xFlag * cl - 00, -00, yFlag * ct - 00);
childView.setFocusable(false);
childView.setClickable(false);
animation.setDuration(300);
}
animation.setFillAfter(true);
animation.setStartOffset((i * 100) / count);
//监听动画状态
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (mStatus == Status.CLOSE) {
childView.setVisibility(GONE);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
//旋转动画
RotateAnimation rotateAnimation = new RotateAnimation(360, 0,//控制菜单收回的时候选择角度
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setFillAfter(true);
rotateAnimation.setDuration(200);
//添加动画
animationSet.addAnimation(rotateAnimation);
animationSet.addAnimation(animation);
childView.startAnimation(animationSet);
//为子菜单项添加点击事件
final int pos = i + 1;
childView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (mOnMenuItemClickListener != null) {
mOnMenuItemClickListener.onClick(childView, pos);
menuItemAnim(pos - 1);
changeStatus();
rotateCButton(mCButton, -90, 0, 200);
}
}
});
}
changeStatus();
}
/**
* 点击子菜单项的动画
*/
private void menuItemAnim(int pos) {
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i);
if (i == pos) {
childView.startAnimation(scaleBigAnim(200));
} else {
childView.startAnimation(scaleSmallAnim(200));
}
//设置子菜单隐藏
childView.setClickable(false);
childView.setFocusable(false);
}
}
/**
* 使菜单项变小并消失的动画
*/
private Animation scaleSmallAnim(int duration) {
AnimationSet animationSet = new AnimationSet(true);
ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.0f);
animationSet.addAnimation(scaleAnimation);
animationSet.addAnimation(alphaAnimation);
animationSet.setDuration(duration);
animationSet.setFillAfter(true);
return animationSet;
}
/**
* 使菜单项变大,透明度降低的动画
*/
private Animation scaleBigAnim(int duration) {
AnimationSet animationSet = new AnimationSet(true);
ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 4.0f, 1.0f, 4.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.0f);
animationSet.addAnimation(scaleAnimation);
animationSet.addAnimation(alphaAnimation);
animationSet.setDuration(duration);
animationSet.setFillAfter(true);
return animationSet;
}
/**
* 改变菜单状态值
*/
private void changeStatus() {
mStatus = (mStatus == Status.CLOSE ? Status.OPEN : Status.CLOSE);
}
/**
* 判断菜单是否打开
*/
public boolean isOpen() {
return mStatus == Status.OPEN;
}
/**
* 主按钮旋转动画
*/
public void rotateCButton(View view, float start, float end, int duration) {
RotateAnimation animation = new RotateAnimation(start, end, Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(duration);
animation.setFillAfter(true);
view.startAnimation(animation);
}
}
没错,代码就这么多,自定义的view里面注释还是写的还是比较清楚的,哪里不知道的可以给我留言,也可以根据自己的需求更改动画之类的,下面再来看一下在activity或者fragment里面如何使用:
srcMenu.setOnMenuItemClickListener(new SrcMenu.OnMenuItemClickListener() {
@Override
public void onClick(View view, int position) {//子菜单点击事件
imgPlus.setImageResource(R.mipmap.icon_dynamic_edit);
fadeOut(rlShan);
if (position == 1) {
startVideo();
}
if (position == 2) {
startCameta();
}
if (position == 3) {
startActivity(ReleaseDynamicActivity.newIntent(activity, list));
}
}
});
srcMenu.setMainOnClickLister(new SrcMenu.OnMainClickListener() {
@Override
public void onClick() {//主菜单点击事件
if (srcMenu.isOpen()) {//打开
imgPlus.setImageResource(R.mipmap.icon_dynamic_close);
fadeIn(rlShan);
} else {//关闭
imgPlus.setImageResource(R.mipmap.icon_dynamic_edit);
fadeOut(rlShan);
}
}
});
到这里这个卫星扇形弹出式菜单就完成了里面有用到某些文件,下面附加粘贴上
1、子菜单弹出后背景需要从显示渐变到阴影,收回同理
public static void fadeIn(View view, float startAlpha, float endAlpha, long duration) {
if (view.getVisibility() == View.VISIBLE) return;
view.setVisibility(View.VISIBLE);
Animation animation = new AlphaAnimation(startAlpha, endAlpha);
animation.setDuration(duration);
view.startAnimation(animation);
}
/**
* 隐藏到显示渐变
*
* @param view
*/
public static void fadeIn(View view) {
fadeIn(view, 0F, 1F, 500);
view.setEnabled(true);
}
/**
* 显示到隐藏渐变
*
* @param view
*/
public static void fadeOut(View view) {
if (view.getVisibility() != View.VISIBLE) return;
// Since the button is still clickable before fade-out animation
// ends, we disable the button first to block click.
view.setEnabled(false);
Animation animation = new AlphaAnimation(1F, 0F);
animation.setDuration(500);
view.startAnimation(animation);
view.setVisibility(View.GONE);
}
2、drawable下的自定义半透明背景 bg_rl_shan_open.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#80000000" />
</shape>
致辞结束!