View事件机制分析

6次阅读

共计 15498 个字符,预计需要花费 39 分钟才能阅读完成。

目录介绍

  • 01.Android 中事件分发顺序

    • 1.1 事件分发的对象是谁
    • 1.2 事件分发的本质
    • 1.3 事件在哪些对象间进行传递
    • 1.4 事件分发过程涉及方法
    • 1.5 Android 中事件分发顺序
  • 02.Activity 的事件分发机制

    • 2.1 源码分析
    • 2.2 点击事件调用顺序
    • 2.3 得出结论
  • 03.ViewGroup 事件的分发机制

    • 3.1 看一下这个案例
    • 3.2 源码分析
    • 3.3 得出结论
  • 04.View 事件的分发机制

    • 4.1 源码分析
    • 4.2 得出结论
    • 4.3 验证结论
  • 05. 思考一下

    • 5.1 onTouch()和 onTouchEvent()的区别
    • 5.2 Touch 事件的后续事件传递

好消息

  • 博客笔记大汇总【16 年 3 月到至今】,包括 Java 基础及深入知识点,Android 技术博客,Python 学习笔记等等,还包括平时开发中遇到的 bug 汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是 markdown 格式的!同时也开源了生活博客,从 12 年起,积累共计 N 篇[近 100 万字,陆续搬到网上],转载请注明出处,谢谢!
  • 链接地址:https://github.com/yangchong2…
  • 如果觉得好,可以 star 一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

01.Android 中事件分发顺序

1.1 事件分发的对象是谁

  • 事件分发的对象是事件。注意,事件分发是向下传递的,也就是父到子的顺序。
  • 当用户触摸屏幕时(View 或 ViewGroup 派生的控件),将产生点击事件(Touch 事件)。

    • Touch 事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成 MotionEvent 对象
  • 主要发生的 Touch 事件有如下四种:

    • MotionEvent.ACTION_DOWN:按下 View(所有事件的开始)
    • MotionEvent.ACTION_MOVE:滑动 View
    • MotionEvent.ACTION_CANCEL:非人为原因结束本次事件
    • MotionEvent.ACTION_UP:抬起 View(与 DOWN 对应)
  • 事件列:

    • 从手指接触屏幕至手指离开屏幕,这个过程产生的一系列事件。即当一个 MotionEvent 产生后,系统需要把这个事件传递给一个具体的 View 去处理
    • 任何事件列都是以 DOWN 事件开始,UP 事件结束,中间有无数的 MOVE 事件,如下图:

1.2 事件分发的本质

  • 将点击事件(MotionEvent)向某个 View 进行传递并最终得到处理

    • 即当一个点击事件发生后,系统需要将这个事件传递给一个具体的 View 去处理。这个事件传递的过程就是分发过程。
    • Android 事件分发机制的本质是要解决,点击事件由哪个对象发出,经过哪些对象,最终达到哪个对象并最终得到处理。

1.3 事件在哪些对象间进行传递

  • 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 和定义布局参数的功能。

1.4 事件分发过程涉及方法

  • 事件分发过程由这几个方法协作完成

    • dispatchTouchEvent()、onInterceptTouchEvent()和 onTouchEvent()

1.5 Android 中事件分发顺序

  • Android 中事件分发顺序:

    • Activity(Window)-> ViewGroup -> View
  • 其中:

    • super:调用父类方法
    • true:消费事件,即事件不继续往下传递
    • false:不消费事件,事件继续往下传递 / 交由给父控件 onTouchEvent()处理
  • 充分理解 Android 分发机制,本质上是要理解:

    • Activity 对点击事件的分发机制
    • ViewGroup 对点击事件的分发机制
    • View 对点击事件的分发机制

02.Activity 的事件分发机制

