支持类似Path的左下角动画旋转菜单及横向划出菜单、圆心弹出菜单
项目地址:https://github.com/daCapricorn/ArcMenu
一、关注3个效果
- 点击中心控制点 的时候,展开效果:
- 中心控制点旋转45度的动画
- 周围children 弹出动画
2.点击中心控制点的时候,收缩动画:
- 中心控制点旋转45度
- 周围children 自旋转并收缩
3.展开时候,点击child
- 被点击的child放大,
- 其他chidren 消失
二、3个java文件
- ArcMenu.java 自定义View ,效果如图;主要有逻辑控制代码
- ArcLayout.java A Layout that arranges its children around its center.主要有子view布局,展开、收缩的动画实现
- RotateAndTranslateAnimation.java 动画:控制objec产生位移,同时以object中心为轴心旋转。
三、展开和收缩代码实现
- 点击中心控制点的时候,展开效果:
- 中心控制点旋转45度的动画
- 周围children 弹出动画
2.点击中心控制点的时候,收缩动画:
- 中心控制点旋转45度
- 周围children 自旋转并收缩
先介绍三个重要的变量
private ArcLayout mArcLayout; //中心控制点图片
private ImageView mHintView; //当前状态: 是否已经展开;就像逻辑控制的开关一样,根据不同状态执行不同的动画效果
private boolean mExpanded = false;
ArcMenu.java中有这部分效果,实现代码
private void init(Context context) { LayoutInflater li = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); li.inflate(R.layout.arc_menu, this); mArcLayout = (ArcLayout) findViewById(R.id.item_layout); //中心控制点 final ViewGroup controlLayout = (ViewGroup) findViewById(R.id.control_hint); controlLayout.setClickable(true); controlLayout.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { //中心控制点自旋转 mHintView.startAnimation(createHintSwitchAnimation(mArcLayout.isExpanded())); //child 的收缩和展开 mArcLayout.switchState(true); } return false; } }); mHintView = (ImageView) findViewById(R.id.control_hint); }
(1)、controlLayout 是一个ViewGroup,xml文件如下:
<FrameLayout android:id="@+id/control_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:background="@drawable/composer_button" > <ImageView android:id="@+id/control_hint" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:duplicateParentState="true" android:src="@drawable/composer_icn_plus" /> </FrameLayout>
图片资源composer_button和composer_icn_plus 是xml文件, composer_button.xml放在drawable下,
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/composer_button_pressed" android:state_pressed="true"/> <item android:drawable="@drawable/composer_button_normal"/> </selector>
控件被按下时候,改变图片。
(2)
中心控制点旋转45度的实现:给controlLayout设置监听器OnTouchListener,点击的时候,就触发旋转动画
/** * 中心控制自旋转动画 * @param expanded 状态:是否已经展开 * @return */ private static Animation createHintSwitchAnimation(final boolean expanded) { //绕中心旋转,收缩时候由45度转到0度,展开时由0度转到45度 Animation animation = new RotateAnimation(expanded ? 45 : 0, expanded ? 0 : 45, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setStartOffset(0); animation.setDuration(100); animation.setInterpolator(new DecelerateInterpolator()); animation.setFillAfter(true); return animation; }
(3)
child展开、收缩动画实现
逻辑控制:
给controlLayout设置监听器OnTouchListener,调用ArcLayout中switchState()函数;这个函数就是个逻辑开关,重要的布局动画都在bindChildAnimation中;它还有个重要功能是修改mExpanded的状态值。
/** * switch between expansion and shrinkage * * @param showAnimation */ public void switchState(final boolean showAnimation) { if (showAnimation) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { bindChildAnimation(getChildAt(i), i, 300); } } mExpanded = !mExpanded; if (!showAnimation) { requestLayout(); } invalidate(); }
布局实现:
private void bindChildAnimation(final View child, final int index, final long duration) { final boolean expanded = mExpanded; final int centerX = getWidth() / 2; //ViewGroup的中心X坐标 final int centerY = getHeight() / 2;//ViewGroup的中心Y坐标 final int radius = expanded ? 0 : mRadius; final int childCount = getChildCount(); final float perDegrees = (mToDegrees - mFromDegrees) / (childCount - 1); Rect frame = computeChildFrame(centerX, centerY, radius, mFromDegrees + index * perDegrees, mChildSize); final int toXDelta = frame.left - child.getLeft(); //展开或收缩动画,child沿X轴位移距离 final int toYDelta = frame.top - child.getTop(); //展开或收缩动画,child沿Y轴位移距离 Log.d(TAG, "toXDelta:"+toXDelta +"\t"+"toYDelta"+toYDelta); Interpolator interpolator = mExpanded ? new AccelerateInterpolator() : new OvershootInterpolator(1.5f); final long startOffset = computeStartOffset(childCount, mExpanded, index, 0.1f, duration, interpolator); //mExpanded为true,收缩动画;为false,展开动画 Animation animation = mExpanded ? createShrinkAnimation(0, toXDelta, 0, toYDelta, startOffset, duration, interpolator) : createExpandAnimation(0, toXDelta, 0, toYDelta, startOffset, duration, interpolator); final boolean isLast = getTransformedIndex(expanded, childCount, index) == childCount - 1; animation.setAnimationListener(new AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (isLast) { postDelayed(new Runnable() { @Override public void run() { onAllAnimationsEnd(); } }, 0); } } }); child.setAnimation(animation); } private static Rect computeChildFrame(final int centerX, final int centerY, final int radius, final float degrees, final int size) { //child的中心点 final double childCenterX = centerX + radius * Math.cos(Math.toRadians(degrees)); final double childCenterY = centerY + radius * Math.sin(Math.toRadians(degrees)); return new Rect((int) (childCenterX - size / 2), (int) (childCenterY - size / 2), (int) (childCenterX + size / 2), (int) (childCenterY + size / 2)); }
final int radius = expanded ? 0 : mRadius;
child尚未展开,expanded值为false,radius的值为mRadius。
中央控制点的坐标是ViewGroup的中心坐标(centerX,centerY),child的坐标分情况,
当child即将展开时候,radius的值为mRadius,
centerX + radius * Math.cos(Math.toRadians(degrees));
centerY + radius * Math.sin(Math.toRadians(degrees));
这个数学公式几何意义如图:已知圆上一点的角度,圆心坐标,半径,求圆上某点坐标。蓝色标出的角度值为degrees,radius就是半径。
同理,当child即将收缩时候,radius值为0,child的中心就是ViewGroup的中心坐标(centerX,centerY)
然后我们就可以根据child的中心点,和size,计算出child所占矩阵左上角(childCenterX - size / 2,childCenterY - size / 2)和右下角坐标(childCenterX + size / 2,childCenterY + size / 2)
最后计算出收缩和展开的时候,child沿X、Y轴的位移
展开动画代码:
private static Animation createExpandAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta, long startOffset, long duration, Interpolator interpolator) { Animation animation = new RotateAndTranslateAnimation(0, toXDelta, 0, toYDelta, 0, 720); animation.setStartOffset(startOffset); animation.setDuration(duration); animation.setInterpolator(interpolator); animation.setFillAfter(true); return animation; }
收缩动画代码:
private static Animation createShrinkAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta, long startOffset, long duration, Interpolator interpolator) { AnimationSet animationSet = new AnimationSet(false); animationSet.setFillAfter(true); //收缩过程中,child 逆时针自旋转360度 final long preDuration = duration / 2; Animation rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); rotateAnimation.setStartOffset(startOffset); rotateAnimation.setDuration(preDuration); rotateAnimation.setInterpolator(new LinearInterpolator()); rotateAnimation.setFillAfter(true); animationSet.addAnimation(rotateAnimation); //收缩过程中位移,并逆时针旋转360度 Animation translateAnimation = new RotateAndTranslateAnimation(0, toXDelta, 0, toYDelta, 360, 720); translateAnimation.setStartOffset(startOffset + preDuration); translateAnimation.setDuration(duration - preDuration); translateAnimation.setInterpolator(interpolator); translateAnimation.setFillAfter(true); animationSet.addAnimation(translateAnimation); return animationSet; }
可以看到,收缩动画集合中,多了一个RotateAnimation自旋转动画。同时有RotateAndTranslateAnimation 对象。
四、点击监听器
展开时候,点击child
- 被点击的child放大,
- 其他chidren 消失
5. * 增加item;此函数供外部调用 6. * @param item 7. * @param listener 8. */ 9. public void addItem(View item, OnClickListener listener) { 10. mArcLayout.addView(item); 11. item.setOnClickListener(getItemClickListener(listener)); 12. } 13. 14. private OnClickListener getItemClickListener(final OnClickListener listener) { 15. return new OnClickListener() { 16. 17. @Override 18. public void onClick(final View viewClicked) { 19. //点击,child放大,其他child消失 20. Animation animation = bindItemAnimation(viewClicked, true, 400); 21. animation.setAnimationListener(new AnimationListener() { 22. 23. @Override 24. public void onAnimationStart(Animation animation) { 25. 26. } 27. 28. @Override 29. public void onAnimationRepeat(Animation animation) { 30. 31. } 32. 33. @Override 34. public void onAnimationEnd(Animation animation) { 35. postDelayed(new Runnable() { 36. 37. @Override 38. public void run() { 39. itemDidDisappear(); 40. } 41. }, 0); 42. } 43. }); 44. //child 被点击的时候,自身放大,其他child消失的效果 45. final int itemCount = mArcLayout.getChildCount(); 46. for (int i = 0; i < itemCount; i++) { 47. View item = mArcLayout.getChildAt(i); 48. if (viewClicked != item) { 49. bindItemAnimation(item, false, 300); 50. } 51. } 52. //中心控制点动画 53. mArcLayout.invalidate(); 54. mHintView.startAnimation(createHintSwitchAnimation(true)); 55. 56. if (listener != null) { 57. listener.onClick(viewClicked); 58. } 59. } 60. }; 61. } 62. 63. private Animation bindItemAnimation(final View child, final boolean isClicked, final long duration) { 64. Animation animation = createItemDisapperAnimation(duration, isClicked); 65. child.setAnimation(animation); 66. 67. return animation; 68. } 69. 70. private void itemDidDisappear() { 71. final int itemCount = mArcLayout.getChildCount(); 72. for (int i = 0; i < itemCount; i++) { 73. View item = mArcLayout.getChildAt(i); 74. item.clearAnimation(); 75. } 76. //参数为false,函数只修改mExpanded的值 77. mArcLayout.switchState(false); 78. } 79. 80. private static Animation createItemDisapperAnimation(final long duration, final boolean isClicked) { 81. AnimationSet animationSet = new AnimationSet(true); 82. //放大缩小动画,isClicked 为true,放大两倍;为false,缩小至0 83. animationSet.addAnimation(new ScaleAnimation(1.0f, isClicked ? 2.0f : 0.0f, 1.0f, isClicked ? 2.0f : 0.0f, 84. Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)); 85. //Alpha改为0,child消失 86. animationSet.addAnimation(new AlphaAnimation(1.0f, 0.0f)); 87. 88. animationSet.setDuration(duration); 89. animationSet.setInterpolator(new DecelerateInterpolator()); 90. animationSet.setFillAfter(true); 91. 92. return animationSet; 93. }
被点击的child放大,主要由ScaleAnimation 动画实现,从1.0f 放大到2.0f.
不被点击的child ,ScaleAnimation动画,从1.0s缩小到0;放大的轴点都是child中心。