Android事件冲突场景分析及一般解决思路

拦截的艺术

先来看接口ViewParent中的一个函数

    /**
     * Called when a child does not want this parent and its ancestors to
     * intercept touch events with
     * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
     *
     * <p>This parent should pass this call onto its parents. This parent must obey
     * this request for the duration of the touch (that is, only clear the flag
     * after this parent has received an up or a cancel.</p>
     * 
     * @param disallowIntercept True if the child does not want the parent to
     *            intercept touch events.
     */
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);

参数disallowIntercept的意思就是childView告诉父容器要不要进行拦截

true :告诉所有父控件不要拦截,事件交由childrenView处理

    Code 1
    //该代码展示的是站在View的角度,在子View的dispatchTouchEvent()中解决冲突的实战代码
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                /**
                 * 子View的所有父ViewGroup都会跳过onInterceptTouchEvent的回调
                 * 相当于ViewGroup的流程是dispatchTouchEvent() --> onTouchEvent()
                 */
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {//水平滑动,使得父类可以执行onInterceptTouchEvent
                    /**
                     * 不让所有的父容器跳过onInterceptTouchEvent()
                     * 相当于ViewGroup的流程是:dispatchTouchEvent() --> onInterceptTouchEvent() --> onTouchEvent()
                     * 也就是ViewGroup拦截以后交给自己的onTouchEvent()来处理
                     */
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }

false:告诉所有父控件拦截。在父控件的onInterceptTouchEvent()中可能类似这样的处理

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            mLastX = x;
            mLastY = y;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                return true;
            }
            return false;
        }else{
            //如果是非down事件,说明子View并没有拦截父类的onInterceptTouchEvent
           //说明该事件交由父类处理,所以不需要再传递给子类,返回true
           return true;
        }
    }

然后接下来的事情交给父控件的onTouchEvent()来处理
Ok,咱们回到拦截的艺术,主要分两个角度

解决冲突的两个角度

ChildView的角度

  • 在View的onTouch()或者子控件的dispatchTouchEvent中,根据具体的业务逻辑来告诉父容器要不要拦截。两种思路下代码都以给出,参见Code 1 , 2
    Code 2
    // //该代码展示的是站在View的角度,在子View的onTouchEvent()中解决冲突的实战代码
    mListView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN) {
            mLastY = event.getY();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            int top = mListView.getChildAt(0).getTop();
            float nowY = event.getY();
            if(!isSvToBottom) {
                // 允许scrollview拦截点击事件, scrollView滑动
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else if(top == 0 && nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
                // 允许scrollview拦截点击事件, scrollView滑动
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else {
                // 不允许scrollview拦截点击事件, listView滑动
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        return false;
    }
});

ViewGroup角度

  • 在onInterceptTouchEvent()中根据具体业务逻辑判断要不要拦截事件
@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:  
               //down事件不拦截,否则无法传给子元素  
               intercepted = false;  
               if (!mScroller.isFinished()) {  
                   mScroller.abortAnimation();  
                   intercepted = true;  
               }  
               break;  
           case MotionEvent.ACTION_MOVE:  
               int deltaX = x - mLastXIntercept;  
               int deltaY = y - mLastYIntercept;  
               //水平滑动则拦截  
               if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {  
                   intercepted = true;  
               } else {  
                   intercepted = false;  
               }  
               break;  
           case MotionEvent.ACTION_UP:  
               //不拦截,否则子元素无法收到  
               intercepted = false;  
               break;  
       }  
      ...
      return intercepted;  
   }  

冲突解决实战

上下滑动与上下滑动的冲突型

