這篇文章我們來探究下Android中關於事件分發機制的一些細節和流程,由於這部分源碼比較繁雜,拆開來講,本文只探究View的事件分發流程,ViewGroup留到之後再說,在分析完這兩者的事件分發機制之後我們來對Android的時間分發機制進行總結。那麼本文就從View的子類Button來着手分析事件分發的流程,之後我們再從源碼角度分析具體實現過程。
舉個栗子
這裏用一個很簡單的小例子來演示View中dispatchTouchEvent、onTouchEvent、TouchListener的執行順序,繼承自Button的TestButton代碼很簡單,幾個log:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
Activity中設置onTouchListener,也是幾個log:
testView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouch ACTION_UP");
break;
default:
break;
}
return false;
}
});
接下來是執行流程,第一次點擊按鈕:
第二次點擊後鼠標移動一下鬆開:
可以看到一次事件分發從ACTION_DOWN開始,到ACTION_UP結束,且其傳遞順序是從dispatchTouchEvent –> onTouch –> onTouchEvent。下面我們來從源碼中對這幾個方法進行查看。
源碼分析
我們先從View的dispatchTouchEvent開始吧:
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
...
return result;
}
我這裏略去了部分代碼,來看核心代碼,可以看到第10行開始這裏開始進行了判斷,若同時附和這幾個條件,則返回true,注意這裏調用了OnTouchListener的onTouch方法,也就是說如果我們調用了View的setOnTouchListener方法那麼在dispatchTouchEvent方法執行過程中,會調用OnTouchListener的onTouch方法,若onTouch方法返回true,則設置result爲true,onTouchEvent不再執行,若onTouch方法返回爲false,則第十行if語句不成立,result爲改變,執行下一個判斷語句,同時會執行onTouchEvent,若返回true則result爲true,反之亦然。
我們繼續來看一看onTouchEvent中的代碼:
public boolean onTouchEvent(MotionEvent event) {
//View狀態爲Disable並且可點擊,返回true
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
return clickable;
}
//若TouchDelegate(觸摸代理類)不爲空,則調用其onTouchEvent方法並返回true
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//若可點擊或者可長按以及長按出現ToolTip
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
這段代碼極長,有興趣的朋友可以通讀一下源碼,我這裏略去了一部分,前兩個判斷已經在註釋裏寫清楚了,不多贅述,我們重要看下switch語句中的內容,接下來一個一個分析:
ACTION_DOWN
ACTION_DOWN是整個Touch流程的起點,代表觸摸點按下操作。我們來看看onTouchEvent中的判斷是怎樣的:
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
//判斷是否爲鼠標右鍵或者手寫筆第一個按鈕,若是,返回true後續代碼不執行
if (performButtonActionOnTouchDown(event)) {
break;
}
//當前視圖是否可滾動(例如:當前是ScrollView視圖,返回true)
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
// 滾動視圖內,先不設置爲按下狀態,因爲用戶之後可能是滾動操作
// 不是此次分析的重點,感興趣可以自己瞭解下
mPrivateFlags |= PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// 不在滾動視圖內,立即反饋爲按下狀態
mPrivateFlags |= PRESSED;
// 刷新爲按下狀態
refreshDrawableState();
//通過Handler發送一個延遲消息來判斷是否是長按,500ms
checkForLongClick(0);
}
break;
各個步驟的註釋已經寫清楚了,下面分解一下各個方法:
首先是performButtonActionOnTouchDown:
protected boolean performButtonActionOnTouchDown(MotionEvent event) {
// 如果是鼠標右鍵,手寫筆第一個按鈕,看BUTTON_SECONDARY註釋
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {
return true;
}
}
return false;
}
很簡單的判斷,如果成功就返回true,不成功就false。
接下來是isInScrollingContainer:
public boolean isInScrollingContainer() {
ViewParent p = getParent();
while (p != null && p instanceof ViewGroup) {
if (((ViewGroup) p).shouldDelayChildPressedState()) {
return true;
}
p = p.getParent();
}
return false;
}
這裏獲取到了當前View的父控件,而後一層一層向上判斷是否處於滾動容器中(shouldDelayChildPressedState返回true),如果是,則返回true
接下來是checkForLongClick:
private void checkForLongClick(int delayOffset) {
// 當前視圖可以執行長按操作
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
// 延遲一段時間(500ms)把runnable添加到消息隊列
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
ACTION_DOWN中的邏輯大概就是這樣
ACTION_MOVE
ACTION_MOVE代表觸摸點發生滑動
case MotionEvent.ACTION_MOVE:
//這個方法暫時不用關注
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
我們先來看看pointInView方法裏做了什麼:
public boolean pointInView(float localX, float localY, float slop) {
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}
這個方法是判斷是否劃出控件可視區域,爲了保證觸摸點發生及輕微變化就導致ACTION_MOVE被執行,這裏加入了一個slop的邊界值,即在視圖上下左右擴大slop
接下來是兩個remove方法,這裏我們放在一起講:
private void removeTapCallback() {
if (mPendingCheckForTap != null) {
mPrivateFlags &= ~PFLAG_PREPRESSED;
removeCallbacks(mPendingCheckForTap);
}
}
private void removeLongPressCallback() {
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
}
這兩個方法主要是刪除觸摸和長按回調(還記得之前按下後發送的長按延時消息嗎)
ACTION_CANCLE
ACTION_CANCLE代表取消觸摸操作,觸摸流程結束
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
這裏代碼很簡單,清除View狀態
ACTION_UP
ACTION_UP代表觸摸點擡起操作,同樣是一個觸摸流程的結束
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();//popWindow之類相關的,不必關心
}
//清除觸摸狀態以及觸摸回調和長按的回調
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 當前視圖處於預按下或者按下狀態,如果失去焦點,獲取焦點狀態
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
//重設按下狀態
setPressed(true, x, y);
}
//長按未觸發
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
//移除長按回調
removeLongPressCallback();
//按下狀態執行點擊事件
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)) {
//執行失敗的話,保證視圖不會永遠處於按下狀態
//直接執行一次
mUnsetPressedState.run();
}
// 清除輕觸回調
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
這短代碼是觸摸流程裏最重要的一部分。代碼中已經註釋清除,不必全部理解,理解流程即可。
到此,我們整個View的觸摸流程就結束了,光看這部分還是很繞的,下一篇文章我們會詳細講講ViewGroup中事件分發流程,並且和本篇做對照,可能會理解的更透徹。
enjoy~
我的個人博客