Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android-----事件分發機制測試系列(三)

android-----事件分發機制測試系列(三)

編輯:關於Android編程

上一篇我們主要主要是從ViewGroup分發的角度測試了下事件分發機制,但沒有涉足多少View的事件分發,也就是說我們沒有為MyRelativeLayout、MyLinearLayout、以及MyButton設置Touch和Click監聽事件,這一篇將來測試下View的事件分發過程,為了比較簡潔的顯示打印信息,我簡化了布局文件,具體的布局文件代碼如下:

 


    
也即布局文件圖是醬紫的:

 

\

具體的測試代碼就是在MainActivity和MyButton中的dispatchTouchEvent以及onTouchEvent方法中打印Log,以及在MyRelativeLayout的dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent中打印Log,並且為MyRelativeLayout、MyButton設置了onTouchListener、onLongClickListener、以及onClickListener事件監聽器;

我們點擊MyButton按鈕,查看Logcat輸出結果如下:

 

06-30 10:27:37.127: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN
06-30 10:27:37.127: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN
06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN
06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN--->false
06-30 10:27:37.132: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN
06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouch--->DOWN
06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN
06-30 10:27:37.144: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.144: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.144: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.144: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.689: I/System.out(2705): MyButton--->onLongClick
06-30 10:27:37.832: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP--->false
06-30 10:27:37.832: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouch--->UP
06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP
06-30 10:27:37.842: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP--->true
06-30 10:27:37.842: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP--->true
06-30 10:27:37.842: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP--->true
06-30 10:27:37.842: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP--->true
06-30 10:27:37.852: I/System.out(2705): MyButton--->OnClick

如果你仔細查看輸出的話,有一部分會讓你覺得很奇怪的,就是輸出的第12行的onLongClick方法和第24行的onClick方法是在dispatchTouchEvent方法執行結束之後才開始執行的,這一點讓我感到很詫異,所以專門寫了這篇博客來試著從代碼層面解釋下這種現象的原因,因為網上看別人的分析過程均沒有涉足到我想要的部分,所以打算自己分析一次View分發過程的源碼,有什麼錯誤還請指正,源碼分析結束之後我們再來看看Logcat輸出或許你會明白點了;

一個事件傳遞到View上面首先執行的就是他的dispatchTouchEvent方法,那麼很自然首先應該從View的dispatchTouchEvent開始分析:

 

 public boolean dispatchTouchEvent(MotionEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }

            if (onTouchEvent(event)) {
                return true;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }

第6行判斷是否過濾掉當前事件系列,什麼情況下會被過濾呢?View在被遮蓋的時候,onFilterTouchEventForSecurity方法會返回true,進而直接在第22行dispatchTouchEvent返回了false;如果當前View沒有被遮蓋的話,執行7--16行的if語句塊,首先獲取到ListenerInfo對象,他是View的靜態內部類,這個對象主要存儲的就是一些我們所設置的事件監聽器了,稍微看看裡面的幾個屬性字段:

 

static class ListenerInfo {
    public OnClickListener mOnClickListener;
    protected OnLongClickListener mOnLongClickListener;
    private OnTouchListener mOnTouchListener;
}
接著走到第9行的if判斷語句處,這個地方有四個判斷條件,第1個li指的就是ListenerInfo對象,第2個li.mOnTouchListener其實是在判斷是否設置Touch事件監聽器,具體li.mOnTouchListener的值等於什麼呢?從下面代碼中可以看出來:

 

 

  public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }
這是一個public類型的方法,我們通常在程序中為某個控件設置Touch監聽器就是調用的這個方法,那麼其實第2個判斷條件是在查看我們是否有設置Touch事件監聽器,第三個條件是在查看我們當前的View是否是enable的,也就是說當前View本身是否能夠接受觸摸事件,第4個就是onTouch方法的返回值了,這個方法可以被重寫,默認情況下是返回false的;如果這個if判斷的四個條件都滿足的話,執行11行,直接返回,也就是當前事件已經分發結束了,從這裡可以看出View事件分發首先執行的是onTouch(當然你必須設置Touch事件監聽器);如果if的四個條件中有一個是false,就會執行第14行的if語句,調用onTouchEvent來處理事件,這裡我們有必要來看看onTouchEvent方法了;

 

該方法是public修飾的,所以你可以在子類中重寫它,方法比較長,我們截段分析:

 

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

首先判斷當前View如果本身就不支持觸摸的話,進入if語句塊,第2行判斷當前事件是UP並且設置了PFLAG_PRESSED標志的話,則調用setPressed將標志置位,因為整個事件的最後一步就是UP了,所以我們必須在事件結束之前將設置的標志還原;

 

public void setPressed(boolean pressed) {
        final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);

        if (pressed) {
            mPrivateFlags |= PFLAG_PRESSED;
        } else {
            mPrivateFlags &= ~PFLAG_PRESSED;
        }

        if (needsRefresh) {
            refreshDrawableState();
        }
        dispatchSetPressed(pressed);
    }
