源码解析---android中ViewGroup的事件分发机制

我的未来我决定 提交于 2019-12-09 12:51:42

                         ViewGroup事件分发机制

1.概述

上一篇我们写过View的事件分发机制,如果你对这还不了解的可以看这一篇文章:

https://my.oschina.net/quguangle/blog/793903

那么今天我们将继续上次未完成的话题,从源码的角度分析ViewGroup的事件分发。首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别?

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

可以看到,我们平时项目里经常用到的各种布局,全都属于ViewGroup的子类。

下面直接上案例:

package qu.com.handlerthread;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

/**
 * Created by quguangle on 2016/11/25.
 */

public class MyLinearLayout extends LinearLayout{
    private static final String TAG = MyLinearLayout.class.getSimpleName();

    public MyLinearLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev)
    {
        int action = ev.getAction();
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "dispatchTouchEvent ACTION_UP");
                break;

            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {

        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent ACTION_UP");
                break;

            default:
                break;
        }

        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev)
    {

        int action = ev.getAction();
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
                break;

            default:
                break;
        }

        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
    {
        Log.e(TAG, "requestDisallowInterceptTouchEvent ");
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }

}

代码依然的还是那么的简单,重写一些相关的方法。

然后看我们的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<qu.com.handlerthread.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <qu.com.handlerthread.MyButton
        android:id="@+id/btnTest"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button"
        android:onClick="btnTest"/>


</qu.com.handlerthread.MyLinearLayout>

Activity

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MyButton";
    private Button btnTest;
    private LinearLayout MyLinearLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btnTest = (Button) findViewById(R.id.btnTest);
        btnTest.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int action = motionEvent.getAction();
                switch (action){
                    case MotionEvent.ACTION_DOWN:
                        Log.e(TAG,"onTouch----ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e(TAG,"onTouch----ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e(TAG,"onTouch----ACTION_UP");
                        break;
                    default:
                        break;
                }
                return true;
                }
        });

        btnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e(TAG,"onClick----");
            }
        });

      
    }

}

布局文件也很简单,自定义MyLinearLayout 中放了一个之前用过的自定义MyButton,然后运行项目,我在点击Button时任然Move下,不然不会出现ACTION_MOVE,看打印Log日志:

从打印的日志来看,大体上事件的流程为:MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent 

我们现在换一种方式:Activity

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MyButton";
    private Button btnTest;
    private LinearLayout MyLinearLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btnTest = (Button) findViewById(R.id.btnTest);
        MyLinearLayout.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int action = motionEvent.getAction();
                switch (action){
                    case MotionEvent.ACTION_DOWN:
                        Log.e(TAG,"onTouch----ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e(TAG,"onTouch----ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e(TAG,"onTouch----ACTION_UP");
                        break;
                    default:
                        break;
                }
                return false;
                }
        });

        btnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e(TAG,"onClick----");
            }
        });

    }

}

当我们点击Button时,打印情况:

我们的MyLinearLayout的onTouch方法并没有执行。

而当我们点击空白区域时又执行了此方法,打印情况:

Oh My Good!你可以先理解成Button的onClick方法将事件消费掉了,因此事件不会再继续向下传递。那就说明Android中的touch事件是先传递到View,再传递到ViewGroup的,这不跟我们上面所说的相矛盾,难道真的是这样吗?

我们从源码中找真相:

2.源码分析

ViewGroup - dispatchTouchEvent

2.1首先是ViewGroup的dispatchTouchEvent----ACTION_DOWN

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }

        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) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                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)) {
                            // offset the event to the view's coordinate system
                            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))  {
                                // Event handled, we have a target now.
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }                                                                                                                                                  ....//other code omitted

由于dispatchTouchEvent方法中代码比较多,因此我们首先分析ACTION_DOWN这部分。

1.进入ACTION_DOWN的处理。

2.将mMotionTarget置为null。

3.进行判断:if(disallowIntercept || !onInterceptTouchEvent(ev))根据判断条件,我们可以将他分为2中可能

  • 当前不允许拦截,即disallowIntercept =true (默认为false)。
  • 当前允许拦截但是不拦截,即disallowIntercept =false,但是onInterceptTouchEvent(ev)返回false

特别提醒的是:disallowIntercept 可以通过viewGroup.requestDisallowInterceptTouchEvent(boolean);进行设置,后面会详细说;而onInterceptTouchEvent(ev)可以进行复写。

注意:如果说我们在这里使onInterceptTouchEvent返回值为false,那么它就不会进入IF,那么我们的button事件就会被屏蔽掉。

4.开始遍历所有的子View

5.获取当前触摸点X,Y的坐标,判断是否落入在子View上,如果是就直接执行child.dispatchTouchEvent(ev)方法,意味这就进入到我们之前讲的View.dispatchTouchEvent(ev),不懂的可以看我前面所讲的,当child.dispatchTouchEvent(ev)返回值为true,就将mMotionTarget=child,然后返回true.

到此ACTION_DOWN源码结束了,但是并没玩,还记得前面我们的疑问吗?

我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true。由5可知当child.dispatchTouchEvent(ev)返回true,那么就会直接进入到IF语句,然后返回true。后面的代码就不会在执行了。

总结:

也就是说ViewGroup捕捉了DOWN事件,如果代码中不做TOUCH事件拦截,则开始查找当前x,y是否在某个子View的区域内,如果在,则把事件分发下去。

2.2首先是ViewGroup的dispatchTouchEvent----ACTION_MOVE

@Override
    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;

       //...ACTION_DOWN

       //...ACTIN_UP or ACTION_CANCEL

        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
	final View target = mMotionTarget;
      

        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            //....
        }
        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);

        return target.dispatchTouchEvent(ev);
    }

同样我们只看ACTION_MOVE代码:

1.把ACTION_DOWN时赋值的mMotionTarget,付给target 。

2.if (!disallowIntercept && onInterceptTouchEvent(ev)) 当前允许拦截且拦截了,才进入IF体,当然了默认是不会拦截的~这里执行了onInterceptTouchEvent(ev)。

3.把坐标系统转化为子View的坐标系统。

4.直接return target.dispatchTouchEvent(ev); 可以看到,正常流程下,ACTION_MOVE在检测完是否拦截以后,直接调用了子View.dispatchTouchEvent,事件分发下去;最后就是ACTION_UP了。

2.3首先是ViewGroup的dispatchTouchEvent----ACTION_UP

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }

        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) {...}
	
	boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);

	if (isUpOrCancel) {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
	final View target = mMotionTarget;
	if(target ==null ){...}
	if (!disallowIntercept && onInterceptTouchEvent(ev)) {...}

        if (isUpOrCancel) {
            mMotionTarget = null;
        }

        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);

        return target.dispatchTouchEvent(ev);
    }

1.判断当前是否是ACTION_UP

2.分别重置拦截标志位以及将DOWN赋值的mMotionTarget置为null,都UP了,当然置为null,下一次DOWN还会再赋值的~最后,修改坐标系统,然后调用target.dispatchTouchEvent(ev);

现在整个ViewGroup的事件分发流程的分析也就到此结束了,我们最后再来简单梳理一下吧:

  • ACTION_DOWN中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则找到包含当前x,y坐标的子View,赋值给mMotionTarget,然后调用 mMotionTarget.dispatchTouchEvent
  • ACTION_MOVE中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)
  • ACTION_UP中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)当然了在分发之前都会修改下坐标系统,把当前的x,y分别减去child.left 和 child.top ,然后传给child;

 

3.关于拦截

3.1如何拦截

上面的总结都是基于:如果没有拦截;那么如何拦截呢?

复写ViewGroup的onInterceptTouchEvent方法:

@Override
	public boolean onInterceptTouchEvent(MotionEvent ev)
	{
		int action = ev.getAction();
		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			//如果你觉得需要拦截
			return true ; 
		case MotionEvent.ACTION_MOVE:
			//如果你觉得需要拦截
			return true ; 
		case MotionEvent.ACTION_UP:
			//如果你觉得需要拦截
			return true ; 
		}
		
		return false;
	}

默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这要该事件就不会往子View传递了,并且如果你在DOWN retrun true ,则DOWN,MOVE,UP子View都不会捕获事件;如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。

原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ; 

3.2如何不被拦截

如果ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;

此时子View希望依然能够响应MOVE和UP时该咋办呢?

android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:

@Override
	public boolean dispatchTouchEvent(MotionEvent event)
	{
		getParent().requestDisallowInterceptTouchEvent(true);  
		int action = event.getAction();

		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
			break;
		case MotionEvent.ACTION_MOVE:
			Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
			break;
		case MotionEvent.ACTION_UP:
			Log.e(TAG, "dispatchTouchEvent ACTION_UP");
			break;

		default:
			break;
		}
		return super.dispatchTouchEvent(event);
	}

getParent().requestDisallowInterceptTouchEvent(true);  这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。

ViewGroup MOVE和UP拦截的源码是这样的:

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)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

 当我们把disallowIntercept设置为true时,!disallowIntercept直接为false,于是拦截的方法体就被跳过了

注:如果ViewGroup在onInterceptTouchEvent(ev)  ACTION_DOWN里面直接return true了,那么子View是木有办法的捕获事件的

3.3如果没有找到合适的子View

我们的实例,直接点击ViewGroup内的按钮,当然直接很顺利的走完整个流程;

但是有两种特殊情况

1、ACTION_DOWN的时候,子View.dispatchTouchEvent(ev)返回的为false ; 

如果你仔细看了,你会注意到ViewGroup的dispatchTouchEvent(ev)的ACTION_DOWN代码是这样的

  if (child.dispatchTouchEvent(ev))  {
                                // Event handled, we have a target now.
                                mMotionTarget = child;
                                return true;
                            }

只有在child.dispatchTouchEvent(ev)返回true了,才会认为找到了能够处理当前事件的View,即mMotionTarget = child;

但是如果返回false,那么mMotionTarget 依然是null

mMotionTarget 为null会咋样呢?

其实ViewGroup也是View的子类,如果没有找到能够处理该事件的子View,或者干脆就没有子View;

那么,它作为一个View,就相当于View的事件转发了~~直接super.dispatchTouchEvent(ev);

源码是这样的:

 final View target = mMotionTarget;
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            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);
        }

我们没有一个能够处理该事件的目标元素,意味着我们需要自己处理~~~就相当于传统的View~

2、那么什么时候子View.dispatchTouchEvent(ev)返回的为true

如果你仔细看了上篇博客,你会发现只要子View支持点击或者长按事件一定返回true~~

源码是这样的:

 
if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {   
             return true ;                                                                                                                                                                                                                                                                                                   }

4.总结

1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;

2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法

3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true);  阻止ViewGroup对其MOVE或者UP事件进行拦截;

好了,那么实际应用中能解决哪些问题呢?比如你需要写一个类似slidingmenu的左侧隐藏menu,主Activity上有个Button、ListView或者任何可以响应点击的View,你在当前View上死命的滑动,菜单栏也出不来;因为MOVE事件被子View处理了~ 你需要这么做:在ViewGroup的dispatchTouchEvent中判断用户是不是想显示菜单,如果是,则在onInterceptTouchEvent(ev)拦截子View的事件;自己进行处理,这样自己的onTouchEvent就可以顺利展现出菜单栏了~~

参考文章:http://blog.csdn.net/lmj623565791/article/details/39102591

 

 

 

 

 

 

 

 

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!