2.1 源码分析

  • 当一个点击事件发生时,事件最先传到 Activity 的 dispatchTouchEvent()进行事件分发

    • 具体是由 Activity 的 Window 来完成
  • 我们来看下 Activity 的 dispatchTouchEvent()的源码

    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 第一步
        // 一般事件列开始都是 DOWN,所以这里基本是 true
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            // 第二步
            onUserInteraction();}
        // 第三步
        if (getWindow().superDispatchTouchEvent(ev)) {return true;}
        return onTouchEvent(ev);
    }
  • 第一步

    • 一般事件列开始都是 DOWN(按下按钮),所以这里返回 true,执行 onUserInteraction()
  • 第二步

    • 先来看下 onUserInteraction()源码
    public void onUserInteraction() {}
    • 从源码可以看出:

      • 该方法为空方法
      • 从注释得知:当此 activity 在栈顶时,触屏点击按 home,back,menu 键等都会触发此方法
      • 所以 onUserInteraction()主要用于屏保
  • 第三步

    • Window 类是抽象类,且 PhoneWindow 是 Window 类的唯一实现类
    • superDispatchTouchEvent(ev)是抽象方法
    • 通过 PhoneWindow 类中看一下 superDispatchTouchEvent()的作用
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);
        //mDecor 是 DecorView 的实例
        //DecorView 是视图的顶层 view,继承自 FrameLayout,是所有界面的父类
    }
  • 接下来我们看 mDecor.superDispatchTouchEvent(event):

    public boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);
    //DecorView 继承自 FrameLayout
    // 那么它的父类就是 ViewGroup
    而 super.dispatchTouchEvent(event)方法,其实就应该是 ViewGroup 的 dispatchTouchEvent()}
  • 得出结果

    • 执行 getWindow().superDispatchTouchEvent(ev)实际上是执行了 ViewGroup.dispatchTouchEvent(event)
    • 这样事件就从 Activity 传递到了 ViewGroup

2.2 点击事件调用顺序

  • 三个方法执行顺序

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {LogUtils.e("yc---------- 事件拦截 ----------");
        return super.onInterceptTouchEvent(e);
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {LogUtils.e("yc---------- 事件分发 ----------");
        return super.dispatchTouchEvent(ev);
    }
    
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent e) {LogUtils.e("yc---------- 事件触摸 ----------");
        return super.onTouchEvent(e);
    }
    • 执行结果如下
    yc---------- 事件分发 ----------
    yc---------- 事件拦截 ----------
    yc---------- 事件触摸 ----------
  • 下面将用一段伪代码来阐述上述三个方法的关系和点击事件传递规则

    // 点击事件产生后,会直接调用 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;
    }
  • 当一个点击事件发生时,调用顺序如下

    • 1. 事件最先传到 Activity 的 dispatchTouchEvent()进行事件分发
    • 2. 调用 Window 类实现类 PhoneWindow 的 superDispatchTouchEvent()
    • 3. 调用 DecorView 的 superDispatchTouchEvent()
    • 4. 最终调用 DecorView 父类的 dispatchTouchEvent(),即 ViewGroup 的 dispatchTouchEvent()

2.3 得出结论

  • 当一个点击事件发生时,事件最先传到 Activity 的 dispatchTouchEvent()进行事件分发,最终是调用了 ViewGroup 的 dispatchTouchEvent()方法
  • 这样事件就从 Activity 传递到了 ViewGroup

03.ViewGroup 事件的分发机制

3.1 看一下这个案例

  • 布局如下:

  • 结果测试

    • 只点击 Button,发现执行顺序:btn1,btn2
    • 再点击空白处,发现执行顺序:btn1,btn2,viewGroup
  • 从上面的测试结果发现:

    • 当点击 Button 时,执行 Button 的 onClick(),但 ViewGroupLayout 注册的 onTouch()不会执行
    • 只有点击空白区域时才会执行 ViewGroupLayout 的 onTouch();
    • 结论:Button 的 onClick()将事件消费掉了,因此事件不会再继续向下传递。

