事件分发(2)-事件分发实例之侧滑菜单

主目录见:Android高级进阶知识(这是总目录索引)
上一篇《从场景到源码分析事件分发》已经很全面地分析了事件的分发流程,如果会了这个流程,那么这个例子应该也是没有问题的,当然前面View绘制的内容也应该学完。废话不多说,直接上图:

侧滑菜单

如果想要这个源码可以点击[我想要源码]进行下载,可以用来了解整理的思想。

一.目标

又到了实例的时间了,这个例子算是比较综合了,今天我们的目标又是什么呢?
1.复习《View和ViewGroup的绘制原理源码分析》的知识;
2.复习《从场景到源码分析事件分发》中事件拦截的知识;
3.同时了解VelocityTracker和Scroller类的使用。

二.实例分析

1.基础使用

1)在xml中使用

<?xml version="1.0" encoding="utf-8"?>
<com.lenovohit.swipemenu.SwipeMenu xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="60dp">
    <include android:id="@+id/item_content" layout="@layout/item_content"/>
    <include android:id="@+id/item_menu" layout="@layout/item_menu"/>
</com.lenovohit.swipemenu.SwipeMenu>

首先第一个include包含的是内容布局,第二个include包含的是删除菜单布局。
2)设置监听

  SwipeMenu slideLayout = (SwipeMenu) convertView;
        slideLayout.setOnStatusChangedListener(new SwipeStatusChangedListener());

然后在Adapter中设置SwipeMenu监听事件来监听点击onDown,打开onOpen,关闭onClose事件。

2.SwipeMenu 实现分析

因为ListView部分很容易,我们都是老司机,没必要再去讲这么基础的东西了,当然你也可以用在RecyclerView中,这都是没问题的。
我们先来看这个自定义控件SwipeMenu 的构造函数:

 public SwipeMenu(@NonNull Context context) {
        super(context);
        init();
    }

    public SwipeMenu(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
         init();
    }

    public SwipeMenu(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

我们知道这都是老套路了,我们直接看我们init()方法做了些啥?

 private void init(){
        mScroller = new Scroller(getContext());
        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
    }

我们看到这里面初始化了Scroller对象和获取到最小滑动距离。想要了解Scroller可以参考这篇[Android Scroller完全解析,关于Scroller你所需知道的一切 ],知识不是特别难。接着在view全部加载完我们就可以获取SwipeMenu中的子视图了:

  @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mContent = getChildAt(0);
        mMenu = getChildAt(1);
    }

这里获取第一个子视图即内容部分视图,第二个子视图即删除菜单部分视图。然后我们就来测量一下SwipeMenu的子视图了:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        measureChildren(widthMeasureSpec,heightMeasureSpec);
        int width = mContent.getMeasuredWidth() + mMenu.getMeasuredWidth();
        int height = Math.max(mContent.getMeasuredHeight(),mMenu.getMeasuredHeight());

        setMeasuredDimension(MeasureSpec.EXACTLY == widthMode ? widthMeasureSpec : width,
                MeasureSpec.EXACTLY == heightMode ? heightMeasureSpec : height);
    }

我们这个地方使用了ViewGroup的measureChildren来测量子视图,这个方法是ViewGroup提供的,里面会根据父类的Spec和子类的padding和LayoutParams中的宽高来共同测量子视图。最后我们根据父类给的mode来进行宽高测量。这样我们的子视图就测量完成了,我们在测量完获取子视图的宽高:

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mContentWidth = mContent.getMeasuredWidth();
        mMenuWidth = mMenu.getMeasuredWidth();
        mSwipeMenuWidth = getMeasuredWidth();
    }

这个方法是在onMeasure之后执行的,其实这个在View的size发生改变后都会执行。测量完毕,又有了各个视图的宽高了,我们就可以进行布局了:

   @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mMenu.layout(mContentWidth,0,mContentWidth + mMenuWidth,getMeasuredHeight());
    }

因为我们是FrameLayout,所以内容布局已经占满父控件了,我们只要将删除菜单放在内容布局右边即可。到这里我们的控件实际已经写完了。但是没有事件响应,只能看看,为了让他功能丰富起来,我们就给他添加事件吧。

3.事件拦截

