首页 > 代码库 > 下角动画旋转菜单、圆心弹出菜单ArcMenu 源码解析

下角动画旋转菜单、圆心弹出菜单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 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.        }
View Code

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

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

 

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