写点什么

全面解析 Android 事件分发机制:一篇足矣!,Alibaba 高并发业务实战文档

用户头像
Android架构
关注
发布于: 刚刚




1. 基础认知


========

1.1 事件分发的对象是谁?

答:点击事件(Touch事件)


  • 定义


当用户触摸屏幕时(ViewViewGroup派生的控件),将产生点击事件(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)

  • 说明:


*/


@Override


public boolean superDispatchTouchEvent(MotionEvent event) {


return mDecor.superDispatchTouchEvent(event);


// mDecor = 顶层 View(DecorView)的实例对象


// ->> 分析 3


}


/**


  • 分析 3:mDecor.superDispatchTouchEvent(event)

  • 定义:属于顶层 View(DecorView)

  • 说明:


*/


public boolean superDispatchTouchEvent(MotionEvent event) {


return super.dispatchTouchEvent(event);


// 调用父类的方法 = ViewGroup 的 dispatchTouchEvent()


// 即 将事件传递到 ViewGroup 去处理,详细请看 ViewGroup 的事件分发机制


}


// 回到最初的调用原处


/**


  • 分析 4:Activity.onTouchEvent()

  • 定义:属于顶层 View(DecorView)

  • 说明:


*/


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 ev


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


ent) {


// 主要是对于处理边界外点击事件的判断:是否是 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()什么时候返回true / false?请继续往下看 ViewGroup 事件的分发机制




2.2 ViewGroup 事件的分发机制


====================


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

2.2.1 源码分析

  1. Android 5.0后,ViewGroup.dispatchTouchEvent()的源码发生了变化(更加复杂),但原理相同;


  1. 本文为了让读者容易理解,故采用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()

  • 作用:是否拦截事件

  • 说明:


*/


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"?>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"


android:id="@+id/my_layout"


android:layout_width="match_parent"


android:layout_height="match_parent"


xmlns:app="http://schemas.android.com/apk/res-auto"


android:focusableInTouchMode="true"


android:orientation="vertical">


<Button


android:id="@+id/button1"


android:layout_width="wrap_content"


android:layout_height="wrap_content"


android:text="按钮 1" />


<Button


android:id="@+id/button2"


android:layout_width="wrap_content"


android:layout_height="wrap_content"


android:text="按钮 2" />


</LinearLayout>


核心代码: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

  • 说明:


*/


/**


  • 条件 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. 详情请看注释


  1. Android 5.0View.onTouchEvent()源码发生了变化(更加复杂),但原理相同;


  1. 本文为了让读者更好理解,所以采用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;

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
全面解析Android事件分发机制:一篇足矣!,Alibaba高并发业务实战文档