3.2 源码分析

  • ViewGroup 的 dispatchTouchEvent()源码分析, 该方法比较复杂,截取几个重要的逻辑片段进行介绍,来解析整个分发流程。

    // 发生 ACTION_DOWN 事件或者已经发生过 ACTION_DOWN, 并且将 mFirstTouchTarget 赋值,才进入此区域,主要功能是拦截器
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {//disallowIntercept:是否禁用事件拦截的功能(默认是 false), 即不禁用
        // 可以在子 View 通过调用 requestDisallowInterceptTouchEvent 方法对这个值进行修改,不让该 View 拦截事件
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        // 默认情况下会进入该方法
        if (!disallowIntercept) {
            // 调用拦截方法
            intercepted = onInterceptTouchEvent(ev); 
            ev.setAction(action);
        } else {intercepted = false;}
    } else {
        // 当没有触摸 targets,且不是 down 事件时,开始持续拦截触摸。intercepted = true;
    }
    • 这一段的内容主要是为判断是否拦截。如果当前事件的 MotionEvent.ACTION_DOWN,则进入判断,调用 ViewGroup onInterceptTouchEvent()方法的值,判断是否拦截。如果 mFirstTouchTarget != null,即已经发生过 MotionEvent.ACTION_DOWN,并且该事件已经有 ViewGroup 的子 View 进行处理了,那么也进入判断,调用 ViewGroup onInterceptTouchEvent()方法的值,判断是否拦截。如果不是以上两种情况,即已经是 MOVE 或 UP 事件了,并且之前的事件没有对象进行处理,则设置成 true,开始拦截接下来的所有事件。这也就解释了如果子 View 的 onTouchEvent()方法返回 false,那么接下来的一些列事件都不会交给他处理。如果 VieGroup 的 onInterceptTouchEvent()第一次执行为 true,则 mFirstTouchTarget = null,则也会使得接下来不会调用 onInterceptTouchEvent(),直接将拦截设置为 true。
  • 当 ViewGroup 不拦截事件的时候,事件会向下分发交由它的子 View 或 ViewGroup 进行处理。

      /* 从最底层的父视图开始遍历,** 找寻 newTouchTarget,即上面的 mFirstTouchTarget
       ** 如果已经存在找寻 newTouchTarget,说明正在接收触摸事件,则跳出循环。*/
    for (int i = childrenCount - 1; i >= 0; i--) {
      final int childIndex = customOrder
        ? getChildDrawingOrder(childrenCount, i) : i;
      final View child = (preorderedList == null)
        ? children[childIndex] : preorderedList.get(childIndex);
    
      // 如果当前视图无法获取用户焦点,则跳过本次循环
      if (childWithAccessibilityFocus != null) {if (childWithAccessibilityFocus != child) {continue;}
         childWithAccessibilityFocus = null;
         i = childrenCount - 1;
      }
      // 如果 view 不可见,或者触摸的坐标点不在 view 的范围内,则跳过本次循环
      if (!canViewReceivePointerEvents(child) 
          || !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);
        continue;
        }
    
       newTouchTarget = getTouchTarget(child);
       // 已经开始接收触摸事件, 并退出整个循环。if (newTouchTarget != null) {
           newTouchTarget.pointerIdBits |= idBitsToAssign;
           break;
        }
    
        // 重置取消或抬起标志位
        // 如果触摸位置在 child 的区域内,则把事件分发给子 View 或 ViewGroup
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // 获取 TouchDown 的时间点
            mLastTouchDownTime = ev.getDownTime();
            // 获取 TouchDown 的 Index
            if (preorderedList != null) {for (int j = 0; j < childrenCount; j++) {if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
               }
             } else {mLastTouchDownIndex = childIndex;}
    
          // 获取 TouchDown 的 x,y 坐标
          mLastTouchDownX = ev.getX();
          mLastTouchDownY = ev.getY();
          // 添加 TouchTarget, 则 mFirstTouchTarget != null。newTouchTarget = addTouchTarget(child, idBitsToAssign);
          // 表示以及分发给 NewTouchTarget
          alreadyDispatchedToNewTouchTarget = true;
          break;
    }
    • dispatchTransformedTouchEvent()方法实际就是调用子元素的 dispatchTouchEvent() 方法。
    • 其中 dispatchTransformedTouchEvent() 方法的重要逻辑如下:
    if (child == null) {handled = super.dispatchTouchEvent(event);
    } else {handled = child.dispatchTouchEvent(event);
    }
    • 由于其中传递的 child 不为空,所以就会调用子元素的 dispatchTouchEvent()。如果子元素的 dispatchTouchEvent()方法返回 true,那么 mFirstTouchTarget 就会被赋值,同时跳出 for 循环。
    // 添加 TouchTarget, 则 mFirstTouchTarget != null。newTouchTarget = addTouchTarget(child, idBitsToAssign);
     // 表示以及分发给 NewTouchTarget
     alreadyDispatchedToNewTouchTarget = true;
    • 其中在 addTouchTarget(child, idBitsToAssign); 内部完成 mFirstTouchTarget 被赋值。如果 mFirstTouchTarget 为空,将会让 ViewGroup 默认拦截所有操作。如果遍历所有子 View 或 ViewGroup,都没有消费事件。ViewGroup 会自己处理事件。

