寻找事件冲突的根源--Android事件分发机制

前言:

事件分发作为android面试必问的面试点,我们是很有必要熟练掌握以及理解的,很多时候我们都知道整个分发流程,先后顺序。这在面试的时候便足够应付。但每当我们在实际开发中,切实遇到这类问题时,理论就发挥不了作用,解决不了问题,所以这篇文章不会介绍事件分发的具体流程,而是从源码的角度来一探究竟view的事件分发机制到底是怎么完成的。

大纲

  1. 事件分发的关键方法
  2. onTouch onClick之间的关系 分析源码了解原因
  3. View的事件分发机制
  4. 事件冲突的原因,如何解决冲突

事件分发的关键方法:

ACTION_DOWN 手指初次接触到屏幕时触发
ACTION_MOVE 手指在屏幕上滑动时触发,会多次触发
ACTION_UP 手指离开屏幕时触发
ACTION_CANCEL 事件被上层拦截时触发

事件分发 :dispathTouchEvent
事件拦截: onInterceptTouchEvent
事件消费: onTouchEvent

onTouch onClick之间的关系 分析源码了解原因

我们应该都被面试官问过这样一个问题,onTouch和onClick的执行顺序,当onTouch 返回ture的时候,onclick 是接收不到的,只有当onTouch 返回false时,onClick才有回调,面试的时候一般这样说其实就可以过了,但我们今天便通过对源码分析来探究为什么会这样。

首先思考一个问题,当我们点击button,同时给button设置 onclick 或者 onTouch 事件以后,这个事件是怎么被回调回来的呢

        Button button = findViewById(R.id.main_settings);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //我是怎么被回调的呢
            }
        });

        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //我是怎么被回调的呢
                return false;
            }
        });

上文说到过,事件分发需要调用dispathTouchEvent方法。那么我们带着这个问题来看看dispathTouchEvent里面做了什么,因为button没有重写dispatchTouchEvent,可以查看父类view的实现。(只贴出关键代码)

 public boolean dispatchTouchEvent(MotionEvent event) {
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

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

我们直接来看第四行,mListenerInfo 把自身赋值给了li,

第五行,li != null && li.mOnTouchListener != null li和li.mOnTouchListener 不为空返回 true

第六行,判断当前点击事件是否是可点击,肯定为ture不然事件也不会走到这里。

第七行,关键方法,i.mOnTouchListener.onTouch 调用onTouch 方法,是否为true在于onTouch的返回值,而这个返回值就是我们在设置button的OnTouchListener时的回调。

所以这个if语句问题关键在于 怎么确保mListenerInfo 不为空,以及li.mOnTouchListener不为空

我们回过头来,看看button.setOnTouchListener()这个方法做了什么

    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

点开getListenerInfo()

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

这里是一个单例判断,如果mListenerInfo为空new一个新的,不为空返回当前的。所以我们可以知道mListenerInfo一定不会空,第五行一定返回true。

继续往下看,当我们的onTouch返回true时,

第十一行,!result 返回false, 我们知道&&的特性,当!result为false时, onTouchEvent便不会执行,而其实我们的onclick事件便是在onTouchEvent中回调,所以这便是为什么onTouch返回ture时,onClick不会执行的根本原因。

(onTouchEvent中,onClick事件是在ACTION_UP 时候被回调,在 ACTION_UP 中会调用performClickInternal()方法,而这个方法内部会返回performClick() ,可以看看performClick实现)

  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;
        }

        return result;
    }

其实和onTouch的判断差不多,判断通过,回调onClick事件。有兴趣的朋友可以自己跟着源码看一下。

View的事件分发机制

相信每个android开发者开发过程中,都会遇见各种各样的事件冲突,如果不了解其中的根源,解决起来肯定头大,各种调试,百度的结果让人心累。而当我们懂的事件冲突的根本原因,定位问题,解决问题时便游刃有余。

任何一个事件的分发都是从Activity开始,Activiyt之上都是c++层处理,我们无需关心。一般情况下,事件都是从用户按下(ACTION_DOWN)的那一刻产生的,当点击事件产生后,事件首先会传递给当前的 Activity,所以我们就从Activity的dispathTouchEvent开始,看看源码中是怎么来处理事件分发的。

  • activity的dispatchTouchEvent
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

我们直接看第二个if语句,getWindow().superDispatchTouchEvent(ev),第一个是给开发者重写的一个空方法,主要是屏保功能,可以不关心,getWindow返回window,而window只有一个实现类,就是phoneWindow,我们来看

  • phoneWindow的superDispatchTouchEvent:
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

返回一个mDecor, mDecor就是我们熟悉的DecorView,继续往下

  • DecorView的superDispatchTouchEvent:
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

