用一张图告诉你Android中的事件传递机制

 

1. 基础认知

1.1 事件分发的对象是谁?

答:点击事件(Touch事件)

  • 定义
    当用户触摸屏幕时(View或 ViewGroup派生的控件),将产生点击事件(Touch事件)

Touch事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象

  • 事件类型(4种)
事件类型 具体动作
MotionEvent.ACTION_DOWN 按下View(所有事件的开始)
MotionEvent.ACTION_UP 抬起View(与DOWN对应)
MotionEvent.ACTION_MOVE 滑动View
MotionEvent.ACTION_CANCEL 结束事件(非人为原因)
  • 特别说明:事件列
    从手指接触屏幕 至 手指离开屏幕,这个过程产生的一系列事件

注:一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件,如下图:

即当一个点击事件(MotionEvent)产生后,系统需把这个事件传递给一个具体的 View去处理。

1.2 事件分发的本质

答:将点击事件(MotionEvent)传递到某个具体的View& 处理的整个过程

即 事件传递的过程 = 分发过程。

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

答:Activity、ViewGroup、View

  • AndroidUI界面由ActivityViewGroupView及其派生类组成


注:示意图

1.4 事件分发的顺序

即 事件传递的顺序:Activity-> ViewGroup-> View

即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View


示意图

1.5 事件分发过程由哪些方法协作完成?

答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()


示意图

下文会对这3个方法进行详细介绍

1.6 总结


示意图

  • 至此,相信大家已经对 Android的事件分发有了感性的认知
  • 下面,我将详细介绍Android事件分发机制

2. 事件分发机制 源码分析

  • 请谨记:Android事件分发流程 = Activity -> ViewGroup -> View

即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View

  • 从上可知,要想充分理解Android分发机制,本质上是要理解:
    1. Activity对点击事件的分发机制
    2. ViewGroup对点击事件的分发机制
    3. View对点击事件的分发机制
  • 下面,我将通过源码,全面解析 事件分发机制

即按顺序讲解:Activity事件分发机制、ViewGroup事件分发机制、View事件分发机制

2.1 Activity的事件分发机制

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

2.1.1 源码分析

