Android事件分发机制--浅显易懂解析

Android事件分发机制的本质
将点击事件向某个View进行传递并且最终得到处理,即当一个点击事件发生后,系统需要将这个事件传递给一个具体的View处理,这个事件的传递过程就是事件分发过程

事件在那些对象传递
Activity、ViewGroup、View分发流程:Activity(Window)-> ViewGroup -> Viewsuper:调用父类方法true:处理事件,事件不在继续往下传递false:不处理事件,事件也不继续传递,交给父控件的onTouchEvent()处理传递:
Activity -> ViewGroup -> View 从上往下调用dispatchTouchEvent()
View -> ViewGroup -> Activity 从下往上调用onTouchEvent()

1. Activity的事件分发
当一个点击事件发生时,事件最先到达Activity的dispatchTouchEvent()进行事件分发

   public boolean dispatchTouchEvent(MotionEvent ev) {
         //一个事件的开始总是从DOWN开始
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //默认空方法,每当按键、触摸、trackBall事件分发到当前的Activity就会被调用,
            //如果想在Activity运行的时候能够感知用户正在与设备交互,重写此方法
            onUserInteraction();
        }
        //getWindow()=Window抽象类,唯一的实现类PhoneWindow
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }


PhoneWindow

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //mDecor=DecorView,是视图顶层View,继承FrameLayout,所有界面的父类
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView

    public boolean superDispatchTouchEvent(MotionEvent event) {
        //由于DecorView继承自FrameLayout,FrameLayout继承自ViewGroup
        //super.dispatchTouchEvent为ViewGroup 的dispatchTouchEvent
        return super.dispatchTouchEvent(event);
    }

即getWindow().superDispatchTouchEvent(ev)就是执行了ViewGroup.dispatchTouchEvent(event)
说明事件就是从Activity传递到ViewGroup中

总结:
1. 事件最先传递到Activity的dispatchTouchEvent()进行事件分发
2. 调用Window唯一实现类PhoneWindow的 superDispatchTouchEvent()
3. 调用DecorView的superDispatchTouchEvent()
4. 最终调用DecorView父类FrameLayout的dispatchTouchEvent()即ViewGroup的dispatchTouchEvent()

2.View事件分发
   public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;
               ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            ListenerInfo li = mListenerInfo;
            //分析(1)
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
         ...
        //如果分析(1)中onTouch()返回false(onTouch()未消费事件),
        return result;
    }


分析(1):
第一个条件:li != null,经过跟踪发现在View中发现了 getListenerInfo() 那么这个方法又是什么时候调用的呢?

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

继续跟踪发现,getListenerInfo()方法在多个地方使用到,比如setOnClickListener(),setOnLongClickListener(),setOnTouchListener()等等,这里也正好找到了li.mOnTouchListener != null赋值地方setOnTouchListener()

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

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

第三个条件:(mViewFlags & ENABLED_MASK) == ENABLED,即判断当前控件是否是enable,一般控件都是默认enable
第四个条件:li.mOnTouchListener.onTouch(this, event),其实这个方法就是回调控件注册的touch事件的onTouch()方法,也就是如果我们在onTouch()里返回true,这四个条件也就满足了,从而让整个方法直接返回了true,如果我们在onTouch()方法返回false,就会执行onTouchEvent()方法。

根据上面的研究,我们可以得出结论:onTouch()方法优先于onClick()执行,如果onTouch()方法返回true(事件被消费)因而事件不会继续向下传递,onClick()方法也将不在执行

我们发现如果条件不通过,则会执行onTouchEvent()方法,可想而知onClick()方法也必然在onTouchEvent()方法中,接着看onTouchEvent源码:

   public boolean onTouchEvent(MotionEvent event) {
        ...
        final int action = event.getAction();
        //只要控件是CLICKABLE(点击)、LONG_CLICKABLE(长按)有一个为true,onTouchEvent就一定消费事件
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        ...
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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 (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }
                        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)) {
                                    //分析(1)
                                    performClickInternal();
                                }
                            }
                        }
                ...
            }

            return true;
        }
        return false;
    }


分析(1):跟踪performClickInternal()方法

   private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();
        return performClick();
    }

   public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            //如果注册了OnClickListener事件就会回调onClick()方法
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }



view的事件分发源码解析到此结束,上总结:
1. onTouch()优先于onClick()先执行
2. 控件被点击时:
(1) 如果onTouch返回false,执行onTouchEvent()方法,进而执行onClick()方法
(2) 如果onTouch返回true,则事件被onTouch()消费,onClick()就不会执行了。

3.ViewGroup事件分发

测试用例:(1)只点击Button (2)点击ViewGroupB
测试结果:当点击Button的时候,执行Button 的Onclick()方法,但是ViewGroupB 的onTouch()不会执行,只有点击了ViewGroupB时才会触发其onTouchEvent()
测试结论:Button的onClick()将事件消费了,因此事件不会继续向下传递

