您的位置:首页 > 移动开发 > Android开发

浅尝安卓事件分发机制

2016-09-07 15:51 507 查看
本文出自http://blog.csdn.net/zhaizu/article/details/50489398,转载请注明出处。

本文简单介绍安卓应用层的事件分发机制,并辅以案例进行分析。

视频版教程:http://v.youku.com/v_show/id_XMTY5MjczMjE3Ng==.html

0. 前言

授人以鱼不如授人以渔。

安卓系统源码是深入学习安卓开发的首选资料,原汁原味,营养丰富。

而且大部分源码带有注释,具有很强的可读性,你只需要战胜的自己的畏惧心理然后 read the fucking source code。

关于阅读源码,我有两个小建议:

1. 选择合适的源代码版本

从 2008 年 9 月份发布的 1.0 版,到 2015 年 10 月份的 6.0,部分源码发生了很大变化。

以应用层的事件分发机制为例,2.3.3 版本的源码仅仅处理单点触控,而在 6.0 版本则具有完善的多点触控。相应的,后者的代码比前者更为复杂,但思路是一脉相承的。

所以,如果首次学习事件分发机制,建议从低版本,如 2.2 或 2.3 版本开始,先了解单点触控触控的流程,然后再过度到最新版本的多点触控,这样学习曲线相对平滑,降低难度。

阅读 2.x 版本的源码,可以参考郭霖大牛的博客《 Android事件分发机制完全解析,带你从源码的角度彻底理解(下)》;更高版本的源码,可以参考这两篇的注释:《Android
Touch事件传递机制全面解析(从WMS到View树)》 和
《Android事件传递之子View和父View的那点事》。

2. 单步调试的重要性

学习源代码,仅仅通过阅读和思考是远远不够的,而且容易陷入死胡同。动手尝试是不可或缺的。

在 OnTouchLisnter.onTouch() 和 OnClickListener.onClick() 方法中打印日志,观察日志输出的时机和顺序,这是一种很直观的方法。

单步调试也是很有效的尝试方法,通过观察不同案例下(如设置监听事件和未设置监听事件)源码的执行路线和中间变量的赋值,我们往往有种茅塞顿开、豁然开朗的感觉。阅读和调试,一静一动,相得益彰。

为了避免发生断点失效或执行顺序混乱等诡异情况,我们要做到如下两点:

调试需要使用 Nexus 真机/模拟器(Genymotion 或 SDK 自带的都行),因为这些机型的 Rom 版本是原生的,与源代码一致;
AndroidStudio 工程的 build.gradle 里面的 compileSdkVersion 要与上述 Rom 版本号一致;例如,如果 build.gradle 文件的 compileSdkVersion=23, 那么在 Nexus 5(6.0系统,版本号 23)真机或模拟器上做单步,才能完美匹配。
其实,知道以上两点之后,大家完全自己去单步调试了。

以下内容都是基于本人阅读源码和单步调试和日常经验总结得出的。

1. 基础知识

1.1 直观认识

先来看个效果图:



假如上述效果图的布局实现方式如下(省略具体属性值):

<code class="hljs xml has-numbering"><span class="hljs-tag"><<span class="hljs-title">FrameLayout</span>></span>
<span class="hljs-tag"><<span class="hljs-title">ImageView</span> /></span>
<span class="hljs-tag"><<span class="hljs-title">TextView</span> /></span>
<span class="hljs-tag"></<span class="hljs-title">FrameLayout</span>></span></code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li></ul><div class="save_code tracking-ad" data-mod="popu_249"><a target=_blank target="_blank" href="javascript:;"><img src="http://static.blog.csdn.net/images/save_snippets.png" alt="" /></a></div>

父View 和子 View 是以“叠罗汉”的方式放在一起的,就像 HTML 里的 CSS(Cascading Style Sheet,层叠样式表)一样。二者通过“父子”关系联系起来,事件也是通过这种联系顺藤摸瓜来寻找“目标”View 的,处于上层的子 View 后于父 View 接收事件,但先于父 View 处理事件(如果愿意消费的话)。



View 层叠关系示意图

1.2 相关代码

本文基于安卓 SDK 23 版本源代码。

View.java 和 ViewGroup.java 中与事件分发相关的几个方法:

View.dispatchTouchEvent(),true 表示事件被消费,否则返回 false;
View.onTouchEvent(),true 表示事件被消费,否则返回 false;
ViewGroup.dispatchTouchEvent(),true 表示事件被消费,否则返回 false;
ViewGroup.onInterceptTouchEvent(),true 表示事件被拦截,否则返回 false;
继承关系如下:



由上图可以看出,ViewGroup 没有重写 onTouchEvent() 方法,仅仅是原样继承(但是 TextView 中对该方法进行了惨无人道的重写)。而且,在 ViewGroup 中见到 super.dispatchTouchEvent() 时一定要记得这是 ViewGroup 把自己当成普通的 View 了,也就是在调用 View.dispatchTouchEvent() 方法。

“手势”的定义:以 ACTION_DOWN 开始,以 ACTION_UP 结束的一连串触摸事件;触摸事件的类型可以分为:

ACTION_DOWN
ACTION_MOVE
ACTION_UP
ACTION_CANCEL
ACTION_POINTER_DOWN

ACTION_POINTER_UP

其中,带 POINTER 的类型是多点触控特有的,我们可以认为一个 pointer 就是一个手指。

2. 点击之后,发生了什么?

以下只讨论单点触控。

本文的 demo 布局是一个 RelativeLayout(后面会用 ScrollView 代替),里面有两个 TextView。我们假设红色的 TextView 上绑定了一个 OnClickListener。在我们自己的布局外面,系统还会再套几层布局,也就是虚线以上的部分。由于尺寸原因,后面的事件消费示意图中我们将省略部分系统层级。



完整的 View 树的层级示意图

这里解释一下 mFirstTouchTarget 这个成员变量。

每个 ViewGroup 实例中都有 mFirstTouchTarget。mFirstTouchTarget 的类型是 TouchTarget,TouchTarget 是一个链表节点的数据结构,每个 TouchTarget 实例里面封装着 mFirstTouchTarget 变量所在实例的一个子 View(也可能是 ViewGroup,以后统称为子 View),表示能接收事件的子 View。

当打头阵的 DOWN(以下将 ACTION_DOWN 简写为 DOWN,对 ACTION_MOVE、ACTION_UP、ACTION_CANCEL 做同样处理) 事件被某个子 View 消费时,mFirstTouchTarget 就指向该子 View,然后后续事件(MOVE / UP)到来时就直奔该子 View 供其消费;而当没有子 View 消费 DOWN 事件时,后续事件到来时,顶层的 DecorView.mFirstTouchTarget 为 null,DecorView 就直接调用 super.dispatchTouchEvent(event)
处理它们,而不再下发了。

单点触控时,mFirstTouchTarget 指向的链表最多只有一个节点;多点触控时可能会有多于一个节点。

关于 DOWN 和 MOVE / UP 事件执行过程,可参见下面代码的注释。

<code class="hljs java has-numbering"><span class="hljs-comment">// more code </span>

<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) {

<span class="hljs-comment">// DOWN</span>
handled = <span class="hljs-keyword">true</span>;

} <span class="hljs-keyword">else</span> {

<span class="hljs-comment">// MOVE and UP</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">// more code </span></code><ul style="" class="pre-numbering"><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></ul><div class="save_code tracking-ad" data-mod="popu_249"><a target=_blank target="_blank" href="javascript:;"><img src="http://static.blog.csdn.net/images/save_snippets.png" alt="" /></a></div>

2.1 存在监听事件

DOWN 之后发生了什么?

DOWN 事件是手势的开始,好似一个侦察兵,任务是检查有没有子 View 愿意消费它及后续事件,如果有做好标记(TouchTarget)。



DOWN 就像一个侦察兵

上层的 ViewGroup 都很“谦虚”,收到 DOWN 事件后,以类似递归的方式询问自己的子 View 是否愿意消费。从顶层的 DecorView 开始,每个 ViewGroup 遍历自己的所有子 View,遍历的顺序与其添加子 View 的顺序相反。找出被点击到的子 View 后,调用其 dispatchTouchEvent() 方法,如果该子 View 绑定了监听事件(如果是 OnTouchListener,其 onTouch() 方法必须返回 true;如果在 OnTouchListener.onTouch()
里面做了处理,但是返回 false,仍然认为是未消费),其 dispatchTouchEvent() 返回 true,然后标记该该子 View:将其封装成 TouchTarget 对象,并将 mFirstTouchTarget 指向该对象;如果以被点击到的 View 为根节点的 View 树没有找到接收该事件的子 View,同样做标记: mFirstTouchTarget = null,alreadyDispatchedToTouchTarget = false。

标记的作用是后续事件到来时,就不用再遍历寻找被击中的子 View 了,而是通过标记直接找到该子 View。

MOVE / UP 之后发生了什么?

后续事件直接交给每个 ViewGroup 的 mFirstTouchTarget 里面的子 View,调用子 View 的 dispatchTouchEvent() 方法,直到红色的 TextView 消费了这些事件。



红色 TextView 上绑定监听事件时的事件消费过程

2.2 没有监听事件

假设所有的 ViewGroup 或 View 上都没有绑定监听事件(或者绑定了 OnTouchListener,但是其 onTouch() 返回 false),红色 TextView 上也没有任何监听事件。DOWN 事件一直被各级 ViewGroup 谦虚的传递到最底层的 TextView,这时连 TextView 也不愿消费之,于是其 dispatchTouchEvent() 返回false,然后 TextView 的父 View 接着执行自己的 dispatchTouchEvent(),走到 OnTouchListener.onTouch()
和 onTouchEvent() 里面,当然这两个都返回 false;然后 TextView 的父 View 的 dispatchTouchEvent() 方法执行完毕,返回到了 TextView 的父 View 的父 View 的dispatchTouchEvent() 方法中,然后执行其 OnTouchListener.onTouch() 和 onTouchEvent() 里面……

最后,各个层级的 mFirstTouchTarget = null,ViewGroup.dispatchTransformedTouchEvent() 方法的 child 参数为 null,每个 ViewGroup 都被当做 View 处理,从最底层开始,依次调用 View.dispatchTouchEvent() 方法(最终调用是 View.onTouchEvent() 方法),直至 Activity.dispatchTouchEvent()。

对于后续的事件,由于 DecorView.mFirstTouchTarget = null,调用dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS),被 DecorView 的父类即 View.dispatchTouchEvent() 方法消费。