我们要做侧滑肯定是要在适当的时机来决定拦截不拦截事件:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                if (!mScroller.isFinished()){
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                if (mIStatusChangedListener != null){
                    mIStatusChangedListener.onDown(this);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastInterceptX;
                int deltaY = y - mLastInterceptY;
                //判断如果x方向滑动大于y方向滑动,则拦截(防止内容里面有控件点击事件消耗了此事件)
                if (Math.abs(deltaX) > Math.abs(deltaY)){
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }

        mLastX = x;
        mLastY = y;

        mLastInterceptX = x;
        mLastInterceptY = y;
        return intercepted;
    }

我们看到这是很典型的拦截事件,我们里面监听了DOWN,MOVE,UP事件。首先我们看到DOWN事件里面:

              intercepted = false;
                if (!mScroller.isFinished()){
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                if (mIStatusChangedListener != null){
                    mIStatusChangedListener.onDown(this);
                }

这几句代码是先判断mScroller是否还在滑动,如果还在滑动则停止,并且拦截这个事件,然后告诉监听器回调我已经开始点击了。然后我们看到MOVE事件里面:

                int deltaX = x - mLastInterceptX;
                int deltaY = y - mLastInterceptY;
                //判断如果x方向滑动大于y方向滑动,则拦截(防止内容里面有控件点击事件消耗了此事件)
                if (Math.abs(deltaX) > Math.abs(deltaY)){
                    intercepted = true;
                }

这几句代码比较重要,我们看到这个地方有个判断x方向滑动大于y方向滑动我们就拦截这个事件,为什么要拦截呢?因为如果我们没有拦截事件,有可能这个滑动事件被子视图消费了,我们的SwipeMenu就不能消费这个滑动事件了。所以我们拦截完这个事件交给我自己来处理:

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;

                int scrollerX = getScrollX() - deltaX;
                //边界检测,如果向右滑动则scrollerX不动,如果左滑则不能超过菜单距离
                if (scrollerX < 0){
                    scrollerX = 0;
                }else if (scrollerX > mMenuWidth){
                    scrollerX = mMenuWidth;
                }
                scrollTo(scrollerX,getScrollY());

                //当x方向滑动大于y方向滑动,且x方向滑动大于最小滑动距离则说明这个控件要滑动了,告诉父控件不要把事件拦截了
                if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > Math.abs(mTouchSlop)){
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_UP:
                int totalX = getScrollX();
                //回弹检测
                if (totalX < mMenuWidth/2){
                    closeSwipeMenu();
                }else{
                    openSwipeMenu();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

我们看到DOWN里面事件跟onInterceptTouchEvent里面是一样的,我们直接来看MOVE事件做了啥:

                int deltaX = x - mLastX;
                int deltaY = y - mLastY;

                int scrollerX = getScrollX() - deltaX;
                //边界检测,如果向右滑动则scrollerX不动,如果左滑则不能超过菜单距离
                if (scrollerX < 0){
                    scrollerX = 0;
                }else if (scrollerX > mMenuWidth){
                    scrollerX = mMenuWidth;
                }
                scrollTo(scrollerX,getScrollY());

                //当x方向滑动大于y方向滑动,且x方向滑动大于最小滑动距离则说明这个控件要滑动了,告诉父控件不要把事件拦截了
                if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > Math.abs(mTouchSlop)){
                    getParent().requestDisallowInterceptTouchEvent(true);
                }

这个地方我们现实判断scrollerx来检测右滑的话则不能让他动,左滑的时候不能让他滑动距离超过删除菜单的距离,是典型的边界检测代码。然后我们看到有个代码非常重要getParent().requestDisallowInterceptTouchEvent(true),这个是让我们的父控件不要拦截这个事件,交给我来处理吧。我们可以从上一篇源码中可以看出,我这里重新贴一下这段代码:

   if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

可以看到我们设置了为true之后disallowIntercept 就会为true,那么父控件intercepted就返回为false即不拦截事件,所以这个地方ListView就不拦截侧滑事件了交给了我们的SwipeMenu来处理。现在就剩最后一步了就是UP事件:

                int totalX = getScrollX();
                //回弹检测
                if (totalX < mMenuWidth/2){
                    closeSwipeMenu();
                }else{
                    openSwipeMenu();
                }

我们这里判断我们滑动了的距离,是滑动了小于删除菜单的一半还是大于删除菜单的一半,如果小于一半就关闭这个菜单,如果大于一半则就打开这个菜单。然后我们来看这里的打开和关闭是怎么实现的:

    public void closeSwipeMenu(){
        int distanceX = 0 - getScrollX();
        mScroller.startScroll(getScrollX(), getScrollY(), distanceX, getScrollY());
        invalidate();
        if (mIStatusChangedListener != null){
            mIStatusChangedListener.onClose(this);
        }
    }

    public void openSwipeMenu(){
        int distanceX = mMenuWidth - getScrollX();
        mScroller.startScroll(getScrollX(), getScrollY(), distanceX, getScrollY());
        invalidate();
        if (mIStatusChangedListener != null){
            mIStatusChangedListener.onOpen(this);
        }
    }

我们看到我们这里是用Scroller对象来操作的滑动,这个类也是为了可以平滑地滑动,我们startScoller完毕然后必须重写下面这个方法:

   @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }

这个方法就是根据总距离和时间来进行平滑地滑动,记得一定要调用invalidate方法。而且我们关闭和打开菜单方法里面还回调了监听器里面的onDown和onOpen方法,这样我们就可以监听到这两个事件。到这里我们就已经讲解完这个控件了。

总结:之所以选这个控件主要是,我们在onInterceptTouchEvent里面我们如果是滑动事件我们拦截了事件,防止被子类消费了我们这个控件啥事件接收不到,而且我们在滑动的时候,我们又告诉我们的父视图,求求你,我滑动了你赶紧不要把我拦截了。所以向上向下都进行了事件了拦截,很典型地利用了事件的分发机制,值得大家思考

我想想,不要骗我

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

推荐阅读更多精彩内容