Decorview父类是ViewGroup,所以最终调到了ViewGroup的dispatchTouchEvent,因为事件分发核心都在这个方法里面,内容较多,我们将一点点展现出来。

  • 我们都知道一个事件的发生是从Down事件开始,所以我们就先来分析一下dispatchTouchEvent方法中,down事件是如何进行的。
     @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
     
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            //判断是否是Down事件,如果是,做一些重置工作
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                //这个方法会让requestDisallowInterceptTouchEvent无效
                resetTouchState();
            }
            // Check for interception.
            final boolean intercepted;
           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 {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

重点关注第16行开始的代码,首先会判断当前事件是否为Down,然后得到一个布尔值disallowIntercept,如果disallowIntercept为true则不会执行拦截方法onInterceptTouchEvent。这里disallowIntercept这个值我们重点介绍一下,我们知道事件分发中有这么一个函数requestDisallowInterceptTouchEvent,请求父类不要拦截事件,我们来看看这个函数做了什么。

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

当我们在view中设置了requestDisallowInterceptTouchEvent为true的时候,这个方法中会将mGroupFlags赋值,从而使得 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0返回true; 也就是disallowIntercept 为ture,导致我们的onInterceptTouchEvent没办法执行,所以这也是平时我们设置了这个方法父类不会拦截子类的原因。但是这里面有一个很大的坑。

当我们当前的事件为Down的时候,requestDisallowInterceptTouchEvent是不生效的。因为上面说到的第9行,判断当前事件是否是Down事件,如果是,会调用resetTouchState方法

    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

当事件为down时,这个方法会执行 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;,从而导致disallowIntercept 为false,所以还是会执行父类的onInterceptTouchEvent拦截方法。

继续往下看,因为篇幅原因 大概写一下这个if语句中重要的一些方法,并加上注释。

if (!canceled && !intercepted) {
      //对子View进行排序,拿到当前GropuView下一层View,而不是所有view
      final ArrayList<View> preorderedList = buildTouchDispatchChildList();
      //拿到最外层view
      final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
     //判断点击事件
     if (!canViewReceivePointerEvents(child)
         //判断clid的位置是否是点击位置
             || !isTransformedTouchPointInView(x, y, child, null)) {
       ev.setTargetAccessibilityFocus(false);
         //如果不符合,重新遍历取下一个child
            ontinue;
    }

      //分发给哪个子View处理事件
      (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
      //addTouchTarget方法,
      //target = TouchTarget.obtain(child, pointerIdBits);
      //mFirstTouchTarget = null;
      //target.next = mFirstTouchTarget;
      //mFirstTouchTarget = target; 
      //所以 target.next == null, mFirstTouchTarget !=null 返回了 mFirstTouchTarget
       addTouchTarget(child, idBitsToAssign);
       alreadyDispatchedToNewTouchTarget = true;
}

这个if是整个事件分发中最为关键的地方,会对所有子view排序,然后遍历。并且其中的几个变量值也影响接下来的事件分发。

            // 如果为down事件 mFirstTouchTarget不为空看else
// 如果为move事件 则交给自己的dispatchTouchEvent处理,具体逻辑可看dispatchTransformedTouchEvent
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    //down事件 next为空上文已经分析
                    final TouchTarget next = target.next;
                    // 两个条件都为true,上文已经分析,所以返回handled = true
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //down事件到此就结束了
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                       
                        //所以会走到这里来,dispatchTransformedTouchEvent 分发事件
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    // target为空,跳出while循环
                    target = next;
                }
            }

以上便是一个down事件的流程,其实无论是down还是move都是需要通过dispatchTouchEvent分发事件,而move事件是可以反复调用此方法的,当一个move事件进来的时候是无法进行分发的,因为此时已经通过down确定了哪个子view来消耗此次事件。最终move逻辑上文也已经注解了。

事件冲突的原因,如何解决冲突

以上便是事件分发的流程,只有搞懂了这些流程,在实际开发中才能解决一些冲突问题。 那么这些事件为什么会冲突。知道流程以后又如何解决呢。

为了更清晰的描述事件冲突,我们有这么一个例子,Viewpager嵌套ListView。我们重写Viewpager的onInterceptTouchEvent方法,无论该方法返回true还是,还是false,Viewpager的左右滑动事件便和ListView上下滑动事件冲突。但是我们去掉这个方法时,冲突事件便没有了。

当onInterceptTouchEvent返回true时,intercepted就为true,从而导致if (!canceled && !intercepted) {}方法不会发生。因此mFirstTouchTarget就为空。

当onInterceptTouchEvent返回false时,

   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 {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
            //当事件为move onInterceptTouchEvent返回true不会进到if中
            //当事件为move onInterceptTouchEvent返回false不会进到for循环中
            //所以无论无何都不会进行事件分发
            //当事件为down 我们拦截事件时候,
            if (mFirstTouchTarget == null) {
                //onInterceptTouchEvent返回true
                //dispatchTransformedTouchEvent 方法传入的child为空
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                 //onInterceptTouchEvent返回false
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
         //当事件变成move的时候,alreadyDispatchedToNewTouchTarget被置为false
       //dispatchTransformedTouchEvent 会调用到listview里面去,所以Viewpager左右滑动无效
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
    

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
         //如果child为空,调用自己的  dispatchTouchEvent ,所以不会执行listview的事件          
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

那么如何解决这些冲突呢。两种方法

  1. 内部拦截法

    重写Viewpager的onInterceptTouchEvent

        @Override
        public boolean onInterceptTouchEvent(MotionEvent event) {
            //内部拦截发
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                super.onInterceptTouchEvent(event);
                return false;
            }
    
            return true;
        }
    

    重写ListView的dispatchTouchEvent

      //内部拦截法
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                }
                case MotionEvent.ACTION_MOVE: {
                    int deltaX = x - mLastX;
                    int deltaY = y - mLastY;
                    if (Math.abs(deltaX) > Math.abs(deltaY)) {
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    break;
    
                }
                default:
                    break;
            }
    
            mLastX = x;
            mLastY = y;
            return super.dispatchTouchEvent(event);
        }
    
  1. 外部拦截法

    重写Viewpager的onInterceptTouchEvent

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

推荐阅读更多精彩内容