Android事件分发机制浅析

这篇文章已经写得非常经典了:


图解 Android 事件分发机制

点击事件传递规则

MotionEvent

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

  • ACTION_DOWN 在屏幕按下时
  • ACTION_MOVE 在屏幕上滑动时
  • ACTION_UP 手指在屏幕抬起时

而我们常会遇到的点击事件一般为以下两种情况:

1.点击屏幕后松手,事件序列为ACTION_DOWN->ACTION_UP
2.点击屏幕后滑动再松开,事件序列为ACTION_DOWN->ACTION_MOVE->ACTION_MOVE....->ACTION_UP

事件分发的三个重要方法

  • dispatchTouchEvent(MotionEvent event)

用来进行事件的分发,如果事件能传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消费当前事件。

  • onInterceptTouchEvent(MotionEvent ev)

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

  • onTouchEvent(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;
   }

通过上述伪代码,可以大致的了解点击事件的传递规则:

点击事件产生后,首先会传递给根ViewGroup,这个时候它的dispatchTouchEvent就会被调用,若此时这个ViewGrouponIterceptTouchEvent方法返回true,则表示当前ViewGroup要拦截这个事件,接着这个事件就会交给此ViewGroup进行处理,即它的onTouchEvent方法就会被调用。
onInterceptTouchEvent方法返回false,则表示当前ViewGroup不拦截这个事件,这时当前事件就会继续传递给它的子元素,接着子元素dispatchTouchEvent方法就会被调用,如此直到事件被最终处理。

当一个View需要处理事件时,如果它设置了onTouchListener,那么onTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用,如果返回true,那么onTouchEvent方法将不会被调用。由此可见,
给View设置的onTouchListener,其优先级比onTouchEvent要高。
onTouchEvent方法中,如果当前设置的有onClickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的onClickListener,其优先级更低,即处于事件传递的尾端。

事件分发的流程

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。

考虑一种情况,如果一个view的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。(程序员世界里的能力强弱问题,难题由上而下的分配,解决不了,交给上级解决)

小结

1.同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最后以up结束;

2.正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个Vew将本该自己处理的事件通过onTouchEvent强行传递给其他View处理;

3.某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceprTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。

4.某个View一旦开始处理事件,如果它不消耗ACTON_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了。

5.如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

6.ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。

7.View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

8.view的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView 的clickable属性默认为false

9.View 的enable.属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longclickable有一个为true,那么它的onTouchEvent就返会true。

10.onclick会发生的前提是当前的View是可点击的,并且他收到了down和up的事件。

11.事件传递过程是由外到内的,理解就是事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterptTouchEvent方法可以再子元素中干预元素的事件分发过程,但是ACTION_DOWN除外。

事件传递的源码解析

Activity对点击事件的分发过程

点击事件用MotionEvent来表示,当一个点击操作发生的时候,事件最先传递给Activity,由ActivitydispatchTouchEvent来进行事件的派发,具体的工作是由Activity内部的window来完成的,window会将事件传递给decor view,decor view一般都是当前界面的底层容器(setContentView所设置的父容器),通过Activity.getWindow.getDecorView()获得。我们先从Activity的dispatchTouchEvent开始分析:

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
//事件交给Activity所依附的window,如果true那就结束了
   //superdispatchTouchEvent(ev)方法也是抽象的,必须找到window的实现类,window的实现类是phonewindow,phoneWindow将事件传递给了DecorView。
     if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
}
//phoneWindow将事件传递给了DecorView.
public boolean superDispatchTouchEvent(MotionEvent ev){
        return mDecor.superDispatchTouchEvent(ev);
    }
 
//从这里开始,事件已经传递到顶级View了,就是在Activity中通过setContentview所设置的View,另外顶级View也叫根View,顶级View一般来说都是VewGroup。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    private DecorView mDecor;
    @Override
    public final View getDecorView(){
        if(mDecor == null){
            installDesor():
        }
        return mDecor;
    }
}
顶级View对事件的分发过程

点击事件达到顶级View(一般是一个ViewGroup)以后,

(1)会调用ViewGroup的dispatchTouchEvent方法
(2)如果顶级ViewGroup拦截事件即 onIntercepTouchEvent返回true,则事件由ViewGroup处理
(3)如果ViewGroup的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。也就是说如果都提供的话,onTouch会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了 mOnClickListener,则onClick会被调用。
(4)如果顶级ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用。

