淺嘗安卓事件分發機制

本文出自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() 方法中打印日誌,觀察日誌輸出的時機和順序,這是一種很直觀的方法。
單步調試也是很有效的嘗試方法,通過觀察不同案例下(如設置監聽事件和未設置監聽事件)源碼的執行路線和中間變量的賦值,我們往往有種茅塞頓開、豁然開朗的感覺。閱讀和調試,一靜一動,相得益彰。

爲了避免發生斷點失效或執行順序混亂等詭異情況,我們要做到如下兩點:

  1. 調試需要使用 Nexus 真機/模擬器(Genymotion 或 SDK 自帶的都行),因爲這些機型的 Rom 版本是原生的,與源代碼一致;
  2. 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. 更多好文

本文出自http://blog.csdn.net/zhaizu/article/details/50489398,轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章