3.3 得出结论

  • Android 事件分发是先传递到 ViewGroup,再由 ViewGroup 传递到 View
  • 在 ViewGroup 中通过 onInterceptTouchEvent()对事件传递进行拦截

    • 1.onInterceptTouchEvent 方法返回 true 代表拦截事件,即不允许事件继续向子 View 传递;
    • 2. 返回 false 代表不拦截事件,即允许事件继续向子 View 传递;(默认返回 false)
    • 3. 子 View 中如果将传递的事件消费掉,ViewGroup 中将无法接收到任何事件。

04.View 事件的分发机制

4.1 源码分析

  • View 中 dispatchTouchEvent()的源码分析

    public boolean dispatchTouchEvent(MotionEvent event) {if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {return true;}  
        return onTouchEvent(event);  
    }
  • 从上面可以看出:

    • 只有以下三个条件都为真,dispatchTouchEvent()才返回 true;否则执行 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
    • 该条件是判断当前点击的控件是否 enable
    • 由于很多 View 默认是 enable 的,因此该条件恒定为 true
    • 第三个条件:mOnTouchListener.onTouch(this, event)
    • 回调控件注册 Touch 事件时的 onTouch 方法
    // 手动调用设置
    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {return false;}  
    });
    • 如果在 onTouch 方法返回 true,就会让上述三个条件全部成立,从而整个方法直接返回 true。
    • 如果在 onTouch 方法里返回 false,就会去执行 onTouchEvent(event)方法。
  • 接下来,我们继续看: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;  
    }
    • 只要 mOnClickListener 不为 null,就会去调用 onClick 方法;
  • 那么,mOnClickListener 又是在哪里赋值的呢?请继续看:

    public void setOnClickListener(OnClickListener l) {if (!isClickable()) {setClickable(true);  
        }  
        mOnClickListener = l;  
    }
    • 当我们通过调用 setOnClickListener 方法来给控件注册一个点击事件时,就会给 mOnClickListener 赋值(不为空),即会回调 onClick()。

4.2 得出结论

  • 1.onTouch()的执行高于 onClick()
  • 2. 每当控件被点击时:

    • 如果在回调 onTouch()里返回 false,就会让 dispatchTouchEvent 方法返回 false,那么就会执行 onTouchEvent();如果回调了 setOnClickListener()来给控件注册点击事件的话,最后会在 performClick()方法里回调 onClick()。

      • onTouch()返回 false(该事件没被 onTouch()消费掉)= 执行 onTouchEvent() = 执行 OnClick()
    • 如果在回调 onTouch()里返回 true,就会让 dispatchTouchEvent 方法返回 true,那么将不会执行 onTouchEvent(),即 onClick()也不会执行;

      • onTouch()返回 true(该事件被 onTouch()消费掉)= dispatchTouchEvent()返回 true(不会再继续向下传递)= 不会执行 onTouchEvent() = 不会执行 OnClick()

4.3 验证结论

  • 在回调 onTouch()里返回 true

    TextView textView = findViewById(R.id.tv_13);
    // 设置 OnTouchListener()
    textView.setOnTouchListener(new View.OnTouchListener() {
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {Log.d("小杨逗比","执行了 onTouch(), 动作是:" + event.getAction());
            return true;
        }
    });
    // 设置 OnClickListener
    textView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {Log.d("小杨逗比","执行了 onClick()");
        }
    });
    • 打印日志如下所示
    • 注意 action 为 0 是 ACTION_DOWN,为 2 是 ACTION_MOVE,为 1 是 ACTION_UP。
    2019-04-04 13:37:58.301 13616-13616/org.yczbj.ycrefreshview D/ 小杨逗比: 执行了 onTouch(), 动作是:0
    2019-04-04 13:37:58.315 13616-13616/org.yczbj.ycrefreshview D/ 小杨逗比: 执行了 onTouch(), 动作是:2
    2019-04-04 13:37:58.405 13616-13616/org.yczbj.ycrefreshview D/ 小杨逗比: 执行了 onTouch(), 动作是:2
    2019-04-04 13:37:58.408 13616-13616/org.yczbj.ycrefreshview D/ 小杨逗比: 执行了 onTouch(), 动作是:1
  • 在回调 onTouch()里返回 false

    • 打印结果如下所示
    2019-04-04 13:41:26.961 14006-14006/org.yczbj.ycrefreshview D/ 小杨逗比: 执行了 onTouch(), 动作是:0
    2019-04-04 13:41:26.978 14006-14006/org.yczbj.ycrefreshview D/ 小杨逗比: 执行了 onTouch(), 动作是:2
    2019-04-04 13:41:27.072 14006-14006/org.yczbj.ycrefreshview D/ 小杨逗比: 执行了 onTouch(), 动作是:2
    2019-04-04 13:41:27.074 14006-14006/org.yczbj.ycrefreshview D/ 小杨逗比: 执行了 onTouch(), 动作是:1
    2019-04-04 13:41:27.076 14006-14006/org.yczbj.ycrefreshview D/ 小杨逗比: 执行了 onClick()
  • 总结:onTouch()返回 true 就认为该事件被 onTouch()消费掉,因而不会再继续向下传递,即不会执行 OnClick()。

