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

                         ViewGroup事件分发机制

1.概述

上一篇咱们写过View的事件分发机制,若是你对这还不了解的能够看这一篇文章:android

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

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

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

能够看到,咱们平时项目里常常用到的各类布局,全都属于ViewGroup的子类。this

下面直接上案例:spa

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);
    }

}

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

而后看咱们的布局文件:日志

<?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>

Activitycode

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日志:orm

从打印的日志来看,大致上事件的流程为: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内的按钮,固然直接很顺利的走完整个流程;

可是有两种特殊状况

一、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~

二、那么何时子View.dispatchTouchEvent(ev)返回的为true

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

源码是这样的:

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

4.总结

一、若是ViewGroup找到了可以处理该事件的View,则直接交给子View处理,本身的onTouchEvent不会被触发;

二、能够经过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给本身处理,则会执行本身对应的onTouchEvent方法

三、子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

相关文章
相关标签/搜索