自定义ViewGroup之侧滑菜单

最近PM2.5对侧滑菜单比较感兴趣,很多页面上都用到了侧滑菜单,之前也在网上看到了很多关于侧滑,有自定义RecyclerView,也有自定义Item的,但是当自己真正去用的时候,发现有很多问题,所以打算自己参考网上的思路自己写一个,果然,看花容易绣花难,写的很艰辛,不过最后还是实现了,下面看看效果图:

侧滑菜单

下面简单分享下实现的思路:

自定义ViewGroup

这个其实没什么太多要说的,主要是有几点需要注意下:

  1. 需要复写三个LayoutParams方法

generateDefaultLayoutParams

当动态向ViewGroup中添加没有参数的child的时候,会自动调用这个方法,将其设置成为默认的参数

generateLayoutParams(AttributeSet attrs)

根据布局中的属性来生成LayoutParams

generateLayoutParams(LayoutParams layoutParams)

代码中动态添加参数

2.在复写onMeasure方法的时候,需要对WrapContent这种情况进行特殊处理,因为很多时候item是包裹child的,高度并没有固定死,所以需要特殊处理,不然会导致菜单栏的内容高度显示不正确处理的方式就是以第一个child也就是内容区域为标准重新测量,代码如下:

    for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            if (i == 0) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                childHeight = child.getMeasuredHeight();
            } else {
                int heightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
                measureChild(child, widthMeasureSpec, heightSpec);
            }
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            if (i > 0) {
                mMaxDistance += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            }
        }

3.onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int mLeftOffset = getPaddingLeft();
        int topOffset = getPaddingTop();
        for (int i = 0; i < childCount; i++) {
            View mChild = getChildAt(i);
            if (mChild.getVisibility() == GONE) {
                continue;
            }
            MarginLayoutParams lp = (MarginLayoutParams) mChild.getLayoutParams();
            mLeftOffset += lp.leftMargin;
            topOffset += lp.topMargin;
            int measuredWidth = mChild.getMeasuredWidth();
            int measuredHeight = mChild.getMeasuredHeight();
            mChild.layout(mLeftOffset, topOffset, mLeftOffset + measuredWidth, topOffset + measuredHeight);
            mLeftOffset += (measuredWidth + lp.rightMargin);
            topOffset = getPaddingTop();
        }
    }

截止到这里,基本的measure跟layout就结束了,这个不是重点,重点在于解决滑动冲突。

View的滑动冲突

三个方法:

事件分发:public boolean dispatchTouchEvent(MotionEvent ev)

Touch 事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会以隧道方式(从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递)将事件传递给最外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由该 View 的 dispatchTouchEvent(MotionEvent ev)方法对事件进行分发。dispatchTouchEvent 的事件分发逻辑如下:

  • 如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;
  • 如果 return false,事件分发分为两种情况:
  1. 如果当前 View 获取的事件直接来自 Activity,则会将事件返回给 Activity 的 onTouchEvent 进行消费;
  2. 如果当前 View 获取的事件来自外层父控件,则会将事件返回给父 View 的 onTouchEvent 进行消费。
  • 如果返回super.dispatchTouchEvent(ev),事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。
事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)

在外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系统默认的 super.dispatchTouchEvent(ev) 情况下,事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件拦截逻辑如下:

  • 如果返回 true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;
  • 如果返回 false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;
  • 如果返回 super.onInterceptTouchEvent(ev),事件默认会被拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理。
事件响应:public boolean onTouchEvent(MotionEvent ev)

在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回super.onInterceptTouchEvent(ev) 的情况下 onTouchEvent 会被调用。onTouchEvent 的事件响应逻辑如下:
● 如果事件传递到当前 View 的 onTouchEvent 方法,而该方法返回了 false,那么这个事件会从当前 View 向上传递,并且都是由上层 View 的 onTouchEvent 来接收,如果传递到上面的 onTouchEvent 也返回 false,这个事件就会“消失”,而且接收不到下一次事件。
● 如果返回了 true 则会接收并消费该事件。
● 如果返回 super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。