到此为止,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的, 如此循环,完成整个事件的分发。

ViewGroup对点击事件的分发过程

点击事件达到顶级view(一般是一个viewGroup)以后,会调用viewgroupdiapatchtouchevent方法,如果viewGroup拦截事件即onInterceptTouchEvent返回true,则事件由viewGroup处理,这是如果viewGroupontouchlistener被设置了,则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拦截时,该不等式不成立

那么当ACTION_DOWN和ACTION_UP事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会被调用,并且同一序列中的其他事件都会默认交给它处理。

当然,这里有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT 标记位,这个标记位是通过 requestDisallowInterceptTouchEvent方法来设置的,一般用于子 View 中。

FLAG_DISALLOW_INTERCEPT 一旦设置后, ViewGroup 将只能拦截ACTION_DOWN事件。为什么说是除了 ACTION_DOWN 以外的其他事件呢?

这是因为 ViewGroup 在分发事件时,如果是ACTION_DOWN 就会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,将导致子 View 中设置的这个标记位无效。 因此,当面对 ACT1ON_DOWN 事件时,ViewGroup 总是会调用自己的 onlnterceptTouchEvent方法来询问自己是否要拦截事件,这一点从源码中也可以看出来。

在下面的代码中,ViewGroup 会在 ACTION_DOWN 事件到来时做重置状态的操作,而在resetTouchState 方法中会对 FLAG_DISALLOW_INTERCEPT 进行重置,因此子 View 调用 request- DisallowInterceptTouchEvent方法并不能影响 ViewGroupACTION DOWN事件的处理。

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
小结

ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onlnterceptTouchEvent 方法,这证实了 前面提到的第3条结论。FLAG_DISALLOW_INTERCEPT这个标志的作用是让 ViewGroup不再拦截事件,当然前提是ViewGroup 不拦截 ACTION_DOWN 事件,这证实 了 前面提到的第11条结论。

3.某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceprTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。

11.事件传递过程是由外到内的,理解就是事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterptTouchEvent方法可以在子元素中干预元素的事件分发过程,但是ACTION_DOWN除外。

那么这段分析对我们有什么价值呢?总结起来有两点:

第一点,onlnterceptTouchEvent不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递到当前的 ViewGroup

另外一点,FLAG_DISALLOW_INTERCEPT 标记位的作用给我们提供了一个思路,当面对滑动冲突时,我们可以是不是考虑用这种方法去解决问题?

接着再看当 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);
            // focus-1 
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

其最后就是分发给子View的dispatchTouchEvent()方法。
查看focus-1处的代码:

if (child == null) {
      handled = super.dispatchTouchEvent(event);
} else {
     handled = child.dispatchTouchEvent(event);
}

child 传递的不是null,因此它会直接调用子元素的 dispatchTouchEvent 方法,这样事件就交由子元素处理了,从而完成了一轮事件分发。

回到ViewGroup的dispatchTouchEvent()方法:

// 事件传递到子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();
                                // focus-1
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

如果子元素的 dispatchTouchEvent 返回 true,这时我们暂时不用考虑事件在子元素内部是怎么分发的,那么 mFirstTouchTarget 就会被赋值同时跳出 for 循环,如下所示。

newTouchTarget = addTouchTarget(child, idBitsToAssign); 
alreadyDispatchedToNewTouchTarget = true;
break;

这几行代码完成了 mFirstTouchTarget 的赋值并终止对子元素的遍历。如果子元素的 dispatchTouchEvent返回 false,ViewGroup 就会把事件分发给下一个子元素(如果还有下一个子元素的话)。

其实 mFirstTouchTarget 真正的赋值过程是在 addTouchTarget 内部完成的,从下面的 addTouchTarget 方法的内部结构可以看出,mFirstTouchTarget 其实是一种单链表结构。 mFirstTouchTarget 是否被赋值,将直接影响到 ViewGroup 对事件的拦截策略,如果 mFirstTouchTarget 为 null,那么 ViewGroup 就默认拦截接下来同一序列中所有的点击事件,这一点在前面已经做了分析。

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
          TouchTarget target = TouchTarget.obtain(child, pointerldBits);
          target.next = mFirstTouchTarget;
          mFirstTouchTarget = target;
          return target;
}

