天天看点

android开发艺术探索:View的事件分发机制点击事件的传递规则事件源码分析

在了解view的时间分发机制之前,我们先了解MotionEvent这个对象

MotionEvent

在手指接触屏幕后所产生的一系列事件中,典型的时间类型有如下几种:

  •  ACTION_DOWN--------手机刚接触屏幕‘
  • ACTION_MOVE----------手指在屏幕上移动
  • ACTION_UP    -----------手指从屏幕上松开的一瞬间

上述三种情况是典型的事件序列,同时通过MotionEvent对象我们可以得到点击事件发生的x和y坐标,为此系统提供了两组方法,getX/getY和getRawX/getRawY。它们的区别其实很简单,getX/getY返回的是相对于当前view左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。

点击事件的传递规则

所谓的点击事件的事件分发,其实即使对MotionEvent事件的分发过程,即当一个MotionEvent产生以后,系统需要把这个事件传递给一个具体的view,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来完成:

  • public boolean dispatchTouchEvent(MotionEvent ev) :用来进行事件的分发。如果事件能够传递给当前view,那么此方法一定会被调用,返回结果受当前view的ontouchEvent和下级view的dispatchTouchEvent方法的影响,表示是否消耗当前事件
          
  • public boolean onTouchEvent(MotionEvent event): 在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中(即action_down,action_move,actiong_up),当前view无法再次接收到事件,也就是说假如在action-down事件中没有消耗,那么action-move,action-up就不会再次触发这个方法,只会走activity中的dispatchTouchEvent和ontouchevent方法
  • public boolean onInterceptTouchEvent(MotionEvent ev):在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。      

上述三个方法的区别用下面的伪代码表示:

/**
  * 点击事件产生后
  */ 
  // 步骤1:调用dispatchTouchEvent()
  public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean consume = false; //代表 是否会消费事件

    // 步骤2:判断是否拦截事件
    if (onInterceptTouchEvent(ev)) {
      // a. 若拦截,则将该事件交给当前View进行处理
      // 即调用onTouchEvent ()方法去处理点击事件
        consume = onTouchEvent (ev) ;

    } else {

      // b. 若不拦截,则将该事件传递到下层
      // 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
      // 直到点击事件被最终处理为止
      consume = child.dispatchTouchEvent (ev) ;
    }

    // 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
    return consume;

   }
           

当一个view需要处理事件时,如果它设置了onTouchListener,那么OnTouchListener中的onTouch方法会被回调,这是事件如何处理还要看onTouch的返回值,如果返回flase,则当前view的OnTouchEvent方法会被调用,如果返回true,那么此方法就不会被调用。由此可见,给view设置的OnTouchListener,其优先级比ontouchevent要高。根据源码来看,我们平时用的onClickListener,其优先级最低。

当一个点击事件产生后,它的传递顺序过程遵循如下顺序:

Activity-----》Window----》View

即事件总是先传递给activity,activity再传给window,最后window在传给顶级的view,顶级的view接收到事件后,就会按照时间分发机制去分发事件,及先后调用上面三个方法。考虑一种情况,如果一个view的onTouchEvent返回false,那么他的父容器的OnTouchEvent将会调用,以此类推。也就是说 如果各个层级都不消耗事件的话,那么从顶级view开始分发后,一直到最上面的view,这是分发,然后经过最上面view的ontouchevent方法去处理此事件,如果ontouchevent不去处理的话,就返回到他的上层的ontouchevent方法去处理,其实就是一个U型。大家慢慢去理解。

事件源码分析

Activity对点击事件的分发过程

当一个点击操作发生时,事件最先传递给当前的activity,由activity的dispatchTouchEvent来进行事件的派发,具体的工作有activity内部的window来完成的。window会将事件传递给decorview,decorview一般就是当前界面的底层容器(即setcontentview 所设置的view的父容器),通过activity。getwindow。getdecorview()可以获得,接下来看activity的dispatchtouchevent。

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
           

首先事件开始交给activity所附属的window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有view的ontouchevent都返回了false,那么activity的ontouchevent就会被调用。其实window分发最后是到了顶层的view,中间过程就不详细说了。

viewGroup对点击事件的分发过程

点击事件达到顶级view(一般是一个viewGroup)以后,会调用viewgroup的diapatchtouchevent方法,如果viewGroup拦截事件即onInterceptTouchEvent返回true,则事件由viewGroup处理,这是如果viewGroup的ontouchlistener被设置了,则onTouch会被调用,如果onTouch返回true,就会屏蔽掉onTouchEvent,如果返回false,会接着执行OnTouchEvent方法,好了  下面我们看一下dispatchtouchevent方法的源码:

// Check for interception.
    final boolean intercepted;
    //这里检查是否拦截事件
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
           

ViewGroup在两种情况下都会判断是否要拦截当前事件

  • 事件类型为ACTION_DOWN:当前由我们触发的点击事件,也即是说ACTION_MOVE和ACTION_UP事件来时,则不触发拦截事件
  • mFirstTouchTarget != null:当ViewGroup不拦截事件并将事件交给子View的时候该不等式成立。反过来,事件被ViewGroup拦截时,该不等式不成立

然后接着看viewgroup遍历子view:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    ......

    final View[] children = mChildren;
    //遍历所有子View
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

        //判断子View是否能接收点击事件
        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);
        //事件传递到子View,下面追踪该方法
        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);
    }

    ......
}
           

ViewGroup直接使用for遍历所有子View,对子View的各种状态进行判断,最后调用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)将事件传递给子View,下面是dispatchTransformedTouchEvent()方法的部分源码

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
           

其最后就是分发给子View的dispatchTouchEvent()方法,在这里,我有一些不明白的地方,但是经过demo测试,如果嵌套了好几个viewgroup类型的view,那么就会多次执行viewgroup的dispatchTouchEvent()方法,如果最上层是一个view的话,比如textivew,那么就会走view的dispatchTouchEvent(),那么接下里就会进入view的事件分发,我自己感觉,也就是说,如果说嵌套的布局里面没有view(类似textview这一类),那么就不会走view的dispatchTouchEvent,只能走viewgroup的dispatchTouchEvent,不知道这样理解对不对,希望看到的大佬给个建议

view对点击事件的分发过程

view的dispatchTouchEvent源码分析

public boolean dispatchTouchEvent(MotionEvent event) {

    boolean result = false;
    ......
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        //这里开始判断
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    ......
    return result;
}
           

从源码判断处看出,首先会判断有没有设置mOnTouchListener,如果mOnTouchListener不为空,那么onTouchEvent就不会被调用,这里可以得到一个结论,若在View中设置了OnTouchListener,那么它的优先级是高于onTouchEvent的,这样可以更好的让我们自己setOnTouchEventListener()处理点击事件

onTouchEvent源码处理事件的具体做法部分

public boolean onTouchEvent(MotionEvent event) {
    ......
    //当View处于不可用状态下,也会消耗点击事件
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

    ......
    //对点击事件的具体处理
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    ......
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();

                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }
                    ......
                }
        }

        return true;
    }
    ......
}
           

从对点击事件的具体处理中看出,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true。在ACTION_UP事件中,会触发PerformClick()方法,如果View设置了OnClickListener,那么PerformClick()方法内部会调用它的onClick()方法

继续阅读