需要注意的是view是没有onInterceptTouchEvent这个方法,只能分发,不存在拦截,只能分发,就跟view没有layout方法是一样的道理。

通过上面的分析,我们需要在onInterceptTouchEvent中进行拦截,然后在onToucheEvent中进行处理

onInterceptTouchEvent

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean consume = false;
        acquireVelocityTracker(ev);
        if (mInterPoint == null)
            mInterPoint = new PointF();
        if (mTouchPoint == null)
            mTouchPoint = new PointF();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                consume = false;
                mInterPoint.set(ev.getRawX(), ev.getRawY());
                mTouchPoint.set(ev.getRawX(), ev.getRawY());
                mPointerId = ev.getPointerId(0);
                break;
            case MotionEvent.ACTION_MOVE:
                float abs = Math.abs(mInterPoint.x - ev.getRawX());
                if (Math.abs(abs) > mTouchSlop) {
                    consume = true;
                } else {
                    consume = isOpened;
                }
                break;
            case MotionEvent.ACTION_UP:
                if (isOpened && ev.getX() < getWidth() - getScrollX()) {
                    closeMenu();
                    consume = true;
                }
                break;
        }
        mInterPoint.set(ev.getRawX(), ev.getRawY());
        mTouchPoint.set(ev.getRawX(), ev.getRawY());
        return consume;
    }

mInterPoint跟mTouchPoint是两个PointF,用来记录onInterceptTouchEvent跟onTouchEvent中的点坐标,isOpened是一个布尔值来记录菜单是否打开,当菜单关闭的时候,点击内容区域是不能进行拦截的,此时需要把点击事件传给child,当菜单打开的时候,此时需要group自己进行处理,需要关闭菜单,所以需要拦截此事件,自己进行处理,onInterceptTouchEvent事件的处理比较简单,就是根据滑动的距离与当前菜单的显示状态比较来判断是否拦截。

onTouchEvent

public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (!mScroller.isFinished())
                    mScroller.abortAnimation();
                int variationX = (int) (mTouchPoint.x - ev.getRawX());
                int variationY = (int) (mTouchPoint.y - ev.getRawY());
                if (Math.abs(variationX) < Math.abs(variationY)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    scrollBy(variationX, 0);
                    int scrollX = getScrollX();
                    if (scrollX > mMaxDistance)
                        scrollTo(mMaxDistance, 0);
                    if (scrollX < 0)
                        scrollTo(0, 0);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                float velocityX = mVelocityTracker.getXVelocity(mPointerId);
                if (Math.abs(velocityX) > 1000) {
                    if (velocityX < -1000)
                        openMenu();
                    else
                        closeMenu();
                } else {
                    if (getScrollX() > mLimit)
                        openMenu();
                    else
                        closeMenu();
                }
                releaseVelocityTracker();
                break;
        }
        mTouchPoint.set(ev.getRawX(), ev.getRawY());
        return true;
    }

onTouchEvent就显得有些麻烦

  • ACTION_DOWN

这里不需要记录点坐标,只需要请求父容器不要拦截事件

  • ACTION_DOWN

首先需要判断此时的滑动方向,如果水平方向上的位移小于垂直方向上的位移,那么就把事件交给父容器处理,否则就自己进行处理

  • ACTION_UP

通过两种方式来确定菜单最终是打开还是关闭,一个是根据速度,一个是根据移动的距离,比较好理解

移动的方式

开启菜单

 private void openMenu() {
        isOpened = true;
        if (getScrollX() == mMaxDistance)
            return;
        mScroller.startScroll(getScrollX(), 0, mMaxDistance - getScrollX(), 0, 1000);
        invalidate();
    }

  

关闭菜单

  private void closeMenu() {
        isOpened = false;
        if (getScrollX() == 0)
            return;
        mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 1000);
        invalidate();

    }

基本上实现了一个菜单的功能,上面只贴出了核心代码,更多代码可以下载Demo下来查看。

项目下载地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容