ScrollView嵌套ListView

  • 网上的解决思路分析:

    • 根据展示数据的个数乘以每一个Item的高度,计算出ListView的总体高度,然后动态的设置ListView的高度
    • 复写ListView的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,让ListView完全展开

    这两种方式最突出的缺陷在于ListView的子控件不能复用,容易造成OOM。

  • 分析冲突点:嵌套之后ListView的高度无法完全显示,且两者存在事件冲突

  • 解决思路:无法显示,复用的问题我们需要在布局中明确的设置ListView的高度。冲突根据ScrollView的状态和ListView的状态共同决定,冲突的解决选择在子View的onTouch()中解决。

    • ScrollView在未滑动到底部时候,如果点击并滑动ListView时候,ListView是不能滑动的,不禁止。
    • 如果ScrollView滑动到底部,且ListView已经到顶部,继续下拉ListView,其实会拉动ScrollView,不禁止。
    • 如果ScrollView滑动到底部,用户向上滑,ListView滑动,禁止ScrollView截断点击事件能力

对于ScrollView的滑动到底部的监听我们可以这样做

boolean isSvToBottom = false;
scroll_view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // 判断 scrollView 当前滚动位置在顶部
                if(scroll_View.getSrollY == 0){
                    isSvToBottom = true;
                }

                // 判断scrollview 滑动到底部
                // scrollY 的值和子view的高度一样,这人物滑动到了底部
                if (scroll_view.getChildAt(0).getHeight() - scroll_view.getHeight() 
                            == scroll_view.getScrollY()){
                    isSvToBottom =false;
                }
                return false;
            }
        });

mListView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN) {
            mLastY = event.getY();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            int top = mListView.getChildAt(0).getTop();
            float nowY = event.getY();
            if(!isSvToBottom) {
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else if(top == 0 && nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else {
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        return false;
    }
});

子控件部分接管型

  • 场景:SwipeRefreshLayout --> ScrollView --> ViewPager,这种布局场景很常见,多见于应用的首页,如美团。

  • 分析冲突点:ScrollView的上下滑动与ViewPager的左右滑动原本是不冲突的,但是加入了SwipeRefreshLayout之后,事件场景就变得复杂。当滑动ViewPager的时候,很容易就触发了SwipeRefreshLayout的下来刷新。

  • 解决思路:因为ViewPager只涉及X轴方向上的滑动,所以我们在MotionEvent_ACTION_MOVE的时候去动态的判断X周方向上是否超过了我们设定的阈值,这个值我们可以自定义,也可以去拿系统的这个值
    mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);

  mViewPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN) {
            mLastX = event.getX();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            if(Math.abs(event.getX() - mLastX) > 60f) {
                mRefreshLayout.setEnabled(false);
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        if(action == MotionEvent.ACTION_UP) {
            mScrollView.requestDisallowInterceptTouchEvent(false);
            mRefreshLayout.setEnabled(true);
        }
        return false;
    }
});

子控件完全接管事件型

ScrollView嵌套Map(第三方地图控件)

  • 分析冲突点:外层套了ScrollView的Map就接受不到事件了,但Map的应用场景是只要用户在Map上触摸,也就是MotionEvent_ACTION_DOWN时候,说明就是要操作Map,就跟ScrollView没有关系。

  • 解决思路:直接在子View的onTouch中告诉ScrollView(父控件)要不要拦截

mMapView.getChildAt(0).setOnTouchListener(new View.OnTouchListener() {
    @Override
     public boolean onTouch(View v, MotionEvent event) {
          if(event.getAction() == MotionEvent.ACTION_UP){
               mScrollView.requestDisallowInterceptTouchEvent(false);
           }else{
               mScrollView.requestDisallowInterceptTouchEvent(true);
           }
         return false;
     }
});

总结

本文重要从子View的角度(内部),和ViewGroup(外部)的两种角度去解决事件冲突的思路,并给出了一些常见的冲突场景以及解决案例,具体的冲突点需要具体分析,主要是分两点来确定

  • 确定冲突的有关控件
  • 找准冲突发生的点
  • 确定是用内部还是外部的方式

注意:内部解决优先于外部解决思路

参考:
自定义水平ScrollView与ListView事件冲突研究
ScrollView滑动处理

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

推荐阅读更多精彩内容