Android——彻底搞懂事件分发机制
2019-11-29 本文已影响0人
softSnowzzz
前言
事件分发,总觉得不好理解,感觉非常麻烦,因为它涉及到的东西实在太多了,到底怎么分发与以下因素都有关:在哪个视图层级(Activity、ViewGroup、View),什么事件类型(DOWN、MOVE、UP、CANCEL),在哪个回调方法(dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent),回调方法返回不同的返回值(true、false)。这些因素都会影响事件的分发,如果单纯死记硬背,就算当时背过了,过一段时间也就忘了,所以,真正理解事件分发,才是搞懂事件分发的关键。要理解事件分发,那我们就需要弄清楚,为什么需要事件分发,它具体又是做了什么呢?
思考
- 事件是什么?
我们从手指接触屏幕那一刻起,到手指抬起,离开屏幕,在这个过程中,可能还伴随着在屏幕上滑动,这就是一个完整的事件序列,也就是说一个完整的事件序列就包括:按下、滑动或不滑动、抬起。也就对应着DOWN、0个或多个MOVE、UP,这是我们最直观感受得出的结论。 - 事件分发机制是为了解决什么问题?
其实仔细想想,事件分发,其实就是事件的传递与处理。事件,由我们人为产生,产生了,系统就需要给我们反馈,这也就是事件传递与处理的过程。 - 事件分发的传递方向?
我们触摸屏幕,得到触摸事件,而我们看到的界面是Activity,也就是说,最开始得到触摸事件的是Activity,View又会被层层ViewGroup包裹,那么事件的传递方向就应该是Activity->ViewGroup->View。
事件分发实现
我们现在知道,事件是Activity->ViewGroup->View这样层层传递的,每个层级都应该有处理事件的能力,显然,我们需要两个方法,一个用来传递事件,一个用来处理时事件。我们分别定义三个类:MActivity,MViewGroup,MView 来分别模拟Activity,ViewGroup和View,一步一步实现事件分发。
-
需求一:将事件能够从Activity传递到最里层View,最里层View能够处理事件。
-
MActivity
public class MActivity {
private MViewGroup mChild;
public MActivity(MViewGroup child) {
mChild = child;
}
//用来传递事件
protected boolean dispatchTouchEvent(MotionEvent event) {
mChild.dispatchTouchEvent(event)
}
//用来处理事件
protected boolean onTouchEvent(MotionEvent event) {
return false;
}
}
-
MViewGroup
public class MViewGroup extends MView {
private MView mChild;
public MViewGroup(MView child) {
mChild = child;
}
//用来传递事件
protected boolean dispatchTouchEvent(MotionEvent event) {
return mChild.dispatchTouchEvent(event)
}
//用来处理事件
protected boolean onTouchEvent(MotionEvent event) {
return false;
}
}
-
MView
public class MView {
//用来传递事件
protected boolean dispatchTouchEvent(MotionEvent event) {
return onTouchEvent(event);
}
//用来处理事件
protected boolean onTouchEvent(MotionEvent event) {
return true;
}
}
- 现在看起来,暂时Activity和ViewGroup功能是一样的,因为我们只是想把事件传递到最里层View,并让这个View处理事件,Activity和ViewGroup只需要传递事件。
- 我们定义了两个方法dispatchTouchEvent(MotionEvent event)和onTouchEvent(MotionEvent event),分别用来传递和处理事件。
思考
这个需求很简单,我们只是需要最里层View处理事件,假如,不光最里层View可以处理事件,包裹它的ViewGroup也能处理事件,这应该怎么办呢?比如,ViewGroup是可以点击的。
- 首先,我们应该让这个事件可以传递到View,因为如果View也是可以点击,需要响应点击事件,这个事件不传给它,它就没法响应了。
- 再假设一种情况,子View不需要处理事件,假如是类似今日头条这种新闻列表,每个item可能有标题,图片多个子View,但他们不需要处理事件,只要点击item就进入新闻详情页面。也就是说子View有处理和不处理事件两种情况。这时候我们需要根据子View处理事件方法不同的返回值做不同处理。
- 总结:事件应该先传给View,View不处理,就交给ViewGroup处理,但假如ViewGroup也不处理,那就交给Activity处理,再想一下,假如同一个事件序列的某一个事件,View和ViewGroup都不处理,那么其之后的事件也直接交给Activity就可以,因为View和ViewGroup都不处理,那么之后的事件传给它们也没意义,毕竟事件已经不完整了。
MActivity
public class MActivity {
private MViewGroup mChild;
private boolean isChildHandled=false;
private boolean isSelfHandled=false;
public MActivity(MViewGroup child) {
mChild = child;
}
protected boolean dispatchTouchEvent(MotionEvent event) {
boolean handled=false;
if(MotionEvent.ACTION_DOWN==event.getAction()){
clearStatus();
handled=mChild.dispatchTouchEvent(event);
if(handled){
isChildHandled=true;
}else {
handled=onTouchEvent(event);
if(handled){
isSelfHandled=true;
}
}
}else {
if(isSelfHandled){
handled=onTouchEvent(event);
} else if(isChildHandled){
handled = mChild.dispatchTouchEvent(event);
}
if(!handled){
handled=onTouchEvent(event);
}
if(MotionEvent.ACTION_UP==event.getAction()){
clearStatus();
}
}
return handled;
}
private void clearStatus() {
isChildHandled=false;
isSelfHandled=false;
}
protected boolean onTouchEvent(MotionEvent event) {
return true;
}
}
MViewGroup
public class MViewGroup extends MView {
private MView mChild;
private boolean isChildHandled=false;
private boolean isSelfHandled=false;
public MViewGroup(MView view) {
mChild=view;
}
@Override
protected boolean dispatchTouchEvent(MotionEvent event) {
boolean handled=false;
if(MotionEvent.ACTION_DOWN==event.getAction()){
clearStatus();
handled=mChild.dispatchTouchEvent(event);
if(handled){
isChildHandled=true;
}else {
handled=onTouchEvent(event);
if(handled){
isSelfHandled=true;
}
}
}else {
if(isSelfHandled){
handled=onTouchEvent(event);
} else if(isChildHandled){
handled = mChild.dispatchTouchEvent(event);
}
if(MotionEvent.ACTION_UP==event.getAction()){
clearStatus();
}
}
return handled;
}
private void clearStatus() {
isChildHandled=false;
isSelfHandled=false;
}
@Override
protected boolean onTouchEvent(MotionEvent event) {
return true;
}
}
MView
public class MView {
protected boolean dispatchTouchEvent(MotionEvent event){
return onTouchEvent(event);
}
protected boolean onTouchEvent(MotionEvent event){
return true;
}
}
总结
- 事件要先传给View,View不处理,后续事件就不传给它了,交给ViewGroup处理,假如ViewGroup也不处理,就交给Activity处理。
- 很显然,这让我们想到了责任链模式,实际上源码也确实是责任链模式。
思考
- 其实我们这么写,会有问题, 假如还是今日头条这个新闻列表,当我们把手放在子View上,子View是个可点击View,这样就会导致子View会响应点击事件,但实际上,我们把手放在子View上,这时候并不能知道是需要响应子View的点击事件还是ViewGroup的滑动事件,需要看接下来的动作才知道,但是按照我们这种写法,子View一看事件来了,就会处理,那列表就永远无法滑动了,那怎么办,其实很明显,单靠DOWN事件,没法判断接下来的动作,得等等看,假如DOWN之后,很多时间内UP,那就响应子View点击事件,假如DOWN之后,一段时间,一直都是MOVE事件,这时候响应ViewGroup的滑动,所以,我们不能单凭DOWN事件,决定事件的处理,我们需要在事件的传递过程中,ViewGroup可以进行干涉,随时可以处理事件,但这样又会有一个新的问题,假如子View在press状态有一个背景高亮的效果,接收到DOWN事件时,子View背景高亮,这时候没问题,但假如ViewGroup拦截了接下来的MOVE事件,那么之后的事件,都不会传递到子View,这就会导致子View背景一直高亮,要解决这个问题,其实也不麻烦,无非就是在ViewGroup拦截的时候,通知一下子View,让子View自己处理一下就可以,那么如何通知子View?肯定不能发送MOVE,UP事件,那就造一个新事件,叫它CANCEL吧,这样看起来就很好了,可是真的很好了吗?想象一下,子View接收到DOWN事件后,开开心心处理着,突然后面的事件都不给它了,就给了它一个CANCEL事件,如果仅仅是涉及到子View UI方面还好,万一子View已经开始和用户交互了,这时候就不太好,所以,子View需要可以告诉爸爸,哪些时候不能拦截自己的事件,这样确实完美了,接下来,就是如何用代码实现了。
MViewParent 新增的一个接口,子View是否不让ViewGroup拦截事件,单独抽出来逻辑比较清晰。
public interface MViewParent {
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
}
MAcitivity 基本和之前一样,只是增加了对CANCEL事件的处理,因为它不需要拦截事件
public class MActivity {
private MViewGroup mChild;
private boolean isChildHandled=false;
private boolean isSelfHandled=false;
public MActivity(MViewGroup child) {
mChild = child;
}
protected boolean dispatchTouchEvent(MotionEvent event) {
boolean handled=false;
if(MotionEvent.ACTION_DOWN==event.getAction()){
clearStatus();
handled=mChild.dispatchTouchEvent(event);
if(handled){
isChildHandled=true;
}else {
handled=onTouchEvent(event);
if(handled){
isSelfHandled=true;
}
}
}else {
if(isSelfHandled){
handled=onTouchEvent(event);
} else if(isChildHandled){
handled = mChild.dispatchTouchEvent(event);
}
if(!handled){
handled=onTouchEvent(event);
}
//cancel事件也要重置标志位。
if (ev.actionMasked == MotionEvent.ACTION_UP
|| ev.actionMasked == MotionEvent.ACTION_CANCEL) {
clearStatus()
}
}
return handled;
}
private void clearStatus() {
isChildHandled=false;
isSelfHandled=false;
}
protected boolean onTouchEvent(MotionEvent event) {
return true;
}
}
MViewGroup
public class MViewGroup extends MView implements MViewParent{
private MView mChild;
private boolean isChildHandled=false;//是否是子View处理事件
private boolean isSelfHandled=false;//是否是自己处理事件
private boolean isDisallowIntercept=false;//子View是否不允许拦截事件
public MViewGroup(MView child) {
mChild=child;
mChild.mParent=this;
}
@Override
protected boolean dispatchTouchEvent(MotionEvent event) {
boolean handled=false;
if(MotionEvent.ACTION_DOWN==event.getAction()){
clearStatus();
//新增ViewGroup是否拦截和子View是否允许ViewGroup拦截判断
if(!isDisallowIntercept&&onInterceptTouchEvent(event)){
isSelfHandled=true;
handled=onTouchEvent(event);
}else {
handled=mChild.dispatchTouchEvent(event);
if(handled){
isChildHandled=true;
}else {
handled=onTouchEvent(event);
if(handled){
isSelfHandled=true;
}
}
}
}else {
if(isSelfHandled){
handled=onTouchEvent(event);
} else if(isChildHandled){
if(!isDisallowIntercept&&onInterceptTouchEvent(event)) {
isSelfHandled=true;
MotionEvent cancel=MotionEvent.obtain(event);
cancel.setAction(MotionEvent.ACTION_CANCEL);
mChild.dispatchTouchEvent(cancel);
cancel.recycle();
}else {
mChild.dispatchTouchEvent(event);
}
}
if(MotionEvent.ACTION_UP==event.getAction()||MotionEvent.ACTION_UP==event.getAction()){
clearStatus();
}
}
return handled;
}
private void clearStatus() {
isChildHandled=false;
isSelfHandled=false;
isDisallowIntercept=false;
}
protected boolean onInterceptTouchEvent(MotionEvent event){
return false;
}
@Override
protected boolean onTouchEvent(MotionEvent event) {
return true;
}
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept){
this.isDisallowIntercept=disallowIntercept;
if(mParent!=null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
}
MView
public class MView {
protected MViewParent mParent;
protected boolean dispatchTouchEvent(MotionEvent event){
return onTouchEvent(event);
}
protected boolean onTouchEvent(MotionEvent event){
return true;
}
}
总结
- 事件分发大体就是这么一个过程,只不过省略了onTouchListener等各种Listener的情况。
- ViewGroup一旦决定拦截某个事件,那么同一个事件序列后面的事件也应该交给它来处理,而不用再询问是否拦截事件。
- View处理了DOWN事件,但之后的事件被拦截,需要同时发送个CANCEL事件给View,同时不需要把事件交给ViewGroup自己的onTouchEvent()处理,这样可以保证,一个事件最终只有一个View处理。
- View和ViewGroup都不处理某事件,Activity会处理这个事件,同一事件序列后面的事件也不会传递给View和ViewGroup,Activity会处理。