星球旋转菜单

今天偶尔看到鸿洋博客实现建行的圆形菜单,效果看起来还不错。
原文在这里实现建行圆形菜单
公司正好需要做一个 星球旋转的菜单,于是就在基础上修改了一下,先看效果图

静态图是这样的,公司的网不允许上传视频,只能传个截图了看看效果了。

1.看下简单的使用

MainActivity

package com.safewaychina.circlemenulayout;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private OvalMenuLayout mOvalMenuLayout;
  
    private static int[] imageIds = {
            R.mipmap.home_star_practice,
            R.mipmap.home_star_extension,
            R.mipmap.home_star_assessment,
            R.mipmap.home_star_learning
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        mOvalMenuLayout = (OvalMenuLayout) findViewById(R.id.id_menulayout);
        OvalMenuAdapter menuAdapter = new OvalMenuAdapter(imageIds);

        menuAdapter.setOnItemClickListener(new OvalMenuAdapter.OnMenuItemClickListener() {
            @Override
            public void itemClick(View view, int position) {
                Toast.makeText(MainActivity.this, imageIds[position], Toast.LENGTH_LONG).show();
            }

        });
        mOvalMenuLayout.setMenuAdapter(menuAdapter);
    }
}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:clipChildren="false"
    >

    <com.safewaychina.circlemenulayout.OvalMenuLayout
        android:id="@+id/id_menulayout"
        android:layout_width="400dp"
        android:layout_height="300dp"
        android:clickable="true"
        android:focusable="true"
        android:layout_centerInParent="true"
        android:clipChildren="false"
        >
    </com.safewaychina.circlemenulayout.OvalMenuLayout>

</RelativeLayout>

1.主活动中,我们主要调用OvalMenuLayout中的setMenuAdapter()方法,看一下干了什么。


    /**
     * 菜单的个数
     */
    private int mMenuItemCount;
    public void setMenuAdapter(AbstractMenuAdapter menuAdapter) {
        this.mMenuAdapter = menuAdapter;
        if (menuAdapter != null) {
            addMenuItems();
        }
    }

    private void addMenuItems() {
        LayoutInflater mInflater = LayoutInflater.from(getContext());
        mMenuItemCount = mMenuAdapter.getCount();
        /**
         * 根据用户设置的参数,初始化view
         */
        for (int i = 0; i < mMenuItemCount; i++) {
            View v = mMenuAdapter.onCreateView(mInflater, this);

            mMenuAdapter.onViewBinder(v, i);

            // 添加view到容器中
            addView(v);
        }
    }

