下角动画旋转菜单、圆心弹出菜单ArcMenu 源码解析

支持类似Path的左下角动画旋转菜单及横向划出菜单、圆心弹出菜单

项目地址:https://github.com/daCapricorn/ArcMenu

 

一、关注3个效果

  1. 点击中心控制点 的时候,展开效果:
  • 中心控制点旋转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中心为轴心旋转。

 

三、展开和收缩代码实现

  1. 点击中心控制点的时候,展开效果:
  • 中心控制点旋转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);
    }
View Code

(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>
View Code

图片资源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>
View Code

控件被按下时候,改变图片。

 

(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;
}
View Code

(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();
    }
View Code

布局实现:

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));
    }
View Code

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;
    }
View Code

收缩动画代码:

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;
}
View Code

可以看到,收缩动画集合中,多了一个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.        }
View Code

被点击的child放大,主要由ScaleAnimation 动画实现,从1.0f 放大到2.0f.

不被点击的child ,ScaleAnimation动画,从1.0s缩小到0;放大的轴点都是child中心。

 

posted @ 2014-12-09 15:39  sue_zheng  Views(1088)  Comments(0Edit  收藏  举报