View的事件分发机制以及滑动冲突

View的事件分发机制以及滑动冲突

[TOC]

点击事件的传递规则

点击时间的分发过程 总是绕不过三个很重要的方法来共同完成:dispatchTouchEvent(MotionEvent ev), onIntercepTouchEvent(MotionEvent ev), onTouchEvent(MotionEvent ev)

public boolean dispatchTouchEvent(MotionEvent ev)

​ 用来进行事件的分发。如果时间能够分发到当前View,那么此方法一定会被调用,返回的结果受View的OntouchEvent和下级View的dispatchEvent方法的影响,表示是否消耗当前事件。

public boolean onIntercepTouchEvent(MotionEvent ev)

​ 只有ViewGroup才会拥有的方法,用于拦截某个事件,如果当前的View拦截某个事件,那么在同一个时间序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

​ **public boolean onTouchEvent(MotionEvent ev) **

​ 在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。

那么这三个方法的调用顺序是如何呢?

public boolean dispatchTouchEvent(MotionEvent ev){
  boolean consume = false;
  if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev); 
  }else{
    consume = child.dispatchTouchEvent(ev);
  }
  return consume;
}

对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,此时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,及它的onTouchEvent方法就会被调用,如果它的onInterceptTouchEvent返回为false,表示它不拦截当前事件,此时当前事件就会继续传递给它的子元素,此时如果是View,则会直接调用onTouchEvent方法。

OnTouchListener, View.onTouchEvent 和OnclickListener的区别

当一个View需要处理事件。设置了OnTouchListener,则OnTouchListener的onTouch方法会被回调,如果onTouch的方法返回True,则View.onTouchEvent方法不会被调用,反之则会被调用。View.onTouchEvent方法中,如果当前设置的有OnclickListener,其优先级最低。

这三者的优先级: OnTouchListener -> View.onTouchEvent -> OnclickListener

当一个点击事件产生后,它的传递过程遵循如下顺序 Activity -> PhoneWindow -> RootView。 由Activity 传给PhoneWindow Window 最后传给顶级View。

关于时间传递的机制,我们首先在这里给一些结论,

  1. 同一事件顺序是指手指接触屏幕的那一刻起, 到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个时间序列以Down事件开始,中间含有数量不一的move事件。最后以up事件结束。
  2. 正常情况下,一个事件序列只能被一个View拦截且消耗,因此一个时间序列的事件不能分别由两个View同时处理,但是我们可以通过代码控制事件传递。
  3. 某个View一旦决定拦截,那么一个事件序列都只能由它来处理,并且它的onIntercepTouchEvent不会再被调用。当一个View决定拦截一个事件后,同一事件的剩下事件也会交给它来处理,也就是说onIntercepTouchEvent不会被再调用。
  4. 某个View一旦开始处理事件,如果他不消耗ACTION_DOWN事件(OnTouchEvent返回为false),那么同一事件中的其他时间都不会再交给它来处理,并且事件将重新交由它的父元素去处理。
  5. 如果View不消耗除了ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理
  6. ViewGroup默认是不拦截任何事件,默认onInterceptTouchEvent方法默认返回False
  7. View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
  8. View的OntouchEvent默认都是会消耗事件,除非它是不可以点击的(clickable longclickable同时为false)。 View的longClickable属性都是false,clickable属性要分情况,Button的clickable属性默认为true,TextView的clickable属性默认为false。 当然 如果给view设置了setOnclickListener 或者setOnLongClickListener 会默认开启。
  9. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子元素,通过requestDisallowInterceptTouchEvent方法可以在子元素中干涉父元素的事件分发过程,但是ACTION_DOWN事件除外。

事件分发的源码分析

点击事件是有MotionEvent来表示,当一个点击事件发生,最先传入的是Activity,由Activity的disPatchEvent来进行事件派发,,具体工作是由Activity内部的Window来完成的。 Window会将时间传递给DecorView,一般DecorView就是当前界面的顶级容器(即是setContentView所设置的View的父容器),如图:

public boolean disPatchTouchEvent(MotionEvent ev){
  if(ev.getAction == MotionEvent.ACTION_DOWN){
        onUserInteraction();
  }
  if(getWindow().superDispatchTouchEvent(ev)){
    return truel
  }
  return onTouchEvent(ev);
}

Window是如何将事件传递给ViewGroup的呢,Window类其实是一个抽象类 它可以控制顶级view的外观和行为策略,Window的唯一实现是PhoneView类,

publlic boolean superDisPatchTouchEvent(MotionEvent event){
  return mDecor.superDispatchTouchEvent(event);
}

这个mDecot其实就是我们getWindow().getDecorView()返回的View,我们通过设置setContentView设置的View就是它的一个子View,自此事件传递到了顶级View,即我们设置的SetContentView所设置的View。

ViewGroup的事件分发机制

我们在看一下ViewGroup对点击事件的分发过程,主要实现在disPatchEvent方法中,