2.可以看到,先取的adapter中数据,然后循环遍历,通过适配器的onCreateView()方法创建出的视图,将视图addView到OvalMenuLayout中。
视图添加进来了,那OvalMenuLayout怎么把子view进行测量和布局的呢,这就要看我们ViewGroup的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法和onLayout(boolean changed, int l, int t, int r, int b)方法了

    private static final double OVAL_A = 340;

    private static final double OVAL_B = 180;

    private int mRadiusX;

    private int mRadiusY;
	   /**
     * 该容器内child item的默认尺寸
     */
    private static final float RADIO_DEFAULT_CHILD_DIMENSION = 1 / 2f;
    
    /**
     * 该容器的内边距,无视padding属性,如需边距请用该变量
     */
    private static final float RADIO_PADDING_LAYOUT = 1 / 12f;
	 /**
     * 该容器的内边距,无视padding属性,如需边距请用该变量
     */
    private float mPadding;

	/**
     * 每个菜单的间隔角度
     */
    private float angleDelay;

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int resWidth = 0;
        int resHeight = 0;

        /**
         * 根据传入的参数,分别获取测量模式和测量值
         */
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int height = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        /**
         * 如果宽或者高的测量模式非精确值
         */
        if (widthMode != MeasureSpec.EXACTLY
                || heightMode != MeasureSpec.EXACTLY) {
            // 主要设置为背景图的高度
            resWidth = getSuggestedMinimumWidth();
            // 如果未设置背景图片,则设置为屏幕宽高的默认值
            resWidth = resWidth == 0 ? getDefaultWidth() : resWidth;

            resHeight = getSuggestedMinimumHeight();
            // 如果未设置背景图片,则设置为屏幕宽高的默认值
            resHeight = resHeight == 0 ? getDefaultWidth() : resHeight;
        } else {
            // 如果都设置为精确值,则直接取小值;
            resWidth = width;
            resHeight = height;
        }

        setMeasuredDimension(resWidth, resHeight);

        // 获得半径
        mRadiusX = getMeasuredWidth();

        mRadiusY = getMeasuredHeight();

        // menu item数量
        final int count = getChildCount();
        // menu item尺寸
        int childSize = (int) (Math.min(mRadiusX, mRadiusY) * RADIO_DEFAULT_CHILD_DIMENSION);
        // menu item测量模式
        int childMode = MeasureSpec.EXACTLY;

        // 迭代测量
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);

            if (child.getVisibility() == GONE) {
                continue;
            }

            // 计算menu item的尺寸;以及和设置好的模式,去对item进行测量
            int makeMeasureSpec = -1;

            makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,
                    childMode);

            child.measure(makeMeasureSpec, makeMeasureSpec);
        }
        mPadding = RADIO_PADDING_LAYOUT * mRadiusX;
    }
    
  private int getDefaultWidth() {
        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int layoutRadius = Math.min(mRadiusX, mRadiusY);

        // Laying out the child views
        final int childCount = getChildCount();

        int left, top;
        // menu item 的尺寸
        int cWidth = (int) (layoutRadius * RADIO_DEFAULT_CHILD_DIMENSION);

        // 根据menu item的个数,计算角度
        angleDelay = 360 / (childCount == 0 ? -1 : childCount);

        // 遍历去设置menuitem的位置
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);

            if (child.getVisibility() == GONE) {
                continue;
            }

            mStartAngle %= 360;

            // tmp cosa 即menu item中心点的横坐标
            left = (int) (mRadiusX
                    / 2
                    + Math.ceil(getXInOval(mStartAngle)) - 1 / 2f
                    * cWidth);
            // tmp sina 即menu item的纵坐标
            top = (int) (mRadiusY
                    / 2
                    - Math.ceil(getYInOval(mStartAngle)) - 1 / 2f
                    * cWidth);


            child.layout(left, top, left + cWidth, top + cWidth);

            if (mMenuAdapter != null) {
                mMenuAdapter.upDateView(child, mStartAngle, i);
            }
            // 叠加尺寸
            mStartAngle += angleDelay;

        }
    }
      private double getYInOval(double degress) {
        double a = OVAL_A;
        double b = OVAL_B;
        double y = (a * b)
                / (Math.sqrt((Math.pow(b, 2)
                * Math.pow(Math.tan(Math.toRadians(degress)), 2) + Math.pow(a, 2)
        )));
        if (degress > 90 && degress < 270) {
            y = -y;
        }
        return y;
    }

    private double getXInOval(double degress) {
        double a = OVAL_A;
        double b = OVAL_B;
        double x = (a * b) / (Math.sqrt((Math.pow(a, 2)
                / Math.pow(Math.tan(Math.toRadians(degress)), 2) + Math.pow
                (b, 2))));
        if (degress < 360 && degress > 180) {
            x = -x;
        }
        return x;
    }

这里面就是数学的计算了,要计算子view的布局,我们通过角度来确定子view的位置。数学关系:通过计算函数y=cotanx 与椭圆 xx / (aa) + y y/(b b) = 1的交点。**

3.处理手势

