Android事件分发机制深度解析(ViewGroup篇)

上一篇我们介绍了View的事件分发机制,相信大家对View的事件分发一定都有了很深的理解了。之前也曾提到,Android的事件分发机制由两部分组成,分别是View的事件分发机制以及ViewGroup的事件分发机制,今天就趁热打铁,带领大家从源代码的级别深入探究一下ViewGroup的事件分发机制,尽可能地让大家对Android的事件分发机制有一个全面而透彻的理解,好了,话不多说,让我们开启美妙的探索之旅吧_

既然我们从View的事件分发延伸到了ViewGroup的事件分发,那便不得不谈一下View,ViewGroup之间的区别与联系了。我们先来看一下Google官方文档对ViewGroup的阐述:

显而易见,ViewGroup继承自View,说明ViewGroup本身也是一个View。再看Class Overview中的解释:

A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the ViewGroup.LayoutParams class which serves as the base class for layouts parameters.

讲得也非常清楚,ViewGroup是一个很特殊的View,其相对于View多了包含子View,子ViewGroup以及定义布局参数的功能。

弄清楚了View与ViewGroup之间的关系,我们先通过一段Demo代码,让大家对ViewGroup的事件分发机制有一个直观的了解。

首先,我们自定义一个布局类,并让它继承自LinearLayout,自定义布局类的目的是为了能够重写布局类中与ViewGroup的事件分发有关的方法,自定义布局类的代码如下:

public class CustomLayout extends LinearLayout {
    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

MainActivity对应的布局文件:

<com.example.eventdispatch.CustomLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/customLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.eventdispatch.MainActivity" >

  <Button
      android:id="@+id/btn1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Button1"
      />

   <Button
      android:id="@+id/btn2"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Button2"
      />
</com.example.eventdispatch.CustomLayout>

在MainActivity中,我们给CustomLayout对象设置了Touch事件,给两个Button对象设置了Click事件,MainActivity对应的代码如下:


public class MainActivity extends ActionBarActivity {
    private CustomLayout customLayout;
    private Button btn1;
    private Button btn2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        customLayout=(CustomLayout)findViewById(R.id.customLayout);
        btn1=(Button)findViewById(R.id.btn1);
        btn2=(Button)findViewById(R.id.btn2);
        customLayout.setOnTouchListener(new OnTouchListener(){

            @Override
            public boolean onTouch(View arg0, MotionEvent arg1) {
                Log.v("TAG","customLayout onTouch");
                return false;
            }
            
        });
        btn1.setOnClickListener(new OnClickListener(){

            @Override
            public void onClick(View arg0) {
                Log.v("TAG","btn1 onClick");
            }
            
        });
        
        btn2.setOnClickListener(new OnClickListener(){

            @Override
            public void onClick(View arg0) {
                Log.v("TAG","btn2 onClick");
            }
            
        });
    }

}

我们点击btn1,输出如下:

我们点击btn2,输出如下:

我们点击空白区域,输出如下:


可以发现,我们点击按钮时仅仅是调用了按钮本身的Click事件,并没有去调用按钮所在布局的Touch事件,那么,这是否可以说明Android中的事件是先传递到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!
我们在CustomLayout中重写onInterceptTouchEvent方法,让它返回一个true试试看:

public class CustomLayout extends LinearLayout {
    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        return true;
    }
}

再次运行程序,点击btn1,输出如下:


点击btn2,输出如下:


点击空白区域,输出如下:


我们发现,将onInterceptTouchEvent方法的返回值改为true之后,无论点击btn1,btn2还是空白区域,都只会触发Layout的Touch事件,如果说事件是从View传递到ViewGroup的,那么ViewGroup怎么可能拦截掉View的事件呢?看来,只有源码才能告诉我们答案了。

上篇文章曾经提到过,在Android中,只要触摸到了任何控件,都会去调用这个控件的dispatchTouchEvent方法,其实这个说法并不准确,更加准确的说法是,触摸到了任何控件,都会首先去调用控件所在布局的dispatchTouchEvent方法,然后在控件所在布局的dispatchTouchEvent方法中,遍历所有的控件,找出当前点击的控件,调用其dispatchTouchEvent方法。

