美文网首页
tabCircleMenu设计

tabCircleMenu设计

作者: king龙123 | 来源:发表于2018-09-12 15:30 被阅读0次

    需求背景

    1、UI:


    图片.png

    2、效果图:


    GIF.gif

    实现分析

    1、首先上面是一个半环形,可先现实一个环形菜单。
    2、需实现增加menu接口addByView,和刷新所有menu接口addByAllView。
    3、应为是环形,只有下半部分可以显示,可根据环形的角度来进行显示控制。
    4、需实现menu点击监听回调,设置选中menu接口。

    代码地址:
    https://github.com/kinglong123/MyCircleMenu

    实现

    环形菜单实现可参考CircleMenu。


    20161029220755275.gif

    分为:
    1.调用方式
    2.此控件onMeasure方法;
    3.onLayout方法的作用;
    4.此控件事件机制dispatchTouchEvent的使用;
    5.数学计算—一个缓冲角度。

    1、调用方式:

            myCircleMenuLayout = (UpCircleMenuLayout) findViewById(R.id.id_mymenulayout);
            myCircleMenuLayout.setMenuItemIconsAndTexts(mItemImgs);//一句设置图片
            myCircleMenuLayout.setOnMenuItemClickListener(new UpCircleMenuLayout.OnMenuItemClickListener() {
    
                @Override
                public void itemClick(int pos) {
                    Toast.makeText(MainActivity.this, mItemTexts[pos],
                            Toast.LENGTH_SHORT).show();
                    switch (pos) {
                        case 0:
                            initFragment1();
                            setTitle("安全中心");
                            break;
                        case 1:
                            initFragment2();
                            setTitle("特色服务");
                            break;
                        case 2:
                            initFragment3();
                            setTitle("投资理财");
                            break;
                        case 3:
                            initFragment4();
                            setTitle("转账汇款");
                            break;
                        case 4:
                            initFragment5();
                            setTitle("我的账户");
                            break;
                        case 5:
                            initFragment1();
                            setTitle("安全中心");
                            break;
                        case 6:
                            initFragment2();
                            setTitle("特色服务");
                            break;
                        case 7:
                            initFragment3();
                            setTitle("投资理财");
                            break;
                        case 8:
                            initFragment4();
                            setTitle("转账汇款");
                            break;
                        case 9:
                            initFragment5();
                            setTitle("我的账户");
                            break;
                    }
                }
    
                @Override
                public void itemCenterClick(View view) {
                    Toast.makeText(MainActivity.this,
                            "you can do something just like ccb  ",
                            Toast.LENGTH_SHORT).show();
                }
            });
    
        }
    

    (2)此控件onMeasure方法讲解:重点讲解迭代测量

    /**
         * 设置布局的宽高,并策略menu item宽高
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int resWidth = 0;
            int resHeight = 0;
            double startAngle = mStartAngle;
    
            double angle = 360 / 10;   //我们传入了10个孩子
            /**
             * 根据传入的参数,分别获取测量模式和测量值
             */
            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 = getDefaultWidth();
    
                resHeight = (int) (resWidth * DEFAULT_BANNER_HEIGTH /
                        DEFAULT_BANNER_WIDTH);
    
            } else {
                // 如果都设置为精确值,则直接取小值;
                resWidth = resHeight = Math.min(width, height);
            }
    
            setMeasuredDimension(resWidth, resHeight);
    
            // 获得直径
            mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight());
    
            // menu item数量
            final int count = getChildCount();
            // menu item尺寸
            int childSize;
    
            // menu item测量模式
            int childMode = MeasureSpec.EXACTLY;
    
            // 迭代测量:根据孩子的数量进行遍历,为每一个孩子测量大小,设置监听回调。
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                startAngle = startAngle % 360;
                if (startAngle > 269 && startAngle < 271 && isTouchUp) {
                    mOnMenuItemClickListener.itemClick(i); //设置监听回调。
                    mCurrentPosition = i;  //本次使用mCurrentPosition,只是把他作为一个temp变量,可以有更多的使用,比如动态设置每个孩子相隔的角度
                    childSize = DensityUtil.dip2px(getContext(), RADIO_TOP_CHILD_DIMENSION);//设置大小
                } else {
                    childSize = DensityUtil.dip2px(getContext(), RADIO_DEFAULT_CHILD_DIMENSION);//设置大小
                }
                if (child.getVisibility() == GONE) {
                    continue;
                }
                // 计算menu item的尺寸;以及和设置好的模式,去对item进行测量
                int makeMeasureSpec = -1;
    
                makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,
                        childMode);
                child.measure(makeMeasureSpec, makeMeasureSpec);
                startAngle += angle;
            }
    //item容器内边距
            mPadding = DensityUtil.dip2px(getContext(), RADIO_MARGIN_LAYOUT);
    
        }
    

    (3)onLayout方法的讲解

    /**
         * 设置menu item的位置
         */
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int layoutRadius = mRadius;
            // Laying out the child views
            final int childCount = getChildCount();
    
            int left, top;
            // menu item 的尺寸
            int cWidth;
    
            // 根据menu item的个数,计算角度
            float angleDelay = 360 / 10;
            // 遍历去设置menuitem的位置
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                  //根据孩子遍历,设置中间顶部那个的大小以及其他图片大小。
                if (mStartAngle > 269 && mStartAngle < 271 && isTouchUp) {
                    cWidth = DensityUtil.dip2px(getContext(), RADIO_TOP_CHILD_DIMENSION);
                    child.setSelected(true);
                } else {
                    cWidth = DensityUtil.dip2px(getContext(), RADIO_DEFAULT_CHILD_DIMENSION);
                    child.setSelected(false);
                }
    
                if (child.getVisibility() == GONE) {
                    continue;
                }
                 //大于360就取余归于小于360度
                mStartAngle = mStartAngle % 360;
    
                float tmp = 0;
                //计算图片布置的中心点的圆半径。就是tmp
                tmp = layoutRadius / 2f - cWidth / 2 - mPadding;
                // tmp cosa 即menu item中心点的横坐标。计算的是item的位置,是计算位置!!!
                left = layoutRadius
                        / 2
                        + (int) Math.round(tmp
                        * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
                        * cWidth) + DensityUtil
                        .dip2px(getContext(), 1);
                // tmp sina 即menu item的纵坐标
                top = layoutRadius
                        / 2
                        + (int) Math.round(tmp
                        * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f * cWidth) + DensityUtil
                        .dip2px(getContext(), 8);
             //接着当然是布置孩子的位置啦,就是根据小圆的来布置的
                child.layout(left, top, left + cWidth, top + cWidth);
    
                // 叠加尺寸
                mStartAngle += angleDelay;
            }
        }
    

    计算小圆的思路


    图片.png

    (4)此控件事件机制dispatchTouchEvent的使用:

    //dispatchTouchEvent是处理触摸事件分发,事件(多数情况)是从Activity的dispatchTouchEvent开始的。执行super.dispatchTouchEvent(ev),事件向下分发。
        //onTouchEvent是View中提供的方法,ViewGroup也有这个方法,view中不提供onInterceptTouchEvent。view中默认返回true,表示消费了这个事件。
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            float x = event.getX();
            float y = event.getY();
    
            getParent().requestDisallowInterceptTouchEvent(true);
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                //直接就是获取x,y值了,还有一个DownTime(附送)
                    mLastX = x;
                    mLastY = y;
                    mDownTime = System.currentTimeMillis();
                    mTmpAngle = 0;
                    break;
                case MotionEvent.ACTION_MOVE:
                    isTouchUp = false;   //注意isTouchUp 这个标记量!!!
                    /**
                     * 获得开始的角度
                     */
                    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;
                    }
                    // 重新布局
                    if (mTmpAngle != 0) {
                        requestLayout();
                    }
    
                    mLastX = x;
                    mLastY = y;
    
                    break;
                case MotionEvent.ACTION_UP:
                //当手指UP啦,就是关键啦,一个缓冲角度,即我们将要固定几个位置,而不是任意位置。我们要设计一个可能的角度去自动帮他选择。
                    backOrPre();
                    break;
            }
            return super.dispatchTouchEvent(event);
        }
    

    MotionEvent事件机制:(此控件我只用了三个)主要的事件类型有:ACTION_DOWN: 表示用户开始触摸。ACTION_MOVE: 表示用户在移动(手指或者其他)。ACTION_UP:表示用户抬起了手指。
    (5)数学计算—一个缓冲角度。

    private void backOrPre() {     //缓冲的角度。即我们将要固定几个位置,而不是任意位置。我们要设计一个可能的角度去自动帮他选择。
            isTouchUp = true;
            float angleDelay = 360 / 10;              //这个是每个图形相隔的角度
            //我们本来的上半圆的图片角度应该是:18,54,90,126,162。所以我们这里是:先让当前角度把初始的18度减去再取余每个图形相隔角度。得到的是什么呢?就是一个图片本来应该在的那堆角度。所以如果是就直接return了。
            if ((mStartAngle-18)%angleDelay==0){
                return;
            }
            float angle = (float)((mStartAngle-18)%36);                 //angle就是那个不是18度开始布局,然后是36度的整数的多出来的部分角度
            //以下就是我们做的缓冲角度处理啦,如果多出来的部分角度大于图片相隔角度的一半就往前进一个,如果小于则往后退一个。
            if (angleDelay/2 > angle){
                mStartAngle -= angle;
            }else if (angleDelay/2<angle){
                mStartAngle = mStartAngle - angle + angleDelay;         //mStartAngle就是当前角度啦,取余36度就是多出来的角度,拿这个多出来的角度去数据处理。
            }
            //然后重新布局onlayout
            requestLayout();
        }
    

    一、半环形实现

    上面我们显示了环形菜单
    现在我们来实现半环形
    1、onMeasure高度的控制中,将宽度设置为

     resHeight = (int) (resWidth/2);
    

    2、在onLayout 布局控控制中,整体子view UI需要向移动height/2

          child.layout(left, top - mRadius / 2, left + cWidth,top + cWidth - mRadius / 2);
    

    因为是办环形所以超过0-180°范围的view应该将其隐藏

    ···
    if (tampStartAngle >= 0 && tampStartAngle <= 180) {
    child.setVisibility(VISIBLE);
    } else {
    child.setVisibility(INVISIBLE);
    }
    ···

    3、在dispatchTouchEvent中,整体子view UI需要向移动height/2,则对滑动的判断需要作出调整

        /**
         * 根据当前位置计算象限
         */
        private int getQuadrant(float x, float y) {
            int tmpX = (int) (x - mRadius / 2);
            int tmpY = (int) (y - mRadius / 2);//新增加了 - mRadius / 2
            if (tmpX >= 0) {
                return tmpY >= 0 ? 4 : 1;
            } else {
                return tmpY >= 0 ? 3 : 2;
            }
    
        }
    

    4、数学计算—一个缓冲角度中backOrPre()

        private void backOrPre() {     //缓冲的角度。即我们将要固定几个位置,而不是任意位置。我们要设计一个可能的角度去自动帮他选择。
            isTouchUp = true;
            if(mTmpAngle ==0){
                return;
            }
            //因为中间的子view的角度是90度,当停止时需要找出那个子view距离90度最近,再将其设置到中间
            double temp =mStartAngle;//手势放开时的角度
            double tempStart=mStartAngle;
            boolean f = true;
            for(int i=-(int)mAngleInterval*(getChildCount()-1)+90;i<=90;i+=mAngleInterval){
                double temp1 =  Math.abs(mStartAngle-i);
                if(f){
                    temp =  temp1;
                    f = false;
                }
                if(temp1<=temp){
                    tempStart  = i;
                    temp = temp1;
                }
    
            }
            mStartAngle = tempStart;
    
    
    
            requestLayout();
        }
    

    这样基本已经实现了半环形设计

    二、需实现动态增加menu接口addByView,和刷新所有menu接口addByAllView。

    addByView

        /**
         *
         *
         * @param view
         */
        public void addByView(View view) {
    
            mMenuItemCount+=1;//个数相应增加
            addView(view);
    
    
        }
    

    addByAllView

        /**
         *
         *
         * @param views
         */
        public void addByAllView(List<View> views) {
    
            removeAllViews();//情况view
    
            mMenuItemCount=views.size();
            mStartAngle = -(int)mAngleInterval*(mMenuItemCount-1) +90; //角度计算
    
            for (View view:views){
                addView(view);
            }
    
    
    
        }
    

    mAngleInterval 是每个子view的角度间隔,可自行设置

        public void setAngleInterval(double angleInterval) {
            mAngleInterval = angleInterval;
        }
    
    

    三、需实现menu滚动到中间回调,已经设置选中第几个menu接口。

    menu滚动到中间回调,只需判断是否是90度。

                if (startAngle == 90 && isTouchUp) {
                    if (mCurrentPosition == i) {
                    } else {
                        if (mOnMenuItemClickListener != null) {
                            mOnMenuItemClickListener.itemClick(count - i - 1);              //设置监听回调。
                        }
                    }
                    mCurrentPosition= i;      //本次使用mCurrentPosition,只是把他作为一个temp变量。可以有更多的使用,比如动态设置每个孩子相隔的角度
                } 
    

    设置选中第几个menu接口,需要位置换算角度,然后重新绘制页面

        private void setStartAngle(int i) {   
    
            double startAngleTemp = -(int) mAngleInterval * (i) + 90;
    
            if (mStartAngle == startAngleTemp) {
                return;
            }
            mStartAngle = startAngleTemp;
    
            requestLayout();
        }
    

    四、滑动冲突间距,内部拦截法:

    内部拦截法:
    1、这种方法需要重写子元素的dispatchTouchEvent方法。
    2、 子 View 可以使用 requestDisallowInterceptTouchEvent 影响去父 View 的分发,可以决定父 View 是否要调用 onInterceptTouchEvent 。比如,requestDisallowInterceptTouchEvent(true),父 View 就不用调用 onInterceptTouchEvent 来判断拦截,而就是不拦截,子view自己处理。 用伪代码表示为:

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
     int x = (int) event.getX();
     int y = (int) event.getY();
    
     switch (event.getAction()) {
     case MotionEvent.ACTION_DOWN: {
         getParent().requestDisallowInterceptTouchEvent(true);
         break;
     }
     case MotionEvent.ACTION_MOVE: {
         int deltaX = x - mLastX;
         int deltaY = y - mLastY;
         if (父容器需要当前触摸事件) {
             getParent().requestDisallowInterceptTouchEvent(false);
         }
         break;
     }
     case MotionEvent.ACTION_UP: {
         break;
     }
     default:
         break;
     }
    
     mLastX = x;
     mLastY = y;
     return super.dispatchTouchEvent(event);
    }
    

    代码:
    在y轴滑动大于x轴滑动时,父view进行拦截;
    在x轴滑动大于y轴滑动时,则自己处理:

        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            float x = event.getX();
            float y = event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    getParent().requestDisallowInterceptTouchEvent(true);//告诉父view不拦截
                    mLastX = x;
                    mLastY = y;
                    mDownTime = System.currentTimeMillis();
                    mTmpAngle = 0;
                    first = 1;
                    mLastMotionX = x;
                    mLastMotionY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    isTouchUp = false;          //注意isTouchUp 这个标记量!!!
                    if (first == 1) {
                        if (Math.abs(x - mLastMotionX) < Math.abs(y - mLastMotionY)) {
                            first = 0;//y轴滑动拦截
                            getParent().requestDisallowInterceptTouchEvent(false);//父view拦截
                            break;
                        } else if (Math.abs(x - mLastMotionX) > Math.abs(y - mLastMotionY)) {
                            //x轴滑动不拦截
                            first = 0;//y轴滑动拦截
                            getParent().requestDisallowInterceptTouchEvent(true);//父view不拦截
                        } else {
                            break;
                        }
    
                    }
                    /**
                     * 获得开始的角度
                     */
                    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;
                    }
    
                    if (mStartAngle > 90) {
                        mStartAngle = 90;
    
                    }
                    if (mStartAngle < -(int) mAngleInterval * (getChildCount() - 1) + 90) {
                        mStartAngle = -(int) mAngleInterval * (getChildCount() - 1) + 90;
    
                    }
                    // 重新布局
                    if (mTmpAngle != 0) {
                        requestLayout();
                    }
    
                    mLastX = x;
                    mLastY = y;
    
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    backOrPre();
                    getParent().requestDisallowInterceptTouchEvent(false);//父view拦截
                    break;
            }
            return super.dispatchTouchEvent(event);
        }
    

    这样完整逻辑也就完成了。

    遇到问题

    addByAllView时,如果在操作UI时,会出现已经存在的子view无法清除。
    原因:
    由于之前添加的childview执行了Animation动画,因为帧动画是对childview的重绘,所以,虽然执行过removeAllViews(); 但是帧动画对view的区域并没有清除掉,以至于感觉removeAllViews方法‘失效’,旧的childview还在界面上

    解决在使用时,新增

             mIdMenu.removeAllViewsInLayout();
    

    完成了。

    相关文章

      网友评论

          本文标题:tabCircleMenu设计

          本文链接:https://www.haomeiwen.com/subject/umsbrftx.html