Android触摸屏事件派发机制详解与源码分析

来源:互联网 发布:淘宝宝贝描述低 编辑:程序博客网 时间:2024/04/28 07:03


    转载:    http://blog.csdn.net/yanbober/article/details/45887547

Android触摸屏事件派发机制详解与源码分析一(View篇)


1 背景

最近在简书和微博还有Q群看见很多人说Android自定义控件(View/ViewGroup)如何学习?为啥那么难?其实答案很简单:“基础不牢,地动山摇。”

不扯蛋了,进入正题。就算你不自定义控件,你也必须要了解Android控件的触摸屏事件传递机制(之所以说触摸屏是因为该系列以触摸屏的事件机制分析为主,对于类似TV设备等的物理事件机制的分析雷同但有区别。哈哈,谁让我之前是做Android TV BOX的,悲催!),只有这样才能将你的控件事件运用的如鱼得水。接下来的控件触摸屏事件传递机制分析依据Android 5.1.1源码(API 22)。

2 基础实例现象

2-1 例子

从一个例子分析说起吧。如下是一个很简单不过的Android实例:
这里写图片描述

<code class="language-xml hljs  has-numbering"><span class="hljs-pi"><?xml version="1.0" encoding="utf-8"?></span><span class="hljs-tag"><<span class="hljs-title">LinearLayout</span> <span class="hljs-attribute">xmlns:android</span>=<span class="hljs-value">"http://schemas.android.com/apk/res/android"</span>    <span class="hljs-attribute">android:orientation</span>=<span class="hljs-value">"vertical"</span>    <span class="hljs-attribute">android:gravity</span>=<span class="hljs-value">"center"</span>    <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"fill_parent"</span>    <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"fill_parent"</span>    <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/mylayout"</span>></span>    <span class="hljs-tag"><<span class="hljs-title">Button</span>        <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/my_btn"</span>        <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"match_parent"</span>        <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"wrap_content"</span>        <span class="hljs-attribute">android:text</span>=<span class="hljs-value">"click test"</span>/></span><span class="hljs-tag"></<span class="hljs-title">LinearLayout</span>></span></code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li></ul>
<code class="language-android hljs java has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ListenerActivity</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Activity</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">View</span>.<span class="hljs-title">OnTouchListener</span>, <span class="hljs-title">View</span>.<span class="hljs-title">OnClickListener</span> {</span>    <span class="hljs-keyword">private</span> LinearLayout mLayout;    <span class="hljs-keyword">private</span> Button mButton;    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span>(Bundle savedInstanceState) {        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState);        setContentView(R.layout.main);        mLayout = (LinearLayout) <span class="hljs-keyword">this</span>.findViewById(R.id.mylayout);        mButton = (Button) <span class="hljs-keyword">this</span>.findViewById(R.id.my_btn);        mLayout.setOnTouchListener(<span class="hljs-keyword">this</span>);        mButton.setOnTouchListener(<span class="hljs-keyword">this</span>);        mLayout.setOnClickListener(<span class="hljs-keyword">this</span>);        mButton.setOnClickListener(<span class="hljs-keyword">this</span>);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouch</span>(View v, MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"OnTouchListener--onTouch-- action="</span>+event.getAction()+<span class="hljs-string">" --"</span>+v);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onClick</span>(View v) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"OnClickListener--onClick--"</span>+v);    }}</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li></ul>

2-2 现象

如上代码很简单,但凡学过几天Android的人都能看懂吧。Activity中有一个LinearLayout(ViewGroup的子类,ViewGroup是View的子类)布局,布局中包含一个按钮(View的子类);然后分别对这两个控件设置了Touch与Click的监听事件,具体运行结果如下:

  1. 当稳稳的点击Button时打印如下:
    这里写图片描述
  2. 当稳稳的点击除过Button以外的其他地方时打印如下:
    这里写图片描述
  3. 当收指点击Button时按在Button上晃动了一下松开后的打印如下:
    这里写图片描述

机智的你看完这个结果指定知道为啥吧?
我们看下onTouch和onClick,从参数都能看出来onTouch比onClick强大灵活,毕竟多了一个event参数。这样onTouch里就可以处理ACTION_DOWN、ACTION_UP、ACTION_MOVE等等的各种触摸。现在来分析下上面的打印结果;在1中,当我们点击Button时会先触发onTouch事件(之所以打印action为0,1各一次是因为按下抬起两个触摸动作被触发)然后才触发onClick事件;在2中也同理类似1;在3中会发现onTouch被多次调运后才调运onClick,是因为手指晃动了,所以触发了ACTION_DOWN->ACTION_MOVE…->ACTION_UP。

如果你眼睛比较尖你会看见onTouch会有一个返回值,而且在上面返回了false。你可能会疑惑这个返回值有啥效果?那就验证一下吧,我们将上面的onTouch返回值改为ture。如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouch</span>(View v, MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"OnTouchListener--onTouch-- action="</span>+event.getAction()+<span class="hljs-string">" --"</span>+v);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>

再次点击Button结果如下:
这里写图片描述
看见了吧,如果onTouch返回true则onClick不会被调运了。

2-3 总结结论

好了,经过这个简单的实例验证你可以总结发现:

  1. Android控件的Listener事件触发顺序是先触发onTouch,其次onClick。
  2. 如果控件的onTouch返回true将会阻止事件继续传递,返回false事件会继续传递。

对于伸手党码农来说其实到这足矣应付常规的App事件监听处理使用开发了,但是对于复杂的事件监听处理或者想自定义控件的码农来说这才是刚刚开始,只是个热身。既然这样那就继续喽。。。

3 Android 5.1.1(API 22) View触摸屏事件传递源码分析

3-1 写在前面的话

其实Android源码无论哪个版本对于触摸屏事件的传递机制都类似,这里只是选用了目前最新版本的源码来分析而已。分析Android View事件传递机制之前有必要先看下源码的一些关系,如下是几个继承关系图:
这里写图片描述
这里写图片描述

怎么样?看了官方这个继承图是不是明白了上面例子中说的LinearLayout是ViewGroup的子类,ViewGroup是View的子类,Button是View的子类关系呢?其实,在Android中所有的控件无非都是ViewGroup或者View的子类,说高尚点就是所有控件都是View的子类。

这里通过继承关系是说明一切控件都是View,同时View与ViewGroup又存在一些区别,所以该模块才只单单先分析View触摸屏事件传递机制。

3-2 从View的dispatchTouchEvent方法说起

在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法(其实这个方法一般都没在具体的控件类中,而在他的父类View中),所以我们先来看下View的dispatchTouchEvent方法,如下:

<code class="language-android hljs cs has-numbering">    <span class="hljs-keyword">public</span> boolean <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent <span class="hljs-keyword">event</span>) {        <span class="hljs-comment">// If the event should be handled by accessibility focus first.</span>        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">event</span>.isTargetAccessibilityFocus()) {            <span class="hljs-comment">// We don't have focus or no virtual descendant has it, do not handle the event.</span>            <span class="hljs-keyword">if</span> (!isAccessibilityFocusedViewOrHost()) {                <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;            }            <span class="hljs-comment">// We have focus and got the event, then use normal event dispatch.</span>            <span class="hljs-keyword">event</span>.setTargetAccessibilityFocus(<span class="hljs-keyword">false</span>);        }        boolean result = <span class="hljs-keyword">false</span>;        <span class="hljs-keyword">if</span> (mInputEventConsistencyVerifier != <span class="hljs-keyword">null</span>) {            mInputEventConsistencyVerifier.onTouchEvent(<span class="hljs-keyword">event</span>, <span class="hljs-number">0</span>);        }        final <span class="hljs-keyword">int</span> actionMasked = <span class="hljs-keyword">event</span>.getActionMasked();        <span class="hljs-keyword">if</span> (actionMasked == MotionEvent.ACTION_DOWN) {            <span class="hljs-comment">// Defensive cleanup for new gesture</span>            stopNestedScroll();        }        <span class="hljs-keyword">if</span> (onFilterTouchEventForSecurity(<span class="hljs-keyword">event</span>)) {            <span class="hljs-comment">//noinspection SimplifiableIfStatement</span>            ListenerInfo li = mListenerInfo;            <span class="hljs-keyword">if</span> (li != <span class="hljs-keyword">null</span> && li.mOnTouchListener != <span class="hljs-keyword">null</span>                    && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(<span class="hljs-keyword">this</span>, <span class="hljs-keyword">event</span>)) {                result = <span class="hljs-keyword">true</span>;            }            <span class="hljs-keyword">if</span> (!result && onTouchEvent(<span class="hljs-keyword">event</span>)) {                result = <span class="hljs-keyword">true</span>;            }        }        <span class="hljs-keyword">if</span> (!result && mInputEventConsistencyVerifier != <span class="hljs-keyword">null</span>) {            mInputEventConsistencyVerifier.onUnhandledEvent(<span class="hljs-keyword">event</span>, <span class="hljs-number">0</span>);        }        <span class="hljs-comment">// Clean up after nested scrolls if this is the end of a gesture;</span>        <span class="hljs-comment">// also cancel it if we tried an ACTION_DOWN but we didn't want the rest</span>        <span class="hljs-comment">// of the gesture.</span>        <span class="hljs-keyword">if</span> (actionMasked == MotionEvent.ACTION_UP ||                actionMasked == MotionEvent.ACTION_CANCEL ||                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {            stopNestedScroll();        }        <span class="hljs-keyword">return</span> result;    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li><li>49</li><li>50</li><li>51</li><li>52</li></ul>

dispatchTouchEvent的代码有点长,咱们看重点就可以。前面都是设置一些标记和处理input与手势等传递,到24行的if (onFilterTouchEventForSecurity(event))语句判断当前View是否没被遮住等,接着26行定义ListenerInfo局部变量,ListenerInfo是View的静态内部类,用来定义一堆关于View的XXXListener等方法;接着if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句就是重点,首先li对象自然不会为null,li.mOnTouchListener呢?你会发现ListenerInfo的mOnTouchListener成员是在哪儿赋值的呢?怎么确认他是不是null呢?通过在View类里搜索可以看到:

<code class="language-java hljs  has-numbering">    <span class="hljs-javadoc">/**     * Register a callback to be invoked when a touch event is sent to this view.     *<span class="hljs-javadoctag"> @param</span> l the touch listener to attach to this view     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setOnTouchListener</span>(OnTouchListener l) {        getListenerInfo().mOnTouchListener = l;    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul>

li.mOnTouchListener是不是null取决于控件(View)是否设置setOnTouchListener监听,在上面的实例中我们是设置过Button的setOnTouchListener方法的,所以也不为null;接着通过位与运算确定控件(View)是不是ENABLED 的,默认控件都是ENABLED 的;接着判断onTouch的返回值是不是true。通过如上判断之后如果都为true则设置默认为false的result为true,那么接下来的if (!result && onTouchEvent(event))就不会执行,最终dispatchTouchEvent也会返回true。而如果if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句有一个为false则if (!result && onTouchEvent(event))就会执行,如果onTouchEvent(event)返回false则dispatchTouchEvent返回false,否则返回true。

这下再看前面的实例部分明白了吧?控件触摸就会调运dispatchTouchEvent方法,而在dispatchTouchEvent中先执行的是onTouch方法,所以验证了实例结论总结中的onTouch优先于onClick执行道理。如果控件是ENABLE且在onTouch方法里返回了true则dispatchTouchEvent方法也返回true,不会再继续往下执行;反之,onTouch返回false则会继续向下执行onTouchEvent方法,且dispatchTouchEvent的返回值与onTouchEvent返回值相同。

所以依据这个结论和上面实例打印结果你指定已经大胆猜测认为onClick一定与onTouchEvent有关系?是不是呢?先告诉你,是的。下面我们会分析。

3-2-1 总结结论

在View的触摸屏传递机制中通过分析dispatchTouchEvent方法源码我们会得出如下基本结论:

  1. 触摸控件(View)首先执行dispatchTouchEvent方法。
  2. 在dispatchTouchEvent方法中先执行onTouch方法,后执行onClick方法(onClick方法在onTouchEvent中执行,下面会分析)。
  3. 如果控件(View)的onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的情况下会调运onTouchEvent,dispatchTouchEvent返回值与onTouchEvent返回一样。
  4. 如果控件不是enable的设置了onTouch方法也不会执行,只能通过重写控件的onTouchEvent方法处理(上面已经处理分析了),dispatchTouchEvent返回值与onTouchEvent返回一样。
  5. 如果控件(View)是enable且onTouch返回true情况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent方法。

上面说了onClick一定与onTouchEvent有关系,那么接下来就分析分析dispatchTouchEvent方法中的onTouchEvent方法。

3-3 继续说说View的dispatchTouchEvent方法中调运的onTouchEvent方法

上面说了dispatchTouchEvent方法中如果onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的情况下会调运onTouchEvent,所以接着看就知道了,如下:

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

我勒个去!一个方法比一个方法代码多。好吧,那咱们继续只挑重点来说明呗。

首先地6到14行可以看出,如果控件(View)是disenable状态,同时是可以clickable的则onTouchEvent直接消费事件返回true,反之如果控件(View)是disenable状态,同时是disclickable的则onTouchEvent直接false。多说一句,关于控件的enable或者clickable属性可以通过java或者xml直接设置,每个view都有这些属性。

接着22行可以看见,如果一个控件是enable且disclickable则onTouchEvent直接返回false了;反之,如果一个控件是enable且clickable则继续进入过于一个event的switch判断中,然后最终onTouchEvent都返回了true。switch的ACTION_DOWN与ACTION_MOVE都进行了一些必要的设置与置位,接着到手抬起来ACTION_UP时你会发现,首先判断了是否按下过,同时是不是可以得到焦点,然后尝试获取焦点,然后判断如果不是longPressed则通过post在UI Thread中执行一个PerformClick的Runnable,也就是performClick方法。具体如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">performClick</span>() {        <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> result;        <span class="hljs-keyword">final</span> ListenerInfo li = mListenerInfo;        <span class="hljs-keyword">if</span> (li != <span class="hljs-keyword">null</span> && li.mOnClickListener != <span class="hljs-keyword">null</span>) {            playSoundEffect(SoundEffectConstants.CLICK);            li.mOnClickListener.onClick(<span class="hljs-keyword">this</span>);            result = <span class="hljs-keyword">true</span>;        } <span class="hljs-keyword">else</span> {            result = <span class="hljs-keyword">false</span>;        }        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);        <span class="hljs-keyword">return</span> result;    }</code>

这个方法也是先定义一个ListenerInfo的变量然后赋值,接着判断li.mOnClickListener是不是为null,决定执行不执行onClick。你指定现在已经很机智了,和onTouch一样,搜一下mOnClickListener在哪赋值的呗,结果发现:

<code class="language-java hljs  has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setOnClickListener</span>(OnClickListener l) {        <span class="hljs-keyword">if</span> (!isClickable()) {            setClickable(<span class="hljs-keyword">true</span>);        }        getListenerInfo().mOnClickListener = l;    }</code>

看见了吧!控件只要监听了onClick方法则mOnClickListener就不为null,而且有意思的是如果调运setOnClickListener方法设置监听且控件是disclickable的情况下默认会帮设置为clickable。

我勒个去!!!惊讶吧!!!猜的没错onClick就在onTouchEvent中执行的,而且是在onTouchEvent的ACTION_UP事件中执行的。

3-3-1 总结结论

  1. onTouchEvent方法中会在ACTION_UP分支中触发onClick的监听。
  2. 当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action。

到此上面例子中关于Button点击的各种打印的真实原因都找到了可靠的证据,也就是说View的触摸屏事件传递机制其实也就这么回事。

4 透过源码继续进阶实例验证

其实上面分析完View的触摸传递机制之后已经足够用了。如下的实例验证可以说是加深阅读源码的理解,还有一个主要作用就是为将来自定义控件打下坚实基础。因为自定义控件中时常会与这几个方法打交道。

4-1 例子

我们自定义一个Button(Button实质继承自View),如下:

<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Button</span> {</span>    <span class="hljs-keyword">public</span> <span class="hljs-title">TestButton</span>(Context context, AttributeSet attrs) {        <span class="hljs-keyword">super</span>(context, attrs);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"dispatchTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"onTouchEvent-- action="</span>+event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.onTouchEvent(event);    }}</code>

其他代码如下:

<code class="language-xml hljs  has-numbering"><span class="hljs-pi"><?xml version="1.0" encoding="utf-8"?></span><span class="hljs-tag"><<span class="hljs-title">LinearLayout</span> <span class="hljs-attribute">xmlns:android</span>=<span class="hljs-value">"http://schemas.android.com/apk/res/android"</span>    <span class="hljs-attribute">android:orientation</span>=<span class="hljs-value">"vertical"</span>    <span class="hljs-attribute">android:gravity</span>=<span class="hljs-value">"center"</span>    <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"fill_parent"</span>    <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"fill_parent"</span>    <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/mylayout"</span>></span>    <span class="hljs-tag"><<span class="hljs-title">com.zzci.light.TestButton</span>        <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/my_btn"</span>        <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"match_parent"</span>        <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"wrap_content"</span>        <span class="hljs-attribute">android:text</span>=<span class="hljs-value">"click test"</span>/></span><span class="hljs-tag"></<span class="hljs-title">LinearLayout</span>></span></code>
<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ListenerActivity</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Activity</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">View</span>.<span class="hljs-title">OnTouchListener</span>, <span class="hljs-title">View</span>.<span class="hljs-title">OnClickListener</span> {</span>    <span class="hljs-keyword">private</span> LinearLayout mLayout;    <span class="hljs-keyword">private</span> TestButton mButton;    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span>(Bundle savedInstanceState) {        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState);        setContentView(R.layout.main);        mLayout = (LinearLayout) <span class="hljs-keyword">this</span>.findViewById(R.id.mylayout);        mButton = (TestButton) <span class="hljs-keyword">this</span>.findViewById(R.id.my_btn);        mLayout.setOnTouchListener(<span class="hljs-keyword">this</span>);        mButton.setOnTouchListener(<span class="hljs-keyword">this</span>);        mLayout.setOnClickListener(<span class="hljs-keyword">this</span>);        mButton.setOnClickListener(<span class="hljs-keyword">this</span>);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouch</span>(View v, MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"OnTouchListener--onTouch-- action="</span>+event.getAction()+<span class="hljs-string">" --"</span>+v);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onClick</span>(View v) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"OnClickListener--onClick--"</span>+v);    }}</code>

其实这段代码只是对上面例子中的Button换为了自定义Button而已。

4-2 现象分析

4-2-1 点击Button(手抽筋了一下)

这里写图片描述

可以发现,如上打印完全符合源码分析结果,dispatchTouchEvent方法先派发down事件,完事调运onTouch,完事调运onTouchEvent返回true,同时dispatchTouchEvent返回true,然后dispatchTouchEvent继续派发move或者up事件,循环,直到onTouchEvent处理up事件时调运onClick事件,完事返回true,同时dispatchTouchEvent返回true;一次完整的View事件派发流程结束。

4-2-2 简单修改onTouchEvent返回值为true

将TestButton类的onTouchEvent方法修改如下,其他和基础代码保持不变:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"onTouchEvent-- action="</span>+event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;    }</code>

点击Button打印如下:
这里写图片描述

可以发现,当自定义了控件(View)的onTouchEvent直接返回true而不调运super方法时,事件派发机制如同4.2.1类似,只是最后up事件没有触发onClick而已(因为没有调用super)。

所以可想而知,如果TestButton类的onTouchEvent修改为如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"onTouchEvent-- action="</span>+event.getAction());        <span class="hljs-keyword">super</span>.onTouchEvent(event);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;    }</code>

点击Button如下:
这里写图片描述

整个派发机制和4.2.1完全类似。

4-2-3 简单修改onTouchEvent返回值为false

将TestButton类的onTouchEvent方法修改如下,其他和基础代码保持不变:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"onTouchEvent-- action="</span>+event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }</code>

点击Button如下:
这里写图片描述
你会发现如果onTouchEvent返回false(也即dispatchTouchEvent一旦返回false将不再继续派发其他action,立即停止派发),这里只派发了down事件。至于后面触发了LinearLayout的touch与click事件我们这里不做关注,下一篇博客会详细解释为啥(其实你可以想下的,LinearLayout是ViewGroup的子类,你懂的),这里你只用知道View的onTouchEvent返回false会阻止继续派发事件。

同理修改如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"onTouchEvent-- action="</span>+event.getAction());        <span class="hljs-keyword">super</span>.onTouchEvent(event);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }</code>

点击Button如下:
这里写图片描述

4-2-4 简单修改dispatchTouchEvent返回值为true

将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"dispatchTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;    }</code>

点击Button如下:
这里写图片描述

你会发现如果dispatchTouchEvent直接返回true且不调运super任何事件都得不到触发。

继续修改如下呢?
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"dispatchTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;    }</code>

点击Button如下:
这里写图片描述

可以发现所有事件都可以得到正常派发,和4.2.1类似。

4-2-5 简单修改dispatchTouchEvent返回值为false

将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"dispatchTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }</code>

点击Button如下:
这里写图片描述

你会发现事件不进行任何继续触发,关于点击Button触发了LinearLayout的事件暂时不用关注,下篇详解。

继续修改如下呢?
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"dispatchTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }</code>

点击Button如下:
这里写图片描述
你会发现结果和4.2.3的第二部分结果一样,也就是说如果dispatchTouchEvent返回false事件将不再继续派发下一次。

4-2-6 简单修改dispatchTouchEvent与onTouchEvent返回值

修改dispatchTouchEvent返回值为true,onTouchEvent为false:

将TestButton类的dispatchTouchEvent方法和onTouchEvent方法修改如下,其他和基础代码保持不变:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"dispatchTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"onTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">super</span>.onTouchEvent(event);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }</code>

点击Button如下:
这里写图片描述

修改dispatchTouchEvent返回值为false,onTouchEvent为true:

将TestButton类的dispatchTouchEvent方法和onTouchEvent方法修改如下,其他和基础代码保持不变:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"dispatchTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"onTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">super</span>.onTouchEvent(event);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;    }</code>

点击Button如下:
这里写图片描述

由此对比得出结论,dispatchTouchEvent事件派发是传递的,如果返回值为false将停止下次事件派发,如果返回true将继续下次派发。譬如,当前派发down事件,如果返回true则继续派发up,如果返回false派发完down就停止了。

4-1 总结

这个例子组合了很多种情况的值去验证上面源码的分析,同时也为自定义控件打下了基础。仔细理解这个例子对于View的事件传递就差不多了。

5 总结View触摸屏事件传递机制

上面例子也测试了,源码也分析了,总得有个最终结论方便平时写代码作为参考依据呀,不能每次都再去分析一遍源码,那得多蛋疼呢!

综合得出Android View的触摸屏事件传递机制有如下特征:

  1. 触摸控件(View)首先执行dispatchTouchEvent方法。
  2. 在dispatchTouchEvent方法中先执行onTouch方法,后执行onClick方法(onClick方法在onTouchEvent中执行,下面会分析)。
  3. 如果控件(View)的onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的情况下会调运onTouchEvent,dispatchTouchEvent返回值与onTouchEvent返回一样。
  4. 如果控件不是enable的设置了onTouch方法也不会执行,只能通过重写控件的onTouchEvent方法处理(上面已经处理分析了),dispatchTouchEvent返回值与onTouchEvent返回一样。
  5. 如果控件(View)是enable且onTouch返回true情况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent方法。
  6. 当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action(也就是说dispatchTouchEvent返回true才会进行下一次action派发)。




Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)


1 背景

还记得前一篇《Android触摸屏事件派发机制详解与源码分析一(View篇)》中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事件疑惑吗?当时说了,在那一篇咱们只讨论View的触摸事件派发机制,这个疑惑留在了这一篇解释,也就是ViewGroup的事件派发机制。

PS:阅读本篇前建议先查看前一篇《Android触摸屏事件派发机制详解与源码分析一(View篇)》,这一篇承接上一篇。

关于View与ViewGroup的区别在前一篇的Android 5.1.1(API 22) View触摸屏事件传递源码分析部分的写在前面的话里面有详细介绍。其实你只要记住类似Button这种控件都是View的子类,类似布局这种控件都是ViewGroup的子类,而ViewGroup又是View的子类而已。具体查阅《Android触摸屏事件派发机制详解与源码分析一(View篇)》。

2 基础实例现象

2-1 例子

这个例子布局等还和上一篇的例子相似,只是重写了Button和LinearLayout而已,所以效果图不在提供,具体参见上一篇。

首先我们简单的自定义一个Button(View的子类),再自定义一个LinearLayout(ViewGroup的子类),其实没有自定义任何属性,只是重写部分方法(添加了打印,方便查看)而已,如下:

<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Button</span> {</span>    <span class="hljs-keyword">public</span> <span class="hljs-title">TestButton</span>(Context context, AttributeSet attrs) {        <span class="hljs-keyword">super</span>(context, attrs);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestButton dispatchTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestButton onTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.onTouchEvent(event);    }}</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li></ul>
<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestLinearLayout</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">LinearLayout</span> {</span>    <span class="hljs-keyword">public</span> <span class="hljs-title">TestLinearLayout</span>(Context context, AttributeSet attrs) {        <span class="hljs-keyword">super</span>(context, attrs);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onInterceptTouchEvent</span>(MotionEvent ev) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestLinearLayout onInterceptTouchEvent-- action="</span> + ev.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.onInterceptTouchEvent(ev);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestLinearLayout dispatchTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestLinearLayout onTouchEvent-- action="</span> + event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.onTouchEvent(event);    }}</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li></ul>

如上两个控件很简单吧,不解释,继续看其他代码:

<code class="language-xml hljs  has-numbering"><span class="hljs-pi"><?xml version="1.0" encoding="utf-8"?></span><span class="hljs-tag"><<span class="hljs-title">com.zzci.light.TestLinearLayout</span> <span class="hljs-attribute">xmlns:android</span>=<span class="hljs-value">"http://schemas.android.com/apk/res/android"</span>    <span class="hljs-attribute">android:orientation</span>=<span class="hljs-value">"vertical"</span>    <span class="hljs-attribute">android:gravity</span>=<span class="hljs-value">"center"</span>    <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"fill_parent"</span>    <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"fill_parent"</span>    <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/mylayout"</span>></span>    <span class="hljs-tag"><<span class="hljs-title">com.zzci.light.TestButton</span>        <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/my_btn"</span>        <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"match_parent"</span>        <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"wrap_content"</span>        <span class="hljs-attribute">android:text</span>=<span class="hljs-value">"click test"</span>/></span><span class="hljs-tag"></<span class="hljs-title">com.zzci.light.TestLinearLayout</span>></span></code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li></ul>
<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ListenerActivity</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Activity</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">View</span>.<span class="hljs-title">OnTouchListener</span>, <span class="hljs-title">View</span>.<span class="hljs-title">OnClickListener</span> {</span>    <span class="hljs-keyword">private</span> TestLinearLayout mLayout;    <span class="hljs-keyword">private</span> TestButton mButton;    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span>(Bundle savedInstanceState) {        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState);        setContentView(R.layout.main);        mLayout = (TestLinearLayout) <span class="hljs-keyword">this</span>.findViewById(R.id.mylayout);        mButton = (TestButton) <span class="hljs-keyword">this</span>.findViewById(R.id.my_btn);        mLayout.setOnTouchListener(<span class="hljs-keyword">this</span>);        mButton.setOnTouchListener(<span class="hljs-keyword">this</span>);        mLayout.setOnClickListener(<span class="hljs-keyword">this</span>);        mButton.setOnClickListener(<span class="hljs-keyword">this</span>);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouch</span>(View v, MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"OnTouchListener--onTouch-- action="</span>+event.getAction()+<span class="hljs-string">" --"</span>+v);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onClick</span>(View v) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"OnClickListener--onClick--"</span>+v);    }}</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li></ul>

到此基础示例的代码编写完成。没有啥难度,很简单易懂,不多解释了。

2-2 运行现象

当直接点击Button时打印现象如下:

<code class="language-txt hljs brainfuck has-numbering"><span class="hljs-comment">TestLinearLayout</span> <span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=0</span><span class="hljs-comment">TestLinearLayout</span> <span class="hljs-comment">onInterceptTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=0</span><span class="hljs-comment">TestButton</span> <span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=0</span><span class="hljs-comment">OnTouchListener</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onTouch</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=0</span> <span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">com</span><span class="hljs-string">.</span><span class="hljs-comment">zzci</span><span class="hljs-string">.</span><span class="hljs-comment">light</span><span class="hljs-string">.</span><span class="hljs-comment">TestButton</span><span class="hljs-comment">TestButton</span> <span class="hljs-comment">onTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=0</span><span class="hljs-comment">TestLinearLayout</span> <span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=1</span><span class="hljs-comment">TestLinearLayout</span> <span class="hljs-comment">onInterceptTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=1</span><span class="hljs-comment">TestButton</span> <span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=1</span><span class="hljs-comment">OnTouchListener</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onTouch</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=1</span> <span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">com</span><span class="hljs-string">.</span><span class="hljs-comment">zzci</span><span class="hljs-string">.</span><span class="hljs-comment">light</span><span class="hljs-string">.</span><span class="hljs-comment">TestButton</span><span class="hljs-comment">TestButton</span> <span class="hljs-comment">onTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=1</span><span class="hljs-comment">OnClickListener</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onClick</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">com</span><span class="hljs-string">.</span><span class="hljs-comment">zzci</span><span class="hljs-string">.</span><span class="hljs-comment">light</span><span class="hljs-string">.</span><span class="hljs-comment">TestButton</span></code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li></ul>

分析:你会发现这个结果好惊讶吧,点击了Button却先执行了TestLinearLayout(ViewGroup)的dispatchTouchEvent,接着执行TestLinearLayout(ViewGroup)的onInterceptTouchEvent,接着执行TestButton(TestLinearLayout包含的成员View)的dispatchTouchEvent,接着就是View触摸事件的分发流程,上一篇已经讲过了。也就是说当点击View时事件派发每一个down,up的action顺序是先触发最父级控件(这里为LinearLayout)的dispatchTouchEvent->onInterceptTouchEvent->然后向前一级传递(这里就是传递到Button View)。

那么继续看,当直接点击除Button以外的其他部分时打印如下:

<code class="language-txt hljs brainfuck has-numbering"><span class="hljs-comment">TestLinearLayout</span> <span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=0</span><span class="hljs-comment">TestLinearLayout</span> <span class="hljs-comment">onInterceptTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=0</span><span class="hljs-comment">OnTouchListener</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onTouch</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=0</span> <span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">com</span><span class="hljs-string">.</span><span class="hljs-comment">zzci</span><span class="hljs-string">.</span><span class="hljs-comment">light</span><span class="hljs-string">.</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-comment">TestLinearLayout</span> <span class="hljs-comment">onTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=0</span><span class="hljs-comment">TestLinearLayout</span> <span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=1</span><span class="hljs-comment">OnTouchListener</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onTouch</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=1</span> <span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">com</span><span class="hljs-string">.</span><span class="hljs-comment">zzci</span><span class="hljs-string">.</span><span class="hljs-comment">light</span><span class="hljs-string">.</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-comment">TestLinearLayout</span> <span class="hljs-comment">onTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span> <span class="hljs-comment">action=1</span><span class="hljs-comment">OnClickListener</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onClick</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">com</span><span class="hljs-string">.</span><span class="hljs-comment">zzci</span><span class="hljs-string">.</span><span class="hljs-comment">light</span><span class="hljs-string">.</span><span class="hljs-comment">TestLinearLayout</span></code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li></ul>

分析:你会发现一个奇怪的现象,派发ACTION_DOWN(action=0)事件时顺序为dispatchTouchEvent->onInterceptTouchEvent->onTouch->onTouchEvent,而接着派发ACTION_UP(action=1)事件时与上面顺序不同的时竟然没触发onInterceptTouchEvent方法。这是为啥呢?我也纳闷,那就留着下面分析源码再找答案吧,先记住这个问题。

有了上面这个例子你是不是发现包含ViewGroup与View的事件触发有些相似又有很大差异吧(PS:在Android中继承View实现的控件已经是最小单位了,也即在XML布局等操作中不能再包含子项了,而继承ViewGroup实现的控件通常不是最小单位,可以包含不确定数目的子项)。具体差异是啥呢?咱们类似上篇一样,带着这个实例疑惑去看源码找答案吧。

3 Android 5.1.1(API 22) ViewGroup触摸屏事件传递源码分析

通过上面例子的打印我们可以确定分析源码的顺序,那就开始分析呗。

3-1 从ViewGroup的dispatchTouchEvent方法说起

前一篇的3-2小节说在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法(其实这个方法一般都没在具体的控件类中,而在他的父类View中)。这其实是思维单单局限在View的角度去看待的,这里通过上面的例子你是否发现触摸控件会先从他的父级dispatchTouchEvent方法开始派发呢?是的,所以咱们先从ViewGroup的dispatchTouchEvent方法说起,如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent ev) {        <span class="hljs-keyword">if</span> (mInputEventConsistencyVerifier != <span class="hljs-keyword">null</span>) {            mInputEventConsistencyVerifier.onTouchEvent(ev, <span class="hljs-number">1</span>);        }        <span class="hljs-comment">// If the event targets the accessibility focused view and this is it, start</span>        <span class="hljs-comment">// normal event dispatch. Maybe a descendant is what will handle the click.</span>        <span class="hljs-keyword">if</span> (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {            ev.setTargetAccessibilityFocus(<span class="hljs-keyword">false</span>);        }        <span class="hljs-keyword">boolean</span> handled = <span class="hljs-keyword">false</span>;        <span class="hljs-keyword">if</span> (onFilterTouchEventForSecurity(ev)) {            <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> action = ev.getAction();            <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> actionMasked = action & MotionEvent.ACTION_MASK;            <span class="hljs-comment">// Handle an initial down.</span>            <span class="hljs-keyword">if</span> (actionMasked == MotionEvent.ACTION_DOWN) {                <span class="hljs-comment">// Throw away all previous state when starting a new touch gesture.</span>                <span class="hljs-comment">// The framework may have dropped the up or cancel event for the previous gesture</span>                <span class="hljs-comment">// due to an app switch, ANR, or some other state change.</span>                cancelAndClearTouchTargets(ev);                resetTouchState();            }            <span class="hljs-comment">// Check for interception.</span>            <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> intercepted;            <span class="hljs-keyword">if</span> (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != <span class="hljs-keyword">null</span>) {                <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != <span class="hljs-number">0</span>;                <span class="hljs-keyword">if</span> (!disallowIntercept) {                    intercepted = onInterceptTouchEvent(ev);                    ev.setAction(action); <span class="hljs-comment">// restore action in case it was changed</span>                } <span class="hljs-keyword">else</span> {                    intercepted = <span class="hljs-keyword">false</span>;                }            } <span class="hljs-keyword">else</span> {                <span class="hljs-comment">// There are no touch targets and this action is not an initial down</span>                <span class="hljs-comment">// so this view group continues to intercept touches.</span>                intercepted = <span class="hljs-keyword">true</span>;            }            <span class="hljs-comment">// If intercepted, start normal event dispatch. Also if there is already</span>            <span class="hljs-comment">// a view that is handling the gesture, do normal event dispatch.</span>            <span class="hljs-keyword">if</span> (intercepted || mFirstTouchTarget != <span class="hljs-keyword">null</span>) {                ev.setTargetAccessibilityFocus(<span class="hljs-keyword">false</span>);            }            <span class="hljs-comment">// Check for cancelation.</span>            <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> canceled = resetCancelNextUpFlag(<span class="hljs-keyword">this</span>)                    || actionMasked == MotionEvent.ACTION_CANCEL;            <span class="hljs-comment">// Update list of touch targets for pointer down, if needed.</span>            <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != <span class="hljs-number">0</span>;            TouchTarget newTouchTarget = <span class="hljs-keyword">null</span>;            <span class="hljs-keyword">boolean</span> alreadyDispatchedToNewTouchTarget = <span class="hljs-keyword">false</span>;            <span class="hljs-keyword">if</span> (!canceled && !intercepted) {                <span class="hljs-comment">// If the event is targeting accessiiblity focus we give it to the</span>                <span class="hljs-comment">// view that has accessibility focus and if it does not handle it</span>                <span class="hljs-comment">// we clear the flag and dispatch the event to all children as usual.</span>                <span class="hljs-comment">// We are looking up the accessibility focused host to avoid keeping</span>                <span class="hljs-comment">// state since these events are very rare.</span>                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()                        ? findChildWithAccessibilityFocus() : <span class="hljs-keyword">null</span>;                <span class="hljs-keyword">if</span> (actionMasked == MotionEvent.ACTION_DOWN                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {                    <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> actionIndex = ev.getActionIndex(); <span class="hljs-comment">// always 0 for down</span>                    <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> idBitsToAssign = split ? <span class="hljs-number">1</span> << ev.getPointerId(actionIndex)                            : TouchTarget.ALL_POINTER_IDS;                    <span class="hljs-comment">// Clean up earlier touch targets for this pointer id in case they</span>                    <span class="hljs-comment">// have become out of sync.</span>                    removePointersFromTouchTargets(idBitsToAssign);                    <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> childrenCount = mChildrenCount;                    <span class="hljs-keyword">if</span> (newTouchTarget == <span class="hljs-keyword">null</span> && childrenCount != <span class="hljs-number">0</span>) {                        <span class="hljs-keyword">final</span> <span class="hljs-keyword">float</span> x = ev.getX(actionIndex);                        <span class="hljs-keyword">final</span> <span class="hljs-keyword">float</span> y = ev.getY(actionIndex);                        <span class="hljs-comment">// Find a child that can receive the event.</span>                        <span class="hljs-comment">// Scan children from front to back.</span>                        <span class="hljs-keyword">final</span> ArrayList<View> preorderedList = buildOrderedChildList();                        <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> customOrder = preorderedList == <span class="hljs-keyword">null</span>                                && isChildrenDrawingOrderEnabled();                        <span class="hljs-keyword">final</span> View[] children = mChildren;                        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = childrenCount - <span class="hljs-number">1</span>; i >= <span class="hljs-number">0</span>; i--) {                            <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> childIndex = customOrder                                    ? getChildDrawingOrder(childrenCount, i) : i;                            <span class="hljs-keyword">final</span> View child = (preorderedList == <span class="hljs-keyword">null</span>)                                    ? children[childIndex] : preorderedList.get(childIndex);                            <span class="hljs-comment">// If there is a view that has accessibility focus we want it</span>                            <span class="hljs-comment">// to get the event first and if not handled we will perform a</span>                            <span class="hljs-comment">// normal dispatch. We may do a double iteration but this is</span>                            <span class="hljs-comment">// safer given the timeframe.</span>                            <span class="hljs-keyword">if</span> (childWithAccessibilityFocus != <span class="hljs-keyword">null</span>) {                                <span class="hljs-keyword">if</span> (childWithAccessibilityFocus != child) {                                    <span class="hljs-keyword">continue</span>;                                }                                childWithAccessibilityFocus = <span class="hljs-keyword">null</span>;                                i = childrenCount - <span class="hljs-number">1</span>;                            }                            <span class="hljs-keyword">if</span> (!canViewReceivePointerEvents(child)                                    || !isTransformedTouchPointInView(x, y, child, <span class="hljs-keyword">null</span>)) {                                ev.setTargetAccessibilityFocus(<span class="hljs-keyword">false</span>);                                <span class="hljs-keyword">continue</span>;                            }                            newTouchTarget = getTouchTarget(child);                            <span class="hljs-keyword">if</span> (newTouchTarget != <span class="hljs-keyword">null</span>) {                                <span class="hljs-comment">// Child is already receiving touch within its bounds.</span>                                <span class="hljs-comment">// Give it the new pointer in addition to the ones it is handling.</span>                                newTouchTarget.pointerIdBits |= idBitsToAssign;                                <span class="hljs-keyword">break</span>;                            }                            resetCancelNextUpFlag(child);                            <span class="hljs-keyword">if</span> (dispatchTransformedTouchEvent(ev, <span class="hljs-keyword">false</span>, child, idBitsToAssign)) {                                <span class="hljs-comment">// Child wants to receive touch within its bounds.</span>                                mLastTouchDownTime = ev.getDownTime();                                <span class="hljs-keyword">if</span> (preorderedList != <span class="hljs-keyword">null</span>) {                                    <span class="hljs-comment">// childIndex points into presorted list, find original index</span>                                    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> j = <span class="hljs-number">0</span>; j < childrenCount; j++) {                                        <span class="hljs-keyword">if</span> (children[childIndex] == mChildren[j]) {                                            mLastTouchDownIndex = j;                                            <span class="hljs-keyword">break</span>;                                        }                                    }                                } <span class="hljs-keyword">else</span> {                                    mLastTouchDownIndex = childIndex;                                }                                mLastTouchDownX = ev.getX();                                mLastTouchDownY = ev.getY();                                newTouchTarget = addTouchTarget(child, idBitsToAssign);                                alreadyDispatchedToNewTouchTarget = <span class="hljs-keyword">true</span>;                                <span class="hljs-keyword">break</span>;                            }                            <span class="hljs-comment">// The accessibility focus didn't handle the event, so clear</span>                            <span class="hljs-comment">// the flag and do a normal dispatch to all children.</span>                            ev.setTargetAccessibilityFocus(<span class="hljs-keyword">false</span>);                        }                        <span class="hljs-keyword">if</span> (preorderedList != <span class="hljs-keyword">null</span>) preorderedList.clear();                    }                    <span class="hljs-keyword">if</span> (newTouchTarget == <span class="hljs-keyword">null</span> && mFirstTouchTarget != <span class="hljs-keyword">null</span>) {                        <span class="hljs-comment">// Did not find a child to receive the event.</span>                        <span class="hljs-comment">// Assign the pointer to the least recently added target.</span>                        newTouchTarget = mFirstTouchTarget;                        <span class="hljs-keyword">while</span> (newTouchTarget.next != <span class="hljs-keyword">null</span>) {                            newTouchTarget = newTouchTarget.next;                        }                        newTouchTarget.pointerIdBits |= idBitsToAssign;                    }                }            }            <span class="hljs-comment">// Dispatch to touch targets.</span>            <span class="hljs-keyword">if</span> (mFirstTouchTarget == <span class="hljs-keyword">null</span>) {                <span class="hljs-comment">// No touch targets so treat this as an ordinary view.</span>                handled = dispatchTransformedTouchEvent(ev, canceled, <span class="hljs-keyword">null</span>,                        TouchTarget.ALL_POINTER_IDS);            } <span class="hljs-keyword">else</span> {                <span class="hljs-comment">// Dispatch to touch targets, excluding the new touch target if we already</span>                <span class="hljs-comment">// dispatched to it.  Cancel touch targets if necessary.</span>                TouchTarget predecessor = <span class="hljs-keyword">null</span>;                TouchTarget target = mFirstTouchTarget;                <span class="hljs-keyword">while</span> (target != <span class="hljs-keyword">null</span>) {                    <span class="hljs-keyword">final</span> TouchTarget next = target.next;                    <span class="hljs-keyword">if</span> (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {                        handled = <span class="hljs-keyword">true</span>;                    } <span class="hljs-keyword">else</span> {                        <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> cancelChild = resetCancelNextUpFlag(target.child)                                || intercepted;                        <span class="hljs-keyword">if</span> (dispatchTransformedTouchEvent(ev, cancelChild,                                target.child, target.pointerIdBits)) {                            handled = <span class="hljs-keyword">true</span>;                        }                        <span class="hljs-keyword">if</span> (cancelChild) {                            <span class="hljs-keyword">if</span> (predecessor == <span class="hljs-keyword">null</span>) {                                mFirstTouchTarget = next;                            } <span class="hljs-keyword">else</span> {                                predecessor.next = next;                            }                            target.recycle();                            target = next;                            <span class="hljs-keyword">continue</span>;                        }                    }                    predecessor = target;                    target = next;                }            }            <span class="hljs-comment">// Update list of touch targets for pointer up or cancel, if needed.</span>            <span class="hljs-keyword">if</span> (canceled                    || actionMasked == MotionEvent.ACTION_UP                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {                resetTouchState();            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {                <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> actionIndex = ev.getActionIndex();                <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> idBitsToRemove = <span class="hljs-number">1</span> << ev.getPointerId(actionIndex);                removePointersFromTouchTargets(idBitsToRemove);            }        }        <span class="hljs-keyword">if</span> (!handled && mInputEventConsistencyVerifier != <span class="hljs-keyword">null</span>) {            mInputEventConsistencyVerifier.onUnhandledEvent(ev, <span class="hljs-number">1</span>);        }        <span class="hljs-keyword">return</span> handled;    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li><li>49</li><li>50</li><li>51</li><li>52</li><li>53</li><li>54</li><li>55</li><li>56</li><li>57</li><li>58</li><li>59</li><li>60</li><li>61</li><li>62</li><li>63</li><li>64</li><li>65</li><li>66</li><li>67</li><li>68</li><li>69</li><li>70</li><li>71</li><li>72</li><li>73</li><li>74</li><li>75</li><li>76</li><li>77</li><li>78</li><li>79</li><li>80</li><li>81</li><li>82</li><li>83</li><li>84</li><li>85</li><li>86</li><li>87</li><li>88</li><li>89</li><li>90</li><li>91</li><li>92</li><li>93</li><li>94</li><li>95</li><li>96</li><li>97</li><li>98</li><li>99</li><li>100</li><li>101</li><li>102</li><li>103</li><li>104</li><li>105</li><li>106</li><li>107</li><li>108</li><li>109</li><li>110</li><li>111</li><li>112</li><li>113</li><li>114</li><li>115</li><li>116</li><li>117</li><li>118</li><li>119</li><li>120</li><li>121</li><li>122</li><li>123</li><li>124</li><li>125</li><li>126</li><li>127</li><li>128</li><li>129</li><li>130</li><li>131</li><li>132</li><li>133</li><li>134</li><li>135</li><li>136</li><li>137</li><li>138</li><li>139</li><li>140</li><li>141</li><li>142</li><li>143</li><li>144</li><li>145</li><li>146</li><li>147</li><li>148</li><li>149</li><li>150</li><li>151</li><li>152</li><li>153</li><li>154</li><li>155</li><li>156</li><li>157</li><li>158</li><li>159</li><li>160</li><li>161</li><li>162</li><li>163</li><li>164</li><li>165</li><li>166</li><li>167</li><li>168</li><li>169</li><li>170</li><li>171</li><li>172</li><li>173</li><li>174</li><li>175</li><li>176</li><li>177</li><li>178</li><li>179</li><li>180</li><li>181</li><li>182</li><li>183</li><li>184</li><li>185</li><li>186</li><li>187</li><li>188</li><li>189</li><li>190</li><li>191</li><li>192</li><li>193</li><li>194</li><li>195</li><li>196</li><li>197</li><li>198</li><li>199</li><li>200</li><li>201</li><li>202</li><li>203</li><li>204</li><li>205</li><li>206</li><li>207</li><li>208</li><li>209</li><li>210</li><li>211</li><li>212</li><li>213</li><li>214</li></ul>

我勒个去!!!这比View的dispatchTouchEvent方法长很多啊,那就只关注重点分析吧。

第一步,17-24行,对ACTION_DOWN进行处理。

因为ACTION_DOWN是一系列事件的开端,当是ACTION_DOWN时进行一些初始化操作,从上面源码中注释也可以看出来,清除以往的Touch状态然后开始新的手势。在这里你会发现cancelAndClearTouchTargets(ev)方法中有一个非常重要的操作就是将mFirstTouchTarget设置为了null(刚开始分析大眼瞄一眼没留意,结果越往下看越迷糊,所以这个是分析ViewGroup的dispatchTouchEvent方法第一步中重点要记住的一个地方),接着在resetTouchState()方法中重置Touch状态标识。

第二步,26-47行,检查是否要拦截。

在dispatchTouchEvent(MotionEvent ev)这段代码中使用变量intercepted来标记ViewGroup是否拦截Touch事件的传递,该变量类似第一步的mFirstTouchTarget变量,在后续代码中起着很重要的作用。if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这一条判断语句说明当事件为ACTION_DOWN或者mFirstTouchTarget不为null(即已经找到能够接收touch事件的目标组件)时if成立,否则if不成立,然后将intercepted设置为true,也即拦截事件。当当事件为ACTION_DOWN或者mFirstTouchTarget不为null时判断disallowIntercept(禁止拦截)标志位,而这个标记在ViewGroup中提供了public的设置方法,如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">requestDisallowInterceptTouchEvent</span>(<span class="hljs-keyword">boolean</span> disallowIntercept) {        <span class="hljs-keyword">if</span> (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != <span class="hljs-number">0</span>)) {            <span class="hljs-comment">// We're already in this state, assume our ancestors are too</span>            <span class="hljs-keyword">return</span>;        }        <span class="hljs-keyword">if</span> (disallowIntercept) {            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;        } <span class="hljs-keyword">else</span> {            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;        }        <span class="hljs-comment">// Pass it up to our parent</span>        <span class="hljs-keyword">if</span> (mParent != <span class="hljs-keyword">null</span>) {            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);        }    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li></ul>

所以你可以在其他地方调用requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,从而禁止执行是否需要拦截的判断。当disallowIntercept为true(禁止拦截判断)时则intercepted直接设置为false,否则调用onInterceptTouchEvent(ev)方法,然后将结果赋值给intercepted。那就来看下ViewGroup与众不同与View特有的onInterceptTouchEvent方法,如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onInterceptTouchEvent</span>(MotionEvent ev) {        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li></ul>

看见了吧,默认的onInterceptTouchEvent方法只是返回了一个false,也即intercepted=false。所以可以说明上面例子的部分打印(dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent),这里很明显表明在ViewGroup的dispatchTouchEvent()中默认(不在其他地方调运requestDisallowInterceptTouchEvent方法设置禁止拦截标记)首先调用了onInterceptTouchEvent()方法。

第三步,49-51行,检查cancel。

通过标记和action检查cancel,然后将结果赋值给局部boolean变量canceled。

第四步,53-函数结束,事件分发。

54行首先可以看见获取一个boolean变量标记split来标记,默认是true,作用是是否把事件分发给多个子View,这个同样在ViewGroup中提供了public的方法设置,如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setMotionEventSplittingEnabled</span>(<span class="hljs-keyword">boolean</span> split) {        <span class="hljs-comment">// TODO Applications really shouldn't change this setting mid-touch event,</span>        <span class="hljs-comment">// but perhaps this should handle that case and send ACTION_CANCELs to any child views</span>        <span class="hljs-comment">// with gestures in progress when this is changed.</span>        <span class="hljs-keyword">if</span> (split) {            mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;        } <span class="hljs-keyword">else</span> {            mGroupFlags &= ~FLAG_SPLIT_MOTION_EVENTS;        }    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li></ul>

接着57行if (!canceled && !intercepted)判断表明,事件不是ACTION_CANCEL并且ViewGroup的拦截标志位intercepted为false(不拦截)则会进入其中。

事件分发步骤中关于ACTION_DOWN的特殊处理

接着67行这个很大的if语句if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE)处理ACTION_DOWN事件,这个环节比较繁琐,也比较重要,如下具体分析。

在79行判断了childrenCount个数是否不为0,然后接着在84行拿到了子View的list集合preorderedList;接着在88行通过一个for循环i从childrenCount - 1开始遍历到0,倒序遍历所有的子view,这是因为preorderedList中的顺序是按照addView或者XML布局文件中的顺序来的,后addView添加的子View,会因为Android的UI后刷新机制显示在上层;假如点击的地方有两个子View都包含的点击的坐标,那么后被添加到布局中的那个子view会先响应事件;这样其实也是符合人的思维方式的,因为后被添加的子view会浮在上层,所以我们去点击的时候一般都会希望点击最上层的那个组件先去响应事件。

接着在106到112行通过getTouchTarget去查找当前子View是否在mFirstTouchTarget.next这条target链中的某一个targe中,如果在则返回这个target,否则返回null。在这段代码的if判断通过说明找到了接收Touch事件的子View,即newTouchTarget,那么,既然已经找到了,所以执行break跳出for循环。如果没有break则继续向下执行走到115行开始到134行,这里你可以看见一段if判断的代码if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)),这个被if的大括弧括起来的一段代码很重要,具体解释如下:

调用方法dispatchTransformedTouchEvent()将Touch事件传递给特定的子View。该方法十分重要,在该方法中为一个递归调用,会递归调用dispatchTouchEvent()方法。在dispatchTouchEvent()中如果子View为ViewGroup并且Touch没有被拦截那么递归调用dispatchTouchEvent(),如果子View为View那么就会调用其onTouchEvent()。dispatchTransformedTouchEvent方法如果返回true则表示子View消费掉该事件,同时进入该if判断。满足if语句后重要的操作有:

  • 给newTouchTarget赋值;
  • 给alreadyDispatchedToNewTouchTarget赋值为true;
  • 执行break,因为该for循环遍历子View判断哪个子View接受Touch事件,既然已经找到了就跳出该外层for循环;

如果115行if判断中的dispatchTransformedTouchEvent()方法返回false,即子View的onTouchEvent返回false(即Touch事件未被消费),那么就不满足该if条件,也就无法执行addTouchTarget(),从而导致mFirstTouchTarget为null(没法对mFirstTouchTarget赋值,因为上面分析了mFirstTouchTarget一进来是ACTION_DOWN就置位为null了),那么该子View就无法继续处理ACTION_MOVE事件和ACTION_UP事件(28行的判断为false,也即intercepted=true了,所以之后一系列判断无法通过)。

如果115行if判断中的dispatchTransformedTouchEvent()方法返回true,即子View的onTouchEvent返回true(即Touch事件被消费),那么就满足该if条件,从而mFirstTouchTarget不为null。

继续看143行的判断if (newTouchTarget == null && mFirstTouchTarget != null)。该if表示经过前面的for循环没有找到子View接收Touch事件并且之前的mFirstTouchTarget不为空则为真,然后newTouchTarget指向了最初的TouchTarget。

通过上面67到157行关于事件分发步骤中ACTION_DOWN的特殊处理可以发现,对于此处ACTION_DOWN的处理具体体现在dispatchTransformedTouchEvent()方法,该方法返回值具备如下特征:

returndescriptionsettrue事件被消费mFirstTouchTarget!=nullfalse事件未被消费mFirstTouchTarget==null

因为在dispatchTransformedTouchEvent()会调用递归调用dispatchTouchEvent()和onTouchEvent(),所以dispatchTransformedTouchEvent()的返回值实际上是由onTouchEvent()决定的。简单地说onTouchEvent()是否消费了Touch事件的返回值决定了dispatchTransformedTouchEvent()的返回值,从而决定mFirstTouchTarget是否为null,进一步决定了ViewGroup是否处理Touch事件,这一点在160行开始的代码中有体现。如下分析事件分发步骤中关于ACTION_DOWN处理之后的其他处理逻辑,也即160行开始剩余的逻辑。

事件分发步骤中关于ACTION_DOWN处理之后的其他处理逻辑

可以看到,如果派发的事件不是ACTION_DOWN就不会经过上面的流程,而是直接从此处开始执行。上面说了,经过上面对于ACTION_DOWN的处理后mFirstTouchTarget可能为null或者不为null。所以可以看见161行代码if (mFirstTouchTarget == null)与else判断了mFirstTouchTarget值是否为null的情况,完全符合如上分析。那我们分情况继续分析一下:

当161行if判断的mFirstTouchTarget为null时,也就是说Touch事件未被消费,即没有找到能够消费touch事件的子组件或Touch事件被拦截了,则调用ViewGroup的dispatchTransformedTouchEvent()方法处理Touch事件(和普通View一样),即子View没有消费Touch事件,那么子View的上层ViewGroup才会调用其onTouchEvent()处理Touch事件。具体就是在调用dispatchTransformedTouchEvent()时第三个参数为null,关于dispatchTransformedTouchEvent方法下面会分析,暂时先记住就行。

这下再回想上面例子,点击Button时为啥触发了Button的一系列touch方法而没有触发父级LinearLayout的touch方法的疑惑?明白了吧?

子view对于Touch事件处理返回true那么其上层的ViewGroup就无法处理Touch事件了,子view对于Touch事件处理返回false那么其上层的ViewGroup才可以处理Touch事件。

当161行if判断的mFirstTouchTarget不为null时,也就是说找到了可以消费Touch事件的子View且后续Touch事件可以传递到该子View。可以看见在源码的else中对于非ACTION_DOWN事件继续传递给目标子组件进行处理,依然是递归调用dispatchTransformedTouchEvent()方法来实现的处理。

到此ViewGroup的dispatchTouchEvent方法分析完毕。

上面说了ViewGroup的dispatchTouchEvent方法详细情况,也知道在其中可能会执行onInterceptTouchEvent方法,所以接下来咱们先简单分析一下这个方法。

3-2 说说ViewGroup的dispatchTouchEvent中可能执行的onInterceptTouchEvent方法

如下系统源码:

<code class="language-java hljs  has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onInterceptTouchEvent</span>(MotionEvent ev) {        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li></ul>

看到了吧,这个方法算是ViewGroup不同于View特有的一个事件派发调运方法。在源码中可以看到这个方法实现很简单,但是有一堆注释。其实上面分析了,如果ViewGroup的onInterceptTouchEvent返回false就不阻止事件继续传递派发,否则阻止传递派发。

对了,还记得在dispatchTouchEvent方法中除过可能执行的onInterceptTouchEvent以外在后面派发事件时执行的dispatchTransformedTouchEvent方法吗?上面分析dispatchTouchEvent时说了下面会仔细分析,那么现在就来继续看看这个方法吧。

3-3 继续说说ViewGroup的dispatchTouchEvent中执行的dispatchTransformedTouchEvent方法

ViewGroup的dispatchTransformedTouchEvent方法系统源码如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTransformedTouchEvent</span>(MotionEvent event, <span class="hljs-keyword">boolean</span> cancel,            View child, <span class="hljs-keyword">int</span> desiredPointerIdBits) {        <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> handled;        <span class="hljs-comment">// Canceling motions is a special case.  We don't need to perform any transformations</span>        <span class="hljs-comment">// or filtering.  The important part is the action, not the contents.</span>        <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> oldAction = event.getAction();        <span class="hljs-keyword">if</span> (cancel || oldAction == MotionEvent.ACTION_CANCEL) {            event.setAction(MotionEvent.ACTION_CANCEL);            <span class="hljs-keyword">if</span> (child == <span class="hljs-keyword">null</span>) {                handled = <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);            } <span class="hljs-keyword">else</span> {                handled = child.dispatchTouchEvent(event);            }            event.setAction(oldAction);            <span class="hljs-keyword">return</span> handled;        }        <span class="hljs-comment">// Calculate the number of pointers to deliver.</span>        <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> oldPointerIdBits = event.getPointerIdBits();        <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;        <span class="hljs-comment">// If for some reason we ended up in an inconsistent state where it looks like we</span>        <span class="hljs-comment">// might produce a motion event with no pointers in it, then drop the event.</span>        <span class="hljs-keyword">if</span> (newPointerIdBits == <span class="hljs-number">0</span>) {            <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;        }        <span class="hljs-comment">// If the number of pointers is the same and we don't need to perform any fancy</span>        <span class="hljs-comment">// irreversible transformations, then we can reuse the motion event for this</span>        <span class="hljs-comment">// dispatch as long as we are careful to revert any changes we make.</span>        <span class="hljs-comment">// Otherwise we need to make a copy.</span>        <span class="hljs-keyword">final</span> MotionEvent transformedEvent;        <span class="hljs-keyword">if</span> (newPointerIdBits == oldPointerIdBits) {            <span class="hljs-keyword">if</span> (child == <span class="hljs-keyword">null</span> || child.hasIdentityMatrix()) {                <span class="hljs-keyword">if</span> (child == <span class="hljs-keyword">null</span>) {                    handled = <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);                } <span class="hljs-keyword">else</span> {                    <span class="hljs-keyword">final</span> <span class="hljs-keyword">float</span> offsetX = mScrollX - child.mLeft;                    <span class="hljs-keyword">final</span> <span class="hljs-keyword">float</span> offsetY = mScrollY - child.mTop;                    event.offsetLocation(offsetX, offsetY);                    handled = child.dispatchTouchEvent(event);                    event.offsetLocation(-offsetX, -offsetY);                }                <span class="hljs-keyword">return</span> handled;            }            transformedEvent = MotionEvent.obtain(event);        } <span class="hljs-keyword">else</span> {            transformedEvent = event.split(newPointerIdBits);        }        <span class="hljs-comment">// Perform any necessary transformations and dispatch.</span>        <span class="hljs-keyword">if</span> (child == <span class="hljs-keyword">null</span>) {            handled = <span class="hljs-keyword">super</span>.dispatchTouchEvent(transformedEvent);        } <span class="hljs-keyword">else</span> {            <span class="hljs-keyword">final</span> <span class="hljs-keyword">float</span> offsetX = mScrollX - child.mLeft;            <span class="hljs-keyword">final</span> <span class="hljs-keyword">float</span> offsetY = mScrollY - child.mTop;            transformedEvent.offsetLocation(offsetX, offsetY);            <span class="hljs-keyword">if</span> (! child.hasIdentityMatrix()) {                transformedEvent.transform(child.getInverseMatrix());            }            handled = child.dispatchTouchEvent(transformedEvent);        }        <span class="hljs-comment">// Done.</span>        transformedEvent.recycle();        <span class="hljs-keyword">return</span> handled;    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li><li>49</li><li>50</li><li>51</li><li>52</li><li>53</li><li>54</li><li>55</li><li>56</li><li>57</li><li>58</li><li>59</li><li>60</li><li>61</li><li>62</li><li>63</li><li>64</li><li>65</li><li>66</li><li>67</li><li>68</li><li>69</li><li>70</li><li>71</li></ul>

看到了吧,这个方法也算是ViewGroup不同于View特有的一个事件派发调运方法,而且奇葩的就是这个方法也很长。那也继续分析吧。。。

上面分析了,在dispatchTouchEvent()中调用dispatchTransformedTouchEvent()将事件分发给子View处理。在此我们需要重点分析该方法的第三个参数(View child)。在dispatchTouchEvent()中多次调用了dispatchTransformedTouchEvent()方法,而且有时候第三个参数为null,有时又不是,他们到底有啥区别呢?这段源码中很明显展示了结果。在dispatchTransformedTouchEvent()源码中可以发现多次对于child是否为null的判断,并且均做出如下类似的操作。其中,当child == null时会将Touch事件传递给该ViewGroup自身的dispatchTouchEvent()处理,即super.dispatchTouchEvent(event)(也就是View的这个方法,因为ViewGroup的父类是View);当child != null时会调用该子view(当然该view可能是一个View也可能是一个ViewGroup)的dispatchTouchEvent(event)处理,即child.dispatchTouchEvent(event)。别的代码几乎没啥需要具体注意分析的。

所以,到此你也会发现ViewGroup没有重写View的onTouchEvent(MotionEvent event) 方法,也就是说接下来的调运关系就是上一篇分析的流程了,这里不在多说。

好了,到此你是不是即明白了上面实例演示的代码结果,也明白了上一篇最后升级实例验证模块留下的点击Button触发了LinearLayout的一些疑惑呢?答案自然是必须的!

4 Android 5.1.1(API 22) ViewGroup触摸屏事件传递总结

如上就是所有ViewGroup关于触摸屏事件的传递机制源码分析与实例演示。具体总结如下:

  1. Android事件派发是先传递到最顶级的ViewGroup,再由ViewGroup递归传递到View的。
  2. 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
  3. 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。


Android触摸屏事件派发机制详解与源码分析三(Activity篇)


1 背景

还记得前面两篇从Android的基础最小元素控件(View)到ViewGroup控件的触摸屏事件分发机制分析吗?你可能看完会有疑惑,View的事件是ViewGroup派发的,那ViewGroup的事件呢?他包含在Activity上,是不是Activity也有类似的事件派发方法呢?带着这些疑惑咱们继续实例验证加源码分析吧。

PS:阅读本篇前建议先查看前一篇《Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)》与《Android触摸屏事件派发机制详解与源码分析一(View篇)》,这一篇承接上一篇。

2 实例验证

2-1 代码

如下实例与前面实例相同,一个Button在LinearLayout里,只不过我们这次重写了Activity的一些方法而已。具体如下:

自定义的Button与LinearLayout:

<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Button</span> {</span>    <span class="hljs-keyword">public</span> <span class="hljs-title">TestButton</span>(Context context, AttributeSet attrs) {        <span class="hljs-keyword">super</span>(context, attrs);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestButton--dispatchTouchEvent--action="</span>+event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestButton--onTouchEvent--action="</span>+event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.onTouchEvent(event);    }}</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li></ul>
<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestLinearLayout</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">LinearLayout</span> {</span>    <span class="hljs-keyword">public</span> <span class="hljs-title">TestLinearLayout</span>(Context context, AttributeSet attrs) {        <span class="hljs-keyword">super</span>(context, attrs);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onInterceptTouchEvent</span>(MotionEvent ev) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestLinearLayout--onInterceptTouchEvent--action="</span>+ev.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.onInterceptTouchEvent(ev);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestLinearLayout--dispatchTouchEvent--action="</span> + event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"TestLinearLayout--onTouchEvent--action="</span>+event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.onTouchEvent(event);    }}</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li></ul>

整个界面的布局文件:

<code class="language-xml hljs  has-numbering"><span class="hljs-tag"><<span class="hljs-title">com.example.yanbo.myapplication.TestLinearLayout</span>    <span class="hljs-attribute">xmlns:android</span>=<span class="hljs-value">"http://schemas.android.com/apk/res/android"</span>    <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"match_parent"</span>    <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"match_parent"</span>    <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/layout"</span>></span>    <span class="hljs-tag"><<span class="hljs-title">com.example.yanbo.myapplication.TestButton</span>        <span class="hljs-attribute">android:text</span>=<span class="hljs-value">"click test"</span>        <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"match_parent"</span>        <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"wrap_content"</span>        <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/button"</span>/></span><span class="hljs-tag"></<span class="hljs-title">com.example.yanbo.myapplication.TestLinearLayout</span>></span></code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li></ul>

整个界面Activity,重写了Activity的一些关于触摸派发的方法(三个):

<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Activity</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">View</span>.<span class="hljs-title">OnClickListener</span>, <span class="hljs-title">View</span>.<span class="hljs-title">OnTouchListener</span> {</span>    <span class="hljs-keyword">private</span> TestButton mButton;    <span class="hljs-keyword">private</span> TestLinearLayout mLayout;    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span>(Bundle savedInstanceState) {        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        mButton = (TestButton) <span class="hljs-keyword">this</span>.findViewById(R.id.button);        mLayout = (TestLinearLayout) <span class="hljs-keyword">this</span>.findViewById(R.id.layout);        mButton.setOnClickListener(<span class="hljs-keyword">this</span>);        mLayout.setOnClickListener(<span class="hljs-keyword">this</span>);        mButton.setOnTouchListener(<span class="hljs-keyword">this</span>);        mLayout.setOnTouchListener(<span class="hljs-keyword">this</span>);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onClick</span>(View v) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"onClick----v="</span> + v);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouch</span>(View v, MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"onTouch--action="</span>+event.getAction()+<span class="hljs-string">"--v="</span>+v);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent ev) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"MainActivity--dispatchTouchEvent--action="</span> + ev.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.dispatchTouchEvent(ev);    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onUserInteraction</span>() {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"MainActivity--onUserInteraction"</span>);        <span class="hljs-keyword">super</span>.onUserInteraction();    }    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        Log.i(<span class="hljs-keyword">null</span>, <span class="hljs-string">"MainActivity--onTouchEvent--action="</span>+event.getAction());        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.onTouchEvent(event);    }}</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li></ul>

如上就是实例测试代码,非常简单,没必要分析,直接看结果吧。

2-2 结果分析

直接点击Button按钮打印如下:

<code class="language-txt hljs brainfuck has-numbering"><span class="hljs-comment">MainActivity</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-comment">MainActivity</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onUserInteraction</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onInterceptTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-comment">TestButton</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-comment">onTouch</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">v=com</span><span class="hljs-string">.</span><span class="hljs-comment">example</span><span class="hljs-string">.</span><span class="hljs-comment">yanbo</span><span class="hljs-string">.</span><span class="hljs-comment">myapplication</span><span class="hljs-string">.</span><span class="hljs-comment">TestButton</span><span class="hljs-comment">TestButton</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-comment">MainActivity</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onInterceptTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-comment">TestButton</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-comment">onTouch</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">v=com</span><span class="hljs-string">.</span><span class="hljs-comment">example</span><span class="hljs-string">.</span><span class="hljs-comment">yanbo</span><span class="hljs-string">.</span><span class="hljs-comment">myapplication</span><span class="hljs-string">.</span><span class="hljs-comment">TestButton</span><span class="hljs-comment">TestButton</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-comment">onClick</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">v=com</span><span class="hljs-string">.</span><span class="hljs-comment">example</span><span class="hljs-string">.</span><span class="hljs-comment">yanbo</span><span class="hljs-string">.</span><span class="hljs-comment">myapplication</span><span class="hljs-string">.</span><span class="hljs-comment">TestButton</span></code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li></ul>

分析可以发现,当点击Button时除过派发Activity的几个新方法之外其他完全符合前面两篇分析的View与ViewGroup的触摸事件派发机制。对于Activity来说,ACTION_DOWN事件首先触发dispatchTouchEvent,然后触发onUserInteraction,再次onTouchEvent,接着的ACTION_UP事件触发dispatchTouchEvent后触发了onTouchEvent,也就是说ACTION_UP事件时不会触发onUserInteraction(待会可查看源代码分析原因)。

直接点击Button以外的其他区域:

<code class="language-txt hljs brainfuck has-numbering"><span class="hljs-comment">MainActivity</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-comment">MainActivity</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onUserInteraction</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onInterceptTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-comment">onTouch</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">v=com</span><span class="hljs-string">.</span><span class="hljs-comment">example</span><span class="hljs-string">.</span><span class="hljs-comment">yanbo</span><span class="hljs-string">.</span><span class="hljs-comment">myapplication</span><span class="hljs-string">.</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=0</span><span class="hljs-comment">MainActivity</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">dispatchTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-comment">onTouch</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">v=com</span><span class="hljs-string">.</span><span class="hljs-comment">example</span><span class="hljs-string">.</span><span class="hljs-comment">yanbo</span><span class="hljs-string">.</span><span class="hljs-comment">myapplication</span><span class="hljs-string">.</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-comment">TestLinearLayout</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">onTouchEvent</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">action=1</span><span class="hljs-comment">onClick</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">v=com</span><span class="hljs-string">.</span><span class="hljs-comment">example</span><span class="hljs-string">.</span><span class="hljs-comment">yanbo</span><span class="hljs-string">.</span><span class="hljs-comment">myapplication</span><span class="hljs-string">.</span><span class="hljs-comment">TestLinearLayout</span></code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li></ul>

怎么样?完全符合上面点击Button结果分析的猜想。

那接下来还是要看看Activity里关于这几个方法的源码了。

3 Android 5.1.1(API 22) Activity触摸屏事件传递源码分析

通过上面例子的打印我们可以确定分析源码的顺序,那就开始分析呗。

3-1 从Activity的dispatchTouchEvent方法说起

3-1-1 开始分析

先上源码,如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-javadoc">/**     * Called to process touch screen events.  You can override this to     * intercept all touch screen events before they are dispatched to the     * window.  Be sure to call this implementation for touch screen events     * that should be handled normally.     *     *<span class="hljs-javadoctag"> @param</span> ev The touch screen event.     *     *<span class="hljs-javadoctag"> @return</span> boolean Return true if this event was consumed.     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">dispatchTouchEvent</span>(MotionEvent ev) {        <span class="hljs-keyword">if</span> (ev.getAction() == MotionEvent.ACTION_DOWN) {            onUserInteraction();        }        <span class="hljs-keyword">if</span> (getWindow().superDispatchTouchEvent(ev)) {            <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;        }        <span class="hljs-keyword">return</span> onTouchEvent(ev);    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li></ul>

哎呦!这次看着代码好少的样子,不过别高兴,浓缩才是精华,这里代码虽少,涉及的问题点还是很多的,那么咱们就来一点一点分析吧。

12到14行看见了吧?上面例子咱们看见只有ACTION_DOWN事件派发时调运了onUserInteraction方法,当时还在疑惑呢,这下明白了吧,不多解释,咱们直接跳进去可以看见是一个空方法,具体下面会分析。

好了,自己分析15到17行,看着简单吧,我勒个去,我怎么有点懵,这是哪的方法?咱们分析分析吧。

首先分析Activity的attach方法可以发现getWindow()返回的就是PhoneWindow对象(PhoneWindow为抽象Window的实现子类),那就简单了,也就相当于PhoneWindow类的方法,而PhoneWindow类实现于Window抽象类,所以先看下Window类中抽象方法的定义,如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-javadoc">/**     * Used by custom windows, such as Dialog, to pass the touch screen event     * further down the view hierarchy. Application developers should     * not need to implement or call this.     *     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">superDispatchTouchEvent</span>(MotionEvent event);</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul>

看见注释没有?用户不需要重写实现的方法,实质也不能,在Activity中没有提供重写的机会,因为Window是以组合模式与Activity建立关系的。好了,看完了抽象的Window方法,那就去PhoneWindow里看下Window抽象方法的实现吧,如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">superDispatchTouchEvent</span>(MotionEvent event) {        <span class="hljs-keyword">return</span> mDecor.superDispatchTouchEvent(event);    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li></ul>

又是看着好简单的样子哦,实际又是一堆问题,继续分析。你会发现在PhoneWindow的superDispatchTouchEvent方法里又直接返回了另一个mDecor对象的superDispatchTouchEvent方法,mDecor是啥?继续分析吧。

在PhoneWindow类里发现,mDecor是DecorView类的实例,同时DecorView是PhoneWindow的内部类。最惊人的发现是DecorView extends FrameLayout implements RootViewSurfaceTaker,看见没有?它是一个真正Activity的root view,它继承了FrameLayout。怎么验证他一定是root view呢?很简单,不知道大家是不是熟悉Android App开发技巧中关于UI布局优化使用的SDK工具Hierarchy Viewer。咱们通过他来看下上面刚刚展示的那个例子的Hierarchy Viewer你就明白了,如下我在Ubuntu上截图的Hierarchy Viewer分析结果:

这里写图片描述

看见没有,我们上面例子中Activity中setContentView时放入的xml layout是一个LinearLayout,其中包含一个Button,上图展示了我们放置的LinearLayout被放置在一个id为content的FrameLayout的布局中,这也就是为啥Activity的setContentView方法叫set content view了,就是把我们的xml放入了这个id为content的FrameLayout中。

赶快回过头,你是不是发现上面PhoneWindow的superDispatchTouchEvent直接返回了DecorView的superDispatchTouchEvent,而DecorView又是FrameLayout的子类,FrameLayout又是ViewGroup的子类。机智的你想到了啥木有?

没想到就继续看下DecorView类的superDispatchTouchEvent方法吧,如下:

<code class="language-java hljs  has-numbering">        <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">superDispatchTouchEvent</span>(MotionEvent event) {            <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.dispatchTouchEvent(event);        }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li></ul>

这回你一定恍然大悟了吧,不然就得脑补前面两篇博客的内容了。。。

搞半天Activity的dispatchTouchEvent方法的15行if (getWindow().superDispatchTouchEvent(ev))本质执行的是一个ViewGroup的dispatchTouchEvent方法(这个ViewGroup是Activity特有的root view,也就是id为content的FrameLayout布局),接下来就不用多说了吧,完全是前面两篇分析的执行过程。

接下来依据派发事件返回值决定是否触发Activity的onTouchEvent方法。

3-1-2 小总结一下

在Activity的触摸屏事件派发中:

  1. 首先会触发Activity的dispatchTouchEvent方法。
  2. dispatchTouchEvent方法中如果是ACTION_DOWN的情况下会接着触发onUserInteraction方法。
  3. 接着在dispatchTouchEvent方法中会通过Activity的root View(id为content的FrameLayout),实质是ViewGroup,通过super.dispatchTouchEvent把touchevent派发给各个activity的子view,也就是我们再Activity.onCreat方法中setContentView时设置的view。
  4. 若Activity下面的子view拦截了touchevent事件(返回true)则Activity.onTouchEvent方法就不会执行。

3-2 继续Activity的dispatchTouchEvent方法中调运的onUserInteraction方法

如下源码:

<code class="language-java hljs  has-numbering">    <span class="hljs-javadoc">/**     * Called whenever a key, touch, or trackball event is dispatched to the     * activity.  Implement this method if you wish to know that the user has     * interacted with the device in some way while your activity is running.     * This callback and {@link #onUserLeaveHint} are intended to help     * activities manage status bar notifications intelligently; specifically,     * for helping activities determine the proper time to cancel a notfication.     *     * <p>All calls to your activity's {@link #onUserLeaveHint} callback will     * be accompanied by calls to {@link #onUserInteraction}.  This     * ensures that your activity will be told of relevant user activity such     * as pulling down the notification pane and touching an item there.     *     * <p>Note that this callback will be invoked for the touch down action     * that begins a touch gesture, but may not be invoked for the touch-moved     * and touch-up actions that follow.     *     *<span class="hljs-javadoctag"> @see</span> #onUserLeaveHint()     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onUserInteraction</span>() {    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li></ul>

搞了半天就像上面说的,这是一个空方法,那它的作用是啥呢?

此方法是activity的方法,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器 就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,自己消失等。

这个方法也分析完了,那就剩下onTouchEvent方法了,如下继续分析。

3-3 继续Activity的dispatchTouchEvent方法中调运的onTouchEvent方法

如下源码:

<code class="language-java hljs  has-numbering">    <span class="hljs-javadoc">/**     * Called when a touch screen event was not handled by any of the views     * under it.  This is most useful to process touch events that happen     * outside of your window bounds, where there is no view to receive it.     *     *<span class="hljs-javadoctag"> @param</span> event The touch screen event being processed.     *     *<span class="hljs-javadoctag"> @return</span> Return true if you have consumed the event, false if you haven't.     * The default implementation always returns false.     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">onTouchEvent</span>(MotionEvent event) {        <span class="hljs-keyword">if</span> (mWindow.shouldCloseOnTouch(<span class="hljs-keyword">this</span>, event)) {            finish();            <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;        }        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li></ul>

看见没有,这个方法看起来好简单的样子。

如果一个屏幕触摸事件没有被这个Activity下的任何View所处理,Activity的onTouchEvent将会调用。这对于处理window边界之外的Touch事件非常有用,因为通常是没有View会接收到它们的。返回值为true表明你已经消费了这个事件,false则表示没有消费,默认实现中返回false。

继续分析吧,重点就一句,mWindow.shouldCloseOnTouch(this, event)中的mWindow实际就是上面分析dispatchTouchEvent方法里的getWindow()对象,所以直接到Window抽象类和PhoneWindow子类查看吧,发现PhoneWindow没有重写Window的shouldCloseOnTouch方法,所以看下Window类的shouldCloseOnTouch实现吧,如下:

<code class="language-java hljs  has-numbering">    <span class="hljs-javadoc">/**<span class="hljs-javadoctag"> @hide</span> */</span>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">shouldCloseOnTouch</span>(Context context, MotionEvent event) {        <span class="hljs-keyword">if</span> (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN                && isOutOfBounds(context, event) && peekDecorView() != <span class="hljs-keyword">null</span>) {            <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;        }        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;    }</code><ul class="pre-numbering" style="display: block;"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li></ul>

这其实就是一个判断,判断mCloseOnTouchOutside标记及是否为ACTION_DOWN事件,同时判断event的x、y坐标是不是超出Bounds,然后检查FrameLayout的content的id的DecorView是否为空。其实没啥太重要的,这只是对于处理window边界之外的Touch事件有判断价值而已。

所以,到此Activity的onTouchEvent分析完毕。

4 Android触摸事件综合总结

到此整个Android的Activity->ViewGroup->View的触摸屏事件分发机制完全分析完毕。这时候你可以回过头看这三篇文章的例子,你会完全明白那些打印的含义与原理。

当然,了解这些源码机制不仅对你写普通代码时有帮助,最重要的是对你想自定义装逼控件时有不可磨灭的基础性指导作用与技巧提示作用。


0 0