美文网首页Android自定义View
自定义View(7) -- 酷狗侧滑菜单

自定义View(7) -- 酷狗侧滑菜单

作者: 曾大稳丶 | 来源:发表于2017-06-26 10:57 被阅读0次
    效果图
    上一篇我们自定义了一个流式布局的ViewGroup,我们为了熟悉自定义ViewGroup,就继续自定义ViewGroup。这篇的内容是是仿照酷狗的侧滑菜单。
    我们写代码之前,先想清楚是怎么实现,解析实现的步骤。实现侧滑的方式很多种,在这里我选择继承HorizontalScrollView,为什么继承这个呢?因为继承这个的话,我们就不用写childViewmove meause layout,这样就节约了很大的代码量和事件,因为内部HorizontalScrollView已经封装好了。我们在这个控件里面放置两个childView,一个是menu,一个是content。然后我们处理拦截和快速滑动事件就可以了。思路想清楚了我们就开始撸码。
    首先我们自定义一个属性,用于打开的时候content还有多少可以看到,也就是打开的时候menu距离右边的距离。
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="SkiddingMenuLayout">
            <attr name="menuRightMargin" format="dimension"/>
        </declare-styleable>
    </resources>
    

    在初始化的时候我们通过menuRightMargin属性获取menu真正的宽度

    public SkiddingMenuLayout(Context context) {
            this(context, null);
        }
    
        public SkiddingMenuLayout(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SkiddingMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
    
            // 初始化自定义属性
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SkiddingMenuLayout);
    
            float rightMargin = array.getDimension(
                    R.styleable.SkiddingMenuLayout_menuRightMargin, DisplayUtil.dip2px(context, 50));
            // 菜单页的宽度是 = 屏幕的宽度 - 右边的一小部分距离(自定义属性)
            mMenuWidth = (int) (DisplayUtil.getScreenWidth(context) - rightMargin);
            array.recycle();
        }
    
    

    接着我们在布局加载完毕的时候我们指定menucontent的宽度

    //xml 布局解析完毕回调的方法
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            //指定宽高
            //先拿到整体容器
            ViewGroup container = (ViewGroup) getChildAt(0);
    
            int childCount = container.getChildCount();
            if (childCount != 2)
                throw new RuntimeException("只能放置两个子View");
            //菜单
            mMenuView = container.getChildAt(0);
            ViewGroup.LayoutParams meauParams = mMenuView.getLayoutParams();
            meauParams.width = mMenuWidth;
            //7.0一下的不加这句代码是正常的   7.0以上的必须加
            mMenuView.setLayoutParams(meauParams);
    
            //内容页
            mContentView = container.getChildAt(1);
            ViewGroup.LayoutParams contentParams = mContentView.getLayoutParams();
            contentParams.width = DisplayUtil.getScreenWidth(getContext());
            //7.0一下的不加这句代码是正常的   7.0以上的必须加
            mContentView.setLayoutParams(contentParams);
        }
    
    

    这里有一个细节,我们在刚进入的时候,菜单默认是关闭的,所以我们需要调用scrollTo()函数移动一下位置,但是发现在onFinishInflate()函数里面调用没有作用,这个是为什么呢?因为我们在xml加载完毕之后,才会真正的执行View的绘制流程,这时候调用scrollTo()这个函数其实是执行了代码的,但是在onLaout()摆放childView的时候,又默认回到了(0,0)位置,所以我们应该在onLayout()之后调用这个函数

    @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
            //进入是关闭状态
            scrollTo(mMenuWidth, 0);
        }
    

    初始化完毕了,接下来我们进行事件的拦截,MOVE的时候相应滑动事件,UP的时候判断是关闭还是打开,然后调用函数即可

    
    //手指抬起是二选一,要么关闭要么打开
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            //    当菜单打开的时候,手指触摸右边内容部分需要关闭菜单,还需要拦截事件(打开情况下点击内容页不会响应点击事件)
            if (ev.getAction() == MotionEvent.ACTION_UP) {
                // 只需要管手指抬起 ,根据我们当前滚动的距离来判断
                int currentScrollX = getScrollX();
                if (currentScrollX > mMenuWidth / 2) {
                    // 关闭
                    closeMenu();
                } else {
                    // 打开
                    openMenu();
                }
                return true;
            }
            return super.onTouchEvent(ev);
        }
    
        /**
         * 打开菜单 滚动到 0 的位置
         */
        private void openMenu() {
            // smoothScrollTo 有动画
            smoothScrollTo(0, 0);
        }
    
        /**
         * 关闭菜单 滚动到 mMenuWidth 的位置
         */
        private void closeMenu() {
            smoothScrollTo(mMenuWidth, 0);
        }
    
    

    到这的话,滑动事件和打开关闭事件都完成了,接下来我们就处理一个效果的问题,这里当从左往右滑动的时候,是慢慢打开菜单,这时候content是有一个慢慢的缩放,menu有一个放大和透明度变小,而反过来关闭菜单的话就是相反的效果,content慢慢放大,menu缩小和透明度变大。这里还有一个细节,就是menu慢慢的退出和进入,滑动的距离不是和移动的距离相同的,所以这里还有一个平移。接下来重写onScrollChanged()函数,然后计算出一个梯度值来做处理

     //滑动改变触发
        @Override
        protected void onScrollChanged(int l, int t, int oldl, int oldt) {
            super.onScrollChanged(l, t, oldl, oldt);
    
    //        //抽屉效果  两种一样
    //        ViewCompat.setTranslationX(mMenuView, l);
    //        ViewCompat.setX(mMenuView, l);
    
    //        Log.e("zzz", "l->" + l + " t->" + t + " oldl->" + oldl + " oldt->" + oldt);
            //主要看l  手指从左往右滑动 由大变小
            //计算一个梯度值 1->0
            float scale = 1.0f * l / mMenuWidth;
    
            //酷狗侧滑效果...
    //        //右边的缩放 最小是0.7f ,最大是1.0f
            float rightScale = 0.7f + 0.3f * scale;
            //设置mContentView缩放的中心点位置
            ViewCompat.setPivotX(mContentView, 0);
            ViewCompat.setPivotY(mContentView, mContentView.getHeight() / 2);
            //设置右边缩放
            ViewCompat.setScaleX(mContentView, rightScale);
            ViewCompat.setScaleY(mContentView, rightScale);
    
            //菜单
            //透明度是半透明到全透明  0.5f-1.0f
            float alpha = 0.5f + (1.0f - scale) * 0.5f;
            ViewCompat.setAlpha(mMenuView, alpha);
    
            //缩放  0.7-1.0
            float leftScale = 0.7f + 0.3f * (1 - scale);
            ViewCompat.setScaleX(mMenuView, leftScale);
            ViewCompat.setScaleY(mMenuView, leftScale);
    
            //退出按钮在右边
            ViewCompat.setTranslationX(mMenuView, 0.2f * l);
        }
    
    

    这样的话我们就完成了效果,但是我们还有几个细节没有处理,首先是快速滑动的问题,还有一个是当打开menu的时候,点击content需要关闭菜单,而不是相应对应的事件。接下来我们对这两个问题进行处理。

    快速滑动问题,这个问题我们采用GestureDetector这个类来做处理,这个类可以处理很多收拾问题:

    
    /**
         * The listener that is used to notify when gestures occur.
         * If you want to listen for all the different gestures then implement
         * this interface. If you only want to listen for a subset it might
         * be easier to extend {@link SimpleOnGestureListener}.
         */
        public interface OnGestureListener {
    
            /**
             * Notified when a tap occurs with the down {@link MotionEvent}
             * that triggered it. This will be triggered immediately for
             * every down event. All other events should be preceded by this.
             *
             * @param e The down motion event.
             */
            boolean onDown(MotionEvent e);
    
            /**
             * The user has performed a down {@link MotionEvent} and not performed
             * a move or up yet. This event is commonly used to provide visual
             * feedback to the user to let them know that their action has been
             * recognized i.e. highlight an element.
             *
             * @param e The down motion event
             */
            void onShowPress(MotionEvent e);
    
            /**
             * Notified when a tap occurs with the up {@link MotionEvent}
             * that triggered it.
             *
             * @param e The up motion event that completed the first tap
             * @return true if the event is consumed, else false
             */
            boolean onSingleTapUp(MotionEvent e);
    
            /**
             * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
             * current move {@link MotionEvent}. The distance in x and y is also supplied for
             * convenience.
             *
             * @param e1 The first down motion event that started the scrolling.
             * @param e2 The move motion event that triggered the current onScroll.
             * @param distanceX The distance along the X axis that has been scrolled since the last
             *              call to onScroll. This is NOT the distance between {@code e1}
             *              and {@code e2}.
             * @param distanceY The distance along the Y axis that has been scrolled since the last
             *              call to onScroll. This is NOT the distance between {@code e1}
             *              and {@code e2}.
             * @return true if the event is consumed, else false
             */
            boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
    
            /**
             * Notified when a long press occurs with the initial on down {@link MotionEvent}
             * that trigged it.
             *
             * @param e The initial on down motion event that started the longpress.
             */
            void onLongPress(MotionEvent e);
    
            /**
             * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
             * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
             * the x and y axis in pixels per second.
             *
             * @param e1 The first down motion event that started the fling.
             * @param e2 The move motion event that triggered the current onFling.
             * @param velocityX The velocity of this fling measured in pixels per second
             *              along the x axis.
             * @param velocityY The velocity of this fling measured in pixels per second
             *              along the y axis.
             * @return true if the event is consumed, else false
             */
            boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
        }
    
        /**
         * The listener that is used to notify when a double-tap or a confirmed
         * single-tap occur.
         */
        public interface OnDoubleTapListener {
            /**
             * Notified when a single-tap occurs.
             * <p>
             * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
             * will only be called after the detector is confident that the user's
             * first tap is not followed by a second tap leading to a double-tap
             * gesture.
             *
             * @param e The down motion event of the single-tap.
             * @return true if the event is consumed, else false
             */
            boolean onSingleTapConfirmed(MotionEvent e);
     
            /**
             * Notified when a double-tap occurs.
             *
             * @param e The down motion event of the first tap of the double-tap.
             * @return true if the event is consumed, else false
             */
            boolean onDoubleTap(MotionEvent e);
    
            /**
             * Notified when an event within a double-tap gesture occurs, including
             * the down, move, and up events.
             *
             * @param e The motion event that occurred during the double-tap gesture.
             * @return true if the event is consumed, else false
             */
            boolean onDoubleTapEvent(MotionEvent e);
        }
    
     /**
         * The listener that is used to notify when a context click occurs. When listening for a
         * context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in
         * {@link View#onGenericMotionEvent(MotionEvent)}.
         */
        public interface OnContextClickListener {
            /**
             * Notified when a context click occurs.
             *
             * @param e The motion event that occurred during the context click.
             * @return true if the event is consumed, else false
             */
            boolean onContextClick(MotionEvent e);
        }
    

    这里我们主要是响应onFling()这个函数,然后判断当前是打开还是关闭状态,在根据快速滑动的手势来执行打开还是关闭的操作:

     @Override
        public boolean onTouchEvent(MotionEvent ev) {
              if (mGestureDetector.onTouchEvent(ev))//快速滑动触发了下面的就不要执行了
                return true;      
          
                //....
        }
    
    
    //快速滑动
        private GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                //快速滑动回调
                //打开的时候从右到左滑动关闭   关闭的时候从左往右打开
    //            Log.e("zzz", "velocityX->" + velocityX);
                // >0 从左往右边滑动  <0 从右到左
                if (mMenuIsOpen) {
                    if (velocityX < 0) {
                        closeMenu();
                        return true;
                    }
                } else {
                    if (velocityX > 0) {
                        openMenu();
                        return true;
                    }
                }
                return super.onFling(e1, e2, velocityX, velocityY);
            }
        };
    
    

    接下来处理menu打开状态下点击content关闭menu,这里我们需要用到onInterceptTouchEvent。当打开状态的时候,我们就把这个事件拦截,然后关闭菜单即可。但是这里有一个问题,当我们拦截了DOWN事件之后,后面的MOVE UP事件都会被拦截并且相应自身的onTouchEvent事件,所以这里我们需要添加一个判断值,判断是否拦截,然后让其onTouchEvent是否继续执行操作

    @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            isIntercept = false;
            if (mMenuIsOpen && ev.getX() > mMenuWidth) {//打开状态  触摸右边关闭
                isIntercept = true;//拦截的话就不执行自己的onTouchEvent
                closeMenu();
                return true;
            }
            return super.onInterceptTouchEvent(ev);
        }
    
     @Override
        public boolean onTouchEvent(MotionEvent ev) {
    
            if (isIntercept)//拦截的话就不执行自己的onTouchEvent
                return true;
        //...
    }
    

    根据我们提出需求,然后分析需求,再完成需求。这一步步我们慢慢进行渗透,最终完成效果,完成之后你会发现其实也就那么一回事。当我们有新需求的时候,我们应该不要恐惧,应该欣然乐观的接收,再慢慢分析,最终完成。这样的话我们才能提高我们的技术。

    本文源码下载地址:https://github.com/ChinaZeng/CustomView

    相关文章

      网友评论

        本文标题:自定义View(7) -- 酷狗侧滑菜单

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