Android View事件分发机制

来源:互联网 发布:淘宝网店做什么好 编辑:程序博客网 时间:2024/05/21 17:18

iew的事件分发机制说白了就是点击事件的传递,也就是一个Down事件,若干个Move事件,一个Up事件构成的事件序列的传递。

 当你手指按了屏幕,点击事件就会遵循Activity->Window→View这一顺序传递。

这一传递过程有三个重要的方法,分别是:

  • boolean dispatchTouchEcent(MotionEvent ev),
  • boolean onInterceptTouchEvent(MotionEvent event),
  • boolean onTouchEvent(MotionEvent event)

 

先一个一个简单介绍下:

 

dispatchTouchEcent:

 

只要事件传递到了当前View,那么dispatchTouchEcent方法就一定会被调用。返回结果表示是否消耗当前事件。

onInterceptTouchEvent:

在dispatchTouchEcent方法内部调用此方法,用来判断是否拦截某个事件。如果当前View拦截了某个事件,那么在这同一个事件序列中,此方法不会再次被调用。返回结果表示是否拦截当前事件。

onTouchEvent:

在dispatchTouchEcent方法内调用此方法,用来处理事件。返回结果表示是否处理当前事件,如果不处理,那么在同一个事件序列里面,当前View无法再收到后续的事件。

当点击事件传递到根ViewGroup里,会执行dispatchTouchEvent,在其内部会先调用onInterceptTouchEvent询问是否拦截事件,若拦截,则执行onTouchEvent方法处理这个事件;

 

若不拦截,则执行子元素的dispatchTouchEvent,进入向下分发的传递,直到事件被处理。

 

在处理一个事件的时候,是有优先级的,如果设置了OnTouchListener,会先执行其内部的onTouch方法,这时若onTouch方法返回true,那么表示事件被处理了,不会向下传递了;

 

如果返回了false,那么事件会继续传递给onTouchEvent方法处理,在onTouchEvent方法中如果当前设置了OnClickListener,那么就会调用其onClick方法。

所以其优先级为:OnTouchListen>onTouchEvent>OnClickListen。

 

这里有一种情况,如果一个View的onTouchEvent返回了false,那么它父容器的onTouchEvent方法将会被调用。

 

既然如此,在开头我们说过事件的传递顺序是Activity->Window→View,所以如果所有的元素都返回了false,那么最后事件就会再次传递到Activity里,由Activity的onTouchEvent方法来处理。

这三种方法的对比:

简单举例:

 展开源码


单击button时,会输出log如下:

可以看到,不管是DOWN,MOVE,UP都会按照下面的顺序执行:

1、dispatchTouchEvent

2、 setOnTouchListener的onTouch

3、onTouchEvent

View的dispatchTouchEvent的源码:

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    // If the event should be handled by accessibility focus first.
    if (event.isTargetAccessibilityFocus()) {
        // We don't have focus or no virtual descendant has it, do not handle the event.
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        // We have focus and got the event, then use normal event dispatch.
        event.setTargetAccessibilityFocus(false);
    }
 
    boolean result = false;
 
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }
 
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();
    }
 
    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;
        }
    }
 
    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
 
    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }
 
    return result;
}

从源码可以看出:

首先判断mOnTouchListener不为null,并且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接return true ; 也就是下面的onTouchEvent(event)不会被执行了;


View的onTouchEvent的源码:

 展开源码

MotionEvent.ACTION_UP 有一个重要的方法

 展开源码

看到了平时使用最多的监听器OnClickListener,有木有很激动!可以看到,只要mOnClickListener不是null,就会去调用它的onClick方法;同时根据事件响应先后顺序,可以知道OnTouchListener会在OnClickListener之前响应,如果 onTouchEvent 直接返回true时,OnClickListener就不会执行了。

 

ViewGroup的事件分发机制

先看一个例子:

 展开源码

对应activity的布局文件是:

 展开源码

点击按钮时,日志如下:

MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent

可以看出,在View上触发事件,最先捕获到事件的为View所在的ViewGroup,然后才会到View自身

让我们从源码一看究竟

ViewGroup - dispatchTouchEvent - ACTION_DOWN


ViewGroup的事件拦截

复写ViewGroup的onInterceptTouchEvent方法:

 折叠源码
@Override 
    public boolean onInterceptTouchEvent(MotionEvent ev) 
    
        int action = ev.getAction(); 
        switch (action) 
        
        case MotionEvent.ACTION_DOWN: 
            //如果你觉得需要拦截 
            return true ;  
         case MotionEvent.ACTION_MOVE: 
             //如果你觉得需要拦截 
             return true ;  
         case MotionEvent.ACTION_UP: 
             //如果你觉得需要拦截 
             return true ;  
        
            
         return false
     }

如何不被拦截:

requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:

 折叠源码
@Override 
    public boolean dispatchTouchEvent(MotionEvent event) 
    
        getParent().requestDisallowInterceptTouchEvent(true);   
        int action = event.getAction(); 
   
        switch (action) 
        
        case MotionEvent.ACTION_DOWN: 
             Log.e(TAG, "dispatchTouchEvent ACTION_DOWN"); 
             break
         case MotionEvent.ACTION_MOVE: 
             Log.e(TAG, "dispatchTouchEvent ACTION_MOVE"); 
             break
         case MotionEvent.ACTION_UP: 
            Log.e(TAG, "dispatchTouchEvent ACTION_UP"); 
            break
   
        default
            break
        
         return super.dispatchTouchEvent(event); 


总结:

1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;

2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法

3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true);  阻止ViewGroup对其MOVE或者UP事件进行拦截;


应用场景:

   滑动冲突的处理

滑动冲突的基本形式分为两种,其他复杂的滑动冲突都可以拆成这两种基本形式:

1:外部滑动方向与内部方向不一致。

2:外部方向与内部方向一致。


滑动冲突的拦截方法有两种:

一种是让事件都经过父容器的拦截处理,如果父容器需要则拦截,如果不需要则不拦截,称为外部拦截法

在这里,首先down事件父容器必须返回false ,因为若是返回true,也就是拦截了down事件,那么后续的move和up事件就都会传递给父容器,子元素就没有机会处理事件了。

其次是up事件也返回了false,一是因为up事件对父容器没什么意义,其次是因为若事件是子元素处理的,却没有收到up事件会让子元素的onClick事件无法触发。

另一种是父容器不拦截任何事件,将所有事件传递给子元素,如果子元素需要则消耗掉,如果不需要则通过requestDisallowInterceptTouchEvent方法交给父容器处理,称为内部拦截法

原创粉丝点击