1. 程式人生 > >Android事件分發機制——View(一)

Android事件分發機制——View(一)

   /**
     * Implement this method to handle touch screen motion events.
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;


        /**
         * 如果一個View是disabled, 並且該View是Clickable或者longClickable, 
         * onTouchEvent()就不執行下面的程式碼邏輯直接返回true, 表示該View就一直消費Touch事件
         */
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // 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 (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        /**
         * 如果不可點選(既不能單擊,也不能長按)則直接返回false
         */
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        /**
                         * 是否需要獲得焦點及用變數focusTaken設定是否獲得了焦點.
                         * 如果我們還沒有獲得焦點,但是我們在觸控屏下又可以獲得焦點,那麼則請求獲得焦點
                         */
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        /**
                         * 判斷是否進行了長按事件的返回值情況,如果為false則移除長按的延遲訊息並繼續往下執行
                         */
                        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) {
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                            /**
                             * ViewConfiguration.getPressedStateDuration() 獲得的是按下效
                             * 果顯示的時間,由PRESSED_STATE_DURATION常量指定,在2.2中為125毫秒
                             */
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;


                case MotionEvent.ACTION_DOWN:
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPrivateFlags |= PREPRESSED;
                    /**
                     * 給mHasPerformedLongPress設定初始值為false
                     */
                    mHasPerformedLongPress = false;
                    /**
                     * 傳送一個延遲訊息延遲時間為ViewConfiguration.getTapTimeout()在2.2的原始碼中此值為115毫秒
                     * 到達115毫秒後會執行CheckForTap()方法,如果在這115毫秒之間使用者觸控移動了,則
                     * 刪除此訊息.否則執行按下狀態,在CheckForTap()中檢查長按.
                     */
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    break;


                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    removeTapCallback();
                    break;


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


                    // Be lenient about moving outside of buttons
                    int slop = mTouchSlop;
                    /**
                     * 當手指在View上面滑動超過View的邊界,
                     */
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                            (y < 0 - slop) || (y >= getHeight() + slop)) {
                        // Outside button
                    /**
                     * 如果手指滑動超過Vie的邊界則移除DOWN事件中設定的檢測
                     */
                        removeTapCallback();
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();


                            // Need to switch from pressed to not pressed
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
            return true;
        }
        return false;
    }
下面我們來拆分一下上面的原始碼首先執行一個if判斷語句
if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // 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 ));
        }
在這裡要特別注意的是此方法中if(((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE))它的範圍這裡把中間的程式碼省略如下:
public boolean onTouchEvent(MotionEvent event) {
        。。。。。。。。。。。
       	此處有省略
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
            。。。。。。。。。。。
            此處有省略
            }
            return true;
        }
        return false;
    }
從上面的簡化程式碼中我們可以看出只要是進入了if判斷語句則onTouchEvent一定會返回true即消費事件,並且進入此if語句的條件為 此View是可以點選的或者是可以長按的。 下面我們來拆分一下上面的原始碼首先執行一個if判斷語句
if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // 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是disabled, 並且該View是Clickable或者longClickable, onTouchEvent()就不執行下面的程式碼邏輯直接返回true, 表示該View就一直消費Touch事件,這一點從上面的程式碼可以看出,如果一個enabled的View,並且是clickable或者longClickable的,onTouchEvent()會執行下面的程式碼邏輯並返回true,這一點從上面的省略程式碼片段可以得出。 綜上,一個clickable或者longclickable的View是一直消費Touch事件的,而一般的View既不是clickable也不是longclickable的(即不會消費Touch事件,只會執行ACTION_DOWN而不會執行ACTION_MOVE和ACTION_UP) Button是clickable的,可以消費Touch事件,但是我們可以通過setClickable()和setLongClickable()來設定View是否為clickable和longClickable。 接著我們來分析一下onTouchEvent中的事件上面的程式碼中有比較詳細的註釋,我在這裡再分析一下 ACTION_DOWN:
             case MotionEvent.ACTION_DOWN:
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPrivateFlags |= PREPRESSED;
                    mHasPerformedLongPress = false;
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    break;
在這個方法中首先給mPrivateFlags設定一個PREPRESSED的標識,然後設定為mHasPerformedLongPress設定一個初始值false,接著會執行一個延遲
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
在這裡ViewConfiguration.getTapTimeout()的值為115毫秒(注意以上原始碼包括時間常量都是2.2原始碼中,其他原始碼可能會稍有不同)這個延遲有什麼作用呢?

在給定的TapTimeout時間之內,使用者的觸控沒有移動,就當作使用者是想點選,而不是滑動.具體的做法是,將 CheckForTap的例項mPendingCheckForTap新增時訊息隊例中,延遲執行。如果在這tagTimeout之間使用者觸控移動了,則刪除此訊息.否則執行按下狀態.然後檢查長按。

經過115毫秒的延遲後會執行CheckForTap方法,這個方法是幹什麼的呢?來看下原始碼
       /**
        * ACTION_DOWN事件延遲115毫秒後呼叫
        */
     private final class CheckForTap implements Runnable {
        public void run() {
            mPrivateFlags &= ~PREPRESSED;
            mPrivateFlags |= PRESSED;
            refreshDrawableState();
            /**
             * 如果View支援長按事件即View是LONG_CLICKABLE的則傳送一個長按事件的檢測
             */
            if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
                postCheckForLongClick(ViewConfiguration.getTapTimeout());
            }
        }
    }
