Android 事件分发机制

前言

说到这个事件分发机制呢,我觉得一直以来都是我的弱项,可能它太抽象了,也与我在实际项目中没怎么使用到过,也没自定义过view有着很大的关系。虽然在面试过程中事件分发是必不可少要问的,但是我也是一知半解的仅能说一点点。所以决定接下来好好学习下这部分知识,试着去尝试自定义一些View。

一、为什么要引入事件分发机制?

①android上的View是树形结构的,View可能会重叠在一起,当我们点击的地方有多个View都可以响应的时候,这个点击事件应该给谁呢。

②Activity是如何响应对其中的某个视图的触摸操作的?

③Activity是如何响应对手机的按键操作的? 

为了解决这些问题而引入了事件分发机制。

二、事件分发机制基础认知

1.MotionEvent事件的4种类型

MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间。

MotionEvent.ACTION_MOVE:手指在屏幕上移动

MotionEvent.ACTION_UP:手指离开屏幕瞬间

MotionEvent.ACTION_CANCEL:取消手势

2.onTouch是优先于onClick执行,事件传递的顺序是先经过onTouch,再传递到onClick。

3.Android中的事件onClick、onLongClick、onScroll等,都是由多个Touch事件(一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP)组成。

4.Android事件分发是从外向里分发,事件处理是从里向外。

5.事件序列:从手指接触屏幕的一瞬间起,直到手指从屏幕上松开的一瞬间所产生的一切事件。任何事件都是从Down事件开始,UP事件结束,中间有无数的MOVE事件。当一个MotionEvent产生之后,系统需要把这个事件传递给一个具体的view去处理。

6.事件分发的本质:将MotionEvent事件传递给一个具体的view进行处理。

7.事件在哪些对象之间进行传递? Activity ViewGroup View

补充:View和ViewGroup的区别?

View类是所有视图(包括ViewGroup)的根基类。

ViewGroup包含一些View或ViewGroup,用户控制子View的布局。

ViewGroup是View的子类,也就是说ViewGroup本身就是一个View,但是它可以包含子View(当然子View也可能是一个ViewGroup)。ViewGroup 处理事件分发的流程,而View本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。

8.事件分发机制的三个过程,事件分发——>事件拦截——>事件响应

(1)事件分发:将事件传递给某个具体的View,实际上就是点击事件由哪个对象发出,经过哪些对象,最终要达到哪个对象,并最终得到处理的过程。

事件分发的顺序:由外向里分发,Activity----ViewGroup----View

事件分发调用的方法:

dispatchTouchEvent:当点击事件能够传递给当前的View时,调用该方法,负责将事件分发到其子view或者当前view中。
返回值:表示是否消费了当前事件。可能是View本身的onTouchEvent方法消费,也可能是子View的dispatchTouchEvent方法中消费。返回true表示事件被消费,本次的事件终止。返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法

onTouchEvent:完成对点击事件的处理
真正对MotionEvent进行处理或者说消费的方法。在dispatchTouchEvent进行调用。
返回值:返回true表示事件被消费,本次的事件终止。返回false表示事件没有被消费,将调用父View的onTouchEvent方法

onInterceptTouchEvent:拦截点击事件(只存在于ViewGroup中)优先级高于onTouchEvent

事件拦截,当一个ViewGroup在接到MotionEvent事件序列时候,首先会调用此方法判断是否需要拦截。特别注意,这是ViewGroup特有的方法,View并没有拦截方法
返回值:是否拦截事件传递,返回true表示拦截了事件,那么事件将不再向下分发而是调用View本身的onTouchEvent方法。返回false表示不做拦截,事件将向下分发到子View的dispatchTouchEvent方法。

上面三个方法用代码来表示下他们之间的关系是:

  public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;//事件是否被消费
        if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
            consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
        }else{
            consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
        }
        return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
    }

三、Activity的事件分发机制

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

  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {//由于事件一般都是为down事件(按下)所以一般都会调用该方法
            onUserInteraction();
        }
//若getWindow().superDispatchTouchEvent(ev)返回true,则dispatchTouchEvent(MotionEvent ev)就返回true,停止事件传递
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev); //否则继续往下调用Activity.onTouchEvent
  }
 public void onUserInteraction() {
 }
 //是个空方法,当触摸点击home,back,menu键的时候回触发该方法,为了实现屏保功能
 接着分析getWindow().superDispatchTouchEvent(ev)
 
  getWinsow()=获取Window类的对象
  Window类是个抽象类,其唯一实现类=PhoneWindow类;即此处的Window类对象=PhoneWindow对象
  Window类的superDispatchTouchEvent()这个抽象方法,由子类PhoneWindow实现
  @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  
//这里的mDecor指的是顶层view(DecorView对象)

    DecorView类是PhoneWindow类的一个内部类。继承自FrameLayout,相当于间接的继承自ViewGroup。

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event); //调用父类ViewGroup的dispatchTouchEvent()
                                                //即将事件传递给ViewGroup去处理。
    }

