答:事件
即当一个 MotionEvent 产生后,系统需要把这个事件传递给一个具体的 View 去处理,
答:将点击事件(MotionEvent)向某个 View 进行传递并最终得到处理
答:Activity、ViewGroup、View
答:dispatchTouchEvent() 、onInterceptTouchEvent() 和 onTouchEvent()
经过上述 3 个问题,相信大家已经对 Android 的事件分发有了感性的认知,接下来,我将详细介绍 Android 事件分发机制。
其中:
接下来,我将详细介绍这 3 个方法及相关流程。
属性 | 介绍 |
---|---|
使用对象 | Activity、ViewGroup、View |
作用 | 分发点击事件 |
调用时刻 | 当点击事件能够传递给当前 View 时,该方法就会被调用 |
返回结果 | 是否消费当前事件,详细情况如下: |
1. 默认情况:根据当前对象的不同而返回方法不同
对象 | 返回方法 | 备注 |
---|---|---|
Activity | super.dispatchTouchEvent() | 即调用父类 ViewGroup 的 dispatchTouchEvent() |
ViewGroup | onIntercepTouchEvent() | 即调用自身的 onIntercepTouchEvent() |
View | onTouchEvent() | 即调用自身的 onTouchEvent() |
2. 返回 true
3. 返回 false
属性 | 介绍 |
---|---|
使用对象 | Activity、ViewGroup、View |
作用 | 处理点击事件 |
调用时刻 | 在 dispatchTouchEvent() 内部调用 |
返回结果 | 是否消费(处理)当前事件,详细情况如下: |
1. 返回 true
2. 返回 false(同默认实现:调用父类 onTouchEvent())
属性 | 介绍 |
---|---|
使用对象 | ViewGroup(注:Activity、View 都没该方法) |
作用 | 拦截事件,即自己处理该事件 |
调用时刻 | 在 ViewGroup 的 dispatchTouchEvent() 内部调用 |
返回结果 | 是否拦截当前事件,详细情况如下: |
下面将用一段伪代码来阐述上述三个方法的关系和点击事件传递规则
- // 点击事件产生后,会直接调用dispatchTouchEvent()方法
- public boolean dispatchTouchEvent(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()
假设 View C 希望处理这个点击事件,即 C 被设置成可点击的(Clickable)或者覆写了 C 的 onTouchEvent 方法返回 true。
事件传递情况:(如下图)
假设 ViewGroup B 希望处理这个点击事件,即 B 覆写了 onInterceptTouchEvent() 返回 true、onTouchEvent() 返回 true。
事件传递情况:(如下图)
假设 ViewGroup B 没有拦截 DOWN 事件(还是 View C 来处理 DOWN 事件),但它拦截了接下来的 MOVE 事件。
特别注意:
其中:
所以,要想充分理解 Android 分发机制,本质上是要理解:
接下来,我将通过源码分析详细介绍 Activity、View 和 ViewGroup 的事件分发机制
- public boolean dispatchTouchEvent(MotionEvent ev) {
- //关注点1
- //一般事件列开始都是DOWN,所以这里基本是true
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- //关注点2
- onUserInteraction();
- }
- //关注点3
- if (getWindow().superDispatchTouchEvent(ev)) {
- return true;
- }
- return onTouchEvent(ev);
- }
关注点 1
一般事件列开始都是 DOWN(按下按钮),所以这里返回 true,执行 onUserInteraction()
关注点 2
先来看下 onUserInteraction() 源码
- /**
- * Called whenever a key, touch, or trackball event is dispatched to the
- * activity. Implement this method if you wish to know that the user has
- * interacted with the device in some way while your activity is running.
- * This callback and #onUserLeaveHint} are intended to help
- * activities manage status bar notifications intelligently; specifically,
- * for helping activities determine the proper time to cancel a notfication.
- *
- *
- All calls
- to your activity's #onUserLeaveHint} callback will
- * be accompanied by calls to #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 for the touch down action
- * that begins a touch gesture, but may not be invoked for the touch-moved
- * and touch-up actions that follow.
- *
- * @see #onUserLeaveHint()
- */
- public void onUserInteraction() {
- }
从源码可以看出:
关注点 3
- @Override
- public 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)
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- //关注点2
- onUserInteraction();
- }
- //关注点3
- if (getWindow().superDispatchTouchEvent(ev)) {
- return true;
- }
- return onTouchEvent(ev);
- }
由于一般事件列开始都是 DOWN,所以这里返回 true,基本上都会进入
的判断
- getWindow().superDispatchTouchEvent(ev)
实际上是执行了
- Activity.dispatchTouchEvent(ev)
- ViewGroup.dispatchTouchEvent(event)
当一个点击事件发生时,调用顺序如下
那么,ViewGroup 的 dispatchTouchEvent() 什么时候返回 true,什么时候返回 false?
答:请继续往下看 ViewGroup 事件的分发机制
在讲解 ViewGroup 事件的分发机制之前我们先来看个 Demo
布局如下:
结果测试:
只点击 Button
再点击空白处
从上面的测试结果发现:
接下来,我们开始进行 ViewGroup 事件分发的源码分析
ViewGroup 的 dispatchTouchEvent() 源码分析
- 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;
- 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);
- final int scrolledXInt = (int) scrolledXFloat;
- final int scrolledYInt = (int) scrolledYFloat;
- final View[] children = mChildren;
- final int count = mChildrenCount;
- //通过for循环,遍历了当前ViewGroup下的所有子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是不是正在点击的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;
- //关注点2
- //条件判断的内部调用了该View的dispatchTouchEvent()方法(具体请看下面的View事件分发机制)
- //实现了点击事件从ViewGroup到View的传递
- if (child.dispatchTouchEvent(ev)) {
- //调用子View的dispatchTouchEvent后是有返回值的
- //如果这个控件是可点击的话,那么点击该控件时,dispatchTouchEvent的返回值必定是true
- //因此会导致条件判断成立
- mMotionTarget = child;
- //于是给ViewGroup的dispatchTouchEvent方法直接返回了true,这样就导致后面的代码无法执行,直接跳出
- //即把ViewGroup的touch事件拦截掉
- return true;
- }
- }
- }
- }
- }
- }
- 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的传递
- return super.dispatchTouchEvent(ev);
- }
- //之后的代码在一般情况下是走不到的了,我们也就不再继续往下分析。
- 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);
- 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;
- }
- return target.dispatchTouchEvent(ev);
- }
ViewGroup 每次在做分发时,需要调用 onInterceptTouchEvent() 是否拦截事件;源码分析如下:
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- return false;
- }
当点击了某个控件:
View 中 dispatchTouchEvent() 的源码分析
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
- mOnTouchListener.onTouch(this, event)) {
- return true;
- }
- return onTouchEvent(event);
- }
从上面可以看出:
- 第一个条件:mOnTouchListener != null;
- 第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED;
- 第三个条件:mOnTouchListener.onTouch(this, event);
第一个条件:mOnTouchListener!= null
- //mOnTouchListener是在View类下setOnTouchListener方法里赋值的
- public void setOnTouchListener(OnTouchListener l) {
- //即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
- mOnTouchListener = l;
- }
第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED
第三个条件:mOnTouchListener.onTouch(this, event)
- //手动调用设置
- button.setOnTouchListener(new OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- return false;
- }
- });
接下来,我们继续看:onTouchEvent(event) 的源码分析
- public boolean onTouchEvent(MotionEvent event) {
- final int 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)) {
- return true;
- }
- }
- //如果该控件是可以点击的就会进入到下两行的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()方法。
- //请往下看关注点1
- 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();
- }
- 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());
- } 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;
- 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
- 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;
- }
- //如果该控件是可以点击的,就一定会返回true
- return true;
- }
- //如果该控件是可以点击的,就一定会返回false
- return false;
- }
关注点 1:
performClick() 的源码分析
- public boolean performClick() {
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
- if (mOnClickListener != null) {
- playSoundEffect(SoundEffectConstants.CLICK);
- mOnClickListener.onClick(this);
- return true;
- }
- return false;
- }
- public void setOnClickListener(OnClickListener l) {
- if (!isClickable()) {
- setClickable(true);
- }
- mOnClickListener = l;
- }
- //设置OnTouchListener()
- button.setOnTouchListener(new View.OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- System.out.println("执行了onTouch(), 动作是:" + event.getAction());
- return true;
- }
- });
- //设置OnClickListener
- button.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(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());
- return false;
- }
- });
- //设置OnClickListener
- button.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- System.out.println("执行了onClick()");
- }
- });
点击 Button,测试结果如下:
总结:onTouch() 返回 true 就认为该事件被 onTouch() 消费掉,因而不会再继续向下传递,即不会执行 OnClick()。
- //&&为短路与,即如果前面条件为false,将不再往下执行
- //所以,onTouch能够得到执行需要两个前提条件:
- //1. mOnTouchListener的值不能为空
- //2. 当前点击的控件必须是enable的。
- mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
- mOnTouchListener.onTouch(this, event)
从上面对事件分发机制分析知:
这里给出 ACTION_MOVE 和 ACTION_UP 事件的传递结论:
不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度。
来源: