Android事件分发机制全解析

Android 从源码的角度分析View的事件分发(上)

问题:Android中的onClick()和onTouch()谁先执行?
下面通过一个具体的实例来解析:
代码如下,布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">
    
    <Button
        android:id="@+id/bt_onclick"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点我吆!"
        android:padding="10dp"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:layout_gravity="center"
        android:textColor="@color/white"
        android:textSize="15sp"/>

</LinearLayout>```

onClick()事件和onTouch()事件代码:

bt_onclick.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "onClick execute");
}
});

bt_onclick.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});```
按钮被点击时执行的结果:

onClick和onTouch事件

可以看出onTouch()方法里能做的事情要比onClick()多些,比如说手指按下、抬起、移动等事件。通过代码我们可以看出,onTouch()方法优先于onClick()方法,并且onTouch()执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(你可能还会执行有多次ACTION_DOWN的执行,如果你手抖一下)。因此事件传递的顺序事先经过onTouch(),在传递到onClick()。
如图,onTouch()方法执行多次:
onTouch()方法执行多次.png

细心的同学可以注意到,onTouch()方法具有返回值,这里我们的返回值是false,如果我们尝试把onTouch()方法里的返回值改成true,再运行一次,可能结果如下:
修改onTouch方法的返回值为true后的结果

我们发现,onClick()方法就不再执行了,为什么会这样呢?你可以先理解成onTouch()方法返回ture就认为这个事件被onTouch()消费掉了,因而不会再继续向下传递了。
下面我们就具体的了解一下,在按钮被按下的一刻,到底发生了什么?
首先你需要知道的一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent()方法。那当我们去点击按钮的时候,就会去调用Button类里的dispatchTouchEvent()方法,可是你会发现Button类里并没有这个方法,那么就到它的父类TextView里去找一找,你会发现TextView里也没有这个方法,那就只好在继续到TextView的父类View里找一找了,这个时候,你会发现在View中找到了dispatchTouchEvent()这个方法,示意图如下:
寻找:View中的dispatchTouchEvent()方法

然后我们来看一下View中的dispatchTouchEvent()方法的源码:

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}```
**这个方法非常简单,只有短短几行代码,我们可以看出,在这个方法内,首先是进行一个判断,如果```mOnTouchListener != null```,```
(mViewFlags & ENABLED_MASK) == ENABLED```和```
mOnTouchListener.onTouch(this, event)```这三个条件都为真时,就返回true,否者就去执行onTouchEvent(event)方法,并返回。**
* 先看一下第一个条件,```mOnTouchListener```这个变量实在哪里赋值的呢?我们通过寻找之后在View里发现了如下方法:

public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
} 通过看源码,我们找到了,mOnTouchListener正是在setOnTouchListener方法里赋值的,也就是说只要我们给控件注册了touch事件,mOnTouchListener```就一定会被赋值。

  • 第二个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable(可用的),按钮默认都是enable(可用的),因此这个条件恒为true。
  • 第三个条件就比较关键了。mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch()方法。也就是说如果我们在onTouch()方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果我们在onTouch()方法里返回false,就会去执行onTouchEvent(event)方法。

现在我们可以结合前面的例子来分析一下,首先在dispatchTouchEvent()方法中最先执行的就是onTouch()方法,因此onTouch()肯定是要优先于onClick()执行的,也是印证了刚刚的打印结果。而如果在onTouch()方法里返回了true,就会让dispatchTouchEvent()方法直接返回true,不会再继续往下执行。而打印结果也证实了如果onTouch()方法返回true,onClick()方法就不会被执行了。
根据以上源码分析,从原理上解释我们前面例子运行的结果。而上面的分析还透漏出一个重要的信息,那就是onClick的调用肯定是在onTouchEvent(event)方法中的,那我们马上来看onTouchEvent的源码,代码如下:

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;  
        }  
    }  
14  if (((viewFlags & CLICKABLE) == CLICKABLE ||  
15        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
16      switch (event.getAction()) {  
            case MotionEvent.ACTION_UP:  
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
                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();  
                            }  
37                          if (!post(mPerformClick)) {  
38                                performClick();  
39                          }  
                        }  
                    }  
                    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;  
