View的点击事件机制源码分析

2,300 阅读12分钟

01.Android中事件分发顺序

Android中事件分发顺序: Activity(Window) -> ViewGroup -> View

其中: super:调用父类方法 true:消费事件,即事件不继续往下传递 false:不消费事件,事件继续往下传递/交由给父控件onTouchEvent()

处理充分理解Android分发机制,本质上是要理解:Activity对点击事件的分发机制ViewGroup对点击事件的分发机制View对点击事件的分发机制

02.Activity的事件分发机制

2.1 源码分析 当一个点击事件发生时,事件最先传到Activity的dispatchTouchEvent()进行事件分发 具体是由Activity的Window来完成,我们来看下Activity的dispatchTouchEvent()的源码

public boolean dispatchTouchEvent(MotionEvent ev) {
//第一步
//一般事件列开始都是DOWN,所以这里基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    //第二步
    onUserInteraction();
}
//第三步
if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
}
return onTouchEvent(ev);
}

第一步 一般事件列开始都是DOWN(按下按钮),所以这里返回true,执行onUserInteraction() 第二步先来看下onUserInteraction()源码

public void onUserInteraction() { 
}

从源码可以看出:该方法为空方法从注释得知:当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法 所以onUserInteraction()主要用于屏保

第三步 Window类是抽象类,且PhoneWindow是Window类的唯一实现类superDispatchTouchEvent(ev)是抽象方法通过PhoneWindow类中看一下superDispatchTouchEvent()的作用

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
     return mDecor.superDispatchTouchEvent(event);
    //mDecor是DecorView的实例
     //DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类
}

接下来我们看mDecor.superDispatchTouchEvent(event):

public boolean superDispatchTouchEvent(MotionEvent event) {
  return super.dispatchTouchEvent(event);
//DecorView继承自FrameLayout
//那么它的父类就是ViewGroup
而super.dispatchTouchEvent(event)方法,其实就应该是ViewGroup的dispatchTouchEvent()
}

得出结果:执行getWindow().superDispatchTouchEvent(ev)实际上是执行了ViewGroup.dispatchTouchEvent(event)这样事件就从 Activity 传递到了 ViewGroup

2.2 点击事件调用顺序

当一个点击事件发生时,调用顺序如下

1.事件最先传到Activity的dispatchTouchEvent()进行事件分发

2.调用Window类实现类PhoneWindow的superDispatchTouchEvent()

3.调用DecorView的superDispatchTouchEvent()

4.最终调用DecorView父类的dispatchTouchEvent(),即ViewGroup的dispatchTouchEvent()

2.3 得出结论:当一个点击事件发生时,事件最先传到Activity的dispatchTouchEvent()进行事件分发,最终是调用了ViewGroup的dispatchTouchEvent()方法,这样事件就从 Activity 传递到了 ViewGroup

03.ViewGroup事件的分发机制 3.1 看一下这个案例 布局如下:

结果测试

只点击Button,发现执行顺序:btn1,btn2

再点击空白处,发现执行顺序:btn1,btn2,viewGroup

从上面的测试结果发现:

当点击Button时,执行Button的onClick(),但ViewGroupLayout注册的onTouch()不会执行

只有点击空白区域时才会执行ViewGroupLayout的onTouch();

结论:Button的onClick()将事件消费掉了,因此事件不会再继续向下传递。

3.2 源码分析

ViewGroup的dispatchTouchEvent()源码分析,该方法比较复杂,截取几个重要的逻辑片段进行介绍,来解析整个分发流程。

// 发生ACTION_DOWN事件或者已经发生过ACTION_DOWN,并且将mFirstTouchTarget赋值,才进入此区域,主要功能是拦截器
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
     //disallowIntercept:是否禁用事件拦截的功能(默认是false),即不禁用
    //可以在子View通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改,不让该View拦截事件
     final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    //默认情况下会进入该方法
    if (!disallowIntercept) {
    //调用拦截方法
    intercepted = onInterceptTouchEvent(ev); 
    ev.setAction(action);
     } else {
      intercepted = false;
    }
} else {
// 当没有触摸targets,且不是down事件时,开始持续拦截触摸。
intercepted = true;
}