如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:
第一种是 ViewGroup 没有子元素;

第二种是子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false,这一般是因为子元素在 onTouchEvent 中返回了 false,在这两种情况下,ViewGroup 会自己处理点击事件,这里就证实了 前面提到的第4条结论,代码如下所示。

4.某个View一旦开始处理事件,如果它不消耗ACTON_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了。

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
   // No touch targets so treat this as an ordinary view.
   handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);

     }

注意上面这段代码,这里第三个参数 child 为 null,从前面的分析可以知道,它会调用 super.dispatchTouchEvent(event), 很显然,这里就转到了 View 的 dispatchTouchEvent 方法,即点击事件开始交由 View 来处理,请看下面的分析。

View对点击事件的处理

View对点击事件的处理要简单一点,注意这里的View不包含ViewGroup。先看它的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;
}

首先判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样的好处是方便在外界处理点击事件。

接着再分析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,不管它是不是DISABLE状态,这就证实了前面提到的第8、9、10条结论:

8.view的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView 的clickable属性默认为false

9.View 的enable.属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longclickable有一个为true,那么它的onTouchEvent就返会true。

10.onclick会发生的前提是当前的View是可点击的,并且他收到了down和up的事件。

在ACTION_UP事件中,会触发PerformClick()方法,如果View设置了OnClickListener,那么PerformClick()方法内部会调用它的onClick()方法。

通过setClickable和setLongClickable会分别改变View的CLICKABLE和LONG_CLICKABLE属性。setOnClickListener会自动将View的CLICKABLE设为true,setOnLongClickListener会自动将View的LONG_CLICKABLE设为true。

View的滑动冲突

常见的滑动冲突的场景

  • 场景1--外部滑动方向和内部滑动方向不一致
  • 场景2--外部滑动方向和内部滑动方向一致
  • 场景3--上面两种情况的嵌套


滑动冲突的处理规则

  • 对于场景1,根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。

如何判断滑动方向?可以通过水平和竖直方向的距离差来判断,比如竖直方向的滑动距离大就判断为竖直滑动,否则判断为水平滑动。

  • 对于场景2,根据业务规则来决定由谁拦截事件。
  • 对于场景3,根据业务规则来决定由谁拦截事件。

滑动冲突的解决方式

针对场景1:

  • 外部拦截法

点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。伪代码如下:

public boolean onInterceptTouchEvent(MotionEvent event){
    boolean intercepted = false;
    int x = (int)event.getX();
    int y = (int)event.getY();
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:{
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE:{
            if(父容器需要当前的点击事件){
                intercepted = true;
            }
            else{
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP:{
            intercepted = false;
            break;
        }
        default:
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

父容器拦截ACTION_DOWN事件时必须返回false,因为如果拦截ACTION_DOWN的话,接下来的事件都会交给父容器处理,子View则没有机会收到事件,更谈不上处理事件。在拦截ACTION_UP事件也必须返回false,因为父容器拦截ACTION_UP事件没有意义,一旦返回true(拦截),子View就不能响应ACTION_UP事件,进一步导致子View的Click事件不能响应。其实这种方法就是在ACTION_MOVE事件做判断,是否拦截ACTION_MOVE的事件,毕竟是滑动事件,最有用也就是ACTION_MOVE事件了。

  • 内部拦截法
    内部拦截稍微复杂点,就是父容器不做拦截,直接传递给子View处理事件。如果符合子View的滑动方式,就消耗这个事件,否则交回给父容器处理。
    主要利用了子View设置父容器的一个标志位FLAG_DISALLOW_INTERCEPT,是否让父容器拦截事件。子View拦截ACTION_DOWN事件时,设置让父容器不能拦截事件。在ACTION_MOVE判断是否符合自己的滑动规则,如果不符合,允许父容器拦截事件。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event){
    boolean intercepted = false;
    int x = (int)event.getX();
    int y = (int)event.getY();
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:{
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE:{
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if(父容器需要当前的点击事件){
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP:{
            break;
        }
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其它事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。父元素修改如下:

public boolean onInterceptTouchEvent(MotionEvent event){
    int action = event.getAction();
    if(action==MotionEvent.ACTION_DOWN){
        return false;
    }
    else{
        return true;
    }
}

为什么父容器不能拦截ACTION_DOWN事件呢?那是因为ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截法就无法起作用了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容