這裡傳給setPressed的參數是false,所以執行7行代碼取反還原;回到onTouchEvent方法中,第7行查看View是否有設置點擊和長點擊,有的話返回true,沒有返回false,從這裡可以看出onTouchEvent的返回值是跟你View是enable還是disable沒有多大關系,只要你設置了clickable或者longClickable,那麼他就會返回true;

 

接著分析onTouchEvent下面代碼:

 

  if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
查看是否有設置事件代理,有的話,則將事件交給代理處理,根據代理事件onTouchEvent方法來判斷是否返回true;

 

接下來的onTouchEvent代碼比較長,我們先來整理一個大體框架:

 

 if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
           ...............
           ...............
           ...............
        return true;
 }
 return false;
可以看到只要clickable和longClickable有一個被設置就會返回true,只有在兩者都沒設置的情況下才會返回false,這也更加印證了onTouchEvent方法的返回值只和你有沒有設置clickable和longClickable有關,和View的enable和disable沒什麼關系;

 

如果clickable和longClickable有一個被設置,那麼進入if語句塊中,該語句塊是一個switch語句,我們按照事件的觸發順序來進行分析,即DOWN--->MOVE--->UP:

先來看DOWN部分:

 

case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true);
                        checkForLongClick(0);
                    }
                    break;
剛進來首先設置mHasPerformedLongPress的值為false,這個比較關鍵了,用來表示是否有執行長點擊事件,如果有設置長點擊事件並且onLongClick方法返回true的話,這個值是會被改變成true的,等會你就看到什麼原因啦,接著第9行判斷當前View是否在正在滾動的控件中,在的話就滿足第13行的if條件語句調用postDelay方法來延期press的反饋,為什麼要這麼做呢?從第11行的注釋看出來是為了防止當前事件是一個滾動事件,進入if語句塊之後執行第14行,設置PREPRESSED標志,這個標志表示的是prepressed狀態,這個狀態存在於ACTION_DOWN和真正意識到是press之間,用於識別是不是tap事件,接著第18行執行了postDelayed方法,參數ViewConfiguration.getTapTimeout()的值是150ms,這個方法的代碼如下:

 

 

public boolean postDelayed(Runnable action, long delayMillis) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.postDelayed(action, delayMillis);
        }
        // Assume that post will succeed later
        ViewRootImpl.getRunQueue().postDelayed(action, delayMillis);
        return true;
    }
可以看到這個方法其實是調用了handler的postDelayed方法,將action加入到了MessageQueue消息隊列中,熟悉handler機制的應該知道隨後調用的將是action的run方法了,也就是mPendingCheckForTap的run方法了,mPendingCheckForTap是CheckForTap類型的對象,具體定義如下:

 

 

 private final class CheckForTap implements Runnable {
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            setPressed(true);
            checkForLongClick(ViewConfiguration.getTapTimeout());
        }
    }
run方法首先是將PREPRESSED標志置位,接著執行setPressed方法,設置PRESSED標志,這個方法在前面又出現過,只不過前面調用的是setPressed(false)而已;接著便調用checkForLongClick來查看是否有長點擊事件了,傳入的參數是150ms;如果當前View不在滾動的控件中的話,則直接執行第19行的else語句,接著調用setPressed以及checkForLongClick方法,這裡執行的內容就和CheckForTap的run方法一致了,只不過傳入的checkForLongClick參數值不同而已,那麼我們就該看看checkForLongClick方法了:

 

 

    private void checkForLongClick(int delayOffset) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.rememberWindowAttachCount();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }
該方法第2行判斷你有沒有設置longClick事件,有的話進入if語句塊,首先還是將mHasPerformedLongPress設置為false,接著第9行同樣調用了postDelayed方法,傳入的第二個參數是ViewConfiguration.getLongPressTimeout()-delayOffset,ViewConfiguration.getLongPressTimeout()的默認值是500ms,從這句話我們可以看出來不管你是通過checkForLongClick(0)還是checkForLongClick(delayOffset)其中delayOffset大於0,調用checkForLongClick方法,其實檢測你是不是長點擊的時間是一致的,都是500ms,你點擊的時間超過500ms的話,會認為是長點擊,很自然調用的是mPendingCheckForLongPress的run方法:

 

 

       public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick()) {
                    mHasPerformedLongPress = true;
                }
            }
        }
注意到這個方法第2行會判斷isPressed(),什麼意思呢?就是說如果你500ms之後還是處於點擊狀態,那麼你就是長點擊了,執行if語句塊中的內容,第4行執行的是performLongClick方法,而這個方法就主要是執行的我們的OnLongClickListener監聽方法了,來看看裡面的代碼:

 

 

