tabCircleMenu设计

需求背景

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();

完成了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容