测试用例:(1)重写Button的onTouch事件返回true,点击Button
测试结果:Button的onTouch消费了事件,onClick()收不到事件

测试用例:(1)单独点击ViewGroupB (2)单独ViewGroupA (3)重写B的onTouch事件返回true
测试结果:(1)当单独点击ViewGroupB的时候A/B 的onTouch()都会被触发;(2)单独单击ViewGroupA时触发了A的onTouch()
(3)单独点击ViewGroupB时A的onTouch()没有被触发了,因为已经被 B消费了

源码分析:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            // Handle an initial down.初始化
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                //恢复mFirstTouchTarget==null
                resetTouchState();
            }
            // Check for interception.
            final boolean intercepted;
            //按下并且首次
            //mFirstTouchTarget:判断当前的ViewGroup是否拦截了事件,如果拦截了mFirstTouchTarget=null,如果没有拦截
            //并交由子view处理,则mFirstTouchTarget!=null
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//分析(1)
                  
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {//分析(2)
                    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;
            }
          }
          ...
       return handled;
    }




分析(1):首先必须是ACTION_DOWN状态,一次完整的事件序列应该是从DOWN开始UP结束,mFirstTouchTarget的意义是:当前ViewGroup是否拦截了事件,如果拦截了,mFirstTouchTarget=null,如果没有拦截交由子View来处理,则mFirstTouchTarget!=null。假设当前的ViewGroup拦截了事件(disallowIntercept=false),mFirstTouchTarget!=null为false,如果这时触发ACTION_DOWN事件,则会执行onInterceptTouchEvent()方法;如果触发的是ACTION_MOVE、ACTION_UP事件,则不再执行onInterceptTouchEvent()方法,而是直接设置了intercepted=true,此后的一个事件序列均由这个ViewGroup处理。

分析(2):FLAG_DISALLOW_INTERCEPT标志主要是禁止ViewGroup拦截除了DOWN之外的事件,一般通过子View的requestDisallowInterceptTouchEvent来设置。
总结:当ViewGroup要拦截事件的时候,那么后续的事件都要由他处理,而不再调用onInterceptTouchEvent()方法,其中onInterceptTouchEvent默认false,如果想要ViewGroup拦截则重写方法返回true。

继续往下面分析:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
       ...
       final int childrenCount = mChildrenCount;
       if (newTouchTarget == null && childrenCount != 0) {
           final float x = ev.getX(actionIndex);
           final float y = ev.getY(actionIndex);
           // Find a child that can receive the event.
           // Scan children from front to back.
           
           final ArrayList<View> preorderedList = buildTouchDispatchChildList();
           final boolean customOrder = preorderedList == null
                   && isChildrenDrawingOrderEnabled();
           final View[] children = mChildren;
           //倒序遍历子元素是否能够接收点击事件(从最上层View开始往内层遍历)
           for (int i = childrenCount - 1; i >= 0; i--) {
               final int childIndex = getAndVerifyPreorderedIndex(
                       childrenCount, i, customOrder);
               final View child = getAndVerifyPreorderedView(
                       preorderedList, children, childIndex);


               // If there is a view that has accessibility focus we want it
               // to get the event first and if not handled we will perform a
               // normal dispatch. We may do a double iteration but this is
               // safer given the timeframe.
               if (childWithAccessibilityFocus != null) {
                   if (childWithAccessibilityFocus != child) {
                       continue;
                   }
                   childWithAccessibilityFocus = null;
                   i = childrenCount - 1;
               }

               //判断触摸点是否在子view的范围或者子view是否正在播放动画,如果不符合要求则continue
               if (!canViewReceivePointerEvents(child)
                       || !isTransformedTouchPointInView(x, y, child, null)) {
                   ev.setTargetAccessibilityFocus(false);
                   continue;
               }

               newTouchTarget = getTouchTarget(child);
               if (newTouchTarget != null) {
                   // Child is already receiving touch within its bounds.
                   // Give it the new pointer in addition to the ones it is handling.
                   newTouchTarget.pointerIdBits |= idBitsToAssign;
                   break;
               }

               resetCancelNextUpFlag(child);
               if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//分析(1)
                   // Child wants to receive touch within its bounds.
                   mLastTouchDownTime = ev.getDownTime();
                   if (preorderedList != null) {
                       // childIndex points into presorted list, find original index
                       for (int j = 0; j < childrenCount; j++) {
                           if (children[childIndex] == mChildren[j]) {
                               mLastTouchDownIndex = j;
                               break;
                           }
                       }
                   } else {
                       mLastTouchDownIndex = childIndex;
                   }
                   mLastTouchDownX = ev.getX();
                   mLastTouchDownY = ev.getY();
                   newTouchTarget = addTouchTarget(child, idBitsToAssign);
                   alreadyDispatchedToNewTouchTarget = true;
                   break;
               }
               // The accessibility focus didn't handle the event, so clear
               // the flag and do a normal dispatch to all children.
               ev.setTargetAccessibilityFocus(false);
           }
           if (preorderedList != null) preorderedList.clear();
       }}}
    ...
    }

