首页 > 代码库 > 下角动画旋转菜单、圆心弹出菜单ArcMenu 源码解析
下角动画旋转菜单、圆心弹出菜单ArcMenu 源码解析
支持类似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 item7. * @param listener8. */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. @Override18. public void onClick(final View viewClicked) {19. //点击,child放大,其他child消失20. Animation animation = bindItemAnimation(viewClicked, true, 400);21. animation.setAnimationListener(new AnimationListener() {22. 23. @Override24. public void onAnimationStart(Animation animation) {25. 26. }27. 28. @Override29. public void onAnimationRepeat(Animation animation) {30. 31. }32. 33. @Override34. public void onAnimationEnd(Animation animation) {35. postDelayed(new Runnable() {36. 37. @Override38. 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,缩小至083. 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中心。
下角动画旋转菜单、圆心弹出菜单ArcMenu 源码解析