「Android 事件分发机制」
本文已参与「新人创作礼」活动,一起开启掘金创作之路
一、事件分发机制
在
Android体系中,事件分发机制占有重要的一份,了解事件的分发机制,对于滑动等冲突才有更深刻的理解。自定义View中能更好的扩展,遇到相关问题能从整个流程上思考,寻找最优解决办法。
-
一个简单的点击事件是怎样一步步被消费处理的呢?谁该处理,谁不该处理又是由什么因素决定的,这是在实际开发中绕不开的问题,尤其是在自定义View的应用场景下。
-
先上图,从整体上大致了解事件是怎样被传递与消费的:
二、从Activity开始
分析一个最简单的初始页面,Activity布局中仅仅包含一个ViewGroup,首先需要了解View的层级结构。如果此时点击ViewGroup,来看看事件是如何传递的。先来搞清楚Activity的层级结构,基于最新的AppCompatActivity的加载流程,看一下代码实现:
- 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是个啥?自从切换到AppCompatActivity以后,加载 setContentView() 跟之前的流程有差异。
2.先看一段关于抽象类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其实是委托类,而这个类是为了兼容Activity而增加的。几乎支持了所有Activity的操作,且方法同名。
3.AppCompatDelegate作为抽象类,那么具体的实现细节得找到它的实现类,也就是-AppCompatDelegateImpl,那么在setContentView(),它到底做了哪些操作呢?而整个调用流程从 #1-#3,加上我们自己定义的CustomActivity应该是:CoustomActivity#setContentView->AppCompatActivity#setContentView->AppCompatActivity#getDelegate->AppCompatDelegate#setContentView
- AppCompatDelegate的实现类AppCompatDelegateImpl
对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的启动流程的话,应该对Decor并不陌生,似乎有点是DecorView的意思,那到底是不是呢?**ensureSubDecor()**创建出来的是什么?
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.通过对createSubDecor创建过程分析,发现它并不是Window中的DecorView,而是在创建DecorView之后创建的一个subDecorView,包括是否是包含actionBar、floating等,也即是相当于之前的DecorView中titleBar。
2.等到subDecorView创建流程走完,此时view的层级已经是Activity->PhoneWindow->DecorView->subDecorView了。
3.当**ensureSubDecor()**执行完毕:
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mAppCompatWindowCallback.getWrapped().onContentChanged();subDecor通过findViewById其实就是一个父亲容器,而这个父亲容器的id已经是确定的了-R.id.content
通过动态加载的方式将我们自己的布局(对应resId)添加到了subDecorView之上。此时的层级Activity->PhoneWindow->DecorView->subDecorView->cutomView.
2.层级关系
- 通过上图,大致了解到Activity的层级关系比较清晰了,在Activity的初始创建,通过addView,将View一层层贴附到容器之中(当然没有分析具体的流程),View Tree直观上,最上层的view则是最后被添加上的。基于这个特点,当事件传递时源码中对子View采用了倒序遍历,增大命中机率。
- 无论是点击事件,滑动事件,或者是触摸事件,总会包含几个状态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默认实现是false,注释里解释的也很清楚,事件到此结束。但是有个前提的是getWindow().superDispatchTouchEvent(ev) = false,而getWindow返回的是window,window作为接口,它的唯一实现PhoneWindow,
superDispatchTouchEvent(ev)调用了父类的方法也即ViewGroup.dispatchTouchEvent:
PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
1.Window的作用更像是一个工人,起到了连接的作用,这里的mDecor = DecorView,DecorView继承自FrameLayout,FrameLayout继承自Viewgroup mDecor.superDispatchTouchEvent(event),最终调用的是Viewgroup中的dispatchTouchEvent方法。
- 总结一下,当事件被activity接收,并可以向下传递,则传递的顺序为activity.dispatchTouchEvent->PhoneWindow.superDispatchTouchEvent(ev)->DecorView.superDispatchTouchEvent(event)->ViewGroup.dispatchTouchEvent,事件由此传递到ViewGroup,重点分析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.当事件传递到ViewGroup的dispatchTouchEvent方法时,之前提到的一个完成的事件序列总是以ACTION_DOWN为开端的,首先就对ACTION_DOWN作了判断。
2.第二步,判断ViewGroup是否需要拦截此次事件,当然默认返回的是false在onInterceptTouchEvent,即默认是不拦截的。
//viewgroup默认是不拦截事件 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.同方法中对子View的遍历操作,注意这里采用的是倒序的形式,判断View是否可见、是否正在执行动画、点击范围是否在其之上、从而来决定View是否消费此次事件:
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
//包含了,长按,点击,ontouch等监听。
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//mOnTouchListener的优先级最高
if (!result && onTouchEvent(event)) {
result = true;
}
}
}
1.在View中是没有拦截事件的方法的,默认就是处理事件,可以认为dispatchTouchEvent是将事件分发给自己处理。
2.ListenerInfo中包含了长按、点击、onTouch等监听,这里有一个细节,如果View设置了mOnTouchListener监听,它的优先级是很高的,在ontouchevent之前。看看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在处理事件时,首先就是对长按做出了判断checkForLongClick,需要注意的是DEFAULT_LONG_PRESS_TIMEOUT这个默认为500的超时时间。分析对长按是如何判断的:
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,默认的500ms,通过handler发送了一条延迟为500ms的Runnable到消息队列当中。如果500ms内事件得以消费,返回true则长按事件会被处理,否则将会在ACTION_UP中将事件移除-removeLongPressCallback。
- View的点击事件的处理
//在onTouchEvent方法的 ACTION_UP分支之中
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
1.点击事件同样也不是直接调用,同样也是通过Runnable的方式post出去的,这样做的好处是点击开始前view的状态更新是不受到影响的。
2.对于不可能点击的状态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; }可以发现,即使是不可能点击的view,依然是会调用到onTouchEvent方法的,只是事件默认没有被处理了。
3.简单总结一下整个流程
- 对于一个ViewGroup,事件产生以后会首先传递到dispatchTouchEvent,如果此时onInterceptTouchEvent返回是true表示要拦截此次事件,重要的是接下来事件会交给这个ViewGroup处理,onTouchEvent就会被调用,如果onInterceptTouchEvent返回的是false,那么事件会继续向下传递给子View,此时子元素的dispatchTouchEvent会被调用,依次类推,直到事件完全被处理完毕。
- 当View需要处理事件时,如果设置了OnTouchListener(优先级是最高的),那么OnTouchListener的onTouch方法会被调用,而OnClickListener的优先级是处于事件传递的末端的。
- 一个完整的事件序列的消费的顺序是Activity->PhoneWindow->View;如果某一个最末端的View的onTouchEvent返回了false即不处理,此时事件上抛,父亲容器的onTouchEvent会被调用,如果所有的View都处理该事件,最终事件被传递到Activity,则Activity的onTouchEvent会被调用。
- 一般情况下一个事件序列只能被一个View拦截消费,同一个事件序列所有事件都会直接交给它处理,并且它的onInterceptTouchEvent不会再被调用。如果子view中调用requestDisallowInterceptTouchEvent,则会决定父view是否拦截事件**(除action_down以外的事件,action_down会重置FLAG_DISALLOW_INTERCEPT的状态值)**
@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);
}
}
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN(onTouchEvent返回了false),那么同一事件序列中其他事件都不会再交给它来处理,事件将重新交给他的父元素处理,即父元素的onTouchEvent会被调用。
- 如果某个View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以收到后续事件,最终这些消失的点击事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件,ViewGroup的onInterceptTouchEvent方法默认返回false,View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。
- View的onTouchEvent方法默认消耗事件(返回true),除非他是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性分情况,Button默认为true,TextView默认为false。disable不会影响事件的消费,即时一个view是disable状态,依然会消费事件,只是用户无感知,即无反馈。
三、有什么用处?
开发中存在仅仅展示列表的情况,也即是不可点击的列表,如果是这个需求该如何实现?当然如果以RecyclerView为例可以在item禁止,那是否可以以事件的传递默认不消费点击的事件呢?
上面提到的,如果某个View不消耗ACTION_DOWN事件也即是onTouchEvent返回false不就可以满足需求了嘛?简单使用:
1.自定义一个不可点击的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,同时onInterceptTouchEvent返回true表示拦截下此次事件并且不消费。