• 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 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

  • 相关阅读:
    Python爬虫编程思想(162): 综合爬虫项目:可视化爬虫
    Verilog中 高位与低位
    android中MVC MVP MVVM三种架构对比
    nginx快速部署一个网站服务 + 多域名 + 多端口
    WebRTC学习笔记七 pion/webrtc
    01-Python的基本概念
    智慧城市服务平台硬件采购清单(参考)
    ElementUI实现登录注册+axios全局配置+CORS跨域
    ArcGIS:对Shapefile文件的编辑
    springboot:java操作docker(docker-java)的基本使用
  • 原文地址:https://blog.csdn.net/s_nshine/article/details/126775428