Android中ViewGroup、View事件分發機制原始碼分析總結(雷驚風)
1.概述
很長時間沒有回想Android中的事件分發機制了,開啟目前的原始碼發現與兩三年前的實現程式碼已經不一樣了,5.0以後發生了變化,更加複雜了,但是萬變不離其宗,實現原理還是一樣的,在這裡將5.0以前的時間分發機制做一下原始碼剖析及總結。會涉及到幾個方法,dispatchTouchEvent()表示事件開始分發方法,在ViewGroup與View中都有,onInterCeptTouchEvent()表示是否攔截當前事件,比如ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCLE,只在ViewGroup中存在,因為一個View的子類已經是最內層View,而ViewGroup的子類還會包含子View,所以它需要可以設定是否攔截當前事件傳遞到子View中,onToutchEvent()表示消費當前事件,OnToutchListener,OnClickListener,OnLongClickListener。
2.原始碼剖析
我會將詳細的程式碼註釋加在原始碼中,首先說一下在Activity與Window中的分發過程。當我們點選螢幕上的一個View時,事件的分發就開始了,首先會呼叫當前Activity的dispatchTouchEvent(MotionEvent ev)方法,我們開啟Activity類看一下它的實現:
只挑主要的說,可以看到呼叫了getWindow().superDispatchTouchEvent()方法,如果這個方法返回true,則表示事件被消費,直接返回true,如果返回false,會執行自己的onTouchEvent()表明如果View樹中都沒有消費事件,當前Activity消費事件。我們知道在Phone上邊PhoneWindow是Window的唯一子類,那我們就去PhoneWindow中找找相關方法:public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
在PhoneWindow中可以看到呼叫了mDocor的super***方法,大家應該都知道mDecor為window中的最頂級View類,他是PhoneWindow的內部類,繼承了FrameLayout。我們進入mDecor的內部看看它的實現://phoneWindow public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
//DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
在DecorView中又呼叫了父類方法,我們最終在GroupView中找到了相關實現,這樣我們就完成了一個點選事件從Activity到ViewGroup的過程。無論是ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCLE任何一個都是如此的一直傳遞流程。
總結:點選--->Activity.dispatchTouchEvent(event)--->PhoneWindow.superDispatchTouchEvent(event)--->DecorView.superDispatchTouchEvent(event)--->ViewGroup.dispatchTouchevent(event);
說完了一個事件從Activity到ViewGroup的傳遞流程,下邊說一下一個事件在ViewGroup中是如何分發的,看一下原始碼:
//ViewGroup中;
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//ViewGroup中按下事件處理;
if (action == MotionEvent.ACTION_DOWN) {
//如果mMotionTarget儲存了View,因為是從新開始的DOWN,所以清空;
if (mMotionTarget != null) {
mMotionTarget = null;
}
//是否禁用攔截事件功能,如果禁用了攔截功能(不攔截)或者onInterceptTouchEvent()返回false,說明不攔截事件。
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
//迴圈每一個子View;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
//判斷當前子View是否包含點選區域;
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
//呼叫子View的dispatchTouchEvent() ,向子事件分發事件;
//1.如果返回true,說明子類或者子類的某個子類中消費了事件,將這個child記錄到mMotionTarget中,每一個ViewGroup都記錄了
//消費了這個事件的子View,像鏈式,最後當前ViewGroup返回true,返回給呼叫這個ViewGroup的外層ViewGroup,一直到Activity中
//的dispatchTouchEvent()中,最後Activity中也返回true;
//2.如果返回false,說明子類或者子類的子類中沒有消費這個事件,所以mMotionTarget不會被賦值,會繼續向下執行程式碼;
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
//ACTION_DOWN事件時,被攔截,或者子類沒有消費ACTION_DOWN,呼叫自己的onTouchEvent()執行onTouch、onClick()方法等;
//ACTION_UP時,子類沒有消費ACTION_DOWN;
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
//呼叫super方法即呼叫到View中的dispatchTouchEvent()方法,還是呼叫的當前View,
//在View中會呼叫自己的onTouchEvent()執行onTouch、onClick()方法等處理;
//這裡兩種情況:1.自身沒有處理返回false;2.自身處理了返回true,本身處理了,不會儲存到mMotionTarget中,而它的外層ViewGroup會儲存他;
//true/false都會一直返回到Activity中。
return super.dispatchTouchEvent(ev);
}
//ACTION_UP或者MOVE時,子類中消費了ACTION_DOWN事件,沒有禁用攔截功能(可以攔截),並且攔截事件;
// 無論 target 是否為 null ,ACTION_DOWN事件的處理都不能走到這裡,在之下都是ACTION_MOVE和ACTION_UP的邏輯
// 如果執行到這裡,說明有響應ACTION_DOWN事件的view物件,這就看我們是否被允許攔截和要不要攔截了
// 如果允許攔截並且攔截了ACTION_MOVE和ACTION_UP事件,則將ACTION_CANCEL事件分發給target
// 然後直接返回true,表示已經響應了該次事件
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
//將子類事件重置為ACTION_CANCEL;
if (!target.dispatchTouchEvent(ev)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
// 如果沒有攔截ACTION_MOVE和ACTION_UP事件,則直接派發給target
return target.dispatchTouchEvent(ev);
}
註釋加了不少,看幾遍就能明白。事件分發機制在ViewGroup中的派發流程:
在ViewGroup的dispatchTouchEvent()中,
1.ACTION_DOWN:首先判斷攔截相關設定,不攔截:獲取每一個子類判斷點選區域是否在當前子類上,
在的話呼叫其dispatchTouchEvent()方法,方法返回true說明當前View處理當前事件序列,記錄到mMotionTarget中返回true,
表明找到了處理當前事件的View,停止事件分發。方法返回false,說明當前子View及其子孫View不處理當前事件,如果所有子View都不處理,
後續程式碼會呼叫當前ViewGroup的super.dispatchTouchEvent(ev),即執行自己的OnTouch(),OnClick()等(自己處理),如果返回了true,
表明自己處理當前事件,如果返回false,自己不處理當前事件。無論返回true還是false最終都是返回到Activity中。
2.ACTION_MOVE:判斷攔截相關:不攔截,呼叫target.dispatchTouchEvent(ev)進行事件分發;攔截:將target的MotionEvent置為Cancel,
mMotionTarget賦值為null,返回true,表明自己消費了事件。這次不會呼叫自己的super.dispatchTouchEvent(ev)。
再次MOVE分發時由於mMotionTarget為空了,所以會呼叫自己的super.dispatchTouchEvent(ev),即自身處理。
3.ACTION_CANCEL或ACTION_UP:回覆狀態預設值,判斷是否攔截,攔截返回true;不攔截向下派發事件。
下邊看一下在Viewgroup的dispatchTouchEvent方法中繼續向下分發事件程式碼if(child.dispatchTouchEvent(ev))執行後如果child也是一個ViewGroup子類那麼跟上邊的邏輯是一樣的,那麼如果child是一個View的子類,比如Button、TextView、ImageView等時,是如何分發事件的,即事件在View類中的傳遞流程,接著看一下原始碼:
//View中的dispatchTouchEvent;
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
重要原始碼就這些,在這裡可以看到首先判斷我們有沒有給當前View設定OnTouchListener事件,並且當前View是enabled狀態的,如果前兩個條件都滿足,則會呼叫我們OnTouchListener中的onTouch()方法,如果上邊三個條件都為true,則直接返回true,表明當前View消費當前事件,如果我們在onTouch()方法中返回false,那麼就會跳過判斷執行自己的onTouchEvent()方法,這裡先提前說一下,呼叫onTouchEvent()方法也有繼續判斷當前View是否消費當前事件的作用,在其內部會根據我們點選在控制元件上停留時間判斷是否執行OnClick,OnLongClick事件。下邊看一下onTouchEvent()原始碼:
//View中onTouchEvent();
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
//當前View是Disabled狀態且是可點選則會消費掉事件
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));
}
//將事件交給代理者處理,直接return true
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//View可以點選或者長按,只要進入判斷,一定反回true,即消費事件;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
//100ms內或者以後都會進入程式碼塊;
if ((mPrivateFlags & 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();
}
//如果是在0到500ms之間或者500秒之後執行了LongClick事件但返回的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
//在pressed狀態,只執行Click事件;
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();
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;
//傳送100毫秒延時訊息,執行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;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
//手指滑出了View範圍,取消100ms計時任務;
removeTapCallback();
//判斷是否包含PRESSED標識,如果包含,移除長按的檢查任務;
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;
}
有些相對來說重要的類或者程式碼我會在下邊貼出來,幫助大家更好地理解,在這裡程式碼就不詳細的講了,在最後我還會總結,相關程式碼:
//點選狀態變為PRESSED類;
private final class CheckForTap implements Runnable {
public void run() {
mPrivateFlags &= ~PREPRESSED;
//設定PRESSED標識
mPrivateFlags |= PRESSED;
//重新整理背景
refreshDrawableState();
//如果View支援長按事件,呼叫postCheckForLongClick()方法;
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick(ViewConfiguration.getTapTimeout());
}
}
}
//ViewConfiguration.getLongPressTimeout() 為500ms;再次傳送500-100=400ms延時訊息
private void postCheckForLongClick(int delayOffset) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
//長按事件發生處理器;
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
//執行LongClick監聽;
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
}
總結:也就是說如果使用者設定了LongClickListener,從使用者觸發ACTION_DOWN開始500ms才能觸發事件,如果達不到500ms,視為點選事件;如果longClickListener中返回true,mHasPerformedLongPress就是true;
//還在100ms內,取消監聽;
private void removeTapCallback() {
if (mPendingCheckForTap != null) {
mPrivateFlags &= ~PREPRESSED;
removeCallbacks(mPendingCheckForTap);
}
}
//取消長按事件任務監聽;
private void removeLongPressCallback() {
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
}
總結:主要就是判斷是否移出當前View,如果移出,根據時間移出相關計時任務;private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
}
}
public void setPressed(boolean pressed) {
if (pressed) {
mPrivateFlags |= PRESSED;
} else {
mPrivateFlags &= ~PRESSED;
}
refreshDrawableState();
dispatchSetPressed(pressed);
}
清除PRESSED,重新整理背景,向下傳遞pressed;
事件分發在View中的流程總結:呼叫View的dispatchTouchEvent(),判斷是否設定了OnTouchListener並且View為Enable的,如果都滿足,呼叫OnTouchListener.onTouch(this, event),如果返回true,
直接返回true,表明當前View處理當前事件,不會呼叫自己的onTouchEvent(),如果onTouth()方法返回false,則呼叫onTouchEvent()。
在onTouchEvent()中如果當前View為可點選或者可長按則一定返回true,否則返回false。
1.ACTION_DOWN:初始化狀態為PREPRESSED,設定mHasPerformedLongPress為flase,傳送100ms任務,如果100ms到後,還沒有UP或者移出當前View,
則將標誌置為PRESSED,如果支援長按事件,傳送另一個400ms計時任務,如果到時間後依然沒有UP,LongClickListener不為空,呼叫執行,返回true,將
mHasPerformedLongPress設定為true。
2.ACTION_MOVE:檢測使用者有沒有移出View,並移除相關時間任務。
3.ACTION_UP:100ms內仍然是PREPRESSED狀態,不會觸發Click;在100到500ms之間,取消仍然執行的計時任務,執行 performClick去執行Onclick;
如果在500ms以後,沒有設定onLongClickListener或者返回false,仍然會執行performClick;返回true,onClick不執行;LongClick在Click前執行。
在事件分發過程中OnTouchListener優先OnClickListener與OnLongClickListener執行,如果返回false,OnClickListener與OnLongClickListener才有可能執行到,返回true不執行。在500ms內擡起手指不會執行OnLongClickListener,會執行OnClickListener,在500ms以後如果OnLongClickListener返回false,OnClickListener會執行,返回true不執行。這就是他們幾個的執行順序及規則,當然這是建立在我們註冊了這三個監聽。今天的總結就到這裡,希望對初學者有幫助,如果哪裡不對,歡迎大神拍磚,謝謝!