Android——彻底搞懂事件分发机制

前言

事件分发,总觉得不好理解,感觉非常麻烦,因为它涉及到的东西实在太多了,到底怎么分发与以下因素都有关:在哪个视图层级(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会处理。

参考

【透镜系列】看穿 > 触摸事件分发 >

你可能感兴趣的:(Android——彻底搞懂事件分发机制)