接着分析下Activity的onTouchEvent()方法

 //当一个点击事件未被Activity下的任何一个View接收、处理时,会调用Activity的onTouchEvent方法
 public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;  //当点击事件在Window边界内时,返回false
  }

  //分析mWindow.shouldCloseOnTouch()
 public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
   //用于处理边界外点击事件的判断,是否是Down事件,event的坐标是否在边界内等。
        if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
                && isOutOfBounds(context, event) && peekDecorView() != null) {
            return true; //返回true:说明事件在边界外,即消费事件
        }
        return false;//返回false:未消费
    }

总结一下Activity的事件分发机制:当一个点击事件发生时,首先传给Activity的dispathTouchEvent()。当Activity下有View进行处理,就需要将事件进行一个传递!

那事件是如何由Activity传递给ViewGroup的呢?首先是传给Window(由于Window是个抽象类,它的唯一实现类是PhoneWindow也就是说传给了PhoneWindow的dispatchEvent),再接着又传给了DecorView的dispatchEvent(DecorView是顶层View,它继承自FrameLayout,由于FrameLayout是ViewGroup的子类,所以ViewGroup也是DecorView的间接父类)至此,就实现了Activity将事件传递给ViewGroup的过程。

当事件未被Activity下的任何一个View接收时,就需要调用Activity自身的onTouchEvent()方法。

四、ViewGroup的事件分发机制

ViewGroup的事件分发机制从dispatchTouchEvent()开始【负责将事件传递到子view中】,贴下关键的代码:

 final boolean intercepted; //是否拦截事件

//mFirstTouchTarget ,如果事件由子view处理时mFirstTouchTarget会被赋值并指向子View
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {

 //FLAG_DISALLOW_INTERCEPT是子类通过requestDisallowInterceptTouchEvent方法进行设置的
 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 
              
                if (!disallowIntercept) {
                    //调用onInterceptTouchEvent方法判断是否需要拦截
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); 
                } else {
                    intercepted = false; //不禁用
                }
            } else {
                intercepted = true; //禁用
            }

当ViewGroup决定拦截事件后,后续事件将默认交给它处理,并且并且不会再调用onInterceptTouchEvent方法来判断是否拦截。子View可以通过设置FLAG_DISALLOW_INTERCEPT标志位来不让ViewGroup拦截除ACTION_DOWN以外的事件。

当ViewGroup不拦截事件,那么事件将下发给子view进行处理。接着源码中做了一个for循环,通过倒序遍历ViewGroup下面所有的子View,然后一个一个判断点击位置是否是该子view的布局区域。如果是调用该View的dispatchTouchEvent(),即实现了点击事件从ViewGroup到子View的传递。如果是点击的是空白处,(就是没有任何View接收事件时         拦截事件需手动复写onInterceptTouchEvent)

 if (!canceled && !intercepted) {

                // If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

总结ViewGroup的事件分发机制:当点击事件传递给ViewGroup的时候,也就是调用ViewGroup的dispatchTouchEvent,然后在其内部调用onInterceptTouchEvent()如果返回true(当前事件被ViewGroup拦截)事件停止往下传递,ViewGroup自己处理事件,调用父类的dispatchTouchEvent(),最终执行自己的onTouchEvent()。  如果返回false(当前事件没有被ViewGroup拦截)默认设置,事件继续往下传递,事件传到子view,即调用子View的dispatchTouchEvent()处理。

onTouchEvent:true ViewGroup处理了当前事件,事件分发结束,逐层返回true结束。 false:ViewGroup不处理事件,事件将抛给上层的Activity的onTouchEvent处理。

五、View的事件分发机制

ViewGroup归根结底也是一个view

必须满足三个条件都为真,才会返回true。

①mOnTouchListener不为null,即调用了setOnTouchListener();

②(mViewFlags & ENABLED_MASK) == ENABLED该条件是判断当前点击的控件是否为 enable,但由于基本 View 都是 enable 的,所以这个条件基本都返回 true。

③li.mOnTouchListener.onTouch(this, event))即我们调用 setOnTouchListener() 时必须覆盖的方法 onTouch() 的返回值。终于知道onTouch()方法优先级高于onTouchEvent(event)方法是怎么来的了。

由于我们在View中常用的两个方法onTouchListener和onClick,在dispatchTouchEvent中会首先判断onTouchListener是否为空,如果不为空。会直接处理。如果为空,就调用onTouchEvent方法,在调用onClick方法。因此在View中onTouchListener的优先级i高于onClick,同时一个手势操作最多只能被其中一个处理。

                                     View事件分发流程图

子view可以通过requestDisallowInterceptTouchEvent方法干涉父View的事件分发过程(ACTION_DOWN事件除外)。

对于View,如果设置了onTouchListener,那么onTouchListener方法中的onTouch方法会被回调。onTouch方法返回true,则onTouchEvent方法不会被调用(onClick事件是在onTouchEvent中调用)所以三者的优先级是onTouch->onTouchListener->onClick

举例分析事件的分发和处理的过程。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值