這個方法可以看到如果View是LONG_CLICKABLE的就是執行postCheckForLongClick(ViewConfiguration.getTapTimeout())這個方法來檢測長按事件,但是一般的View不是LONG_CLICKABLE的,可能有的人會有疑問,如果View不是LONG_CLICKABLE的怎麼執行長按事件啊?此時我們需要呼叫setOnLongClickListener實現OnLongClickListener介面 原始碼如下:
    /**
     * Register a callback to be invoked when this view is clicked and held. If this view is not
     * long clickable, it becomes long clickable.
     *
     * @param l The callback that will run
     *
     * @see #setLongClickable(boolean)
     */
    public void setOnLongClickListener (OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable( true);
        }
        mOnLongClickListener = l;
    }
從原始碼中我們可以看到設定了OnLongClickListener後如果這個View不是LONG_CLICKABLE的,那麼就把它設定成LONG_CLICKABLE的。這樣我們回到CheckForTap方法在View是LONG_CLICKABLE的情況下就會呼叫postCheckForLongClick方法,這個方法的原始碼如下
   private void postCheckForLongClick(int delayOffset) {
    /**
     * 設定mHasPerformedLongPress為false表示長按事件還未觸發
     */
        mHasPerformedLongPress = false;
        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.rememberWindowAttachCount();
        /**
         * 此delayOffset是從上面的CheckForTap類中傳過來的值為ViewConfiguration.getTapTimeout()
         * ViewConfiguration.getLongPressTimeout()在2.2中為500毫秒,也就是經過500-115毫秒後會執行CheckForLongPress方··         * 法在CheckForLongPress方法中會呼叫執行長按事件的方法,由於在ACTION_DOWN事件中有一個延遲訊息延遲115毫秒後          * 執行CheckForTap中的run方法所以這裡500-115+115=500也就是說從按下起經過500毫秒會觸發長按事件的執行
         */
        postDelayed(mPendingCheckForLongPress,ViewConfiguration.getLongPressTimeout() - delayOffset);
    }
在上面的方法會有一個延遲經過500-115毫秒後會執行CheckForLongPress方法。
class CheckForLongPress implements Runnable {


        private int mOriginalWindowAttachCount;
        /**
         * 因為等待形成長按的過程中,介面可能發生變化如Activity的pause及restart,這個時候,長按應當失效.
         * View中提供了mWindowAttachCount來記錄View的attach次數.當檢查長按時的attach次數與長按到形成時.
         * 的attach一樣則處理,否則就不應該再當前長按. 所以在將檢查長按的訊息新增時隊伍的時候,要記錄下當前的windowAttach          *Count.
         */
        public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick()) {
                /**
                 * 執行長按事件後返回值為true,設定mHasPerformedLongPress為true此時會遮蔽點選事件
                 */
                    mHasPerformedLongPress = true;
                }
            }
        }


        public void rememberWindowAttachCount() {
            mOriginalWindowAttachCount = mWindowAttachCount;
        }
    }
從CheckForLongPress的run方法中可以看到如果performLongClick()的返回值為true mHasPerformedLongPress才為true,那麼我們看看performLongClick它的做了哪些動作呢?我們來看看原始碼
    /**
     * Call this view's OnLongClickListener, if it is defined. Invokes the context menu
     * if the OnLongClickListener did not consume the event.
     *
     * @return True there was an assigned OnLongClickListener that was called, false
     *         otherwise is returned.
     */
    public boolean performLongClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        /**
         * 到了重點可以看到在這裡會執行我們為View設定的長按事件的回撥,這裡的mOnLongClickListener就是我們自己給View設定的長按的監聽,
         * 從這裡也可以得出一個結論即長按事件是在ACTION_DOWN中執行的
         */
        if (mOnLongClickListener != null) {
            handled = mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {
            handled = showContextMenu();
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }
終於來了個重點我們看到在其中有個判斷
if (mOnLongClickListener != null) {
            handled = mOnLongClickListener.onLongClick(View.this);
        }
也就是說如果你設定了長按的監聽,那麼mOnLongClickListener!=null此時就會執行我們重寫的onLongClick()方法,這裡我們也得出一個結論: 即長按事件是在ACTION_DOWN中執行的。

到這裡我們可以總結一下:首先當執行ACTION_DOWN事件後會設定一個PREPRESSED標識,如果這次點選持續115毫秒後就會發送一個檢測長按的延遲任務,這個任務的延遲時間是500-115毫秒,這個115毫秒就是檢測PREPRESSED所經歷的時間,所以這樣算一下就可以知道當按鈕從按下的那一刻起經歷了500毫秒就會觸發長按事件(注意這個Android 2.2中的原始碼,其它的系統的時間會稍有差異) 通過以上的分析我們還可以得出如下結論:

1、如果此時設定了長按的回撥,則執行長按時的回撥,且如果長按的回撥返回true;才把mHasPerformedLongPress置為ture;

2、否則,如果沒有設定長按回調或者長按回調返回的是false;則mHasPerformedLongPress依然是false; 一般的View預設是不消費touch事件的,我們要想執行點選事件必須要呼叫setOnClickListener()來設定OnClickListener介面,我們看看這個方法的原始碼就知道了
public void setOnClickListener (OnClickListener l) {
        if (!isClickable()) {
            setClickable( true);
        }
        mOnClickListener = l;
    }
看到沒?當我們設定了onClickListener時如果isClickable()=false,就執行 setClickable(true)。