前言
今天,我将全面总结Android的事件分发机制,我能保证这是市面上的最全面、最清晰、最易懂的
- 本文秉着“结论先行、详细分析在后”的原则,即先让大家感性认识,再通过理性分析从而理解问题;
- 所以,请各位读者先记住结论,再往下继续看分析;
文章较长,阅读需要较长时间,建议收藏等充足时间再进行阅读
1. 基础认知
答:事件
当用户触摸屏幕时(View或ViewGroup派生的控件),将产生点击事件(Touch事件)。
Touch事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象
主要发生的Touch事件有如下四种:
事件列:从手指接触屏幕至手指离开屏幕,这个过程产生的一系列事件
任何事件列都是以DOWN事件开始,UP事件结束,中间有无数的MOVE事件,如下图:
即当一个MotionEvent 产生后,系统需要把这个事件传递给一个具体的 View 去处理,
答:将点击事件(MotionEvent)向某个View进行传递并最终得到处理
即当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理。这个事件传递的过程就是分发过程。
答:Activity、ViewGroup、View
一个点击事件产生后,传递顺序是:Activity(Window) -> ViewGroup -> View
Android的UI界面是由Activity、ViewGroup、View及其派生类组合而成的
View是所有UI组件的基类
一般Button、ImageView、TextView等控件都是继承父类View
ViewGroup是容纳UI组件的容器,即一组View的集合(包含很多子View和子VewGroup),
- 其本身也是从View派生的,即ViewGroup是View的子类
- 是Android所有布局的父类或间接父类:项目用到的布局(LinearLayout、RelativeLayout等),都继承自ViewGroup,即属于ViewGroup子类。
- 与普通View的区别:ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。
答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()
下文会对这3个方法进行详细介绍
这里的对象是指Activity、ViewGroup、View
Android事件分发顺序:Activity(Window) -> ViewGroup -> View
接下来,我将详细介绍这3个方法及相关流程。
属性 | 介绍 |
---|---|
使用对象 | Activity、ViewGroup、View |
作用 | 分发点击事件 |
调用时刻 | 当点击事件能够传递给当前View时,该方法就会被调用 |
返回结果 | 是否消费当前事件,详细情况如下: |
1. 默认情况:根据当前对象的不同而返回方法不同
对象 | 返回方法 | 备注 |
---|---|---|
Activity | super.dispatchTouchEvent() | 即调用父类ViewGroup的dispatchTouchEvent() |
ViewGroup | onIntercepTouchEvent() | 即调用自身的onIntercepTouchEvent() |
View | onTouchEvent() | 即调用自身的onTouchEvent() |
2. 返回true
3. 返回false
将事件回传给父控件的onTouchEvent()处理
Activity例外:返回false=消费事件
后续事件(Move、Up)会继续分发到该View(与onTouchEvent()区别)
属性 | 介绍 |
---|---|
使用对象 | Activity、ViewGroup、View |
作用 | 处理点击事件 |
调用时刻 | 在dispatchTouchEvent()内部调用 |
返回结果 | 是否消费(处理)当前事件,详细情况如下: |
与dispatchTouchEvent()类似
1. 返回true
2. 返回false(同默认实现:调用父类onTouchEvent())
属性 | 介绍 |
---|---|
使用对象 | ViewGroup(注:Activity、View都没该方法) |
作用 | 拦截事件,即自己处理该事件 |
调用时刻 | 在ViewGroup的dispatchTouchEvent()内部调用 |
返回结果 | 是否拦截当前事件,详细情况如下: |
下面将用一段伪代码来阐述上述三个方法的关系和点击事件传递规则
// 点击事件产生后,会直接调用dispatchTouchEvent()方法publicbooleandispatchTouchEvent(MotionEvent ev) { //代表是否消耗事件boolean consume = false; if (onInterceptTouchEvent(ev)) { //如果onInterceptTouchEvent()返回true则代表当前View拦截了点击事件//则该点击事件则会交给当前View进行处理//即调用onTouchEvent ()方法去处理点击事件 consume = onTouchEvent (ev) ; } else { //如果onInterceptTouchEvent()返回false则代表当前View不拦截点击事件//则该点击事件则会继续传递给它的子元素//子元素的dispatchTouchEvent()就会被调用,重复上述过程//直到点击事件被最终处理为止 consume = child.dispatchTouchEvent (ev) ; } return consume; }
下面我将利用例子来说明常见的点击事件传递情况
我们将要讨论的布局层次如下:
假设用户首先触摸到屏幕上View C上的某个点(如图中黄色区域),那么Action_DOWN事件就在该点产生,然后用户移动手指并最后离开屏幕。
一般的事件传递场景有:
注:虽然ViewGroup B的onInterceptTouchEvent方法对DOWN事件返回了false,后续的事件(MOVE、UP)依然会传递给它的onInterceptTouchEvent()
这一点与onTouchEvent的行为是不一样的。
假设View C希望处理这个点击事件,即C被设置成可点击的(Clickable)或者覆写了C的onTouchEvent方法返回true。
最常见的:设置Button按钮来响应点击事件
事件传递情况:(如下图)
假设ViewGroup B希望处理这个点击事件,即B覆写了onInterceptTouchEvent()返回true、onTouchEvent()返回true。
事件传递情况:(如下图)
调用onTouchEvent()处理事件(DOWN事件将不再往上传递给A的onTouchEvent())
该事件列的其他事件(Move、Up)将直接传递给B的onTouchEvent()
该事件列的其他事件(Move、Up)将不会再传递给B的onInterceptTouchEvent方法,该方法一旦返回一次true,就再也不会被调用了。
假设ViewGroup B没有拦截DOWN事件(还是View C来处理DOWN事件),但它拦截了接下来的MOVE事件。
后续又来了一个MOVE事件,该MOVE事件才会直接传递给B的onTouchEvent()
- 后续事件将直接传递给B的onTouchEvent()处理
- 后续事件将不会再传递给B的onInterceptTouchEvent方法,该方法一旦返回一次true,就再也不会被调用了。
C再也不会收到该事件列产生的后续事件。
特别注意:
其中:
所以,要想充分理解Android分发机制,本质上是要理解:
接下来,我将通过源码分析详细介绍Activity、View和ViewGroup的事件分发机制
具体是由Activity的Window来完成
publicbooleandispatchTouchEvent(MotionEvent ev) { //关注点1//一般事件列开始都是DOWN,所以这里基本是trueif (ev.getAction() == MotionEvent.ACTION_DOWN) { //关注点2 onUserInteraction(); } //关注点3if (getWindow().superDispatchTouchEvent(ev)) { returntrue; } return onTouchEvent(ev); }
关注点1
一般事件列开始都是DOWN(按下按钮),所以这里返回true,执行onUserInteraction()
关注点2
先来看下onUserInteraction()源码
/** * Called whenever a key, touch, or trackball event is dispatched tothe * activity. Implement this method if you wish to know thatthe user has * interacted withthe device insome way while your activity isrunning. * This callback and {@link #onUserLeaveHint} are intended to help * activities manage status bar notifications intelligently; specifically, * for helping activities determine the proper timeto cancel a notfication. * *
All calls to your activity's {@link #onUserLeaveHint} callback will * be accompanied by calls to {@link #onUserInteraction}. This * ensures that your activity will be told of relevant user activity such * as pulling down the notification pane and touching an item there. * *
Note that this callback will be invoked forthe touch down action * that begins a touch gesture, but may not be invoked forthe touch-moved * and touch-up actions that follow. * * @see #onUserLeaveHint() */public void onUserInteraction() { }
关注点3
@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event);//mDecor是DecorView的实例//DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类}
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event);//DecorView继承自FrameLayout//那么它的父类就是ViewGroup而super.dispatchTouchEvent(event)方法,其实就应该是ViewGroup的dispatchTouchEvent()}
所以:
getWindow().superDispatchTouchEvent(ev)
实际上是执行了ViewGroup.dispatchTouchEvent(event)
publicbooleandispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { //关注点2 onUserInteraction(); } //关注点3if (getWindow().superDispatchTouchEvent(ev)) { returntrue; } return onTouchEvent(ev); }
由于一般事件列开始都是DOWN,所以这里返回true,基本上都会进入getWindow().superDispatchTouchEvent(ev)
的判断
Activity.dispatchTouchEvent(ev)
实际上是执行了ViewGroup.dispatchTouchEvent(event)
当一个点击事件发生时,调用顺序如下
如果ViewGroup的dispatchTouchEvent()返回true就不执行Activity的onTouchEvent()方法;如果返回false,就执行。
那么,ViewGroup的dispatchTouchEvent()什么时候返回true,什么时候返回false?
答:请继续往下看 ViewGroup事件的分发机制
4.2 ViewGroup事件的分发机制 在讲解ViewGroup事件的分发机制之前我们先来看个Demo 布局如下:
结果测试:
只点击Button
再点击空白处
从上面的测试结果发现:
接下来,我们开始进行ViewGroup事件分发的源码分析
ViewGroup的dispatchTouchEvent()源码分析
- 详情请看注释
- Android 5.0后ViewGroup的dispatchTouchEvent()的源码发生了变化(更加复杂),但原理相同;
- 本文为了让读者更好理解dispatchTouchEvent()源码分析,所以采用Android 5.0前的版本
publicbooleandispatchTouchEvent(MotionEvent ev) { finalint action = ev.getAction(); finalfloat xf = ev.getX(); finalfloat yf = ev.getY(); finalfloat scrolledXFloat = xf + mScrollX; finalfloat scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { mMotionTarget = null; } //看这个If判断语句//第一个判断值disallowIntercept:是否禁用事件拦截的功能(默认是false)//可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。//第二个判断值: !onInterceptTouchEvent(ev):对onInterceptTouchEvent()返回值取反//如果我们在onInterceptTouchEvent()中返回false,就会让第二个值为true,从而进入到条件判断的内部//如果我们在onInterceptTouchEvent()中返回true,就会让第二个值为false,从而跳出了这个条件判断。//关于onInterceptTouchEvent()请看下面分析(关注点1)if (disallowIntercept || !onInterceptTouchEvent(ev)) { ev.setAction(MotionEvent.ACTION_DOWN); finalint scrolledXInt = (int) scrolledXFloat; finalint scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; finalint count = mChildrenCount; //通过for循环,遍历了当前ViewGroup下的所有子Viewfor (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是不是正在点击的View//如果是,则进入条件判断内部if (frame.contains(scrolledXInt, scrolledYInt)) { finalfloat xc = scrolledXFloat - child.mLeft; finalfloat yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; //关注点2//条件判断的内部调用了该View的dispatchTouchEvent()方法(具体请看下面的View事件分发机制)//实现了点击事件从ViewGroup到View的传递if (child.dispatchTouchEvent(ev)) { //调用子View的dispatchTouchEvent后是有返回值的//如果这个控件是可点击的话,那么点击该控件时,dispatchTouchEvent的返回值必定是true//因此会导致条件判断成立 mMotionTarget = child; //于是给ViewGroup的dispatchTouchEvent方法直接返回了true,这样就导致后面的代码无法执行,直接跳出//即把ViewGroup的touch事件拦截掉returntrue; } } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } final View target = mMotionTarget; //关注点3//没有任何View接收事件的情况,即点击空白处情况if (target == null) { ev.setLocation(xf, yf); if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; } //调用ViewGroup的父类View的dispatchTouchEvent()//因此会执行ViewGroup的onTouch()、onTouchEvent()//实现了点击事件从ViewGroup到View的传递returnsuper.dispatchTouchEvent(ev); } //之后的代码在一般情况下是走不到的了,我们也就不再继续往下分析。if (!disallowIntercept && onInterceptTouchEvent(ev)) { finalfloat xc = scrolledXFloat - (float) target.mLeft; finalfloat yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; ev.setAction(MotionEvent.ACTION_CANCEL); ev.setLocation(xc, yc); if (!target.dispatchTouchEvent(ev)) { } mMotionTarget = null; returntrue; } if (isUpOrCancel) { mMotionTarget = null; } finalfloat xc = scrolledXFloat - (float) target.mLeft; finalfloat 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; } return target.dispatchTouchEvent(ev); }
ViewGroup每次在做分发时,需要调用onInterceptTouchEvent()是否拦截事件;源码分析如下:
publicbooleanonInterceptTouchEvent(MotionEvent ev) { returnfalse; }
因为子View也需要该事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()和return false是一样的 = 不会拦截
- 实现了点击事件从ViewGroup到View的传递
- 此处是关于View.dispatchTouchEvent()的分析,详情请看下面的View事件分发机制。
- onInterceptTouchEvent方法返回true代表拦截事件,即不允许事件继续向子View传递;
- 返回false代表不拦截事件,即允许事件继续向子View传递;(默认返回false)
- 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { returntrue; } return onTouchEvent(event); }
第一个条件:mOnTouchListener != null;第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED;第三个条件:mOnTouchListener.onTouch(this, event);
第一个条件:mOnTouchListener!= null
//mOnTouchListener是在View类下setOnTouchListener方法里赋值的publicvoidsetOnTouchListener(OnTouchListener l) { //即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空) mOnTouchListener = l; }
第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED
第三个条件:mOnTouchListener.onTouch(this, event)
//手动调用设置button.setOnTouchListener(new OnTouchListener() { @OverridepublicbooleanonTouch(View v, MotionEvent event) { returnfalse; } });
接下来,我们继续看:onTouchEvent(event)的源码分析
- 详情请看注释
- Android 5.0后View的onTouchEvent()的源码发生了变化(更加复杂),但原理相同;
- 本文为了让读者更好理解onTouchEvent()源码分析,所以采用Android 5.0前的版本
publicbooleanonTouchEvent(MotionEvent event) { finalint viewFlags = mViewFlags; 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)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { returntrue; } } //如果该控件是可以点击的就会进入到下两行的switch判断中去;if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { //如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。switch (event.getAction()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; // 在经过种种判断之后,会执行到关注点1的performClick()方法。//请往下看关注点1if ((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(); } if (!mHasPerformedLongPress) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state 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)) { //关注点1//请往下看performClick()的源码分析 performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { mPrivateFlags |= PRESSED; refreshDrawableState(); postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } elseif (!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; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); break; case MotionEvent.ACTION_MOVE: finalint x = (int) event.getX(); finalint 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 removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } //如果该控件是可以点击的,就一定会返回truereturntrue; } //如果该控件是可以点击的,就一定会返回falsereturnfalse; }
关注点1:
performClick()的源码分析
publicbooleanperformClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); returntrue; } returnfalse; }
publicvoidsetOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); } mOnClickListener = l; }
onTouch()返回false(该事件没被onTouch()消费掉) = dispatchTouchEvent()返回false(继续向下传递) = 执行onTouchEvent() = 执行OnClick()
//设置OnTouchListener() button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("执行了onTouch(), 动作是:" + event.getAction()); returntrue; } });//设置OnClickListener button.setOnClickListener(new View.OnClickListener() { @Override publicvoidonClick(View v) { System.out.println("执行了onClick()"); } });
点击Button,测试结果如下:
//设置OnTouchListener() button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("执行了onTouch(), 动作是:" + event.getAction()); returnfalse; } });//设置OnClickListener button.setOnClickListener(new View.OnClickListener() { @Override publicvoidonClick(View v) { System.out.println("执行了onClick()"); } });
点击Button,测试结果如下:
总结:onTouch()返回true就认为该事件被onTouch()消费掉,因而不会再继续向下传递,即不会执行OnClick()。
如果在onTouch方法中返回true将事件消费掉,onTouchEvent()将不会再执行。
特别注意:请看下面代码
//&&为短路与,即如果前面条件为false,将不再往下执行//所以,onTouch能够得到执行需要两个前提条件://1. mOnTouchListener的值不能为空//2. 当前点击的控件必须是enable的。mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)
即如果在执行ACTION_DOWN时返回false,后面一系列的ACTION_MOVE和ACTION_UP事件都不会执行
请记住:接收了ACTION_DOWN事件的函数不一定能收到后续事件(ACTION_MOVE、ACTION_UP)
这里给出ACTION_MOVE和ACTION_UP事件的传递结论:
黑线:ACTION_DOWN事件传递方向
红线:ACTION_MOVE和ACTION_UP事件传递方向
黑线:ACTION_DOWN事件传递方向
红线:ACTION_MOVE和ACTION_UP事件传递方向
相关文章阅读
Android开发:最全面、最易懂的Android屏幕适配解决方案
Android开发:史上最全的Android消息推送解决方案
Android开发:最全面、最易懂的Webview详解
Android开发:JSON简介及最全面解析方法!
Android四大组件:Service服务史上最全面解析
Android四大组件:BroadcastReceiver史上最全面解析
不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度。