分析(1):dispatchTransformedTouchEvent()事件分发实际的处理方法

   private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                //如果没有子view
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        } 
        ...
        return handled;
    }



如果存在子View,则调用View的dispatchTouchEvent()方法,如果ViewGroup没有子View,则调用super.dispatchTouchEvent()方法即View的dispatchTouchEvent()方法就回到了View的分发上面了。

4. 点击事件分发的传递规则
从上面分析得出,伪代码

fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    var result = false
    if (onInterceptTouchEvent(ev)) {
        result = super.onTouchEvent(ev)
    } else {
        result = child.dispatchTouchEvent(ev)
    }
    return result
}


onInterceptTouchEvent()方法和onTouchEvent()方法都是在dispatchTouchEvent()方法中调用的。
我们知道他们的传递规则是由上而下,如果到最底层的View一直还没有消耗事件则又从下而上传递
如下图:(网络配图侵权删)

举个栗子:(摘抄自刘望舒的《Android进阶之光》)

在金庸的《倚天屠龙记》中,武当派实力强劲,按照身份和实力区分,分别是武当掌门张
三丰、武当七侠、武当弟子。这时突然有一个敌人来犯,这个消息首先会汇报给武当掌门张三丰。张三丰
当然不会亲自出马,因此他就将应战的任务交给武当七侠之一的宋远桥(onInterceptTouchEvent()返回
false);宋远桥威名远扬,也不会应战,因此他就把应战的任务交给武当弟子宋青书
(onInterceptTouchEvent()返回 false);武当弟子宋青书没有手下,他只能自己应战。在这里我们把武当
掌门张三丰比作顶层 ViewGroup,将武当七侠之一的宋远桥比作中层ViewGroup,武当弟子宋青书比作底层
View。那么事件的传递流程就是:武当掌门张三丰(顶层ViewGroup)→武当七侠宋远桥(中层
ViewGroup)→武当弟子宋青书(底层View)。因此得出结论,事件由上而下传递返回值的规则如下:为
true,则拦截,不继续向下传递;为false,则不拦截,继续向下传递。
接下来讲解点击事件由下而上的传递。当点击事件传给底层的 View 时,如果其onTouchEvent()方法
返回true,则事件由底层的View消耗并处理;如果返回false则表示该View不做处理,则传递给父View的
onTouchEvent()处理;如果父View的onTouchEvent()仍旧返回false,则继续传递给该父View的父View
处理,如此反复下去。
再返回上面武侠的例子。武当弟子宋青书发现来犯的敌人是混元霹雳手成昆,他打不过成昆
(onTouchEvent()返回  false),于是就跑去找宋远桥,宋远桥来了,发现自己也打不过成昆
(onTouchEvent()返回  false),就去找武当掌门张三丰,张三丰用太极拳很轻松地打败了成昆
(onTouchEvent()返回true)。因此得出结论,事件由下而上传递返回值的规则如下:为true,则处理
了,不继续向上传递;为false,则不处理,继续向上传递。


5.总结
onToutch()和onTouchEvent()的区别
这两个方法都是在View的dispatchTouchEvent()中调用的;但onTouch优先于onTouchEvent()执行,如果在onTouch()中返回true,onTouchEvent() 将不再执行。
另外,onTouch能够得到执行只需两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable,因为如果是非enable,那么注册的onTouch事件永远无法得到执行,对于这一类控件,重写onTouchEvent方法来监听touch事件

Touch事件的后续事件层级传递
当dispatchTouchEvent()在进行事件分发的时候,只有前一个事件返回true,才会收到后一个事件(比如:在执行ACTION_DOWN时返回了false,后面一系列的ACTION_MOVE、ACTION_UP事件都不会执行了)
dispatchTouchEvent()和onTouchEvent()消费事件、终结事件传递返回 true,那么收到ACTION_DOWN,也会收到ACTION_MOVE、ACTION_UP
如果在某个对象(Activity、ViewGroup、View)的onTouchEvent()消费事件(返回true),那么ACTION_MOVE和ACTION_UP事件从上往下到这个View后就不在往下传递了,而是直接传给自己的onTouchEvent()并结束本次事件传递的过程。

完结散花。。
————————————————
版权声明:本文为CSDN博主「吴唐人」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wu996489865/article/details/103652082

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s_nshine

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值