final boolean intercepted
if(actionMasked == MotionEvent.Action_Down || mFirsrtTouchTarget != null){
  final boolean disallowIntercept = (GroupFlag & FLAG_DISALLOW_INTERCEPT) != 0;
  if(!disallowIntercept){
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action);
  }else{
    intercepted = false;
  } 
}else{
  intercepted = true;
}

由ViewGroup的子元素成功处理时, mFirsrtTouchTarget 会被赋值并指向子元素。所以一旦事件是由当前的ViewGroup拦截,接下来同一时序的其他事件都会默认交给ViewGroup来处理。

另外一个特殊情况就是FLAG_DISALLOW_INTERCEPT标识符,这这个一般通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View,一般设置ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件,因为ViewGroup在事件分发的时候会重置FLAG_DISALLOW_INTERCEPT标识符,这意味着当面对ACTION_DOWN的时候,ViewGroup一定会调用onInterceptTouchEvent来判断自己是否需要拦截事件

                 final ArrayList<View> preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(childIndex);                                                                           
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))                            {                           
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }

首先遍历所有的ViewGroup的所有子元素,判断是否能够接受到点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内,dispatchTransformedTouchEvent就是调用了child的dispatchTouchEvent方法, 如果子元素的disPatchTouchEvent返回false 则会继续分发给下一个子元素(如果有的话), 在addTouchTarget(child, idBitsToAssign) 给mFirsrtTouchTarget 赋值。mFirsrtTouchTarget 是否为null直接影响ViewGroup对事件的拦截策略。

            dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
            {
                if (child == null) {
                handled = super.dispatchTouchEvent(event);
                } else {
                handled = child.dispatchTouchEvent(event);
                }
            }

如果遍历了所有的子元素事件都没有被合适处理,这包含两种情况: 第一种是ViewGroup没有子元素,第二种是子元素处理了点击事件,但是在dispatchTouchEvenr中返回了false 一般是子元素在OnTouchEvent中返回了false,这是ViewGroup会自己处理点击事件。

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);

View的事件分发

         if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }

首先判断是否有设置OnTouchListener,如果onTouchListener中的Touch返回true,那么onTouchEvent就不会被调用。

                if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
                switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }                  
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                   

只要View的CLICKABLE或者是LONG_CLICKABLE 那么他就会消耗这件事,当ACTION_UP事件发生 会触发performClick() 如果设置了OnclickListener ,performClick就是调用Onclick方法。

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
    }

设置Viewd的OnclickListener和OnLongClickListener()会自动将View的Clickable,LongClickAble为true。

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
    
     public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

View的滑动冲突

常见的滑动冲突场景

  • 场景1 ------ 外部滑动方向和内部滑动方向不一致
  • 场景2------- 外部滑动方向和内部滑动方向一致
  • 场景3------- 上面两种情况的嵌套

对应处理的方法:

  1. 当用户左右滑动时,需要外部拦截点击事件,当用户需要上下滑动时,需要让内部拦截点击事件
  2. 这种场景无法根据滑动的角度, 距离差已经速度差来做判断 一般需要从业务上找到突破口
  3. 同上,一般也是从业务的需要上得出相应的处理规则

两种滑动冲突的解决方案:

  1. 外部拦截法

    所谓的外部拦截法就是所有的点击事件都先经过父容器的拦截处理,如果父容器需要此事件则拦截 如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制,外部拦截法需要重写父容器的onInterceptTouchEvent方法。伪代码如下

     
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            int action = ev.getAction();
            float y = ev.getY();
    
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mLastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dy = y - mLastY;
                   if(父容器需要当前点击事件){
                     return true;
                   }
                    break;
            }
           // 默认返回的都是false 
            return super.onInterceptTouchEvent(ev);
        }
    
    1. 内部拦截法

      内部拦截法主要是父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗,否则交由父容器进行处理,需要配合requestDisallowInterceptTouchEvent()才能正常工作,一般内部拦截法比较的复杂,它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:

      
        @Override
          public boolean dispatchTouchEvent(MotionEvent ev) {
              int action = ev.getAction();
              float y = ev.getY();
      
              switch (action) {
                  case MotionEvent.ACTION_DOWN:
                    parent.requestDisallowInterceptTouchEvent(true)
                      break;
                  case MotionEvent.ACTION_MOVE:              
                     if(父容器需要当前点击事件){
                       return parent.requestDisallowInterceptTouchEvent(false);
                     }
                      break;
              }
             // 默认返回的都是false 
              return super.dispatchTouchEvent(ev);
          }
      
      

      面对不同的滑动策越的时候只需要修改ACTION_MOVE事件即可,其他不需要动也不能改动。除了子元素需要处理外,父元素也要默认拦截除了ACTION_DOWN以外的所有其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)时,父元素才能继续拦截所需要的事件。

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

推荐阅读更多精彩内容