概述
现在很多App会在入口比较浅的页面添加一些快捷操作入口,一方面是为了方便用户操作,一方面是为了提高产品一些关键入口的使用率,让用户能够在浏览信息流的过程中能快速切换至其他一些功能页面。例如豆瓣的首页 (右下角红框选中部分):
豆瓣菜单
本文将仿照这种菜单效果进行实现,最终效果如下:
弧形菜单效果图
需要定制的特性
1.菜单展开半径
2.设置菜单主按钮Icon
3.设置菜单子项的各个Icon
4.展开和收缩的动画时长
5.所有菜单按钮的宽高
6.是否在展开收缩的同时旋转主按钮
注:理论上可以设置无数个菜单项,但是会出现重叠情况(空间有限),这种情况得自行调整按钮数量和宽高。
实现思路
可以看到这个菜单是由多个按钮组合而成,所以可以考虑用ViewGroup来作为载体,其中的子View再通过属性动画进行配合达成效果,而各个菜单项的弹出角度可以针对90°来进行弧度平分,再通过三角函数得到最终展开的目标坐标,关键要注意View的宽高边距的计算,否则可能会出现超出边界的情况。
1)初始化基本框架
由于菜单是由多个按钮叠加在一个平面,所以可以考虑采用继承FrameLayout,然后根据设置的Icon资源Id的数量来作为按钮的数量进行初始化,代码如下:
List<ImageView> mImgViews = new ArrayList<>(); List<Integer> mMenuItemResIds = new ArrayList<>(); /** * 初始化主按钮 * @param context */ private void initMenuView(Context context) { mMenuIv = new ImageView(context); mMenuIv.setImageResource(mMenuResId); FrameLayout.LayoutParams params = new LayoutParams(mMenuWidth, mMenuWidth); params.bottomMargin = mMenuItemWidth / 2; params.rightMargin = mMenuItemWidth / 2; params.gravity = Gravity.BOTTOM | Gravity.RIGHT; addView(mMenuIv, params); mMenuIv.setOnClickListener(this); } /** * 初始化菜单子项按钮 * @param context */ private void initMenuItemViews(Context context) { mImgViews.clear(); for (int index = 0; index < mMenuItemResIds.size(); index++) { ImageView menuItem = new ImageView(context); menuItem.setImageResource(mMenuItemResIds.get(index)); FrameLayout.LayoutParams params = new LayoutParams(mMenuItemWidth, mMenuItemWidth); params.bottomMargin = mMenuItemWidth / 2; params.rightMargin = mMenuItemWidth / 2; params.gravity = Gravity.BOTTOM | Gravity.RIGHT; menuItem.setTag(index); menuItem.setOnClickListener(this); addView(menuItem, params); menuItem.setScaleX(0f); menuItem.setScaleY(0f); mImgViews.add(menuItem); } }
可以看到都设置在了父容器的右下角,且设置了margin值,这是由于我们点击菜单瞬间有放大双倍的效果,所以这里需要为其边缘腾出一点空间,否则处于边缘的菜单项放大时,会有部分被切掉影响观感,记得为每个子项设置Tag(这里设置为下标),后面触发点击事件时会用到。
2)展开菜单
前面说过了,主要是根据平分弧度的思路来计算,从效果图中可以看出,我们的整个展开角度是90°,那么每个菜单项的角度应该是90°/(菜单的数量-1),计算出这个角度有什么作用呢?可以先通过下图帮忙理解:
弧形菜单弹出距离计算示意图
可以看到,要做弹出动画,就需要计算出弹出的横向距离和纵向距离,刚才计算出来的角度在这就派上用场啦,利用三角函数可以得到:
tranX = 弹出半径*sin(90 * i / (count - 1));
tranY = 弹出半径*cos(90 * i / (count - 1));
再结合透明度和大小的变化,代码如下:
/** * 菜单展开动画 */ private void startOpenAnim() { int count = mMenuItemResIds.size(); List<Animator> animators = new ArrayList<>(); for (int i = 0; i < count; i++) { int tranX = -(int) (mRadius * Math.sin(Math.toRadians(90 * i / (count - 1)))); int tranY = -(int) (mRadius * Math.cos(Math.toRadians(90 * i / (count - 1)))); ObjectAnimator animatorX = ObjectAnimator.ofFloat(mImgViews.get(i), "translationX", 0f, tranX); ObjectAnimator animatorY = ObjectAnimator.ofFloat(mImgViews.get(i), "translationY", 0f, tranY); ObjectAnimator alpha = ObjectAnimator.ofFloat(mImgViews.get(i), "alpha", 0, 1); ObjectAnimator scaleX = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleX", 0.1f, 1); ObjectAnimator scaleY = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleY", 0.1f, 1); animators.add(animatorX); animators.add(animatorY); animators.add(alpha); animators.add(scaleX); animators.add(scaleY); } AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(mDuration); animatorSet.playTogether(animators); animatorSet.start(); }
3)收回菜单
上一步已经理解了如何展开菜单,回收菜单自然就容易多了,没错,就是反其道而行之:
/** * 菜单收回动画 */ private void startCloseAnim() { int count = mMenuItemResIds.size(); List<Animator> animators = new ArrayList<>(); for (int i = 0; i < count; i++) { int tranX = -(int) (mRadius * Math.sin(Math.toRadians(90 * i / (count - 1)))); int tranY = -(int) (mRadius * Math.cos(Math.toRadians(90 * i / (count - 1)))); ObjectAnimator animatorX = ObjectAnimator.ofFloat(mImgViews.get(i), "translationX", tranX, 0f); ObjectAnimator animatorY = ObjectAnimator.ofFloat(mImgViews.get(i), "translationY", tranY, 0f); ObjectAnimator alpha = ObjectAnimator.ofFloat(mImgViews.get(i), "alpha", 1, 0); ObjectAnimator scaleX = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleX", 1, 0.3f); ObjectAnimator scaleY = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleY", 1, 0.3f); animators.add(animatorX); animators.add(animatorY); animators.add(alpha); animators.add(scaleX); animators.add(scaleY); } AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(mDuration); animatorSet.playTogether(animators); animatorSet.start(); }
其实主要就是在做位移动画的时候,从tranX和tranY位移到0,回到原来的位置。
4)菜单子项点击动画
以上完成了菜单的展开和收缩,基本的模样已经出来了,还可以为其子项添加一些点击效果,让整个View更为生动,代码如下:
/** * 菜单子项点击动画 * * @param index 子项下标 */ private void startClickItemAnim(int index) { int count = mMenuItemResIds.size(); List<Animator> animators = new ArrayList<>(); //当前被点击按钮放大且逐渐变透明,造成消散效果 ObjectAnimator clickItemAlpha = ObjectAnimator.ofFloat(mImgViews.get(index), "alpha", 1, 0); ObjectAnimator clickItemScaleX = ObjectAnimator.ofFloat(mImgViews.get(index), "scaleX", 1, 2); ObjectAnimator clickItemScaleY = ObjectAnimator.ofFloat(mImgViews.get(index), "scaleY", 1, 2); animators.add(clickItemAlpha); animators.add(clickItemScaleX); animators.add(clickItemScaleY); for (int i = 0; i < count; i++) { if (index == i) { //过滤当前被点击的子项 continue; } //其他选项缩小且变透明 ObjectAnimator alpha = ObjectAnimator.ofFloat(mImgViews.get(i), "alpha", 1, 0); ObjectAnimator scaleX = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleX", 1, 0.1f); ObjectAnimator scaleY = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleY", 1, 0.1f); animators.add(alpha); animators.add(scaleX); animators.add(scaleY); } AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(500); animatorSet.playTogether(animators); animatorSet.start(); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { //点击动画结束之后要将所有子项归位 resetItems(); } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); }
首先传进来一个index参数,其实就是之前我们在初始化的时候为每个子View设置的Tag,在每次onClick的时候,通过 view.getTag()
获取到对应的下标,传进来之后,循环遍历所有子View,根据这个下标来判断当前点击的是哪个菜单项,将其做放大消散的动画效果,其他菜单项则单纯消散即可。
并且这里注意,要在动画结束时,将所有子项设置回展开之前的位置,否则当再次点击菜单按钮时,菜单项会在圆弧上闪现了一下,体验很差,因此要在onAnimationEnd的回调中重置所有子项,重置代码如下:
/** * 重置所有子项位置 */ private void resetItems() { int count = mImgViews.size(); for (int i = 0; i < mImgViews.size(); i++) { int tranX = (int) (mRadius * Math.sin(Math.toRadians(90 * i / (count - 1)))); int tranY = (int) (mRadius * Math.cos(Math.toRadians(90 * i / (count - 1)))); mImgViews.get(i).setTranslationX(tranX); mImgViews.get(i).setTranslationY(tranY); } mIsOpen = false; }
5)旋转主菜单按钮
我们还可以在展开收缩的同时,还可以为菜单按钮添加上一些花样,将其旋转一下,使整个动画更加自然:
/** * 旋转主菜单按钮 * * @param startAngel 起始角度 * @param endAngel 结束角度 */ private void rotateMenu(int startAngel, int endAngel) { if (!mCanRotate) { return; } ObjectAnimator clickItemAlpha = ObjectAnimator.ofFloat(mMenuIv, "rotation", startAngel, endAngel); clickItemAlpha.setDuration(mDuration); clickItemAlpha.start(); }
6)添加外部点击监听
提供一个供外界设置banner数据的方法:
ClickMenuListener mItemListener; public void setClickItemListener(ClickMenuListener mItemListener) { this.mItemListener = mItemListener; } public interface ClickMenuListener { void clickMenuItem(int resId); } @Override public void onClick(View view) { if (view == mMenuIv) { ... } else { ... if (mItemListener != null && index < mMenuItemResIds.size()) { mItemListener.clickMenuItem(mMenuItemResIds.get(index)); } } }
就是正常的暴露接口,将菜单对应的资源id传出去,供外界判断点击的是哪个菜单项。
应用
xml布局中引用(这里的宽高由设置的弧长半径决定,只需设置wrap_conetnt即可):
<com.zjywidget.widget.arcmenu.YArcMenuView android:id="@+id/arc_menu" android:layout_width="match_parent" android:layout_height="wrap_content" app:spread_radius="150dp" app:duration="1000" app:menu_width="64dp" app:menu_item_width="64dp" app:can_rotate="true" app:layout_constraintRight_toRightOf="parent" app:layout_constraintBottom_toBottomOf="parent" />
Acitivity中实例代码如下:
mArcMenuView = findViewById(R.id.arc_menu); List<Integer> menuItems = new ArrayList<>(); menuItems.add(R.drawable.ic_menu_camera); menuItems.add(R.drawable.ic_menu_photo); menuItems.add(R.drawable.ic_menu_share); mArcMenuView.setMenuItems(menuItems); mArcMenuView.setClickItemListener(new YArcMenuView.ClickMenuListener() { @Override public void clickMenuItem(int resId) { switch (resId){ case R.drawable.ic_menu_camera: Toast.makeText(getApplicationContext(), "点击了相机", Toast.LENGTH_SHORT).show(); break; case R.drawable.ic_menu_photo: Toast.makeText(getApplicationContext(), "点击了相册", Toast.LENGTH_SHORT).show(); break; case R.drawable.ic_menu_share: Toast.makeText(getApplicationContext(), "点击了分享", Toast.LENGTH_SHORT).show(); break; } } });
作者:Android小Y
链接:https://www.jianshu.com/p/220da4460e5d