Android侧滑菜单-一个零耦合的侧滑菜单

话不多说先上图
在这里插入图片描述

目前功能如下

  • 支持启用或禁用侧滑菜单
  • 支持菜单在条目的左边或者右边
  • 支持滑动阻塞或非阻塞
  • 支持点击了menu后是否自动关闭menu
  • 支持menu打开和关闭的回调监听
  • 可快速打开和关闭menu

【点我】更多详细使用说明请移步github

写之前已经有自己的思路,后面看了几个博客的思路想碰撞下,看看会不会有新的启发,果不其然还是有的!!毕竟一个人的思维容易固定化;

我想要的结果就是,功能都在的情况下,不跟列表产生耦合!

先大致讲下思路:
自然是继承ViewGroup,重写它的onMeasure,onLayout;
xml中SwipeMenuLayout为最外层的布局,嵌套的view,第一个view是列表的itemView,之后的都是menuItem;
onMeasure中需要让第一个itemView的宽度为Match_parent;
onLayout中需要根据菜单在左在右的boolean值来控制menu的布局位置;
剩下的就是在dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent中来处理手指的事件分发逻辑了。

下面贴下主要代码;

onMeasure中第一个view必须是Match_parent,高度的话如果itemView是wrap_content,就是子view中最高的一个作为SwipeMenuLayout的高度

	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取测量模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //内容view的宽度
        int contentWidth = 0;
        int contentMaxHeight = 0;
        mMenuWidth = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childAt = getChildAt(i);
            if (childAt.getVisibility() == View.GONE) {
                continue;
            }
            LayoutParams layoutParams = childAt.getLayoutParams();
            if (i == 0) {
                //让itemView的宽度为parentView的宽度
                layoutParams.width = getMeasuredWidth();
                mContentView = childAt;
            }
            //测量子view的宽高
            measureChild(childAt, widthMeasureSpec, heightMeasureSpec);
            //如果parentView测量模式不是精准的
            if (heightMode != MeasureSpec.EXACTLY) {
                contentMaxHeight = Math.max(contentMaxHeight, childAt.getMeasuredHeight());
            }
            //child测量结束后才能获取宽高
            if (i == 0) {
                contentWidth = childAt.getMeasuredWidth();
            } else {
                mMenuWidth += childAt.getMeasuredWidth();
            }
        }
        //取最大值 重新测量
        int height = Math.max(getMeasuredHeight(), contentMaxHeight);
        setMeasuredDimension(contentWidth, height);
    }

onLayout中根据isEnableLeftMenu来确定menu是从左边开始布局还是从右边开始布局

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int pLeft = getPaddingLeft();
        int pTop = getPaddingTop();
        int left = 0;
        int right = 0;

        for (int i = 0; i < childCount; i++) {
            View childAt = getChildAt(i);
            if (childAt.getVisibility() == View.GONE) {
                continue;
            }
            if (i == 0) {
                childAt.layout(pLeft, pTop, pLeft + childAt.getMeasuredWidth(), pTop + childAt.getMeasuredHeight());
                left += pLeft + childAt.getMeasuredWidth();
            } else {
                //放置左侧
                if (isEnableLeftMenu) {
                    childAt.layout(right - childAt.getMeasuredWidth(), pTop, right, pTop + childAt.getMeasuredHeight());
                    right -= childAt.getMeasuredWidth();
                } else {
                    //放置右侧
                    childAt.layout(left, pTop, left + childAt.getMeasuredWidth(), pTop + childAt.getMeasuredHeight());
                    left += childAt.getMeasuredWidth();
                }
            }
        }
    }

下面是主要的一个事件分发的逻辑,如果有对这个分发走向不太清楚的可以参考下这边文章:图解 Android 事件分发机制
贴一下图吧,先大致看下
在这里插入图片描述
这个流程图走向很清晰了,我们在onInterceptTouchEvent的move中判断滑动距离,如果滑动距离大于一个值,我们认为它是滑动了,不是点击,这个值可以通过如下方式获取

 //获取滑动的最小值,大于这个值就认为他是滑动  默认是8
 mScaledTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();