这一段的内容主要是为判断是否拦截。如果当前事件的MotionEvent.ACTION_DOWN,则进入判断,调用ViewGroup onInterceptTouchEvent()方法的值,判断是否拦截。如果mFirstTouchTarget != null,即已经发生过MotionEvent.ACTION_DOWN,并且该事件已经有ViewGroup的子View进行处理了,那么也进入判断,调用ViewGroup onInterceptTouchEvent()方法的值,判断是否拦截。如果不是以上两种情况,即已经是MOVE或UP事件了,并且之前的事件没有对象进行处理,则设置成true,开始拦截接下来的所有事件。这也就解释了如果子View的onTouchEvent()方法返回false,那么接下来的一些列事件都不会交给他处理。如果VieGroup的onInterceptTouchEvent()第一次执行为true,则mFirstTouchTarget = null,则也会使得接下来不会调用onInterceptTouchEvent(),直接将拦截设置为true。

当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View或ViewGroup进行处理。

 /* 从最底层的父视图开始遍历,
** 找寻newTouchTarget,即上面的mFirstTouchTarget
** 如果已经存在找寻newTouchTarget,说明正在接收触摸事件,则跳出循环。
 */
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
 ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
 ? children[childIndex] : preorderedList.get(childIndex);

 // 如果当前视图无法获取用户焦点,则跳过本次循环
 if (childWithAccessibilityFocus != null) {
     if (childWithAccessibilityFocus != child) {
     continue;
  }
  childWithAccessibilityFocus = null;
  i = childrenCount - 1;
 }
  //如果view不可见,或者触摸的坐标点不在view的范围内,则跳过本次循环
  if (!canViewReceivePointerEvents(child) 
     || !isTransformedTouchPointInView(x, y, child, null)) {
   ev.setTargetAccessibilityFocus(false);
 continue;
 }

newTouchTarget = getTouchTarget(child);
 // 已经开始接收触摸事件,并退出整个循环。
if (newTouchTarget != null) {
     newTouchTarget.pointerIdBits |= idBitsToAssign;
    break;
 }

//重置取消或抬起标志位
//如果触摸位置在child的区域内,则把事件分发给子View或ViewGroup
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    // 获取TouchDown的时间点
    mLastTouchDownTime = ev.getDownTime();
    // 获取TouchDown的Index
    if (preorderedList != null) {
       for (int j = 0; j < childrenCount; j++) {
           if (children[childIndex] == mChildren[j]) {
                mLastTouchDownIndex = j;
                break;
            }
       }
     } else {
             mLastTouchDownIndex = childIndex;
            }

  //获取TouchDown的x,y坐标
  mLastTouchDownX = ev.getX();
  mLastTouchDownY = ev.getY();
  //添加TouchTarget,则mFirstTouchTarget != null。
  newTouchTarget = addTouchTarget(child, idBitsToAssign);
  //表示以及分发给NewTouchTarget
  alreadyDispatchedToNewTouchTarget = true;
  break;
}

dispatchTransformedTouchEvent()方法实际就是调用子元素的dispatchTouchEvent()方法。其中dispatchTransformedTouchEvent()方法的重要逻辑如下:

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

由于其中传递的child不为空,所以就会调用子元素的dispatchTouchEvent()。如果子元素的dispatchTouchEvent()方法返回true,那么mFirstTouchTarget就会被赋值,同时跳出for循环。

//添加TouchTarget,则mFirstTouchTarget != null。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//表示以及分发给NewTouchTarget
 alreadyDispatchedToNewTouchTarget = true;

其中在addTouchTarget(child, idBitsToAssign);内部完成mFirstTouchTarget被赋值。如果mFirstTouchTarget为空,将会让ViewGroup默认拦截所有操作。如果遍历所有子View或ViewGroup,都没有消费事件。ViewGroup会自己处理事件。

3.3 得出结论

Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View在ViewGroup中通过onInterceptTouchEvent()对事件传递进行拦截

1.onInterceptTouchEvent方法返回true代表拦截事件,即不允许事件继续向子View传递;

2.返回false代表不拦截事件,即允许事件继续向子View传递;(默认返回false)

3.子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。

04.View事件的分发机制

4.1 源码分析View中dispatchTouchEvent()的源码分析

第一个条件:mOnTouchListener != null;
第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED;
第三个条件:mOnTouchListener.onTouch(this, event);

下面,我们来看看下这三个判断条件: 第一个条件:mOnTouchListener!= null

//mOnTouchListener是在View类下setOnTouchListener方法里赋值的
public void setOnTouchListener(OnTouchListener l) { 
//即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
mOnTouchListener = l;  
}