88        }  
89      return true;  
90    }  
91    return false;  
}  ```
首先在第14行我们可以看出,如果该控件是可以点击的就会进入到第16行的switch判断中去,而如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。在经过种种的判断之后,会执行到第38行的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进行赋值,然后每当控件被点击时,都会在performClick()方法里回调被点击控件的onClick()方法。**

这样View的整个事件分发的流程就让我们搞清楚了!不过别高兴的太早,现在还没结束,还有一个很重要的知识点需要说明,就是touch事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的```ACTION_DOWN,ACTION_MOVE,ACTION_UP```等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当```dispatchTouchEvent```在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

说到这里,很多的朋友肯定要有巨大的疑问了。这不是在自相矛盾吗?前面的例子中,明明在```onTouch```事件里面返回了false,```ACTION_DOWN```和```ACTION_UP```不是都得到执行了吗?其实你只是被假象所迷惑了,让我们仔细分析一下,在前面的例子当中,我们到底返回的是什么。
参考着我们前面分析的源码,首先在onTouch事件里返回了false,就一定会进入到```onTouchEvent```方法中,然后我们来看一下```onTouchEvent```方法的细节。由于我们点击了按钮,就会进入到第14行这个if判断的内部,然后你会发现,不管当前的action是什么,最终都一定会走到第89行,返回一个true。

是不是有一种被欺骗的感觉?明明在```onTouch```事件里返回了false,系统还是在```onTouchEvent```方法中帮你返回了true。就因为这个原因,才使得前面的例子中```ACTION_UP```可以得到执行。

那我们可以换一个控件,将Button控件替换成ImageView,然后给它注册一个touch事件,并返回false。如下所示:

imageView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});```
运行一下程序,点击ImageView,你会发现结果如下:

在ACTION_DOWN执行完后,后面的一系列action都不会得到执行了。这又是为什么呢?因为ImageView和Button按钮不同,它是默认不可点击的,因此在onTouchEvent的第14行判断时无法进入到if的内部,直接跳到第91行返回了false,也就导致后面其它的action都无法执行了。

好了,关于View的事件分发,我想讲的东西全都在这里了。现在我们再来回顾一下开篇时提到的那三个问题,相信每个人都会有更深一层的理解。

总结:
1.onTouch和onTouchEvent有什么区别,又该如何使用?
从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二是当前点击的控件必须是enable(可用的)。因此如果你有一个控件是非enable的话,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

2. 为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?
如果你阅读了Android实现图片滚动控件,含页签功能,让你的应用像淘宝一样炫起来 这篇文章。当时我在图片轮播器里使用Button,主要就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable="true"的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。

Android 从源码的角度分析ViewGroup的事件分发(下)

问题:什么是ViewGroup?它与普通的View有什么区别?
顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子ViewGroup,是Android中所有布局的父类或者间接父类,像LinerLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。ViewGroup继承结构示意图如下所示:


我们可以看到,我们平时常用到的各种布局,全都属于ViewGroup的子类。
简单介绍完了ViewGroup,我们现在通过一个简单的Demo来演示一下ViewGroup的事件分发流程吧。
首先我们来自定义一个布局,命名MyLayout,继承自LinearLayout,如下所示:

public class MyLayout extends LinearLayout{
    public MyLayout(Context context) {
        super(context);
    }
    public MyLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public MyLayout(Context context, @Nullable AttributeSet attrs
                                         , int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<app.wangling.com.myapplication.MyLayout
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/bt_button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="点击按钮1"/>

    <Button
        android:id="@+id/bt_button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="点击按钮2"/>

</app.wangling.com.myapplication.MyLayout>

点击事件:

 my_layout.setOnTouchListener(new View.OnTouchListener() {
       @Override
       public boolean onTouch(View v, MotionEvent event) {
             Log.d("TAG", "myLayout on touch");
             return false;
       }
  });

  bt_button1.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
            Log.d("TAG", "You clicked button1");
       }
   });

   bt_button2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
             Log.d("TAG", "You clicked button2");
        }
   });```
我们在MyLayout的onTouch方法,和Button1、Button2的onClick方法中都打印了一句话。现在运行一下项目,效果图如下所示:
![](http://upload-images.jianshu.io/upload_images/3416944-7194c64f56289bbb?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
分别点击一下Button1、Button2和空白区域,打印结果如下所示:
![](http://upload-images.jianshu.io/upload_images/3416944-380625281a4f4e98?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
你会发现,当点击按钮的时候,MyLayout注册的onTouch()方法并不会执行,只有点击空白区域的时候该方法才会被执行。你可以先理解为Button的onClick()方法将事件消费掉了,因此事件不会再继续向下传递。
疑问:那么,我们可以看出Android中的touch事件是先传递到View,再传递到ViewGroup?现在下结论还未免过早,让我们再做一次实验。
查阅文档可以看出,ViewGroup中有一个onInterceptTouchEvent()方法,我们来看一下方法中的源码:
/**
 * Implement this method to intercept all touch screen motion events.  This
 * allows you to watch events as they are dispatched to your children, and
 * take ownership of the current gesture at any point.
 *
 * <p>Using this function takes some care, as it has a fairly complicated
 * interaction with {@link View#onTouchEvent(MotionEvent)
 * View.onTouchEvent(MotionEvent)}, and using it requires implementing
 * that method as well as this one in the correct way.  Events will be
 * received in the following order:
 *
 * <ol>
 * <li> You will receive the down event here.
 * <li> The down event will be handled either by a child of this view
 * group, or given to your own onTouchEvent() method to handle; this means
 * you should implement onTouchEvent() to return true, so you will
 * continue to see the rest of the gesture (instead of looking for
 * a parent view to handle it).  Also, by returning true from
 * onTouchEvent(), you will not receive any following
 * events in onInterceptTouchEvent() and all touch processing must
 * happen in onTouchEvent() like normal.
 * <li> For as long as you return false from this function, each following
 * event (up to and including the final up) will be delivered first here
 * and then to the target's onTouchEvent().
 * <li> If you return true from here, you will not receive any
 * following events: the target view will receive the same event but
 * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
 * events will be delivered to your onTouchEvent() method and no longer
 * appear here.
 * </ol>
 *
 * @param ev The motion event being dispatched down the hierarchy.
 * @return Return true to steal motion events from the children and have
 * them dispatched to this ViewGroup through onTouchEvent().
 * The current target will receive an ACTION_CANCEL event, and no further
 * messages will be delivered here.
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}
如果不看源码,你还真的被注释吓到,这么多引文注释呀,可是看了源码如此简单!只有一行代码,返回一个false。
既然是布尔型的返回,那么只有两种可能,我们在MyLayout中重写这个方法,然后返回一个true试试看,代码如下:

public class MyLayout extends LinearLayout{
public MyLayout(Context context) {
super(context);
}
public MyLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyLayout(Context context, @Nullable AttributeSet attrs
, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
}```
再次运行项目,然后分别点击Button1,Button2和空白区域,打印结果如下所示:



你会发现,不管你点击哪里,永远都只会触发MyLayout的touch事件了,按钮的点击事件完全被屏蔽了!这是为什么呢?如果Android中的touch事件是先传递到View,再传递到ViewGroup的,那么MyLayout又怎么可能屏蔽Button的点击事件呢?
哈哈!看来只能通过阅读源码,搞清楚Android中的ViewGroup的事件分发机制了,才能解决我们心中的疑惑了,不过这里我想先给大家透漏一句,Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View。记住我前面所讲到的。只要你触摸了任何控件,就一定调用该控件的dispatchTouchEvent()方法,这个说法是没有错的,只不过还不够完整。实际情况是,当你点击了某一个控件,首先会去调用该控件所在布局的dispatchTouchEvent()方法,然后在布局的dispatchTouchEvent()方法中找到被点击的相应的控件,再去调用该控件的dispatchTouchEvent()方法。如果我们点击了MyLayout中的按钮,会先去调用MyLayout的dispatchTouchEvent()方法,可是你会发现MyLayout中并没有这个方法。那就在到它的父类LinearLayout中找一找,发现也没有这个方法。那只好继续再找LinearLayout的父类ViewGroup,你终于在ViewGroup中看到了这个方法,按钮的dispatchTouchEvent()方法就是这里调用的。修改后的示意图如下所示:



那还等什么?快来看一看ViewGroup中的dispatchTouchEvent()方法中的源码吧,代码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {  
    final int action = ev.getAction();  
    final float xf = ev.getX();  
    final float yf = ev.getY();  
    final float scrolledXFloat = xf + mScrollX;  
    final float scrolledYFloat = yf + mScrollY;  
    final Rect frame = mTempRect;  
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (action == MotionEvent.ACTION_DOWN) {  
        if (mMotionTarget != null) {  
            mMotionTarget = null;  
        }  
13    if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
14          ev.setAction(MotionEvent.ACTION_DOWN);  
            final int scrolledXInt = (int) scrolledXFloat;  
            final int scrolledYInt = (int) scrolledYFloat;  
            final View[] children = mChildren;  
            final int count = mChildrenCount;  
19          for (int i = count - 1; i >= 0; i--) {  
20             final View child = children[i];  
21             if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
22                        || child.getAnimation() != null) {  
23                  child.getHitRect(frame);  
24                  if (frame.contains(scrolledXInt, scrolledYInt)) {  
                        final float xc = scrolledXFloat - child.mLeft;  
                        final float yc = scrolledYFloat - child.mTop;  
                        ev.setLocation(xc, yc);  
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
29                      if (child.dispatchTouchEvent(ev))  {  
30                          mMotionTarget = child;  
31                          return true;  
                        }  
                    }  
                }  
            }  
        }  
    }  
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
            (action == MotionEvent.ACTION_CANCEL);  
    if (isUpOrCancel) {  
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
    }  
    final View target = mMotionTarget;  
44  if (target == null) {  
45      ev.setLocation(xf, yf);  
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        }  
50      return super.dispatchTouchEvent(ev);  
    }  
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
        final float xc = scrolledXFloat - (float) target.mLeft;  
        final float yc = scrolledYFloat - (float) target.mTop;  
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        ev.setLocation(xc, yc);  
        if (!target.dispatchTouchEvent(ev)) {  
        }  
        mMotionTarget = null;  
        return true;  
    }  
    if (isUpOrCancel) {  
        mMotionTarget = null;  
    }  
    final float xc = scrolledXFloat - (float) target.mLeft;  
    final float yc = scrolledYFloat - (float) target.mTop;  
    ev.setLocation(xc, yc);  
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        mMotionTarget = null;  
    }  
    return target.dispatchTouchEvent(ev);  
}  

这个方法代码比较长,我们只能挑重点看,首先在第13行可以看到一个条件判断,如果disallowIntercept!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent()方法对这个值进行修改。那么dang第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent()方法的返回值取反 ! 也就是说:如果我们在onInterceptTouchEvent()方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent()方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。
那我们重点来看下条件判断的内部是怎么实现的。在第19行通过一个for循环,遍历了当前ViewGroup下的所有子View,然后在第24行判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,然后在第29行调用了该View的dispatchTouchEvent,之后的流程就和 上面所讲到的Android事件分发机制完全解析,带你从源码的角度彻底理解(上)中讲解的是一样的了。我们也因此证实了,按钮点击事件的处理确实就是在这里进行的。

然后需要注意一下,调用子View的dispatchTouchEvent后是有返回值的。我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true。因此会导致第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,也是印证了我们前面的Demo打印的结果,如果按钮的点击事件得到执行,就会把MyLayout的touch事件拦截掉。

那如果我们点击的不是按钮,而是空白区域呢?这种情况就一定不会在第31行返回true了,而是会继续执行后面的代码。那我们继续往后看,在第44行,如果target等于null,就会进入到该条件判断内部,这里一般情况下target都会是null,因此会在第50行调用super.dispatchTouchEvent(ev)。这句代码会调用到哪里呢?当然是View中的dispatchTouchEvent方法了,因为ViewGroup的父类就是View。之后的处理逻辑又和前面所说的是一样的了,也因此MyLayout中注册的onTouch方法会得到执行。之后的代码在一般情况下是走不到的了,我们也就不再继续往下分析。

再看一下整个ViewGroup事件分发过程的流程图吧,相信可以帮助大家更好地去理解:


现在整个ViewGroup的事件分发流程的分析也就到此结束了,我们最后再来简单梳理一下吧。
**
1.Android事件分发是先传递到ViewGroup,再由ViewGroup传递给View的。
2.在ViewGroup中可以通过onInterceptTouchEvent()方法对事件传递进行拦截,onInterceptTouchEvent()方法返回true代表不允许事件继续向子View进行传递;返回false代表不对事件进行拦截,默认返回false。
3.子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。
**

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

推荐阅读更多精彩内容