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

2016-04-12  本文已影响528人  伐冰

上一篇我们介绍了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

上一篇下一篇

猜你喜欢

热点阅读