05. 思考一下

5.1 onTouch()和 onTouchEvent()的区别

  • 这两个方法都是在 View 的 dispatchTouchEvent 中调用,但 onTouch 优先于 onTouchEvent 执行。
  • 如果在 onTouch 方法中返回 true 将事件消费掉,onTouchEvent()将不会再执行。
  • 特别注意:请看下面代码

    //&& 为短路与,即如果前面条件为 false,将不再往下执行
    // 所以,onTouch 能够得到执行需要两个前提条件://1. mOnTouchListener 的值不能为空
    //2. 当前点击的控件必须是 enable 的。mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)
  • 因此如果你有一个控件是非 enable 的,那么给它注册 onTouch 事件将永远得不到执行。对于这一类控件,如果我们想要监听它的 touch 事件,就必须通过在该控件中重写 onTouchEvent 方法来实现。

5.2 Touch 事件的后续事件(MOVE、UP)层级传递

  • 如果给控件注册了 Touch 事件,每次点击都会触发一系列 action 事件(ACTION_DOWN,ACTION_MOVE,ACTION_UP 等)
  • 当 dispatchTouchEvent 在进行事件分发的时候,只有前一个事件(如 ACTION_DOWN)返回 true,才会收到后一个事件(ACTION_MOVE 和 ACTION_UP)

    • 即如果在执行 ACTION_DOWN 时返回 false,后面一系列的 ACTION_MOVE 和 ACTION_UP 事件都不会执行
  • 从上面对事件分发机制分析知:

    • dispatchTouchEvent()和 onTouchEvent()消费事件、终结事件传递(返回 true)
    • 而 onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用,对后续的 ACTION_MOVE 和 ACTION_UP 事件接收起到非常大的作用
    • 请记住:接收了 ACTION_DOWN 事件的函数不一定能收到后续事件(ACTION_MOVE、ACTION_UP)
  • 这里给出 ACTION_MOVE 和 ACTION_UP 事件的传递结论

    • 如果在某个对象(Activity、ViewGroup、View)的 dispatchTouchEvent()消费事件(返回 true),那么收到 ACTION_DOWN 的函数也能收到 ACTION_MOVE 和 ACTION_UP
    • 如果在某个对象(Activity、ViewGroup、View)的 onTouchEvent()消费事件(返回 true),那么 ACTION_MOVE 和 ACTION_UP 的事件从上往下传到这个 View 后就不再往下传递了,而直接传给自己的 onTouchEvent()并结束本次事件传递过程。

其他介绍

01. 关于博客汇总链接

  • 1. 技术博客汇总
  • 2. 开源项目汇总
  • 3. 生活博客汇总
  • 4. 喜马拉雅音频汇总
  • 5. 其他汇总

02. 关于我的博客

  • github:https://github.com/yangchong211
  • 知乎:https://www.zhihu.com/people/…
  • 简书:http://www.jianshu.com/u/b7b2…
  • csdn:http://my.csdn.net/m0_37700275
  • 喜马拉雅听书:http://www.ximalaya.com/zhubo…
  • 开源中国:https://my.oschina.net/zbj161…
  • 泡在网上的日子:http://www.jcodecraeer.com/me…
  • 邮箱:yangchong211@163.com
  • segmentfault 头条:https://segmentfault.com/u/xi…
  • 掘金:https://juejin.im/user/593943…

项目地址:https://github.com/yangchong2…

正文完
 0