在dispatchTouchEvent(MotionEvent event) 中处理手势事件。

 /**
     * 当每秒移动角度达到该值时,认为是快速移动
     */
    private static final int FLINGABLE_VALUE = 300;

    /**
     * 如果移动角度达到该值,则屏蔽点击
     */
    private static final int NOCLICK_VALUE = 3;

    /**
     * 当每秒移动角度达到该值时,认为是快速移动
     */
    private int mFlingableValue = FLINGABLE_VALUE;

  private float mLastX, mLastY;
  private Runnable mRunnable;
  
	@Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                mDownTime = System.currentTimeMillis();
                mTmpAngle = 0;

                removeCallbacks(mRunnable);

                break;
            case MotionEvent.ACTION_MOVE:

                /**
                 * 获得开始的角度
                 */
                float start = getAngle(mLastX, mLastY);
                /**
                 * 获得当前的角度
                 */
                float end = getAngle(x, y);

                // 如果是一、四象限,则直接end-start,角度值都是正值
                if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) {
                    mStartAngle += end - start;
                    mTmpAngle += end - start;
                } else
                // 二、三象限,色角度值是付值
                {
                    mStartAngle += start - end;
                    mTmpAngle += start - end;
                }
                // 重新布局
                requestLayout();

                mLastX = x;
                mLastY = y;

                break;
            case MotionEvent.ACTION_UP:

                // 计算,每秒移动的角度
                float anglePerSecond = mTmpAngle * 1000
                        / (System.currentTimeMillis() - mDownTime);


                if (Math.abs(anglePerSecond) > mFlingableValue) {
                    // // TODO: 2018/9/18  快速滚动 post一个任务,不断减速滚动到指定位置
                    Log.d("LyjLog", "  快速滚动:" + anglePerSecond);
                    post(mRunnable = new AutoFlingRunnable(anglePerSecond));
                    return true;
                } else {
                    Log.d("LyjLog", "  缓慢滚动:" + anglePerSecond);
                    // // TODO: 2018/9/18  缓慢滚动 去自动滚动到指定位置
                    //                    mRunnable = new AutoFlingRunnable(mStartAngle);
                    //                    post(mRunnable);
                    rollToNearPosition(mStartAngle, anglePerSecond > 0);
                }

                // 如果当前旋转角度超过NOCLICK_VALUE屏蔽点击
                if (Math.abs(mTmpAngle) > NOCLICK_VALUE) {
                    return true;
                }

                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }
    
 private float getAngle(float x, float y) {
        double tmpx = x - mRadiusX / 2d;
        double tmpy = y - mRadiusY / 2d;

        return (float) (Math.asin(tmpy / Math.hypot(tmpx, tmpy)) * 180 / Math.PI);
    }
    
 private int getQuadrant(float x, float y) {
        int tmpX = (int) (x - mRadiusX / 2);
        int tmpY = (int) (y - mRadiusY / 2);
        if (tmpX >= 0) {
            return tmpY >= 0 ? 4 : 1;
        } else {
            return tmpY >= 0 ? 3 : 2;
        }
    }
    
 private void rollToNearPosition(double velocity, boolean upOrDown) {

        mStartAngle %= 360;
        Log.d("LyjLog", "  rollToNearPosition: mStartAngle= " + mStartAngle);
        float targetAngle = getNearAngle(velocity, upOrDown);
        ValueAnimator valueAnimator = ValueAnimator.ofFloat((float) mStartAngle, targetAngle);
        valueAnimator.setInterpolator(new DecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float d = (float) animation.getAnimatedValue();
                mStartAngle = d;
                requestLayout();
                Log.d("LyjLog", "  rollToNearPosition: targetAngle= " + mStartAngle);
            }
        });
        valueAnimator.start();
    }

private float getNearAngle(double velocity, boolean upOrDown) {
        velocity %= 360;
        float per = 1 / 3;
        float targetAngle = (float) mStartAngle;
        childPositions = new float[getChildCount()];
        for (int i = 0; i < childPositions.length; i++) {
            childPositions[i] = i * angleDelay;
        }
        if (upOrDown) {
            //向前旋转
            if (velocity > childPositions[childPositions.length - 1] + angleDelay * per) {
                targetAngle = 360;
            } else {
                for (int i = 0; i < childPositions.length; i++) {
                    if (velocity < childPositions[i] + angleDelay * per) {
                        targetAngle = childPositions[i];
                        break;
                    }
                }
            }
        } else {
            //向后旋转
            if (velocity < childPositions[0] + angleDelay * (1 - per)) {
                targetAngle = 0;
            } else {
                for (int i = childPositions.length - 1; i >= 0; i--) {
                    if (velocity > childPositions[i] + angleDelay * (1 - per)) {
                        targetAngle = childPositions[i] + angleDelay;
                        break;
                    }
                }
            }
        }
        return targetAngle;
    }