public boolean performLongClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {
            handled = showContextMenu();
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }
同樣OnLongClickListener存儲在ListenerInfo對象裡面,第7行執行了onLongClick方法,並且獲取到返回值,回到前面的run方法第4行會判斷這個返回值是true的話,執行mHasPerformedLongPress=true語句,我們有必要說明下mHasPerformedLongPress
的作用,他是用於表示你的onLongClick是否返回true的,如果沒有設置LongClick監聽事件或者設置了LongClick監聽事件但是onLongClick方法返回false,那麼mHasPerformedLongPress的值將是false,隨後在UP事件判斷中才會執行接下來的click點擊事件,如果設置了LongClick監聽事件並且onLongClick方法返回true,那麼在隨後的UP事件判斷中將不再會執行click點擊事件,從這裡我們可以看出其實onLongClick方法是優先於onClick執行的,這也就解釋了我們平常使用onLongClick方法有返回值而onClick方法沒有返回值的問題了;這樣的話DOWN事件處理結束了;

 

從DOWN事件的處理中,我們可以知道longclick是在它裡面進行檢測的,並且如果500ms之後還處於press狀態的話會調用它的performLongClick,而這個方法是在子線程中調用的,所以就出現了我們上面Log輸出第12行在dispatchTouchEvent返回之後才執行的結果;

接下來分析的是MOVE事件:

 

        case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
MOVE相對來說比較簡單,首先是獲取你當前觸摸處的位置,接著第6行判斷你觸摸的地方是否處於當前View的邊界內,不處於的話會執行7--15行代碼,處於的話不做任何事,我們來看看不處於情況下做了些什麼,首先執行第8行的removeTapCallback方法,這個方法:

 

 

private void removeTapCallback() {
        if (mPendingCheckForTap != null) {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            removeCallbacks(mPendingCheckForTap);
        }
    }
主要是置位PREPRESSED標志,並且從當前的MessageQueue消息隊列中移出封裝有mPendingCheckForTap這個線程的消息,為什麼要這麼做呢?因為你都已經不在我當前View的控制范圍內了,我也沒必要看你接下來的一些操作了;第9行如果我們設置了PRESSED標志的話,說明在DOWN事件中也在MessageQueue裡面添加了封裝有監聽長點擊事件的Message,那麼就需要調用第11行的removeLongPressCallback方法,將該Message從MessageQueue中移出,並且13行調用setPressed方法將PRESSED標志置位;

 

接下來就是UP事件了:

 

        case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true);
                       }

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;
UP事件代碼相對來說比較多,第3行的判斷條件說明只要你在MOVE的過程中沒有移出邊界都會滿足,接著第19行會判斷mHasPerformedLongPress的值,這個值只有在你設置了longclick監聽事件,並且在onLongClick方法中返回true的情況下才會是true,否則均是false,這個在上面已經說過了,我們假定這裡mHasPerformedLongPress的值是false,進入if語句塊,首先調用removeLongPressCallback,從MessageQueue中移出長點擊監聽Message,接著第28--33行的代碼比較關鍵,這裡將是解釋我們上面Log輸出的重要部分,如果沒有PerformClick對象則創建,並在第31行通過post方法將PerformClick對象添加到MessageQueue消息隊列中,接下來將是執行PerformClick的run方法了:

 

 

private final class PerformClick implements Runnable {
        public void run() {
            performClick();
        }
    }

 

很明顯PerformClick是一個線程,在他的run方法裡面也會執行performClick,也就是說不管第31行post方法有沒有執行成功都會執行performClick方法的,那麼這裡為什麼要用到post通過子線程來執行performClick而不是直接執行performClick呢?根據官方的注釋看到這樣做的目的是為了在click執行之前讓view上面的其他visual 狀態能夠更新,來看看performClick方法:

 

public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            return true;
        }

        return false;
    }
可以看到如果我們設置了OnClickListener監聽器的話,會執行第7行代碼,也就執行了我們的onclick方法了;因為如果我們調用post的話,performClick是執行在PerformClick類型的子線程中的,所以我們上面的Log輸出會出現onClick方法在dispatchTouchEvent事件返回之後才執行的情況了;UP事件後面的一些操作是用於狀態置位的,我們再次不做過多牽涉;

 

這樣的話,View的事件分發源碼分析完畢了,我們做個小結以此來解釋上面的Log輸出:

(1)View中如果我們設置了onTouchListener、onLongClickListener以及onClickListener的話,三者的執行順序是onTouch--->onLongClick--->onClick;

(2)如果我們在onLongClick方法中返回true的話,那麼隨後的onClick方法將不再會執行;

(3)我們的onLongClick方法以及onClick方法可能會在dispatchTouchEvent方法返回之後才去執行,原因在於onLongClick方法是在CheckForLongPress類型的子線程中執行的,onClick是在PerformClick類型的子線程中執行的,也即解釋了上面Log輸出第12行出現在第9行之後,以及第24行出現在第21行之後的問題;

好了,這篇先到這裡了,下篇從實例測試的角度進行不同情況下的分析;

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved