1. 程式人生 > >Android事件分發機制流程詳解(二)

Android事件分發機制流程詳解(二)

前言:上一篇我們已經從事件分發執行流程入手,一起來了解並分析了事件分發的經過,大家應該從分析中能對事件分發的有個總體的認識,並且我相信應該也能自己分析出事件會如何執行,其實就那麼點東西,弄明白了就不難了,但是今天我們還是要來看看activity,viewgroup,view的相關原始碼來學習一下他們的工作原理,那就開始吧!

首先來結合我們上一篇的工程情況來看看:
這裡寫圖片描述

再次強調,1個點選事件發生後,事件先傳到Activity、再傳到ViewGroup、最終再傳到 View,那麼我們分別從Activity對點選事件的分發機制、ViewGroup對點選事件的分發機制、View對點選事件的分發機制來分析原始碼。

一、Activity對點選事件的分發機制

從我們上一天的分析可以看到,當一個新的點選事件發生時,首先是會進入到Activity的dispatchTouchEvent()方法內,那麼我們先來看看Activity的dispatchTouchEvent()的原始碼:

public boolean dispatchTouchEvent(MotionEvent ev) {
    //由於每次點選事件第一個肯定是ACTION_DOWN,所以這裡肯定是true
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if
(getWindow().superDispatchTouchEvent(ev)) { //如果getWindow().superDispatchTouchEvent(ev)是true的話 //那麼dispatchTouchEvent()返回的就是true,事件被消費,傳遞結束 return true; } //否則就會執行onTouchEvent方法 return onTouchEvent(ev); }
  • activity的onUserInteraction方法:
/**
 * 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. * * @see #onUserLeaveHint() */ public void onUserInteraction() { }

這裡返回的是一個空方法,當我們不知道這個方法是幹啥用的時候,官方的註釋就是最好的答案,也許你英語不好,但是我們有線上翻譯不是麼?這個方法主要是給使用者提供告知和手機的一些互動的情況,要是用得到的話就實現它然後重寫程式碼,這裡我們用不到,不做了解。

  • activity的getWindow().superDispatchTouchEvent(ev)

通過滑鼠點進去後發現,這是Window的抽象類

/**
 * 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.
 *
 */
public abstract boolean superDispatchTouchEvent(MotionEvent event);

我們只能看看Window的實現類了
這裡寫圖片描述
可以看到Window唯一的實現類只有PhoneWindow,那麼我們進到PhoneWindow中查詢superDispatchTouchEvent方法,程式碼如下:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

接著點進mDecor中去看看,會發現進入了DecorView中:

public boolean superDispatchTouchEvent(MotionEvent event) {
  return super.dispatchTouchEvent(event);
}

繼續往下找,發現進入了ViewGroup中:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...具體的到ViewGroup中講解
}

現在再來審視一下這張圖
這裡寫圖片描述
發現沒有,事件通過getWindow().superDispatchTouchEvent(ev)往下一直傳遞到ViewGroup中,如果ViewGroup的dispatchTouchEvent返回了false,那麼我們這裡就會執行onTouchEvent方法,交由我們自己實現的onTouchEvent方法處理,如果ViewGroup的dispatchTouchEvent返回了true,那麼我們這return true,事件就會往下執行,符合我們上一篇首圖的邏輯。那麼接下來我們順著往下走來看看onTouchEvent方法。

  • activity的onTouchEvent()方法(系統預設的,如果未重寫執行此流程)

來看看原始碼:

/**
 * 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.
 *
 * @param event The touch screen event being processed.
 *
 * @return Return true if you have consumed the event, false if you haven't.
 * The default implementation always returns false.
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

這段程式碼表示:onTouchEvent方法在觸控式螢幕幕事件沒有被任何檢視處理時呼叫,即沒有任何檢視(ViewGroup、View)消費事件時,Activity的onTouchEvent方法會執行。如果你已經消費了這個事件,返回true,如果你沒有,返回false,預設的實現總是返回false。即只有在點選事件在Window邊界外才會返回true,一般情況都返回false。

下面我們來看看mWindow.shouldCloseOnTouch(this, event)方法。

  • Window的shouldCloseOnTouch()方法
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
   if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
            && isOutOfBounds(context, event) && peekDecorView() != null) {
        return true;
    }
    return false;
}

這個方法主要用來判斷是否是邊界外點選事件+是否是DOWN事件+event的座標是否在邊界內。如果是在邊界外並且是DOWN事件,那麼就意味著返回true,那麼就是消費事件,如果不是則返回false,不消費事件,到這裡Activity的事件分發已經執行完畢了。

因為我們在MainActivity中已經重寫了onTouchEvent方法,所以上述過程當getWindow().superDispatchTouchEvent(ev)返回false的時候,事件就會交由我們MainActivity中的onTouchEvent方法處理,所以看看我們上一篇的測試3
這裡寫圖片描述
並且執行結果也和我們上述分析一致:
這裡寫圖片描述

二、ViewGroup對點選事件的分發機制

由於我的api較新,所以這裡的程式碼是Android7.0的程式碼,並且由於ViewGroup中的dispatchTouchEvent方法較長,所以咱們這裡只擇出部分關鍵程式碼來分析如何運作的。

// Check for interception.
//定義一個變數是否攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    //如果是down事件並且傳入物件不為空則進入
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        //如果不禁止攔截的話執行,是否攔截由自己的onInterceptTouchEvent方法決定
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        //如果是設定了禁止攔截的話,那麼表示不攔截,事件向下傳遞
        intercepted = false;
    }
} else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}
  • ViewGroup的onInterceptTouchEvent方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

實現此方法攔截所有觸控式螢幕運動事件,這允許我們觀看事件,因為它們被分派給它的孩子,並在任何時候對當前的手勢擁有所有權。使用這個函式需要一些注意,因為它是相當複雜的,您將在這裡收到down事件,down事件將由這個viewgroup的子節點處理,或者由您自己的onTouchEvent()方法處理。這意味著您應該實現onTouchEvent()以返回true。同樣,通過從onTouchEvent()返回true,您將不會在onInterceptTouchEvent()中接收任何後續事件,並且所有的觸控處理都必須在onTouchEvent()中發生,就像normal一樣。只要您從這個函式返回false,每個後續事件(包括最終的up)將首先在這裡傳遞,然後傳遞到目標的onTouchEvent()。
當前的目標將接收ACTION_CANCEL事件,並且不會在這裡傳遞更多的訊息。

盜取網上一篇部落格的其中的一張圖結合分析的結論來看看
這裡寫圖片描述

到這裡相信大家對ViewGroup如何分發事件有了自己的理解了,下面接著來看看View是處理事件的。

三、View對點選事件的分發機制

鑑於Android7.0的程式碼關於View這塊比較複雜,咱們還是看看Android2.0的程式碼吧,雖然程式碼簡單,但是工作流程肯定是一樣的,只不過後期做了優化,咱們瞭解原理就好了。

public boolean dispatchTouchEvent(MotionEvent event) {
   if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
 }
  • mOnTouchListener

關於onTouchListener的賦值在這個方法裡:

public void setOnTouchListener(OnTouchListener l) {
   mOnTouchListener = l;
}

可以看到,只要我們給view設定了setOnTouchListener,那麼mOnTouchListener一定是有值的。

  • (mViewFlags & ENABLED_MASK) == ENABLED

這個條件是判斷當前點選的控制元件是否enable,因為我們既然要給這個view設定點選事件的話,那麼我們肯定會給它setOnClickListener,所以這個條件應該是成立的。

  • mOnTouchListener.onTouch(this, event)

這個事件需要我們註冊了onTouchListener後進行回撥返回結果,舉個例子,在咱們上一篇中咱們也進行了註冊。

view.setOnTouchListener(new View.OnTouchListener() {
   @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("執行了onTouch(), 動作是:" + event.getAction());
        return false;
    }
});

我們已經分析過:
return false;那麼上述的if條件不成立,那麼就會執行onTouchEvent方法,和我們上一篇驗證的結果一樣。

return true;上述條件成立,事件傳遞結束,直接被消費掉,和我們驗證的結果也一樣。

接下來我們來看看onTouchEvent方法。

  • View.onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;

    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 (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        //如果view是可點選的,包括長按,那麼就進入switch條件中
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                if ((mPrivateFlags & PRESSED) != 0) {
                    // 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 (!mHasPerformedLongPress) {
                        // This is a tap, so remove the longpress check
                       if (mPendingCheckForLongPress != null) {
                           removeCallbacks(mPendingCheckForLongPress);
                        }

                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            //執行這個方法,見詳解
                            performClick();
                        }
                    }

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

                    if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }
                }
                break;

            case MotionEvent.ACTION_DOWN:
                mPrivateFlags |= PRESSED;
                refreshDrawableState();
                if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
                    postCheckForLongClick();
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
                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 = ViewConfiguration.get(mContext).getScaledTouchSlop();
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                        (y < 0 - slop) || (y >= getHeight() + slop)) {
                    // Outside button
                    if ((mPrivateFlags & PRESSED) != 0) {
                        // Remove any future long press checks
                        if (mPendingCheckForLongPress != null) {
                            removeCallbacks(mPendingCheckForLongPress);
                        }

                        // Need to switch from pressed to not pressed
                        mPrivateFlags &= ~PRESSED;
                        refreshDrawableState();
                    }
                } else {
                    // Inside button
                    if ((mPrivateFlags & PRESSED) == 0) {
                        // Need to switch from not pressed to pressed
                        mPrivateFlags |= PRESSED;
                        refreshDrawableState();
                    }
                }
                break;
       }
       //如果該控制元件可點選的話,那麼一定是return true;
        return true;
    }
    //如果該控制元件不可點選,一定return false;
    return false;
}
  • 詳解performClick()
public boolean performClick() {
   sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

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

    return false;
}

如果我們給view.setOnClickListener(new View.OnClickListener() {});那麼這裡mOnClickListener一定不為null,所以return true;如果沒設定click監聽,那麼就return false;然後事件根據返回結果進行傳遞或者消費。

總結:

通過上一篇各種情況下執行流程和本篇原始碼分析,我們應該能夠清楚的瞭解了Android事件分發到底是個什麼東西了,最好的學習方法還是寫個demo,然後有不明白的地方點進去看看原始碼,不要求全部看懂,找找我們關心的重點,能大概分析清楚是怎麼工作的即可,祝大家學習愉快~