第二个条件:(mViewFlags & ENABLED_MASK)==ENABLED该条件是判断当前点击的控件是否enable由于很多View默认是enable的,因此该条件恒定为true 第三个条件:mOnTouchListener.onTouch(this, event) 回调控件注册Touch事件时的onTouch方法

接下来,我们继续看:onTouchEvent(event)的源码分析

public boolean onTouchEvent(MotionEvent event) {  
final int viewFlags = mViewFlags;  
if ((viewFlags & ENABLED_MASK) == DISABLED) {  
    // 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));  
}  
if (mTouchDelegate != null) {  
    if (mTouchDelegate.onTouchEvent(event)) {  
        return true;  
    }  
}  
 //如果该控件是可以点击的就会进入到下两行的switch判断中去;

if (((viewFlags & CLICKABLE) == CLICKABLE ||  
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
//如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。

    switch (event.getAction()) {  
        case MotionEvent.ACTION_UP:  
            boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
           // 在经过种种判断之后,会执行到关注点1的performClick()方法。
           //请往下看关注点1
            if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
                // take focus if we don't have it already and we should in  
                // touch mode.  
                boolean focusTaken = false;  
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                    focusTaken = requestFocus();  
                }  
                if (!mHasPerformedLongPress) {  
                    // 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)) {  
        //关注点1
        //请往下看performClick()的源码分析
                            performClick();  
                        }  
                    }  
                }  
                if (mUnsetPressedState == null) {  
                    mUnsetPressedState = new UnsetPressedState();  
                }  
                if (prepressed) {  
                    mPrivateFlags |= PRESSED;  
                    refreshDrawableState();  
                    postDelayed(mUnsetPressedState,  
                            ViewConfiguration.getPressedStateDuration());  
                } else if (!post(mUnsetPressedState)) {  
                    // If the post failed, unpress right now  
                    mUnsetPressedState.run();  
                }  
                removeTapCallback();  
            }  
            break;  
        case MotionEvent.ACTION_DOWN:  
            if (mPendingCheckForTap == null) {  
                mPendingCheckForTap = new CheckForTap();  
            }  
            mPrivateFlags |= PREPRESSED;  
            mHasPerformedLongPress = false;  
            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
            break;  
        case MotionEvent.ACTION_CANCEL:  
            mPrivateFlags &= ~PRESSED;  
            refreshDrawableState();  
            removeTapCallback();  
            break;  
        case MotionEvent.ACTION_MOVE:  
            final int x = (int) event.getX();  
            final int y = (int) event.getY();  
            // Be lenient about moving outside of buttons  
            int slop = mTouchSlop;  
            if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                    (y < 0 - slop) || (y >= getHeight() + slop)) {  
                // Outside button  
                removeTapCallback();  
                if ((mPrivateFlags & PRESSED) != 0) {  
                    // Remove any future long press/tap checks  
                    removeLongPressCallback();  
                    // Need to switch from pressed to not pressed  
                    mPrivateFlags &= ~PRESSED;  
                    refreshDrawableState();  
                }  
            }  
            break;  
    }  
//如果该控件是可以点击的,就一定会返回true
    return true;  
    }  
//如果该控件是不可以点击的,就一定会返回false
    return false;  
}

关注点1:performClick()的源码分析

public boolean performClick() {  
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  

if (mOnClickListener != null) {  
    playSoundEffect(SoundEffectConstants.CLICK);  
    mOnClickListener.onClick(this);  
    return true;  
}  
return false;  

} 只要mOnClickListener不为null,就会去调用onClick方法;那么,mOnClickListener又是在哪里赋值的呢?请继续看:

public void setOnClickListener(OnClickListener l) {  
if (!isClickable()) {  
    setClickable(true);  
}  
mOnClickListener = l;  
}

当我们通过调用setOnClickListener方法来给控件注册一个点击事件时,就会给mOnClickListener赋值(不为空),即会回调onClick()。

4.2 得出结论

1.onTouch()的执行高于onClick()

2.每当控件被点击时:

如果在回调onTouch()里返回false,就会让dispatchTouchEvent方法返回false,那么就会执行onTouchEvent();如果回调了setOnClickListener()来给控件注册点击事件的话,最后会在performClick()方法里回调onClick()。 onTouch()返回false(该事件没被onTouch()消费掉) = 执行onTouchEvent() = 执行OnClick() 如果在回调onTouch()里返回true,就会让dispatchTouchEvent方法返回true,那么将不会执行onTouchEvent(),即onClick()也不会执行; onTouch()返回true(该事件被onTouch()消费掉) = dispatchTouchEvent()返回true(不会再继续向下传递) = 不会执行onTouchEvent() = 不会执行OnClick()

4.3 验证结论

在回调onTouch()里返回true

TextView textView = findViewById(R.id.tv_13);
//设置OnTouchListener()
textView.setOnTouchListener(new View.OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
    Log.d("小杨逗比","执行了onTouch(), 动作是:" + event.getAction());
    return true;
}
});
//设置OnClickListener
textView.setOnClickListener(new View.OnClickListener() {
  @Override
public void onClick(View v) {
    Log.d("小杨逗比","执行了onClick()");
}
});

打印日志如下所示 注意action为0是ACTION_DOWN,为2是ACTION_MOVE,为1是ACTION_UP。

2019-04-04 13:37:58.301 13616-13616/org.yczbj.ycrefreshview D/小杨逗比: 执行了onTouch(), 动作是:0
2019-04-04 13:37:58.315 13616-13616/org.yczbj.ycrefreshview D/小杨逗比: 执行了onTouch(), 动作是:2
2019-04-04 13:37:58.405 13616-13616/org.yczbj.ycrefreshview D/小杨逗比: 执行了onTouch(), 动作是:2
2019-04-04 13:37:58.408 13616-13616/org.yczbj.ycrefreshview D/小杨逗比: 执行了onTouch(), 动作是:1

在回调onTouch()里返回false 打印结果如下所示

2019-04-04 13:41:26.961 14006-14006/org.yczbj.ycrefreshview D/小杨逗比: 执行了onTouch(), 动作是:0
2019-04-04 13:41:26.978 14006-14006/org.yczbj.ycrefreshview D/小杨逗比: 执行了onTouch(), 动作是:2
2019-04-04 13:41:27.072 14006-14006/org.yczbj.ycrefreshview D/小杨逗比: 执行了onTouch(), 动作是:2
2019-04-04 13:41:27.074 14006-14006/org.yczbj.ycrefreshview D/小杨逗比: 执行了onTouch(), 动作是:1
2019-04-04 13:41:27.076 14006-14006/org.yczbj.ycrefreshview D/小杨逗比: 执行了onClick()

总结:onTouch()返回true就认为该事件被onTouch()消费掉,因而不会再继续向下传递,即不会执行OnClick()。

05.思考一下

5.1 onTouch()和onTouchEvent()的区别 这两个方法都是在View的dispatchTouchEvent中调用,但onTouch优先于onTouchEvent执行。如果在onTouch方法中返回true将事件消费掉,onTouchEvent()将不会再执行。

特别注意:请看下面代码

//&&为短路与,即如果前面条件为false,将不再往下执行
//所以,onTouch能够得到执行需要两个前提条件:
//1. mOnTouchListener的值不能为空
//2. 当前点击的控件必须是enable的。
mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
     mOnTouchListener.onTouch(this, event)

因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

5.2 Touch事件的后续事件(MOVE、UP)层级传递

如果给控件注册了Touch事件,每次点击都会触发一系列action事件(ACTION_DOWN,ACTION_MOVE,ACTION_UP等),当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到后一个事件(ACTION_MOVE和ACTION_UP)即如果在执行ACTION_DOWN时返回false,后面一系列的ACTION_MOVE和ACTION_UP事件都不会执行

从上面对事件分发机制分析知:

dispatchTouchEvent()和 onTouchEvent()消费事件、终结事件传递(返回true)而onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用,对后续的ACTION_MOVE和ACTION_UP事件接收起到非常大的作用 请记住:接收了ACTION_DOWN事件的函数不一定能收到后续事件(ACTION_MOVE、ACTION_UP)

这里给出ACTION_MOVE和ACTION_UP事件的传递结论:

如果在某个对象(Activity、ViewGroup、View)的dispatchTouchEvent()消费事件(返回true),那么收到ACTION_DOWN的函数也能收到ACTION_MOVE和ACTION_UP如果在某个对象(Activity、ViewGroup、View)的onTouchEvent()消费事件(返回true),那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent()并结束本次事件传递过程。