Android必知必會--事件分發機制

事件序列解析

所謂的安卓事件是什麼?具體來講的就是點擊和滑動兩個操做;抽象着來講就是下面的表格。web

MotionEvent/事件類型 具體操做
ACTION_DOWN 點下View
ACTION_UP 擡起View
ACTION_MOVE 滑動View
ACTION_CANCEL 非人爲因素取消

事件序列通常組成:微信

點擊的事件組成就是:Down --> Up編輯器

滑動的事件組成就是:Down --> Move --> Move .... --> Up函數

事件分發

  1. 使用到的函數
    • dispatchTouchEvent():用於事件分發
    • onTouchEvent():消費事件
    • onInterceptTouchEvent():判斷是否攔截事件,僅存在於ViewGroup
  2. 分發對象
    • Activity
    • ViewGroup
    • View

Activity的事件分發

public boolean dispatchTouchEvent(MotionEvent ev) {
// 從判斷語句中能夠得出全部事件的起點就是Down
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 實現屏保功能
onUserInteraction();
}
// 向上傳遞至ViewGroup,調用其dispatchTouchEvent
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

文章中我已經添加了註釋內容,其中getWindow()得到就是一個Window抽象類,根據其子類PhoneWindow咱們能夠很容易得知最後調用的其實就是ViewGroupdispatchTouchEvent()方法佈局

/**
* 實際上就是判斷事件是不是DOWN事件,event的座標是否在邊界內等
*/

public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}

最後就是Activity中的onTouchEvent()方法了,這個模塊乾的事情在註釋中也就很清晰明瞭了。post

ViewGroup的事件分發

public boolean dispatchTouchEvent(MotionEvent ev) {
// ········
// 初始化Down事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 丟棄以前手頭上乾的事情,從新開始響應Down事件
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 檢查是否須要攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 這個與運算是用於影響除Down之外的事件的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// 當前按壓的位置沒有控件,或者當前控件並不可被點擊,直接被ViewGroup攔截
intercepted = true;
}
// ········
/**
*這個判斷裏面一樣的仍是判斷響應的事件,而後就是經過一個for循環判斷位置來判斷當前的子控件是否在對應的位置內
* 還有很是重要的一點就是這個循環的判斷仍是倒敘的
*/

if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked ==MotionEvent.ACTION_HOVER_MOVE) {
········
if (newTouchTarget == null && childrenCount != 0) {
········
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// 後面的篇幅主要用於判斷當前的控件的各類屬性是不是知足須要的。好比說位置、是否能夠點擊、是否隱藏等一系列信息
········
}
········
}

倒序是爲了什麼呢? 這個問題一樣是一個開發過程當中常見,可是卻很容易被忽略的問題。this

你是否有見過這樣的一段代碼,若是是ButtonAButtonB這兩個按鈕是在同一個位置出現,ButtonA略大於ButtonB,也就是下圖所示url

對應在XML佈局文件中的代碼通常相似於下面這段。spa

<Layout>
<Button id="A"/>
<Button id="B"/>
</layout>

若是出現點擊事件發生在ButtonA上時,只要它有足夠的能力勢必會被ButtonA所消費。可是若是時出如今ButtonB的區域呢? 這也就體現了倒序去進行事件消費的做用了,若是正序去進行消費的話,那這個事件最後的消費者必定是ButtonA,而咱們須要的實際效果是ButtonB在有足夠能力消費時交由它進行完成。.net

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;
}

onInterceptTouchEvent(MotionEvent ev)函數可知,默認其實並不會去攔截。因此就通常狀況而言,dispatchTouchEvent()方法是須要去循環遍歷子控件集合去尋找對應的控件的。

使用一個僞代碼解釋以上的邏輯

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
// 本身攔截,本身消費
onTouchEvent(ev);
}else{
// 不攔截,分發給子View進行消費
consume = child.dispatchTouchEvent(ev);
}
return consume;
}

View事件分發

public boolean dispatchTouchEvent(MotionEvent event) {
// ·····
boolean result = false;

if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

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

if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return result;
}

經過以往的實踐,咱們知道只有經過設置了監聽器的View纔可以去監聽事件,那麼在dispatchTouchEvent()方法中也是同樣的,若是View並無被設置監聽器,變量result也不會被賦值成爲true。

從代碼中很容易看出onTouch()方法的優先級大於onTouchEvent()方法。

public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

// ·····
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
// ·····
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();

if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
// ·····
}
// ·····
mIgnoreNextUpEvent = false;
break;
// ·····
}
return true;
}
return false;
}

onTouchEvent()方法中其實具體幹了一件事情,那就是區別究竟是長按事件仍是點擊事件。

那麼先行判斷的是長按事件仍是點擊事件呢?答案很明顯,在代碼行中removeLongPressCallback();有一個這樣的函數,這就是去除長按事件回調的函數,因此答案就是長按事件是第一個被判斷的事件,而後纔是點擊事件。

判斷這個方法的事件的方法就是經過作出Up動做時的時間和作出Down動做時的時間間隔。若是Down和Up兩個動做之間的時間間隔小於500ms,就是點擊事件。

總結


本文分享自微信公衆號 - 告物(ClericYi_Android)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索