美文网首页架构Android开发经验谈Android技术知识
Android上一个类似PathMenu效果的自定义View源

Android上一个类似PathMenu效果的自定义View源

作者: 炎之铠 | 来源:发表于2017-08-18 10:04 被阅读99次

    出处
    炎之铠邮箱:yanzhikai_yjk@qq.com
    博客地址:http://blog.csdn.net/totond
    本文原创,转载请注明本出处!
    本项目GitHub地址:https://github.com/totond/YMenuView
    欢迎 Star or Fork!
    本文章已授权微信公众号code小生独家发布!

    效果总图:


    前言

    网上这种类似PathMenu的菜单很多,但是基本都不符合我项目的需求,想看他们的源码实现然后做出修改,进行二次开发来适应我的项目需求,但是发现——以我现在的能力,如果不是以前做过类似的功能,看别人的代码,很难很快地找出主要实现思路,而且不同的作者的代码有不同的风格(特别是命名),于是就自己按照自己的思路来实现,然后把实现思路都写出来分享一下,让大家了解我这个自定义View控件是怎么实现的,到时候大家根据需求修改源码,进行二次开发的时候也可以参考,也希望和大家一起探讨怎样实现更好。

    需求

    做这个控件的目的是为了实现一个平板上的全屏视频播放器的菜单栏,点击之后会弹出一堆按钮来让用户选择 ,这样的话网上很多开源控件都能实现,问题就是这个播放器是要支持Android7.0的分屏功能,(平板比较坑爹,还打开了Freeform模式的入口,这个Freeform模式可以让用户自由调节APP的界面宽高,就像在Windows桌面的那些应用窗口一样),要适应分屏功能,APP的宽高可能会改变,这些按钮的位置分布情况也要根据宽高来改变,想想就蛋疼。而网上很多的这类型PathMenu是固定分布方式的,所以就做出了这个可以调整选项位置的自定义菜单控件——YMenuView(取名技术不好不知道怎么取,就用这个挫名字啦(≧▽≦)/)。

    介绍

    基本上使用介绍都在我的GitHub地址上说明了:https://github.com/totond/YMenuView

    具体实现

    具体实现思路

    思路大概如下图:



      其中主要难点是第二个和第三个。简单来说,本质上YMenuView是一个ViewGroup,然后在里面动态生成一些控件,点击MenuButton的时候就会把一堆OptionButton显示/消失,这个过程加上一些动画,就形成最后的效果。

    创建ViewGroup

    这部分其实没什么好说的,就是创建一个名为YMenuView的ViewGroup,然后获取一些自定义的属性(自定义属性的介绍可以自行搜索或者看看我的笔记,这里不多说了),为下一步的创建MenuButton和OptionButton做准备,获取的属性在项目的Github地址上有详细的说明了。篇幅原因,这里就放出部分重要的属性图示:

    创建MenuButton和OptionButton

    如上图所示,MenuButton就是那个用于按下弹出菜单的按钮,OptionButton就是可弹出收回的选项按钮。

    MenuButton

    下面先来看看如何创建MenuButton:

    private void setMenuButton() {
            mYMenuButton = new Button(mContext);
            //设置MenuButton的大小位置
            LayoutParams layoutParams = new LayoutParams(mYMenuButtonWidth, mYMenuButtonHeight);
            layoutParams.setMarginEnd(mYMenuButtonRightMargin);
            layoutParams.bottomMargin = mYMenuButtonBottomMargin;
            layoutParams.addRule(ALIGN_PARENT_RIGHT);
            layoutParams.addRule(ALIGN_PARENT_BOTTOM);
            //生成ID
            mYMenuButton.setId(generateViewId());
    
            mYMenuButton.setLayoutParams(layoutParams);
            //设置打开关闭事件
            mYMenuButton.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (!isShowMenu) {
                        showMenu();
                    } else {
                        closeMenu();
                    }
                }
            });
            mYMenuButton.setBackgroundResource(mMenuButtonBackGroundId);
            addView(mYMenuButton);
        }
    

    主要是动态生成一个Button,利用LayoutParams来控制它的位置,生成ID是为后面要使用mYMenuButton的信息做准备,然后就是设置点击事件来控制菜单开关(开关的操作实现后面讲),最后就是设置背景和addView()加入父ViewGroup视图。

    OptionButton

    说是Button,但是实际上OptionButton是继承ImageView,因为我发现ImageView除了可以通过setImageDrawable()方法设置图片资源之外,还可以通过setBackground()方法设置背景,这样的话可以很容易实现给图片加框的效果(demo中的OptionButton效果就是通过圆形shape和图片资源合成的),下面的OptionButton创建过程中,位置计算是比较复杂的:

        private void initBan() {
            //对Ban数组进行从小到大排序
            Arrays.sort(banArray);
        }
    
    //设置选项按钮
        private void setOptionButtons() throws Exception {
            optionButtonList = new ArrayList<>(optionPositionCount);
            initBan();
            boolean isBan = true;
            for (int i = 0,n = 0; i < optionPositionCount; i++) {
                if (isBan && banArray.length > 0) {
                    //Ban判断
                    if (i > banArray[n] || banArray[n] > optionPositionCount - 1) {
                        throw new Exception("Ban数组设置不合理,含有负数、重复数字或者超出范围");
                    } else if (i == banArray[n]) {
                        if (n < banArray.length - 1) {
                            n++;
                        }else {
                            isBan = false;
                        }
                        continue;
                    }
                }
    
                OptionButton button = new OptionButton(mContext);
                //设置动画的模式和时长
                button.setSD_Animation(mOptionSD_AnimationMode);
                button.setDuration(mOptionSD_AnimationDuration);
                int btnId = generateViewId();
                button.setId(btnId);
    
                RelativeLayout.LayoutParams layoutParams = new LayoutParams(mYOptionButtonWidth, mYOptionButtonHeight);
    
                //计算OptionButton的位置
                int position = i % optionColumns;
    
                layoutParams.rightMargin = mYOptionToMenuRightMargin
                        + mYOptionHorizontalMargin * position
                        + mYOptionButtonWidth * position;
    
                layoutParams.bottomMargin = mYOptionToMenuBottomMargin
                        + (mYOptionButtonHeight + mYOptionVerticalMargin) * (i / optionColumns);
                layoutParams.addRule(ALIGN_PARENT_BOTTOM);
                layoutParams.addRule(ALIGN_PARENT_RIGHT);
    
                button.setLayoutParams(layoutParams);
                addView(button);
                optionButtonList.add(button);
            }
        }
    

    先不看Ban判断,看下面的位置计算,OptionButton的布局是矩形的,每一排中的每个ImageView从左到右的,而optionColumns是列数(也就是每排的个数),通过这个属性和总个数就可以确定所有OptionButton的布局。如下面就是optionPositionCount = 8,optionColumns = 3的效果:


      然后再看Ban判断,这段代码的目的就是让序号为Ban数组里面的位置跳过这一轮循环,不放置OptionButton。所以Ban这个功能可以通过setBanArray(int... banArray)方法设置banArray数组,里面填入位置序号,然后这个位置就不放OptionButton了,如下图,就是设置了banArray = {0,2,6}optionPositionCount = 8

      前面只是生成了OptionButton,后面还要为它们设置图片和背景:
        //设置选项按钮的background
        public void setOptionBackGrounds(@DrawableRes Integer drawableId){
            for (int i = 0; i < optionButtonList.size(); i++) {
                if (drawableId == null){
                    optionButtonList.get(i).setBackground(null);
                }else {
                    optionButtonList.get(i).setBackgroundResource(drawableId);
                }
    
            }
    
        }
    
        //设置选项按钮的图片资源,顺便设置点击事件
        private void setOptionsImages(int... drawableIds) throws Exception {
            this.drawableIds = drawableIds;
            if (optionPositionCount > drawableIds.length + banArray.length) {
                throw new Exception("Drawable资源数量不足");
            }
    
            for (int i = 0; i < optionButtonList.size(); i++) {
                optionButtonList.get(i).setOnClickListener(new MyOnClickListener(i));
                if (drawableIds == null){
                    optionButtonList.get(i).setImageDrawable(null);
                }else {
                    optionButtonList.get(i).setImageResource(drawableIds[i]);
                }
    
            }
        }
    

    实现动画

    MenuButton的动画没什么好说的,开关动画就是两个旋转(一个逆时针,一个顺时针),动画已经在xml写好了,太简单了就不展示出来,想看的话直接看源码好了:

    //初始化MenuButton的点击动画
        private void initMenuAnim() {
            menuOpenAnimation = AnimationUtils.loadAnimation(mContext, R.anim.rotate_open);
            menuCloseAnimation = AnimationUtils.loadAnimation(mContext, R.anim.rotate_close);
            animationListener = new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
                    mYMenuButton.setClickable(false);
    
                }
    
                @Override
                public void onAnimationEnd(Animation animation) {
                    mYMenuButton.setClickable(true);
                }
    
                @Override
                public void onAnimationRepeat(Animation animation) {
    
                }
            };
            menuOpenAnimation.setDuration(mOptionSD_AnimationDuration);
            menuCloseAnimation.setDuration(mOptionSD_AnimationDuration);
            menuOpenAnimation.setAnimationListener(animationListener);
            menuCloseAnimation.setAnimationListener(animationListener);
        }
    

    还开放了方法,可以在外部改变这个开关动画:

        //设置MenuButton弹出菜单选项时候MenuButton自身的动画,默认为顺时针旋转180度,为空则是关闭动画
        public void setMenuOpenAnimation(Animation menuOpenAnimation) {
            menuOpenAnimation.setAnimationListener(animationListener);
            this.menuOpenAnimation = menuOpenAnimation;
    
        }
    
        //设置MenuButton收回菜单选项时候MenuButton自身的动画,默认为逆时针旋转180度,为空则是关闭动画
        public void setMenuCloseAnimation(Animation menuCloseAnimation) {
            menuCloseAnimation.setAnimationListener(animationListener);
            this.menuCloseAnimation = menuCloseAnimation;
        }
    

    然后重点就是OptionButton的动画了,它的动画有四种:

    sd_animMode 描述
    FROM_BUTTON_LEFT 选项从菜单键左边缘飞入
    FROM_BUTTON_TOP 选项从菜单键上边缘飞入
    FROM_RIGHT 选项从View左边缘飞入
    FROM_BOTTOM 选项从View左边缘飞入

    这些动画封装在OptionButton里面,因为动画的设置需要用到自身的位置信息,所以需要注册OnGlobalLayoutListener来监听,等自身Layout完毕之后再设置,不然getLeft()等方法返回的都是0:

        private void init(){
            setClickable(true);
    
            //在获取到宽高参数之后再进行初始化
            ViewTreeObserver viewTreeObserver = getViewTreeObserver();
            viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    if (getX() != 0 && getY() != 0 && getWidth() != 0 && getHeight() != 0) {
                        setShowAndDisappear();
                        //设置完后立刻注销,不然会不断回调,浪费很多资源
                        getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    }
    
                }
            });
    
        }
    

    因为进入和退出的动画只是基本只是相反,篇幅原因这里就只展示退出动画的实现(看的时候要注意动画的初始坐标零点默认都是基于View的左上角顶点):

        private void setShowAndDisappear() {
            setShowAnimation(mDuration);
            setDisappearAnimation(mDuration);
            //在这里才设置Gone很重要,让View可以一开始就触发onGlobalLayout()进行初始化
            setVisibility(GONE);
        }
    
        public void setDisappearAnimation(int duration) {
            //获取父ViewGroup的对象,用于获取宽高参数
            YMenuView parent = (YMenuView) getParent();
            AlphaAnimation alphaAnimation = new AlphaAnimation(1,0);
            alphaAnimation.setDuration(duration);
            TranslateAnimation translateAnimation = new TranslateAnimation(0,0,0,0);
            switch (mSD_Animation) {
                case FROM_BUTTON_LEFT:
                    //从MenuButton的左边移入
                    translateAnimation= new TranslateAnimation(0,parent.getYMenuButton().getX() - getRight()
                            ,0,0);
                    translateAnimation.setDuration(duration);
                    break;
                case FROM_RIGHT:
                    //从右边缘移出
                    translateAnimation = new TranslateAnimation(0, (parent.getWidth()- getX()),
                            0, 0);
                    translateAnimation.setDuration(duration);
                    break;
                case FROM_BUTTON_TOP:
                    //从MenuButton的上边移入
                    translateAnimation = new TranslateAnimation(0, 0,
                            0, parent.getYMenuButton().getY() - getBottom());
                    translateAnimation.setDuration(duration);
                    break;
                case FROM_BOTTOM:
                    //从下边缘移出
                    translateAnimation = new TranslateAnimation(0,0,0,parent.getHeight() - getY());
                    translateAnimation.setDuration(duration);
            }
            disappearAnimation = new AnimationSet(true);
            disappearAnimation.addAnimation(translateAnimation);
            disappearAnimation.addAnimation(alphaAnimation);
            disappearAnimation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
    
              
    
                @Override
                public void onAnimationEnd(Animation animation) {
                    setVisibility(GONE);
                }
    
                @Override
                public void onAnimationRepeat(Animation animation) {
    
                }
            });
        }
    

    实现点击事件

    由于OptionButton都是在代码动态生成的,所以它们的ID也是动态生成的,不能作为switch语句的case条件,所以这里自己写了一个接口OnOptionsClickListener,来让OptionButton的每次点击都调用OnOptionsClickListener的带索引参数的方法,这样就实现让点击事件可以在外部实现并加以区分OptionButton了:

        //用于让用户在外部实现点击事件的接口,index可以区分OptionButton
        public interface OnOptionsClickListener {
            public void onOptionsClick(int index);
        }
    
    
        private class MyOnClickListener implements OnClickListener {
            private int index;
    
            public MyOnClickListener(int index) {
                this.index = index;
            }
    
            @Override
            public void onClick(View v) {
                if (mOnOptionsClickListener != null) {
                    mOnOptionsClickListener.onOptionsClick(index);
                }
            }
        }
    
        //设置选项按钮的图片资源,顺便设置点击事件
        private void setOptionsImages(int... drawableIds) throws Exception {
            this.drawableIds = drawableIds;
            if (optionPositionCount > drawableIds.length + banArray.length) {
                throw new Exception("Drawable资源数量不足");
            }
    
            for (int i = 0; i < optionButtonList.size(); i++) {
                optionButtonList.get(i).setOnClickListener(new MyOnClickListener(i));
                if (drawableIds == null){
                    optionButtonList.get(i).setImageDrawable(null);
                }else {
                    optionButtonList.get(i).setImageResource(drawableIds[i]);
                }
    
            }
        }
    

    这样做完之后,外部就可以通过实现OnOptionsClickListener接口来实现点击事件了。

    结尾

    以上就是YMenuView的主要思路了,至于一些细节大家有兴趣的话可以去代码的GitHub上Fork下来或者直接下载下来看看,有什么意见或者建议的话也可以在issue上提出。
      虽然YMenuView的实现挺简单的,功能也不多,但是足够实现我的需求了,我写这篇文章的目的就是把思路记录下来,还有让有类似需求的朋友们参考一下,看了之后二次开发也方便一些。

    后话

    最近刚正式入职,事情比较多,很多天晚上忙完都是懒得开电脑,所以没怎么写博客。虽然写博客耗时比较长,但是我觉得这是一件很有意义的事情,不但总结巩固了自己的知识,还能帮助他人,我要坚持下去。现在快稳定下来了,后面再忙都会抽多点时间来总结的,在这里说一下,激励下自己。

    相关文章

      网友评论

      • gyymz1993:有问题想咨询
        炎之铠:@gyymz1993 那你可以参考我的源码里面的生成OptionButton的方法,把OptionButton的位置设置为另外三个角落,之后写相应的进出动画就行啦
        gyymz1993: @炎之铠 我想从底部的一个点生成可以正四边形的view,就像你的四个的时候的效果一样,只是是根据底部的那个的位置生成其他的三个
        炎之铠:问题长吗?不长的话可以在这里提出,长的话可以在GitHub上的issue提出。复杂的话直接QQ私聊?

      本文标题: Android上一个类似PathMenu效果的自定义View源

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