在CustomLayout中点击Button时,会先去调用CustomLayout的dispatchTouchEvent方法,我们发现CustomLayout中是没有这个方法的,我们到CustomLayout的父类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;  
        }  
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
            ev.setAction(MotionEvent.ACTION_DOWN);  
            final int scrolledXInt = (int) scrolledXFloat;  
            final int scrolledYInt = (int) scrolledYFloat;  
            final View[] children = mChildren;  
            final int count = mChildrenCount;  
            for (int i = count - 1; i >= 0; i--) {  
                final View child = children[i];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                        || child.getAnimation() != null) {  
                    child.getHitRect(frame);  
                    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;  
                        if (child.dispatchTouchEvent(ev))  {  
                            mMotionTarget = child;  
                            return true;  
                        }  
                    }  
                }  
            }  
        }  
    }  
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
            (action == MotionEvent.ACTION_CANCEL);  
    if (isUpOrCancel) {  
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
    }  
    final View target = mMotionTarget;  
    if (target == null) {  
        ev.setLocation(xf, yf);  
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        }  
        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);  
}  

ViewGroup的dispatchTouchEvent方法很长,我们先去看if (disallowIntercept || !onInterceptTouchEvent(ev))这一句,第一个判断条件是disallowIntercept,它是一个布尔变量,代表是否禁用掉事件拦截功能,默认值为false,那么能不能进入这个if判断就完全依赖于第二个判断条件了,第二个判断条件是!onInterceptTouchEvent(ev),就是对onInterceptTouchEvent方法的返回值取反。如果onInterceptTouchEvent的返回值为true,就无法进入该if判断中,事件就无法传递到子View中(进入了该if判断,事件才能往子View传递,大家先暂时这样理解着)。

我们接着去看看这个if判断中做了什么事情。从** for (int i = count - 1; i >= 0; i--) ** 这一句开始看,它会去遍历所有子View,找出当前正在点击的View,调用该View的dispatchTouchEvent方法,如果该View的dispatchTouchEvent方法返回true,则整个 ViewGroup的dispatchTouchEvent方法直接返回true,ViewGroup设置的事件便得不到处理了。

由上篇文章可知,如果一个控件是可点击的,那么点击它,它的dispatchTouchEvent方法定然是返回true的,现在我们可以回过头来分析下之前的Demo代码了,当CustomLayout 中的onInterceptTouchEvent方法返回false时(默认情况),点击按钮,首先回去调用按钮所在布局的dispatchTouchEvent方法,在if (disallowIntercept || !onInterceptTouchEvent(ev))处,因为当前onInterceptTouchEvent返回false,取反为true,所以能进入到该if判断中,事件便从我们的ViewGroup传递到子View中了,之后,找到当前点击的按钮,调用其dispatchTouchEvent方法,因为按钮是可点击的,所以按钮的dispatchTouchEvent方法会返回true,从而导致ViewGroup的dispatchTouchEvent方法直接返回true,CustomLayout中的Touch事件自然得不到执行了。如果当前点击的是空白区域呢?那自然不会像刚才一样直接返回true的,代码会继续向下执行,我们看到下面这段代码:

if (target == null) {  
        ev.setLocation(xf, yf);  
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        }  
        return super.dispatchTouchEvent(ev);  
    }  

它会去判断target是否为null,一般情况下target都是为null的,之后便会去调用super.dispatchTouchEvent(ev)方法。为啥要调super.dispatchTouchEvent(ev)方法呢?因为我们的ViewGroup本身就是一个View,调用super.dispatchTouchEvent(ev)方法就是去处理ViewGroup本身设置的一些事件。所以,当我们点击空白区域时,CustomLayout中的Touch事件会被执行。

理解了onInterceptTouchEvent方法返回false时的运行过程,再去分析onInterceptTouchEvent方法返回true时的输出结果就是小菜一碟了。当onInterceptTouchEvent方法返回true时,if (disallowIntercept || !onInterceptTouchEvent(ev))这个判断肯定是进不去的,之后便会执行到super.dispatchTouchEvent(ev),所以,无论是点击Button,还是点击空白区域,都只会调用CustomLayout的Touch事件。

到这里,View的事件分发机制与ViewGroup的事件分发机制的源码解析就基本结束了,可以看到,这两者是紧密联系,密不可分的,真正的项目中也可能会有各种涉及到事件分发的复杂业务场景,但只要熟悉源码,我们便所向披靡,无所畏惧,任凭业务场景千变万化,我们都能妥善处理好事件分发的相关问题!

参考:http://blog.csdn.net/guolin_blog/article/details/9153747

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

推荐阅读更多精彩内容