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

效果图

上一篇我们自定义了一个流式布局的 ViewGroup,我们为了熟悉自定义 ViewGroup,就继续自定义 ViewGroup。这篇的内容是是仿照酷狗的侧滑菜单。
我们写代码之前,先想清楚是怎么实现,解析实现的步骤。实现侧滑的方式很多种,在这里我选择继承 HorizontalScrollView,为什么继承这个呢?因为继承这个的话,我们就不用写 childViewmove meause layout,这样就节约了很大的代码量和事件,因为内部 HorizontalScrollView已经封装好了。我们在这个控件里面放置两个 childView,一个是 menu,一个是 content。然后我们处理拦截和快速滑动事件就可以了。思路想清楚了我们就开始撸码。
首先我们自定义一个属性,用于打开的时候 content还有多少可以看到,也就是打开的时候 menu距离右边的距离。



    
        
    

在初始化的时候我们通过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.
         * 

* 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) -- 酷狗侧滑菜单)