自定义旋转卫星菜单

经常在应用中看到卫星菜单,自己也学着写了一个继承自ViewGroup的CustomMenu的卫星菜单,不同之处是带了旋转,由于使用了属性动画,所以只支持3.0以上,还有就是界面变的难看了,囧~,上图(gif录制不流畅,见谅啊):
这里写图片描述

1. 自定义属性:

为了偷懒,只定义两了两个属性,分别表示子菜单的大小和中心那个显示和隐藏按钮的大小。

<declare-styleable name="CustomMenu">
        <attr name="itemSize" format="dimension"/>
        <attr name="centerSize" format="dimension"/>
</declare-styleable>

2、 定义菜单的布局文件和中心按钮的布局文件:

子菜单布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="3dp"
    android:clickable="true"
    android:background="@drawable/item_select"
    >
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher"
        android:id="@+id/item_iv"
        />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="测试"
        android:layout_marginTop="2dp"
        android:id="@+id/item_tv"
        />
</LinearLayout>

中心按钮布局

<?xml version="1.0" encoding="utf-8"?>
<TextView     xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/tv_center"
    android:background="@drawable/item_select"
    android:gravity="center"/>

这两个布局没什么好说的,背景是一个shape,很简单,就不贴代码了。

3、 构造函数:

  public CustomMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray  typedArray=context.obtainStyledAttributes(attrs,R.styleable.CustomMenu);
        itemSize =  typedArray.getDimension(R.styleable.CustomMenu_itemSize,200f);
        centerSize =  typedArray.getDimension(R.styleable.CustomMenu_centerSize,400f);
        typedArray.recycle();
        init();
    }

很简单就读取了下设置的属性,然后进入init()方法初始化一些变量:

private void init(){
        //忽视Padding
        setPadding(0, 0, 0, 0);
        inflater=LayoutInflater.from(getContext());
        tv_center= (TextView) inflater.inflate(R.layout.menu_center_item_layout,null);
        tv_center.setText("关");
        tv_center.setOnClickListener(new OnCenterViewListenner());
        addView(tv_center);

        scroller=new Scroller(getContext());
        gestureDetector=new GestureDetector(getContext(),new GestureListener());
        animator= ValueAnimator.ofInt(0,1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if(!scroller.isFinished()){
                    scroller.computeScrollOffset();
                    setMenuRotation(scroller.getCurrX());
                }
            }
        });
    }

在init()方法中,首先将padding设置0了,即忽略了所有的Padding,这样有利于后面的Measure和Layout,接下来inflate出了中心的那个按钮,其实就是个TextView并给设置的点击监听之后加入到CustomMenu中成为第一个子View。最后初始化了scroller、gestureDetector、animator这几个变量都是进行动画用的,后面用到。

4、添加子菜单的方法:

  public void addItem(int drawableRes,String text){
        View linearLayout=  inflater.inflate(R.layout.munu_item_layout, null);
        ImageView iv= (ImageView) linearLayout.findViewById(R.id.item_iv );
        iv.setImageResource(drawableRes);
        TextView tv= (TextView) linearLayout.findViewById(R.id.item_tv);
        tv.setText(text);
        final int position=getChildCount()-1;
        linearLayout.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onItemClickListener != null) {
                    onItemClickListener.click(position);
                }
            }
        });
        addView(linearLayout);
    }

添加子菜单方法也很简单,inflate出布局之后,给其中的ImageView和TextView设置相关的值,根据子View的数量设置点击的监听,其中onItemClickListener是自定义的一个接口,代码如下:

  public  interface  OnItemClickListener {
        void click(int position);
    }