再贴下事件分发的逻辑代码,基本都有注释在,看起来也很清晰;

	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mFirstRawX = ev.getRawX();
                getParent().requestDisallowInterceptTouchEvent(false);
                //关闭上一个打开的SwipeMenuLayout
                chokeIntercept = false;
                if (null != mCacheView) {
                    if (mCacheView != this) {
                        mCacheView.closeMenuAnim();
                        chokeIntercept = isOpenChoke;
                    }
                    //屏蔽父类的事件,只要有一个侧滑菜单处于打开状态, 就不给外层布局上下滑动了
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //多指触摸状态改变
                isFingerTouch = false;
                //如果已经侧滑出菜单,菜单范围内的点击事件不拦截
                if (Math.abs(getScrollX()) == Math.abs(mMenuWidth)) {
                    //菜单范围的判断
                    if ((isEnableLeftMenu && ev.getX() < mMenuWidth)
                            || (!isEnableLeftMenu && ev.getX() > getMeasuredWidth() - mMenuWidth)) {
                        //点击菜单关闭侧滑
                        if (isClickMenuAndClose) {
                            closeMenuAnim();
                        }
                        break;
                    }
                    //否则点击了item, 直接动画关闭
                    closeMenuAnim();
                    return true;
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
	
	@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!this.isEnableSwipe) {
            return super.onInterceptTouchEvent(ev);
        }
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //多跟手指的触摸处理  isFingerTouch为true的话 表示之前已经有一个down事件了,
                if (isFingerTouch) {
                    return true;
                } else {
                    isFingerTouch = true;
                }
                //第一个触点的id, 此时可能有多个触点,但至少一个,计算滑动速率用
                mPointerId = ev.getPointerId(0);
                mLastRawX = ev.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                //大于系统给出的这个数值,就认为是滑动了 事件进行拦截,在onTouch中进行逻辑操作
                if (Math.abs(ev.getRawX() - mFirstRawX) >= mScaledTouchSlop) {
                    longClickable(false);
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

	@Override
    public boolean onTouchEvent(MotionEvent ev) {
        //如果关闭了侧滑 直接super
        if (!this.isEnableSwipe) {
            return super.onTouchEvent(ev);
        }
        acquireVelocityTracker(ev);
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:
                //有阻塞
                if (chokeIntercept) {
                    break;
                }
                //计算移动的距离
                float gap = mLastRawX - ev.getRawX();
                //view滑动
                scrollBy((int) (gap), 0);
                if (Math.abs(gap) > mScaledTouchSlop || Math.abs(getScrollX()) > mScaledTouchSlop) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                //超过范围的话--->归位
                //目前是右滑的话 (菜单在左边)
                if (isEnableLeftMenu) {
                    if (getScrollX() < -mMenuWidth) {
                        scrollTo(-mMenuWidth, 0);
                    } else if (getScrollX() > 0) {
                        scrollTo(0, 0);
                    }
                } else {
                    if (getScrollX() < 0) {
                        scrollTo(0, 0);
                    } else if (getScrollX() > mMenuWidth) {
                        scrollTo(mMenuWidth, 0);
                    }
                }
                //重新赋值
                mLastRawX = ev.getRawX();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //unitis值为1000(毫秒)时间单位内运动了多少个像素 正负最多为mScaledMaximumFlingVelocity
                mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
                float velocityX = mVelocityTracker.getXVelocity(mPointerId);
                //释放VelocityTracker
                recycleVelocityTracker();
                if (!chokeIntercept && Math.abs(ev.getRawX() - mFirstRawX) >= mScaledTouchSlop) {
                    //获取x方向的运动速度
                    Log.d(TAG, "onTouchEvent: " + velocityX);
                    //滑动速度超过1000  认为是快速滑动了
                    if (Math.abs(velocityX) > 1000) {
                        if (velocityX < -1000) {//左滑了
                            if (!isEnableLeftMenu) {
                                //展开Menu
                                expandMenuAnim();
                            } else {
                                //关闭Menu
                                closeMenuAnim();
                            }
                        } else {//右滑了
                            if (!isEnableLeftMenu) {
                                //关闭Menu
                                closeMenuAnim();
                            } else {
                                //展开Menu
                                expandMenuAnim();
                            }
                        }
                    } else {
                        //超过菜单布局的40% 就展开 反之关闭
                        if (Math.abs(getScrollX()) > mMenuWidth * 0.4) {//否则就判断滑动距离
                            //展开Menu
                            expandMenuAnim();
                        } else {
                            //关闭Menu
                            closeMenuAnim();
                        }
                    }
                    return true;
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

剩下的部分就是一些不是很关键的了,需要看的话请移步到GitHub中Android侧滑菜单-SwipeMenuLayout

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值