美文网首页Android知识
Android-自定义ViewGroup(二) 侧滑菜单

Android-自定义ViewGroup(二) 侧滑菜单

作者: _SHYII | 来源:发表于2017-03-03 12:33 被阅读0次

    为加深自定义ViewGroup实现思想,所以自己写了一个SlidingMenu,实现方式类似
    Android-自定义ViewGroup(一) 水平滑动;优点在于添加了SlidingMenu的padding(onMeasure中测量,onLayout中布局)及子ViewGroup的Margin(onLayout中布局)。
      下面说实现思想:SlidingMenu中添加两个布局
    (1)onMeasure中先对子ViewGroup进行测量,调用measureChildren()方法;measureChildren(widthMeasureSpec, heightMeasureSpec);其次要设置自身SlidingMenu的宽高,当int widthMode = MeasureSpec.getMode(widthMeasureSpec)
    得到mode为EXACTLY时,设置getSize得到的宽高,其他模式时对子ViewGroup进行测量得到所有子ViewGroup宽高的总和,传入setMeasuredDimension(),如下:
    setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, heightMode == MeasureSpec.EXACTLY ? heightSize : height);
    onMeasure代码如下:

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            measureChildren(widthMeasureSpec, heightMeasureSpec);
    
            //menu、content总宽
            int width = 0;
            //menu、content总高
            int height = 0;
            //可以单独获取子ViewGroup并测量宽高,此处用for是因为所有自定义ViewGroup都适用。
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                //必须重写generateLayoutParams(attrs)方法,设置适当的LayoutParams
                MarginLayoutParams childLp = (MarginLayoutParams) child.getLayoutParams();
                //单独的宽高
                width += child.getMeasuredWidth() + childLp.leftMargin + childLp.rightMargin;
                height += child.getHeight() + childLp.topMargin + childLp.bottomMargin;
                //menuWidth为menu的宽,考虑了此ViewGroup的leftPadding值,用作onTouch限定滑动边界、onLayout布局
                if (i == 0) {
                    menuWidth = getPaddingLeft() + childLp.leftMargin + childLp.rightMargin + child.getMeasuredWidth();
                }
            }
            width += getPaddingLeft() + getPaddingRight();
            height += getPaddingTop() + getPaddingBottom();
            //精确模式设置测量得到的尺寸,否则去设置子ViewGroup的宽高测量总和
            setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, heightMode == MeasureSpec.EXACTLY ? heightSize : height);
        }
    

    (2)布局只要注意将SlidingMenu的padding和子ViewGroup的margin考虑,没什么复杂的,具体看代码:

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
            //获取此SlidingMenu的leftPadding,布局时无需考虑rightPadding
            int left = getPaddingLeft();
            for (int i = 0; i < getChildCount(); i++) {
                LinearLayout child = (LinearLayout) getChildAt(i);
                if (null != child && child.getVisibility() != GONE) {
                    MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                    int lc = left + lp.leftMargin;
                    int tc = lp.topMargin + getPaddingTop();
                    int rc = lc + child.getMeasuredWidth();
                    int bc = tc + child.getMeasuredHeight();
    
                    //menuWidth为左边菜单宽度
                    child.layout(lc - menuWidth, tc, rc - menuWidth, bc);
                    left += rc + lp.rightMargin;
                }
            }
        }
    

    (3)注意:以上即完成了SlidingMenu的布局,但是注意我们控件继承的是ViewGroup,通过child.getLayoutParams()拿到的是ViewGroup的布局管理器,需重写该方法的返回值拿到我们要的margin属性。如下:

        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new MarginLayoutParams(getContext(), attrs);
        }
    

    关于MarginLayoutParams可以去看下,它是ViewGroup的子类,又是包括LinearLayout、RelativeLayout、RecyclerVeiw等布局管理器的父类,可以去了解下。

    layoutparms.png

    (4)以上就完成了SlidingMenu的布局,此时我们要处理滑动事件,处理滑动事件时首先得确定手指左右滑动,并拦截交由SlidingMenu自身去消费事件。拦截与消费即在onInterceptTouchEvent、onTouchEvent判断水平移动时return true。并在onTouchEvent的MotionEvent.ACTION_MOVE中通过Scroller去设置滑动;

    关于Scroller的固定用法如下:

        Scroller scroller = new Scroller(getContext());
    
        int scrollX = getScrollX();
        int delta = destx - scrollX;
        //300ms内滑向destX,效果就是慢慢滑动
        scroller.startScroll(scrollX, 0, delta, 0, 300);
        invalidate();
    
        /**
         * 滚动时需要重写的方法,用于控制滚动
         */
        @Override
        public void computeScroll() {
            //判断滚动时候停止
            if (scroller.computeScrollOffset()) {
                //滚动到指定的位置
                scrollTo(scroller.getCurrX(), scroller.getCurrY());
                //这句话必须写,否则不能实时刷新
                postInvalidate();
            }
        }
    

    另外VelocityTracker速度追踪器,及TouchSlop最小有效滑动距离的概念比较简单,有兴趣的可自行了解

    下面是该SlidIngMenu的所有代码:

    public class ZhanfSlideMenu extends ViewGroup {
    
        private Scroller scroller;
        private VelocityTracker velocityTracker;
    
        public ZhanfSlideMenu(Context context) {
            this(context, null);
        }
    
        public ZhanfSlideMenu(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public ZhanfSlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init() {
    
            scroller = new Scroller(getContext());
            velocityTracker = VelocityTracker.obtain();
            int touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    
        }
    
        /**
         * 滚动时需要重写的方法,用于控制滚动
         */
        @Override
        public void computeScroll() {
            //判断滚动时候停止
            if (scroller.computeScrollOffset()) {
                //滚动到指定的位置
                scrollTo(scroller.getCurrX(), scroller.getCurrY());
                //这句话必须写,否则不能实时刷新
                postInvalidate();
            }
        }
    
        //分别记录上次滑动的坐标
        private int mLastX = 0;
        private int mLastY = 0;
    
        //分别记录上次滑动的坐标(onINterceptTouchEvent)
        private int mLastXIntercept = 0;
        private int mLastYIntercept = 0;
    
        int menuWidth;
        //要移动的点的位置
        int destx;
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //如果动画还没有结束,再次点击时结束上次动画,即开启这次新的ACTION_DOWN的动画
                    if (!scroller.isFinished()) {
                        scroller.abortAnimation();
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    int deltaX = x - mLastXIntercept;
                    int deltaY = y - mLastYIntercept;
                    if (Math.abs(deltaX) > Math.abs(deltaY)) {
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_UP:
    
                    break;
                default:
                    break;
            }
            mLastX = x;
            mLastY = y;
            mLastXIntercept = x;
            mLastYIntercept = y;
            return super.onInterceptTouchEvent(event);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            velocityTracker.addMovement(event);
            velocityTracker.computeCurrentVelocity(1000);
    
            int x = (int) event.getX();
            int y = (int) event.getY();
            int scrollX = getScrollX();
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //如果动画还没有结束,再次点击时结束上次动画,即开启这次新的ACTION_DOWN的动画
                    if (!scroller.isFinished()) {
                        scroller.abortAnimation();
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    int deltaX = x - mLastX;
                    int deltaY = y - mLastY;
                    if (Math.abs(deltaX) > Math.abs(deltaY) && scrollX >= -menuWidth && scrollX <= 0) {
                        scrollBy(-deltaX, 0);
                    }
    
                    break;
                case MotionEvent.ACTION_UP:
                    int xVelocity = (int) velocityTracker.getXVelocity();//获取X方向手指滑动的速度,之前必须调用computeCurrentVelocity()方法
                    int yVelocity = (int) velocityTracker.getYVelocity();
                    if (Math.abs(xVelocity) > 200 && Math.abs(xVelocity) > Math.abs(yVelocity)) {
                        destx = xVelocity > 0 ? (-menuWidth) : 0;
                    } else {
                        destx = (scrollX < (-menuWidth / 2)) ? (-menuWidth) : 0;
                    }
    
                    int delta = destx - scrollX;
                    scroller.startScroll(scrollX, 0, delta, 0, 300);
                    invalidate();
                    break;
                default:
                    break;
            }
    
            mLastX = x;
            mLastY = y;
    
            return true;
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            measureChildren(widthMeasureSpec, heightMeasureSpec);
    
            //menu、content总宽
            int width = 0;
            //menu、content总高
            int height = 0;
            //可以单独获取子ViewGroup并测量宽高,此处用for是因为所有自定义ViewGroup都适用。
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                //必须重写generateLayoutParams(attrs)方法,设置适当的LayoutParams
                MarginLayoutParams childLp = (MarginLayoutParams) child.getLayoutParams();
                //单独的宽高
                width += child.getMeasuredWidth() + childLp.leftMargin + childLp.rightMargin;
                height += child.getHeight() + childLp.topMargin + childLp.bottomMargin;
                //menuWidth为menu的宽,考虑了此ViewGroup的leftPadding值,用作onTouch限定滑动边界、onLayout布局
                if (i == 0) {
                    menuWidth = getPaddingLeft() + childLp.leftMargin + childLp.rightMargin + child.getMeasuredWidth();
                }
            }
            width += getPaddingLeft() + getPaddingRight();
            height += getPaddingTop() + getPaddingBottom();
            //精确模式设置测量得到的尺寸,否则去设置子ViewGroup的宽高测量总和
            setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, heightMode == MeasureSpec.EXACTLY ? heightSize : height);
        }
    
        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new MarginLayoutParams(getContext(), attrs);
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
            //获取此SlidingMenu的leftPadding,布局时无需考虑rightPadding
            int left = getPaddingLeft();
            for (int i = 0; i < getChildCount(); i++) {
                LinearLayout child = (LinearLayout) getChildAt(i);
                if (null != child && child.getVisibility() != GONE) {
                    MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                    int lc = left + lp.leftMargin;
                    int tc = lp.topMargin + getPaddingTop();
                    int rc = lc + child.getMeasuredWidth();
                    int bc = tc + child.getMeasuredHeight();
    
                    //menuWidth为左边菜单宽度
                    child.layout(lc - menuWidth, tc, rc - menuWidth, bc);
                    left += rc + lp.rightMargin;
                }
            }
        }
    }
    

    Qusetion:关于实现SlidingMenu的实现就是这么简单,但也遇上了一个问题,就是在onTouchEvent的Move事件中设置了左右边界,当在边界处手指慢慢移动左右边界正常显示不会滑动。但是当在边界处快速滑动的时候,左右能继续弹性滑动一小段距离,可以看到白色的背景。关于这个问题有待进一步研究。

    参考:
    《Android开发艺术探索》

    相关文章

      网友评论

        本文标题:Android-自定义ViewGroup(二) 侧滑菜单

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