5、测量onMeasure

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.i("onMeasure","onMeasure");
        int w=MeasureSpec.getSize(widthMeasureSpec);
        int h=MeasureSpec.getSize(heightMeasureSpec);
        //保证绘制区域为正方形
        if(w>=h){
            setMeasuredDimension(h,h);
            bound=new RectF(0,0,h,h);
            radius =(h- itemSize)/2;
        }else{
            setMeasuredDimension(w,w);
            bound=new RectF(0,0,w,w);
            radius =(w- itemSize)/2;
        }
        //测量子View
        for(int i=0;i<getChildCount();i++){
            View view=getChildAt(i);
            if(view.getId()!=R.id.tv_center) {
                view.measure(MeasureSpec.makeMeasureSpec((int) itemSize, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec((int) itemSize, MeasureSpec.EXACTLY));
            }else{
                //测量中心按钮
                view.measure(MeasureSpec.makeMeasureSpec((int) centerSize, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec((int) centerSize, MeasureSpec.EXACTLY));
            }
        }

   }

在onMeasure中,会使用MeasureSpec.getSize计算建议的宽高,取两者的最小值设置整个控件的宽高,保证整个控件为一个正方形。之后使用MeasureSpec.EXACTLY模式对子View进行测量,其中itemSize和centerSize分别为用户设置的子菜单的大小和中心按钮的大小。

6、布局onLayout

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count=getChildCount();
        float verAngle=360.0f/(count-1);
        View childView;
        for(int i=0;i<count;i++){
            childView= getChildAt(i);
            if(childView.getId()!=R.id.tv_center) {
                RectF rr = getItemLayout(i, verAngle, bound.centerX(), bound.centerY());
                childView.layout((int) rr.left, (int) rr.top, (int) rr.right, (int) rr.bottom);
                childView.setTag(rr);
            }else{
                childView.layout((int)( bound.centerX()- centerSize /2), (int) (bound.centerY()- centerSize /2), (int) (bound.centerX()+ centerSize /2), (int)( bound.centerY()+ centerSize /2));
            }
        }
    }

在onLayout中需要根据子菜单的数量(去掉中心按钮)计算每两个子菜单之间的旋转角度,之后遍历所有的子View,如果碰到的是中心按钮,就使用bound的中心向四个方向扩展半个centerSize作为其layout,其中bound为一个RectF类,记录着整个控件的边界,在上面的onMeasure中计算出的。
如果循环遍历到的是子菜单,就使用getItemLayout计算出子菜单的边界,设置成子菜单的layout,最后将这个边界保存在子菜单的Tag中,用来后期的动画。
getItemLayout方法:

  private RectF getItemLayout(int index,float verAngle,float offsetX,float offsetY){
        float angle=index*verAngle+mRotation;
        float centerX= (float) (radius *Math.cos(Math.toRadians(angle)));
        float centerY= (float) (radius *Math.sin(Math.toRadians(angle)));
        RectF rectF=new RectF(centerX- itemSize /2,centerY- itemSize /2,centerX+ itemSize /2,centerY+ itemSize /2);
        rectF.offset(offsetX,offsetY);
        return rectF;
    }

该方法根据子菜单的序号、每个子菜单相距角度、中心的X,Y坐标、开始角度(mRotation)使用简单的三角函数计算出每个子菜单中心的点的坐标,最后四个方向扩展一个半个itemSize,到这里计算出的Rect都是围绕在控件左上角的(0,0),我们想要这些RectF的围绕在整个控件的中心的,所以需要使用offset将其向右和向下偏移半个控件距离。

到这里整个控件就可以显示了,只不过不可以旋转也没动画。

7、控件的旋转

控件的旋转有两种选择,1)手指在子View上滑动时,不让控件旋转。2)手指在子View上滑动照样能旋转。我选择的第二种,需要复写dispatchTouchEvent方法,如果需要第一种,请复写onTouchEvent方法。

  @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
      super.dispatchTouchEvent(ev);
        return gestureDetector.onTouchEvent(ev);
    }

在dispatchTouchEvent将事件交给gestureDetector,来看下gestureDetector代码:

 class GestureListener extends GestureDetector.SimpleOnGestureListener{
        @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            float scaleScroll=vectorToScalarScroll(distanceX,distanceY,e2.getX()-bound.centerX(),e2.getY()-bound.centerY());
            setMenuRotation((int) (mRotation-scaleScroll/4));
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            scroller.fling(mRotation,
                    0,
                    (int)vectorToScalarScroll(velocityX,velocityY,e2.getX()-bound.centerX(),e2.getY()-bound.centerY())/4,
                           0,
                            Integer.MIN_VALUE,
                           Integer.MAX_VALUE,
                            0,
                            0);
            animator.setDuration(scroller.getDuration());
            animator.start();
            return true;

        }

        @Override
        public boolean onDown(MotionEvent e) {
            if(!scroller.isFinished()){
                if(animator.isRunning()){
                    animator.cancel();
                }
                scroller.forceFinished(true);
            }
            return true;
        }
    }