/**  * 源码分析:Activity.dispatchTouchEvent()  */     public boolean dispatchTouchEvent(MotionEvent ev) {            // 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true            if (ev.getAction() == MotionEvent.ACTION_DOWN) {                onUserInteraction();                // ->>分析1            }            // ->>分析2            if (getWindow().superDispatchTouchEvent(ev)) {                return true;                // 若getWindow().superDispatchTouchEvent(ev)的返回true                // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束                // 否则:继续往下调用Activity.onTouchEvent            }            // ->>分析4            return onTouchEvent(ev);        }/**  * 分析1:onUserInteraction()  * 作用:实现屏保功能  * 注:  *    a. 该方法为空方法  *    b. 当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法  */      public void onUserInteraction() {       }      // 回到最初的调用原处/**  * 分析2:getWindow().superDispatchTouchEvent(ev)  * 说明:  *     a. getWindow() = 获取Window类的对象  *     b. Window类是抽象类,其唯一实现类 = PhoneWindow类;即此处的Window类对象 = PhoneWindow类对象  *     c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现  */    @Override    public boolean superDispatchTouchEvent(MotionEvent event) {        return mDecor.superDispatchTouchEvent(event);        // mDecor = 顶层View(DecorView)的实例对象        // ->> 分析3    }/**  * 分析3:mDecor.superDispatchTouchEvent(event)  * 定义:属于顶层View(DecorView)  * 说明:  *     a. DecorView类是PhoneWindow类的一个内部类  *     b. DecorView继承自FrameLayout,是所有界面的父类  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup  */    public boolean superDispatchTouchEvent(MotionEvent event) {        return super.dispatchTouchEvent(event);        // 调用父类的方法 = ViewGroup的dispatchTouchEvent()        // 即 将事件传递到ViewGroup去处理,详细请看ViewGroup的事件分发机制    }    // 回到最初的调用原处/**  * 分析4:Activity.onTouchEvent()  * 定义:属于顶层View(DecorView)  * 说明:  *     a. DecorView类是PhoneWindow类的一个内部类  *     b. DecorView继承自FrameLayout,是所有界面的父类  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup  */  public boolean onTouchEvent(MotionEvent event) {        // 当一个点击事件未被Activity下任何一个View接收 / 处理时        // 应用场景:处理发生在Window边界外的触摸事件        // ->> 分析5        if (mWindow.shouldCloseOnTouch(this, event)) {            finish();            return true;        }                return false;        // 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕    }/**  * 分析5:mWindow.shouldCloseOnTouch(this, event)  */    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {    // 主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等    if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN            && isOutOfBounds(context, event) && peekDecorView() != null) {        return true;    }    return false;    // 返回true:说明事件在边界外,即 消费事件    // 返回false:未消费(默认)}// 回到分析4调用原处

2.1.2 总结

  • 当一个点击事件发生时,从Activity的事件分发开始(Activity.dispatchTouchEvent()

那么,ViewGroupdispatchTouchEvent()什么时候返回truefalse?请继续往下看ViewGroup事件的分发机制


2.2 ViewGroup事件的分发机制

从上面Activity事件分发机制可知,ViewGroup事件分发机制从dispatchTouchEvent()开始

2.2.1 源码分析

  1. Android 5.0后,ViewGroup.dispatchTouchEvent()的源码发生了变化(更加复杂),但原理相同;
  2. 本文为了让读者容易理解,故采用Android 5.0前的版本
/**  * 源码分析:ViewGroup.dispatchTouchEvent()  */     public boolean dispatchTouchEvent(MotionEvent ev) {     ... // 仅贴出关键代码        // 重点分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件            if (disallowIntercept || !onInterceptTouchEvent(ev)) {              // 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改            // 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反                    // a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部                    // b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断                    // c. 关于onInterceptTouchEvent() ->>分析1                ev.setAction(MotionEvent.ACTION_DOWN);                  final int scrolledXInt = (int) scrolledXFloat;                  final int scrolledYInt = (int) scrolledYFloat;                  final View[] children = mChildren;                  final int count = mChildrenCount;          // 重点分析2            // 通过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,从而找到当前被点击的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;                          // 条件判断的内部调用了该View的dispatchTouchEvent()                        // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)                        if (child.dispatchTouchEvent(ev))  {                         mMotionTarget = child;                          return true;                         // 调用子View的dispatchTouchEvent后是有返回值的                        // 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立                        // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出                        // 即把ViewGroup的点击事件拦截掉                                }                              }                          }                      }                  }              }              boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||                      (action == MotionEvent.ACTION_CANCEL);              if (isUpOrCancel) {                  mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;              }              final View target = mMotionTarget;          // 重点分析3        // 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)        if (target == null) {              ev.setLocation(xf, yf);              if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {                  ev.setAction(MotionEvent.ACTION_CANCEL);                  mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;              }                          return super.dispatchTouchEvent(ev);            // 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()            // 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())            // 此处需与上面区别:子View的dispatchTouchEvent()        }         ... }/**  * 分析1:ViewGroup.onInterceptTouchEvent()  * 作用:是否拦截事件  * 说明:  *     a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)  *     b. 返回false = 不拦截(默认)  */  public boolean onInterceptTouchEvent(MotionEvent ev) {          return false;  }   // 回到调用原处

2.2.2 总结

  • 结论:Android事件分发总是先传递到ViewGroup、再传递到View
  • 过程:当点击了某个控件时


示意图

2.2.3 Demo讲解

  • 布局如下

     

布局文件:activity_main.xml

<?xml version="1.0" encoding="utf-8"?>    

核心代码:MainActivity.java

/**  * ViewGroup布局(myLayout)中有2个子View = 2个按钮  */    public class MainActivity extends AppCompatActivity {    Button button1,button2;    ViewGroup myLayout;    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        button1 = (Button)findViewById(R.id.button1);        button2 = (Button)findViewById(R.id.button2);        myLayout = (LinearLayout)findViewById(R.id.my_layout);        // 1.为ViewGroup布局设置监听事件        myLayout.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Log.d("TAG", "点击了ViewGroup");            }        });        // 2. 为按钮1设置监听事件        button1.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Log.d("TAG", "点击了button1");            }        });        // 3. 为按钮2设置监听事件        button2.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Log.d("TAG", "点击了button2");            }        });    }}
  • 结果测试

    示意图

从上面的测试结果发现:

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

2.3 View事件的分发机制

从上面ViewGroup事件分发机制知道,View事件分发机制从dispatchTouchEvent()开始

2.3.1 源码分析

/**  * 源码分析:View.dispatchTouchEvent()  */  public boolean dispatchTouchEvent(MotionEvent event) {          if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&                  mOnTouchListener.onTouch(this, event)) {              return true;          }         return onTouchEvent(event);    }  // 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()  //     1. mOnTouchListener != null  //     2. (mViewFlags & ENABLED_MASK) == ENABLED  //     3. mOnTouchListener.onTouch(this, event)  // 下面对这3个条件逐个分析/**  * 条件1:mOnTouchListener != null  * 说明:mOnTouchListener变量在View.setOnTouchListener()方法里赋值  */  public void setOnTouchListener(OnTouchListener l) {     mOnTouchListener = l;      // 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)        } /**  * 条件2:(mViewFlags & ENABLED_MASK) == ENABLED  * 说明:  *     a. 该条件是判断当前点击的控件是否enable  *     b. 由于很多View默认enable,故该条件恒定为true  *//**  * 条件3:mOnTouchListener.onTouch(this, event)  * 说明:即 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)  */    button.setOnTouchListener(new OnTouchListener() {          @Override          public boolean onTouch(View v, MotionEvent event) {                   return false;          }      });    // 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束    // 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)

接下来,我们继续看:onTouchEvent(event)的源码分析

  1. 详情请看注释
  2. Android 5.0后 View.onTouchEvent()源码发生了变化(更加复杂),但原理相同;
  3. 本文为了让读者更好理解,所以采用Android 5.0前的版本
/**  * 源码分析:View.onTouchEvent()  */  public boolean onTouchEvent(MotionEvent event) {      final int viewFlags = mViewFlags;      if ((viewFlags & ENABLED_MASK) == DISABLED) {                   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)) {                  switch (event.getAction()) {                     // a. 若当前的事件 = 抬起View(主要分析)                    case MotionEvent.ACTION_UP:                          boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;                              ...// 经过种种判断,此处省略                            // 执行performClick() ->>分析1                            performClick();                              break;                      // b. 若当前的事件 = 按下View                    case MotionEvent.ACTION_DOWN:                          if (mPendingCheckForTap == null) {                              mPendingCheckForTap = new CheckForTap();                          }                          mPrivateFlags |= PREPRESSED;                          mHasPerformedLongPress = false;                          postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());                          break;                      // c. 若当前的事件 = 结束事件(非人为原因)                    case MotionEvent.ACTION_CANCEL:                          mPrivateFlags &= ~PRESSED;                          refreshDrawableState();                          removeTapCallback();                          break;                    // d. 若当前的事件 = 滑动View                    case MotionEvent.ACTION_MOVE:                          final int x = (int) event.getX();                          final int y = (int) event.getY();                                  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() {          if (mOnClickListener != null) {              playSoundEffect(SoundEffectConstants.CLICK);              mOnClickListener.onClick(this);              return true;              // 只要我们通过setOnClickListener()为控件View注册1个点击事件            // 那么就会给mOnClickListener变量赋值(即不为空)            // 则会往下回调onClick() & performClick()返回true        }          return false;      }  

2.3.2 总结

  • 每当控件被点击时:

注:onTouch()的执行 先于 onClick()

2.3.3 Demo讲解

下面我将用Demo验证上述的结论

/**  * 结论验证1:在回调onTouch()里返回false  */   // 1. 通过OnTouchListener()复写onTouch(),从而手动设置返回false   button.setOnTouchListener(new View.OnTouchListener() {            @Override            public boolean onTouch(View v, MotionEvent event) {                System.out.println("执行了onTouch(), 动作是:" + event.getAction());                           return false;            }        });    // 2. 通过 OnClickListener()为控件设置点击事件,为mOnClickListener变量赋值(即不为空),从而往下回调onClick()    button.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                System.out.println("执行了onClick()");            }        });/**  * 结论验证2:在回调onTouch()里返回true  */   // 1. 通过OnTouchListener()复写onTouch(),从而手动设置返回true   button.setOnTouchListener(new View.OnTouchListener() {            @Override            public boolean onTouch(View v, MotionEvent event) {                System.out.println("执行了onTouch(), 动作是:" + event.getAction());                           return true;            }        });    // 2. 通过 OnClickListener()为控件设置点击事件,为mOnClickListener变量赋值(即不为空)    // 但由于dispatchTouchEvent()返回true,即事件不再向下传递,故不调用onClick())    button.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                System.out.println("执行了onClick()");            }                    });
  • 测试结果

    示意图

2.4 总结

若您已经看到此处,那么恭喜你,你已经能非常熟悉掌握Android的事件分发机制了



作者:Carson_Ho
链接:https://www.jianshu.com/p/38015afcdb58
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

更多相关文章

  1. Android(安卓)RIL学习
  2. Android五大布局详解
  3. Handler工作机制【简】——学习笔记
  4. Android使用ViewFlipper做页面切换,与手势滑动切换的使用
  5. Android中自定义控件和属性
  6. Andorid第三方字体库导入
  7. RxJava使用(一)基本使用
  8. NestedScrollingParent, NestedScrollingChild 详解
  9. Android常用控件TextView的属性详解

随机推荐

  1. 在四大主件以外的类中 怎么使用Context
  2. Android(安卓)PackageManager 卸载包的方
  3. 自学android 坑2
  4. Android在子线程中更新UI(二)
  5. 源码剖析: Notification的发送
  6. Android(安卓)WebView的简单使用
  7. android PreferenceActivity使用
  8. android checkBox 的选中和取消选中
  9. Rect and RectF in Android(安卓)SDK
  10. Android(安卓)P ActivityManagerService(