2.3 ViewGroup 拦截后续事件

我们把 RelativeLayout 换成可以上下滑动的 ScrollView,其子 View 是一个 TextView,TextView 绑定了监听事件。

如果给 TextView 的背景设置了按压态,我们按住 TextView 不松手也不滑动时,我们能看到背景变色,说明 DOWN 确实被 TextView 消费了。

仍然保持手指的按压状态,然后上下滑动,ScrollView 开始滚动,说明 MOVE 被 TextView 的父 View 即 ScrollView 消费了,这时 TextView 会接收到 CANCEL 事件。



声称对 DOWN “感兴趣”但是后续事件被父 View 拦截的子 View 会收到 CANCEL 事件

3. 应用案例

3.0 案例零: 优化层级的重要性

从上面的分析可以看出,不管有没有设置监听事件,每次点击,都会触发从 View 树的自上而下的单路径遍历,层级越多遍历耗时越多,响应时间越大,用户体验越差。从这个角度,也可以说明布局层级优化的重要性。关于布局优化,请移步《 布局优化技巧笔记》

还说明,没事别闲的蛋疼在屏幕上乱摸乱点,虽然屏幕没什么反应,但是屏幕后面是有代码在空跑的,当然也在耗电。

3.1 案例一:不同种类监听事件的优先级

如果给某个 View 同时绑定 OnClickListener,OnTouchListener,TouchDelegate(关于 mTouchDelegate 的使用参见《用 TouchDelegate 扩大子 View 的点击区域》)三个事件(简直丧心病狂),那么它们的执行顺序是怎样的呢?

这时我们需要查看 View.dispatchTouchEvent() 方法,看里面是怎么处理这些监听事件的:

<code class="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">// some code</span>

<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-comment">// some code</span>

<span class="hljs-keyword">return</span> result;
}

<span class="hljs-keyword">public</span> boolean <span class="hljs-title">onTouchEvent</span>(MotionEvent <span class="hljs-keyword">event</span>) {

<span class="hljs-comment">// some code</span>

<span class="hljs-keyword">if</span> ((viewFlags & ENABLED_MASK) == DISABLED) {
<span class="hljs-keyword">if</span> (action == 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}

<span class="hljs-keyword">if</span> (mTouchDelegate != <span class="hljs-keyword">null</span>) {
<span class="hljs-keyword">if</span> (mTouchDelegate.onTouchEvent(<span class="hljs-keyword">event</span>)) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
}
}

<span class="hljs-comment">// some code </span>

<span class="hljs-keyword">if</span> (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
<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-comment">// more code</span>
}

<span class="hljs-keyword">public</span> boolean <span class="hljs-title">performClick</span>() {
final boolean result;
final 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>;
}

<span class="hljs-comment">// more code </span>
}</code><ul style="" class="pre-numbering"><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></ul><div class="save_code tracking-ad" data-mod="popu_249"><a target=_blank target="_blank" href="javascript:;"><img src="http://static.blog.csdn.net/images/save_snippets.png" alt="" /></a></div>

我们可以看到,现在 View.dispatchTouchEvent() 方法中执行了 mOnTouchListener,然后进入 View.onTouchEvent() 方法,依次执行了 mTouchDelegate.onTouchEvent(event) 和 performClick(),后者执行了 OnClickListener.onClick(this)。

所以,三者的执行顺序为: OnTouchListener,TouchDelegate,OnClickListener。

同时,我们应当注意到在 View.onTouchEvent() 方法的注释:

<code class="hljs vhdl has-numbering"> // A disabled view that <span class="hljs-keyword">is</span> clickable still consumes the touch
// events, it just doesn<span class="hljs-attribute">'t</span> respond <span class="hljs-keyword">to</span> them.</code><ul style="" class="pre-numbering"><li>1</li><li>2</li></ul><div class="save_code tracking-ad" data-mod="popu_249"><a target=_blank target="_blank" href="javascript:;"><img src="http://static.blog.csdn.net/images/save_snippets.png" alt="" /></a></div>

即,同时满足 disabled 和 clickable = true 的 View 会消费触摸事件,但不会有任何反应。

3.2 案例二:如何更好的绑定监听事件?

回头看下这个例子:



<code class="hljs xml has-numbering"><span class="hljs-tag"><<span class="hljs-title">FrameLayout</span>></span>
<span class="hljs-tag"><<span class="hljs-title">ImageView</span> /></span>
<span class="hljs-tag"><<span class="hljs-title">TextView</span> /></span>
<span class="hljs-tag"></<span class="hljs-title">FrameLayout</span>></span></code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li></ul><div class="save_code tracking-ad" data-mod="popu_249"><a target=_blank target="_blank" href="javascript:;"><img src="http://static.blog.csdn.net/images/save_snippets.png" alt="" /></a></div>

需求:点击图片和数字具有相同的响应点击事件(如页面跳转等)。

这种情况下,只给 FrameLayout 绑定监听事件即可,虽然 DOWN 事件会下发到 ImageView 和 TextView,询问它们是否愿意消费,被拒绝后 DOWN 事件向上传递,依次经过 ImageView或TextView(具体是谁取决于点击坐标落在哪个的区域范围内) 和 FrameLayout 的 onTouchEvent() 方法,最后被 FrameLayout.onTouchEvent() 消费。后续事件将直接被 FrameLayout.onTouchEvent() 消费,而不再下发;



事件消费过程示意图

如果能继承 FrameLayout(假设为 MyFrameLayout),并重写 MyFrameLayout 的 onInterceptTouchEvent() 方法:

<code class="hljs java has-numbering">    <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) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
}</code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li></ul><div class="save_code tracking-ad" data-mod="popu_249"><a target=_blank target="_blank" href="javascript:;"><img src="http://static.blog.csdn.net/images/save_snippets.png" alt="" /></a></div>

这样 MyFrameLayout 直接拦截点击事件进行消费,不再下发给 ImageVIew 和 TextView,此时的事件消费过程为:



事件消费过程示意图

如果不在 FrameLayout 上绑定,而是给 ImageView 和 TextView 绑定,所有的事件都会经过 FrameLayout 继续下发给ImageView 或 TextView,方法调用层数增加,相应时间延长;

如果在 FrameLayout、ImageView 和 TextView 上同时绑定事件,则根据事件的落点进行判断。例如,落在 ImageView 范围内的事件(当然也落在了 FrameLayout 范围内)只触发 ImageView 的事件;落在 FrameLayout 范围内但是既未落在 TextView 范围内也未落在 ImageView 范围内的事件,才会由 FrameLayout 进行消费。这种效果与我们的直观认识一致的。

3.3 案例三:瀑布流效果

这是一个经典案例(虽然在实际开发中很少见到),通过重写 ViewGroup.onInterceptTouchEvent() 方法和 ViewGroup.onTouchEvent() 方法来控制事件的分发过程,详见该博客《Android事件分发机制练习—打造属于自己的瀑布流》

3.4 案例四:ClickableSpan 的 Bug

请移步《TextView ClickableSpan 事件触发的坑》

3.5 案例五:优雅的隐藏 PopupWindow

3.6 案例六:左划露出删除按钮

3.7 案例七:优雅的隐藏输入框和软键盘

4. 更多好文

Mastering the Andrdoid Touch System
《 Android事件分发机制完全解析,带你从源码的角度彻底理解(下)》
《Android Touch事件传递机制全面解析(从WMS到View树)》
《Android事件传递之子View和父View的那点事》
Android笔记:触摸事件的分析与总结—-TouchEvent处理机制
Android事件传递机制
Android Touch事件派发流程源码分析
Understanding Android Input Touch Events System Framework (dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent, OnTouchListener.onTouch)
Handling single and multi touch on Android - Tutorial
Android Touch and Multi-touch Event Handling
Android Development - What I wish I had known earlier
本文出自http://blog.csdn.net/zhaizu/article/details/50489398,转载请注明出处。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 事件分发 view