private class AutoFlingRunnable implements Runnable {
        private double angelPerSecond;
        /**
         * 默认每秒旋转角度
         */
        private final double DEFAULT_ANGLE = 80f;
        /**
         * 刷新间隔,一秒60帧
         */
        private final long DURATION = 1000 / 70;
        /**
         * 加速度
         */
        private final float DECELERATION = 3;


        public AutoFlingRunnable(float velocity) {
            this.angelPerSecond = velocity;

        }

        @Override
        public void run() {

            if (Math.abs(angelPerSecond) < DEFAULT_ANGLE) {
                rollToNearPosition(mStartAngle, angelPerSecond > 0);
                removeCallbacks(mRunnable);
                return;
            } else {
                //当前每秒旋转角度大于默认旋转角度
                mStartAngle += angelPerSecond / DURATION;
                if (angelPerSecond > 0) {
                    angelPerSecond -= DECELERATION;
                } else {
                    angelPerSecond += DECELERATION;
                }
            }
            Log.d("LyjLog", "angelPerSecond:" + angelPerSecond + "  mStartAngle:" + mStartAngle);
            requestLayout();
            postDelayed(this, DURATION);
        }
    }

处理手势的逻辑,
1.快速滑动,不断旋转减速停在离自己最近的位置。
2.缓慢旋转,停留在离自己最近的位置
缓慢旋转通过ValueAnimator来实现,快速滑动使用了Runnable。
如果需要增加公转的功能,修改下Runnable里的逻辑就好了。不断的让他自己重新布局增加mStartAngle就可以实现。

最后在构造方法里面设置几个属性

    public OvalMenuLayout(Context context) {
        this(context, null);
    }

    public OvalMenuLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public OvalMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setPadding(0, 0, 0, 0);
        setClickable(true);
        setClipChildren(false);
    }

基本我们布局和操作都完成了。最后再看看我们实现了AbstractMenuAdapter的
OvalMenuAdapter类里面,简单的适配器。主要确定数据的逻辑处理和创建出每个View。模仿RecyclerView的适配器。

public class OvalMenuAdapter extends AbstractMenuAdapter {


    /**
     * 菜单项的图标
     */
    private int[] mItemImags;


    public OvalMenuAdapter(int[] mItemImags) {
        // 参数检查
        this.mItemImags = mItemImags;
        mItemCount = mItemImags.length;
    }

    @Override
    public int getCount() {
        return mItemCount;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container) {
        View view = inflater.inflate(R.layout.circle_menu_item, container,
                false);
        return view;
    }

    @Override
    public void onViewBinder(View itemView, int position) {

        final int i = position;
        ImageView iv = (ImageView) itemView
                .findViewById(R.id.id_circle_menu_item_image);
        if (iv != null) {
            iv.setImageResource(mItemImags[i]);
            iv.setOnClickListener(new View.OnClickListener() {
                                      @Override
                                      public void onClick(View v) {
                                          if (mOnMenuItemClickListener != null) {
                                              mOnMenuItemClickListener.itemClick(v, i);
                                          }
                                      }
                                  }
            );
        }
    }

    @Override
    public void upDateView(View v, double angle, int position) {
        /**
         * don't  see
         * 0.8*-abs(cos(1/2*(3.1415*(sin(1/2*x)))))+1.2
         */
        float d = (float) (0.8f
                * -Math.abs(Math.cos(0.5f * Math.PI * Math.sin(Math.toRadians(angle) / 2))) + 1.2);

        v.setScaleY(d);
        v.setScaleX(d);
        if (d > 1) {
            d = 1;
        }
        v.setAlpha(d);
    }


    public interface OnMenuItemClickListener {
        void itemClick(View view, int pos);
    }

    private OvalMenuAdapter.OnMenuItemClickListener mOnMenuItemClickListener;

    public void setOnItemClickListener(OvalMenuAdapter.OnMenuItemClickListener l) {
        this.mOnMenuItemClickListener = l;
    }
}

布局文件 circle_menu_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:gravity="center"
              android:orientation="vertical">

    <ImageView
        android:id="@+id/id_circle_menu_item_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="visible"/>

</LinearLayout>

好了,到这里我们的菜单基本上就完成了。基本上就是数学关系的处理,文章就结束了~~

猜你喜欢

转载自blog.csdn.net/lyjSmile/article/details/82777900