zoukankan      html  css  js  c++  java
  • Android View事件分发源码分析

    引言

      上一篇文章我们介绍了View的事件分发机制,今天我们从源码的角度来学习下事件分发机制。

    Activity对点击事件的分发过程

      事件最先传递给当前Activity,由Activity的dispatchTouchEvent进行事件分发,具体的工作是有Activity内部的Window来完成的。Window会将事件传递给DecorView,DecorView一般就是当前界面的底层容器(即setContentView所设置的View的父容器)。下面我们来看一下Activity的dispatchTouchEvent方法的源码。源代码如下:

     1     /**
     2      * Called to process touch screen events.  You can override this to
     3      * intercept all touch screen events before they are dispatched to the
     4      * window.  Be sure to call this implementation for touch screen events
     5      * that should be handled normally.
     6      * 
     7      * @param ev The touch screen event.
     8      * 
     9      * @return boolean Return true if this event was consumed.
    10      */
    11     public boolean dispatchTouchEvent(MotionEvent ev) {
    12         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    13             onUserInteraction();
    14         }
    15         if (getWindow().superDispatchTouchEvent(ev)) {
    16             return true;
    17         }
    18         return onTouchEvent(ev);
    19     }

      我们分析上面的源代码,事件首先交给Activity所属的Window进行分发,如果返回true,那么说明事件被子View拦截并且处理了。如果返回false说明事件没人处理,所有的View的onTouchEvent都返回false,那么这时候只有Activity的onTouchEvent会被调用了(还记得上一篇文章写的那些结论吗?看看第5条)。

      那么Window是如何将事件传递给ViewGroup的呢?我们看源代码会知道,Window是一个抽象类,Window的superDispatchTouchEvent方法也是一个抽象方法,我们需要找到Window的实现类才行。Window的实现类是PhoneWindow类,我们来看PhoneWindow是如何处理点击事件的。代码如下:

    1     @Override
    2     public boolean superDispatchTouchEvent(MotionEvent event) {
    3         return mDecor.superDispatchTouchEvent(event);
    4     }

      看到这里,逻辑变得很清晰了,PhoneWindow将事件直接传递给DecorView,那DecorView又是什么呢?在上一篇文章中,我们聊到过,DecorView是顶层的View。我们接着方法往下看,我们看到如下代码:

    1 public boolean superDispatchTouchEvent(MotionEvent event) {
    2             return super.dispatchTouchEvent(event);
    3         }

      DecorView事件分发也是要依靠父类方法的,DecorView继承自FrameLayout,而后者继承自ViewGroup。很显然DecorView的事件分发过程调用的是ViewGroup里面的方法。

    ViewGroup对事件分发过程

      上面的分析已经讲明,事件到达顶层View后会调用ViewGroup的dispatchTouchEvent方法进行分发。下面的逻辑要分两种情况来说:

      第一种:如果ViewGroup的onInterceptTouchEvent方法返回true,表示ViewGroup需要拦截该事件,这个事件会由ViewGroup来进行处理。

      第二种:如果ViewGroup的onInterceptTouchEvent方法返回false,表示ViewGroup不需要拦截该事件,这时候这个事件会传递给它所在的点击事件链上的子View,这时候子View的dispatchTouchEvent会被调用,这时候事件就有顶级View传递到了下一级的View。接下来的传递过程和上面的过程类似,如此往复,直到整个事件的分发。

      下面我们来看下ViewGroup中事件分发代码的逻辑,先看第一段。

     1 // Check for interception.
     2 final boolean intercepted;
     3 if (actionMasked == MotionEvent.ACTION_DOWN
     4         || mFirstTouchTarget != null) {
     5     final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
     6     if (!disallowIntercept) {
     7         intercepted = onInterceptTouchEvent(ev);
     8         ev.setAction(action); // restore action in case it was changed
     9     } else {
    10         intercepted = false;
    11     }
    12 } else {
    13     // There are no touch targets and this action is not an initial down
    14     // so this view group continues to intercept touches.
    15     intercepted = true;
    16 }

      从上面的代码我们知道,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget!=null。那么什么情况下mFirstTouchTarget!=null这个条件成立呢?通过后面的代码逻辑我们知道,当ViewGroup不拦截事件,并将事件交给子View处理时,mFirstTouchTarget!=null这个条件就是成立的。反过来说,一旦事件由ViewGroup拦截并且自己来处理时,mFirstTouchTarget!=null就是不成立的,那么当ACTION_MOVE和ACTION_UP事件到来时,由于if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件为false就导致ViewGroup的onInterceptTouchEvent方法不会被调用,并且同一事件序列中的其他事件都会默认交给他来处理。

      这里还有一种特殊情况,就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent这个方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT设置后,ViewGroup将无法拦截除ACTION_DOWN以外的其他点击事件。在进行事件分发时,如果是ACTION_DOWN事件,会重置这个标记位,这导致子View中设置的这个标记位无效,因此,当面对ACTION_DOWN事件时,ViewGroup总会调用自己的dispatchTouchEvent方法来询问自己是否要拦截事件。代码如下:

    // 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);
        resetTouchState();
    }

      注意这段代码是在上面一个代码段之前的哦,去源码看就知道了。

      下面我们来看ViewGroup不拦截事件的时候,事件会向下分发交给它子View进行处理,这段代码如下:

     1 final int childrenCount = mChildrenCount;
     2 if (newTouchTarget == null && childrenCount != 0) {
     3     final float x = ev.getX(actionIndex);
     4     final float y = ev.getY(actionIndex);
     5     // Find a child that can receive the event.
     6     // Scan children from front to back.
     7     final ArrayList<View> preorderedList = buildOrderedChildList();
     8     final boolean customOrder = preorderedList == null
     9             && isChildrenDrawingOrderEnabled();
    10     final View[] children = mChildren;
    11     for (int i = childrenCount - 1; i >= 0; i--) {
    12         final int childIndex = customOrder
    13                 ? getChildDrawingOrder(childrenCount, i) : i;
    14         final View child = (preorderedList == null)
    15                 ? children[childIndex] : preorderedList.get(childIndex);
    16 
    17         // If there is a view that has accessibility focus we want it
    18         // to get the event first and if not handled we will perform a
    19         // normal dispatch. We may do a double iteration but this is
    20         // safer given the timeframe.
    21         if (childWithAccessibilityFocus != null) {
    22             if (childWithAccessibilityFocus != child) {
    23                 continue;
    24             }
    25             childWithAccessibilityFocus = null;
    26             i = childrenCount - 1;
    27         }
    28 
    29         if (!canViewReceivePointerEvents(child)
    30                 || !isTransformedTouchPointInView(x, y, child, null)) {
    31             ev.setTargetAccessibilityFocus(false);
    32             continue;
    33         }
    34 
    35         newTouchTarget = getTouchTarget(child);
    36         if (newTouchTarget != null) {
    37             // Child is already receiving touch within its bounds.
    38             // Give it the new pointer in addition to the ones it is handling.
    39             newTouchTarget.pointerIdBits |= idBitsToAssign;
    40             break;
    41         }
    42 
    43         resetCancelNextUpFlag(child);
    44         if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    45             // Child wants to receive touch within its bounds.
    46             mLastTouchDownTime = ev.getDownTime();
    47             if (preorderedList != null) {
    48                 // childIndex points into presorted list, find original index
    49                 for (int j = 0; j < childrenCount; j++) {
    50                     if (children[childIndex] == mChildren[j]) {
    51                         mLastTouchDownIndex = j;
    52                         break;
    53                     }
    54                 }
    55             } else {
    56                 mLastTouchDownIndex = childIndex;
    57             }
    58             mLastTouchDownX = ev.getX();
    59             mLastTouchDownY = ev.getY();
    60             newTouchTarget = addTouchTarget(child, idBitsToAssign);
    61             alreadyDispatchedToNewTouchTarget = true;
    62             break;
    63         }
    64 
    65         // The accessibility focus didn't handle the event, so clear
    66         // the flag and do a normal dispatch to all children.
    67         ev.setTargetAccessibilityFocus(false);
    68     }
    69     if (preorderedList != null) preorderedList.clear();
    70 }

      这段代码逻辑是这样的,首先遍历ViewGroup的所有子元素,然后判断子元素时候能够接收到点击事件。能否接收到点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某一个子元素满足这两个条件,那么事件就会传递给它。

      我们看代码第44行,dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法。后面会介绍child这个参数时候为null带来的影响。我们来看这段代码:

    1 if (child == null) {
    2     handled = super.dispatchTouchEvent(event);
    3 } else {
    4     handled = child.dispatchTouchEvent(event);
    5 }

      从上面的代码我们看出来,如果child为null那么就直接调用View的dispatchTouchEvent方法,进行事件的处理。如果child!=null就调用child.dispatchTouchEvent方法进行下一轮的事件分发。如果子元素的dispatchTouchEvent方法返回true,那么mFirstTouchTarget会被赋值,并且跳出for循环(第60行代码),代码如下:

    1 newTouchTarget = addTouchTarget(child, idBitsToAssign);
    2 alreadyDispatchedToNewTouchTarget = true;
    3 break;

      如果子元素的dispatchTouchEvent返回false,ViewGroup会把事件分发给下一个子元素进行处理(结合前两段代码)。

      其实mFirstTouchTarget真正的赋值过程是在addTouchTarget方法内部完成的,mFirstTouchTarget是一种单链表结构,其是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中的所有点击事件,这一点在前面已经介绍过。

      如果遍历ViewGroup后事件都没有被合适的处理,那么这包含两种情况,第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回false,这一般是onTouchEvent方法返回false,在这两种情况下,ViewGroup会自己处理点击事件。看下面代码:

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

      看到第三个参数传递的null了吗?上面我们分析过,会调用View的dispatchTouchEvent方法。这时候点击事件就交给View来处理了。

    View对点击事件的处理

      View对点击事件的处理稍微简单一些,注意这里的View不包括ViewGroup。我们来看它的dispatchTouchEvent方法源代码:

     1 /**
     2      * Pass the touch screen motion event down to the target view, or this
     3      * view if it is the target.
     4      *
     5      * @param event The motion event to be dispatched.
     6      * @return True if the event was handled by the view, false otherwise.
     7      */
     8     public boolean dispatchTouchEvent(MotionEvent event) {
     9         // If the event should be handled by accessibility focus first.
    10         if (event.isTargetAccessibilityFocus()) {
    11             // We don't have focus or no virtual descendant has it, do not handle the event.
    12             if (!isAccessibilityFocusedViewOrHost()) {
    13                 return false;
    14             }
    15             // We have focus and got the event, then use normal event dispatch.
    16             event.setTargetAccessibilityFocus(false);
    17         }
    18 
    19         boolean result = false;
    20 
    21         if (mInputEventConsistencyVerifier != null) {
    22             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    23         }
    24 
    25         final int actionMasked = event.getActionMasked();
    26         if (actionMasked == MotionEvent.ACTION_DOWN) {
    27             // Defensive cleanup for new gesture
    28             stopNestedScroll();
    29         }
    30 
    31         if (onFilterTouchEventForSecurity(event)) {
    32             //noinspection SimplifiableIfStatement
    33             ListenerInfo li = mListenerInfo;
    34             if (li != null && li.mOnTouchListener != null
    35                     && (mViewFlags & ENABLED_MASK) == ENABLED
    36                     && li.mOnTouchListener.onTouch(this, event)) {
    37                 result = true;
    38             }
    39 
    40             if (!result && onTouchEvent(event)) {
    41                 result = true;
    42             }
    43         }
    44 
    45         if (!result && mInputEventConsistencyVerifier != null) {
    46             mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    47         }
    48 
    49         // Clean up after nested scrolls if this is the end of a gesture;
    50         // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    51         // of the gesture.
    52         if (actionMasked == MotionEvent.ACTION_UP ||
    53                 actionMasked == MotionEvent.ACTION_CANCEL ||
    54                 (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
    55             stopNestedScroll();
    56         }
    57 
    58         return result;
    59     }

      因为View(这里不包括ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以他只能自己处理事件。从上面的代码中,我们可以看出View对点击事件的处理过程,首先会判断有没有设置OnTouchListener。如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener优先级是高于onTouchEvent的。

      接下来我们来分析onTouchEvent的实现。我们分段来介绍,部分实现如下:

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

      当View处于不可用状态下时View照样会消耗点击事件。如果View设置有代理,那么还会执行TouchDelagate的onTouchEvent方法,这个onTouchEvent的工作机制应该和OnTouchListener类似。代码如下:

    1 if (mTouchDelegate != null) {
    2     if (mTouchDelegate.onTouchEvent(event)) {
    3         return true;
    4     }
    5 }

      下面我们再来看一下onTouchEvent方法对点击事件的具体处理,代码如下:

      1 if (((viewFlags & CLICKABLE) == CLICKABLE ||
      2         (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
      3         (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
      4     switch (action) {
      5         case MotionEvent.ACTION_UP:
      6             boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
      7             if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
      8                 // take focus if we don't have it already and we should in
      9                 // touch mode.
     10                 boolean focusTaken = false;
     11                 if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
     12                     focusTaken = requestFocus();
     13                 }
     14 
     15                 if (prepressed) {
     16                     // The button is being released before we actually
     17                     // showed it as pressed.  Make it show the pressed
     18                     // state now (before scheduling the click) to ensure
     19                     // the user sees it.
     20                     setPressed(true, x, y);
     21                }
     22 
     23                 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
     24                     // This is a tap, so remove the longpress check
     25                     removeLongPressCallback();
     26 
     27                     // Only perform take click actions if we were in the pressed state
     28                     if (!focusTaken) {
     29                         // Use a Runnable and post this rather than calling
     30                         // performClick directly. This lets other visual state
     31                         // of the view update before click actions start.
     32                         if (mPerformClick == null) {
     33                             mPerformClick = new PerformClick();
     34                         }
     35                         if (!post(mPerformClick)) {
     36                             performClick();
     37                         }
     38                     }
     39                 }
     40 
     41                 if (mUnsetPressedState == null) {
     42                     mUnsetPressedState = new UnsetPressedState();
     43                 }
     44 
     45                 if (prepressed) {
     46                     postDelayed(mUnsetPressedState,
     47                             ViewConfiguration.getPressedStateDuration());
     48                 } else if (!post(mUnsetPressedState)) {
     49                     // If the post failed, unpress right now
     50                     mUnsetPressedState.run();
     51                 }
     52 
     53                 removeTapCallback();
     54             }
     55             mIgnoreNextUpEvent = false;
     56             break;
     57 
     58         case MotionEvent.ACTION_DOWN:
     59             mHasPerformedLongPress = false;
     60 
     61             if (performButtonActionOnTouchDown(event)) {
     62                 break;
     63             }
     64 
     65             // Walk up the hierarchy to determine if we're inside a scrolling container.
     66             boolean isInScrollingContainer = isInScrollingContainer();
     67 
     68             // For views inside a scrolling container, delay the pressed feedback for
     69             // a short period in case this is a scroll.
     70             if (isInScrollingContainer) {
     71                 mPrivateFlags |= PFLAG_PREPRESSED;
     72                 if (mPendingCheckForTap == null) {
     73                     mPendingCheckForTap = new CheckForTap();
     74                 }
     75                 mPendingCheckForTap.x = event.getX();
     76                 mPendingCheckForTap.y = event.getY();
     77                 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
     78             } else {
     79                 // Not inside a scrolling container, so show the feedback right away
     80                 setPressed(true, x, y);
     81                 checkForLongClick(0);
     82             }
     83             break;
     84 
     85         case MotionEvent.ACTION_CANCEL:
     86             setPressed(false);
     87             removeTapCallback();
     88             removeLongPressCallback();
     89             mInContextButtonPress = false;
     90             mHasPerformedLongPress = false;
     91             mIgnoreNextUpEvent = false;
     92             break;
     93 
     94         case MotionEvent.ACTION_MOVE:
     95             drawableHotspotChanged(x, y);
     96 
     97             // Be lenient about moving outside of buttons
     98             if (!pointInView(x, y, mTouchSlop)) {
     99                 // Outside button
    100                 removeTapCallback();
    101                 if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
    102                     // Remove any future long press/tap checks
    103                     removeLongPressCallback();
    104 
    105                     setPressed(false);
    106                 }
    107             }
    108             break;
    109     }
    110 
    111     return true;
    112 }

      我们从上面的代码中看到,只要View的CLICKABLE和LONG_CLICKABLE其中有一个true,就会消耗掉事件。即onTouchEvent方法true,不管是不是DISABLE状态。然后就是当ACTION_UP事件发生时,会触发performClick方法。如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。代码如下:

     1 /**
     2  * Call this view's OnClickListener, if it is defined.  Performs all normal
     3  * actions associated with clicking: reporting accessibility event, playing
     4  * a sound, etc.
     5  *
     6  * @return True there was an assigned OnClickListener that was called, false
     7  *         otherwise is returned.
     8  */
     9 public boolean performClick() {
    10     final boolean result;
    11     final ListenerInfo li = mListenerInfo;
    12     if (li != null && li.mOnClickListener != null) {
    13         playSoundEffect(SoundEffectConstants.CLICK);
    14         li.mOnClickListener.onClick(this);
    15         result = true;
    16     } else {
    17         result = false;
    18     }
    19 
    20     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    21     return result;
    22 }

      最后再说一句,调用setOnClickListener和setOnLongClickListener可以改变View的CLICKABLE和LONG_CLICKABLE属性。

      最后再说一句,下一篇文章通过一个简单的例子来介绍滑动冲突。(*^__^*)

  • 相关阅读:
    hdu 5001(概率DP)
    hdu 5505(数论-gcd的应用)
    csu 1749: Soldiers ' Training(贪心)
    Button Bashing(搜索)
    Jury Jeopardy(反向模拟)
    interesting Integers(数学暴力||数论扩展欧几里得)
    湖南省第六届省赛题 Biggest Number (dfs+bfs,好题)
    csu 1551(线段树+DP)
    csu 1555(线段树经典插队模型-根据逆序数还原序列)
    csu 1552(米勒拉宾素数测试+二分图匹配)
  • 原文地址:https://www.cnblogs.com/dreamGong/p/6378765.html
Copyright © 2011-2022 走看看