在onDown方法判断当前是否在自动旋转中,如果在就停止。在onScroll中调用了一个vectorToScalarScroll方法计算旋转角度,这个方法我是从谷歌的例子抄到,使用了操蛋的矢量计算,我就不解释了,因为我自己看的也迷迷糊糊的。而在快速移动时会调用onFling方法,在此方法中使用scroller配合animato进行自动滚动。Scroller会根据传入的mRotation和通过vectorToScalarScroll计算出的加速度来计算需要完成自动旋转的时间和在旋转期间的旋转角度。这时候可以看下在init()方法中对animator的声明啦:

 gestureDetector=new GestureDetector(getContext(),new GestureListener());
        animator= ValueAnimator.ofInt(0,1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if(!scroller.isFinished()){
                    scroller.computeScrollOffset();
                    setMenuRotation(scroller.getCurrX());
                }
            }
        });

animator其实是个ValueAnimator,并对其添加的更新的监听,默认情况下载animator启动之后,每隔10ms调用一次这个监听方法,直到设置的时间(很好的计时器,有木有?)。在更新时,取得scroller中的getCurrX()的值,也是旋转角度,传给setMenuRotation方法:

  public void  setMenuRotation(int angle){
        mRotation=angle%360;
        requestLayout();
    }

很简单,设置下mRotation,请求从新布局。

到这里,控件就可以旋转和自动旋转啦。接下来看下子菜单的隐藏与显示动画:

//显示隐藏菜单
    public void showOrHideMenu( final boolean isShow) {
        AnimatorSet set = new AnimatorSet();
        int childCounts = getChildCount();
        View childView;
        RectF rr;
        if (isShow) {
            for (int i = 1; i < childCounts; i++) {
                childView = getChildAt(i);
                rr = (RectF) childView.getTag();
                //移动
                set.playTogether(ObjectAnimator.ofFloat(childView, View.X, bound.centerX() - itemSize / 2, rr.centerX() - itemSize / 2));
                set.playTogether(ObjectAnimator.ofFloat(childView, View.Y, bound.centerY() - itemSize / 2, rr.centerY() - itemSize / 2));
                //旋转
                set.playTogether(ObjectAnimator.ofFloat(childView, View.ROTATION, 0, 360));
                //透明度
                set.playTogether(ObjectAnimator.ofFloat(childView, View.ALPHA, 0, 1));
            }
        } else{
            for (int i = 1; i < childCounts; i++) {
                childView = getChildAt(i);
                rr = (RectF) childView.getTag();
                //移动
                set.playTogether(ObjectAnimator.ofFloat(childView, View.X, rr.centerX() - itemSize / 2, bound.centerX() - itemSize / 2));
                set.playTogether(ObjectAnimator.ofFloat(childView, View.Y, rr.centerY() - itemSize / 2, bound.centerY() - itemSize / 2));
                //旋转
                set.playTogether(ObjectAnimator.ofFloat(childView, View.ROTATION, 0, 360));
                //透明度
                set.playTogether(ObjectAnimator.ofFloat(childView, View.ALPHA, 1, 0));
            }
        }
        set.setDuration(250);
        set.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                if(isShow) {
                    showOrHideChlidViews(true);
                }
            }
            @Override
            public void onAnimationEnd(Animator animation) {
                if(!isShow) {
                    showOrHideChlidViews(false);
                }
            }
            @Override
            public void onAnimationCancel(Animator animation) {
            }
            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });
        set.start();
    }

有点长,有不少冗余,但很简单,就是对子菜单设置一个AnimatorSet,在AnimatorSet添加三个同时执行的偏移动画、旋转动画、改变透明度动画。如果是要隐藏菜单,就在动画结束时将子菜单设置成GONE,如果是要显示菜单就在动画开始将子菜单设置VISIABLE。其中的showOrHideChlidViews方法就是控制子菜单的GONE和VISIABLE,我就不贴代码。接下来就可以在点击中心按钮时调用这个方法啦,这个也简单,就不贴代码了。可以看我的代码,走你 ——>

我是代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值