「Android 事件分發(fā)機(jī)制」
一、事件分發(fā)機(jī)制
在
Android體系中,事件分發(fā)機(jī)制占有重要的一份,了解事件的分發(fā)機(jī)制,對(duì)于滑動(dòng)等沖突才有更深刻的理解。自定義View中能更好的擴(kuò)展,遇到相關(guān)問題能從整個(gè)流程上思考,尋找最優(yōu)解決辦法。
- 一個(gè)簡單的點(diǎn)擊事件是怎樣一步步被消費(fèi)處理的呢?誰該處理,誰不該處理又是由什么因素決定的,這是在實(shí)際開發(fā)中繞不開的問題,尤其是在自定義View的應(yīng)用場景下。
-
先上圖,從整體上大致了解事件是怎樣被傳遞與消費(fèi)的:
view事件分發(fā).png
二、從Activity開始
分析一個(gè)最簡單的初始頁面,Activity布局中僅僅包含一個(gè)ViewGroup,首先需要了解View的層級(jí)結(jié)構(gòu)。如果此時(shí)點(diǎn)擊ViewGroup,來看看事件是如何傳遞的。先來搞清楚Activity的層級(jí)結(jié)構(gòu),基于最新的AppCompatActivity的加載流程,看一下代碼實(shí)現(xiàn):
- CustomActivity中setContentView()
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView();
}
- AppCompatActivity中
//#1
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
//#2
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
//#3 AppCompatDelegateImpl
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
1.AppCompatDelegate是個(gè)啥?自從切換到AppCompatActivity以后,加載setContentView()跟之前的流程有差異。
2.先看一段關(guān)于抽象類AppCompatDelegate注釋:
This class represents a delegate which you can use to extend AppCompat's support to any Activity.When using an AppCompatDelegate, you should call the following methods instead of the Activity method of the same name...了解到,AppCompatDelegate其實(shí)委托類,而這個(gè)類是為了兼容Activity而增加的。幾乎支持了所有Activity的操作,且方法同名。
3.AppCompatDelegate作為抽象類,那么具體的實(shí)現(xiàn)細(xì)節(jié)得找到它的實(shí)現(xiàn)類,也就是-AppCompatDelegateImpl,那么在setContentView(),它到底做了哪些操作呢?而整個(gè)調(diào)用流程從#1-#3,加上我們自己定義的CustomActivity應(yīng)該是:CoustomActivity#setContentView->AppCompatActivity#setContentView->AppCompatActivity#getDelegate->AppCompatDelegate#setContentView
- AppCompatDelegate的實(shí)現(xiàn)類AppCompatDelegateImpl
對(duì)setContentView簡單分析,看看具體做了哪些操作:
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
1.ensureSubDecor()
如果熟悉Activity的啟動(dòng)流程的話,應(yīng)該對(duì)Decor并不陌生,似乎有點(diǎn)是DecorView的意思,那到底是不是呢?ensureSubDecor()創(chuàng)建出來的是什么?
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
}
//.....
}
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
//.....
ensureWindow();
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
if (!mWindowNoTitle) {
if (!mWindowNoTitle) {
// If we're floating, inflate the dialog title decor
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
// Floating windows can never have an action bar, reset the flags
mHasActionBar = mOverlayActionBar = false;
} else if (mHasActionBar) {
}
}
mWindow.setContentView(subDecor);
//....
return subDecor;
}
1.通過對(duì)createSubDecor創(chuàng)建過程分析,發(fā)現(xiàn)它并不是Window中的DecorView,而是在創(chuàng)建DecorView之后創(chuàng)建的一個(gè)subDecorView,包括是否是包含actionBar、floating等,也即是相當(dāng)于之前的DecorView中titleBar。
2.等到subDecorView創(chuàng)建流程走完,此時(shí)view的層級(jí)已經(jīng)是Activity->PhoneWindow->DecorView->subDecorView了。
activity層級(jí).png
3.當(dāng)ensureSubDecor()執(zhí)行完畢:
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mAppCompatWindowCallback.getWrapped().onContentChanged();subDecor通過findViewById其實(shí)就是一個(gè)父親容器,而這個(gè)父親容器的id已經(jīng)是確定的了-R.id.content
通過動(dòng)態(tài)加載的方式將我們自己的布局(對(duì)應(yīng)resId)添加到了subDecorView之上。此時(shí)的層級(jí)Activity->PhoneWindow->DecorView->subDecorView->cutomView.
2.層級(jí)關(guān)系
- 通過上圖,大致了解到Activity的層級(jí)關(guān)系比較清晰了,在Activity的初始創(chuàng)建,通過addView,將View一層層貼附到容器之中(當(dāng)然沒有分析具體的流程),View Tree直觀上,最上層的view則是最后被添加上的?;谶@個(gè)特點(diǎn),當(dāng)事件傳遞時(shí)源碼中對(duì)子View采用了倒序遍歷,增大命中機(jī)率。
- 無論是點(diǎn)擊事件,滑動(dòng)事件,或者是觸摸事件,總會(huì)包含幾個(gè)狀態(tài)ACTION_DOWN--ACTION_UP、ACTION_DOWN--MOVE--MOVE...--ACTION_UP.既然事件首先作用到Activity之上,那么從Activity入手。
Activity中的dispatchTouchEvent();
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
* @param ev The touch screen event.
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public void onUserInteraction() {
}
/**
* 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.
* @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默認(rèn)實(shí)現(xiàn)是false,注釋里解釋的也很清楚,事件到此結(jié)束。但是有個(gè)前提的是getWindow().superDispatchTouchEvent(ev) = false,而getWindow返回的是window,window作為接口,它的唯一實(shí)現(xiàn)PhoneWindow,
superDispatchTouchEvent(ev)調(diào)用了父類的方法也即ViewGroup.dispatchTouchEvent:
PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
1.window的作用更像是一個(gè)工人,起到了連接的作用,這里的mDecor = DecorView,DecorView繼承自FrameLayout,F(xiàn)rameLayout繼承自Viewgroup
mDecor.superDispatchTouchEvent(event),最終調(diào)用的是Viewgroup中的dispatchTouchEvent方法。
- 總結(jié)一下,當(dāng)事件被activity接收,并可以向下傳遞,則傳遞的順序?yàn)?strong>activity.dispatchTouchEvent->PhoneWindow.superDispatchTouchEvent(ev)->DecorView.superDispatchTouchEvent(event)->ViewGroup.dispatchTouchEvent,事件由此傳遞到ViewGroup,重點(diǎn)分析dispatchTouchEvent:
1.VIewGroup#dispatchTouchEvent()
//...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//判斷viewgroup是否需要攔截此次事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
}
//.....
1.當(dāng)事件傳遞到ViewGroup的dispatchTouchEvent方法時(shí),之前提到的一個(gè)完成的事件序列總是以ACTION_DOWN為開端的,首先就對(duì)ACTION_DOWN作了判斷。
2.第二步,判斷ViewGroup是否需要攔截此次事件,當(dāng)然默認(rèn)返回的是false在onInterceptTouchEvent,即默認(rèn)是不攔截的。
//viewgroup默認(rèn)是不攔截事件 return false
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;
}
3.同方法中對(duì)子View的遍歷操作,注意這里采用的是倒序的形式,判斷View是否可見、是否正在執(zhí)行動(dòng)畫、點(diǎn)擊范圍是否在其之上、從而來決定View是否消費(fèi)此次事件:
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
2.VIew#dispatchTouchEvent()
//view中的dispatchtouchevent方法
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
//包含了,長按,點(diǎn)擊,ontouch等監(jiān)聽。
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//mOnTouchListener的優(yōu)先級(jí)最高
if (!result && onTouchEvent(event)) {
result = true;
}
}
}
1.在View中是沒有攔截事件的方法的,默認(rèn)就是處理事件,可以認(rèn)為dispatchTouchEvent是將事件分發(fā)給自己處理。
2.ListenerInfo中包含了長按、點(diǎn)擊、onTouch等監(jiān)聽,這里有一個(gè)細(xì)節(jié),如果View設(shè)置了mOnTouchListener監(jiān)聽,它的優(yōu)先級(jí)是很高的,在ontouchevent之前??纯?strong>ontouchevent中做了哪些操作。
- View的onTouchEvent()
public boolean onTouchEvent(MotionEvent event) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
case MotionEvent.ACTION_DOWN:
if (!clickable) {
checkForLongClick(ViewConfiguration.getLongPressTimeout(), x, y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
}
/**
* Defines the default duration in milliseconds before a press turns into
* a long press
*/
private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500;
1.View在處理事件時(shí),首先就是對(duì)長按做出了判斷checkForLongClick,需要注意的是DEFAULT_LONG_PRESS_TIMEOUT這個(gè)默認(rèn)為500的超時(shí)時(shí)間。分析對(duì)長按是如何判斷的:
private void checkForLongClick(long delay, float x, float y, int classification) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
mPendingCheckForLongPress.setClassification(classification);
postDelayed(mPendingCheckForLongPress, delay);
}
}
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().postDelayed(action, delayMillis);
return true;
}
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
private boolean mOriginalPressedState;
private int mClassification;
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) {
recordGestureClassification(mClassification);
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
}
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
2.這里的delay的值就是DEFAULT_LONG_PRESS_TIMEOUT,默認(rèn)的500ms,通過handler發(fā)送了一條延遲為500ms的Runnable到消息隊(duì)列當(dāng)中。如果500ms內(nèi)事件得以消費(fèi),返回true則長按事件會(huì)被處理,否則將會(huì)在ACTION_UP中將事件移除-removeLongPressCallback。
- View的點(diǎn)擊事件的處理
//在onTouchEvent方法的 ACTION_UP分支之中
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
1.點(diǎn)擊事件同樣也不是直接調(diào)用,同樣也是通過Runnable的方式post出去的,這樣做的好處是點(diǎn)擊開始前view的狀態(tài)更新是不受到影響的。
2.對(duì)于不可能點(diǎn)擊的狀態(tài)clickable,事件是不是就不處理了呢?答案是否定的:
if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; }可以發(fā)現(xiàn),即使是不可能點(diǎn)擊的view,依然是會(huì)調(diào)用到onTouchEvent方法的,只是事件默認(rèn)沒有被處理了。
3.簡單總結(jié)一下整個(gè)流程
- 對(duì)于一個(gè)ViewGroup,事件產(chǎn)生以后會(huì)首先傳遞到dispatchTouchEvent,如果此時(shí)onInterceptTouchEvent返回是true表示要攔截此次事件,重要的是接下來事件會(huì)交給這個(gè)ViewGroup處理,onTouchEvent就會(huì)被調(diào)用,如果onInterceptTouchEvent返回的是false,那么事件會(huì)繼續(xù)向下傳遞給子View,此時(shí)子元素的dispatchTouchEvent會(huì)被調(diào)用,依次類推,直到事件完全被處理完畢。
- 當(dāng)View需要處理事件時(shí),如果設(shè)置了OnTouchListener(優(yōu)先級(jí)是最高的),那么OnTouchListener的onTouch方法會(huì)被調(diào)用,而OnClickListener的優(yōu)先級(jí)是處于事件傳遞的末端的。
- 一個(gè)完整的事件序列的消費(fèi)的順序是Activity->PhoneWindow->View;如果某一個(gè)最末端的View的onTouchEvent返回了false即不處理,此時(shí)事件上拋,父親容器的onTouchEvent會(huì)被調(diào)用,如果所有的View都處理該事件,最終事件被傳遞到Activity,則Activity的onTouchEvent會(huì)被調(diào)用。
- 一般情況下一個(gè)事件序列只能被一個(gè)View攔截消費(fèi),同一個(gè)事件序列所有事件都會(huì)直接交給它處理,并且它的onInterceptTouchEvent不會(huì)再被調(diào)用。如果子view中調(diào)用requestDisallowInterceptTouchEvent,則會(huì)決定父view是否攔截事件(除action_down以外的事件,action_down會(huì)重置FLAG_DISALLOW_INTERCEPT的狀態(tài)值)
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
- 某個(gè)View一旦開始處理事件,如果它不消耗ACTION_DOWN(onTouchEvent返回了false),那么同一事件序列中其他事件都不會(huì)再交給它來處理,事件將重新交給他的父元素處理,即父元素的onTouchEvent會(huì)被調(diào)用。
- 如果某個(gè)View不消耗除ACTION_DOWN以外的其他事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的onTouchEvent并不會(huì)被調(diào)用,并且當(dāng)前View可以收到后續(xù)事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給Activity處理。
- ViewGroup默認(rèn)不攔截任何事件,ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false,View沒有onInterceptTouchEvent方法,一旦有事件傳遞給它,那么它的onTouchEvent方法就會(huì)被調(diào)用。
- View的onTouchEvent方法默認(rèn)消耗事件(返回true),除非他是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)。View的longClickable屬性默認(rèn)都為false,clickable屬性分情況,Button默認(rèn)為true,TextView默認(rèn)為false。disable不會(huì)影響事件的消費(fèi),即時(shí)一個(gè)view是disable狀態(tài),依然會(huì)消費(fèi)事件,只是用戶無感知,即無反饋。
三、有什么用處?
開發(fā)中存在僅僅展示列表的情況,也即是不可點(diǎn)擊的列表,如果是這個(gè)需求該如何實(shí)現(xiàn)?當(dāng)然如果以RecyclerView為例可以在item禁止,那是否可以以事件的傳遞默認(rèn)不消費(fèi)點(diǎn)擊的事件呢?
上面提到的,如果某個(gè)View不消耗ACTION_DOWN事件也即是onTouchEvent返回false不就可以滿足需求了嘛?簡單使用:
1.自定義一個(gè)不可點(diǎn)擊的RecyclerView
/**
* Created by Sai
* on 2022/01/28 16:35.
*/
public class UnClickableRecyclerView extends RecyclerView {
public UnClickableRecyclerView(@NonNull @NotNull Context context) {
super(context);
}
public UnClickableRecyclerView(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs) {
super(context, attrs);
}
public UnClickableRecyclerView(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
return false;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
return true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
}
1.重寫onTouchEvent返回為false,同時(shí)onInterceptTouchEvent返回true表示攔截下此次事件并且不消費(fèi)。

