Android ViewGroup 事件分發機制詳解

Hongyang發表於2014-09-10

上一篇已經完整的解析了Android View的事件分發機制,今天給大家程式碼ViewGroup事件分發的原始碼解析~~凡是自定義ViewGroup實現各種滑動效果的,不可避免的會出現很多事件的衝突,對ViewGroup事件分發機制的瞭解,也有益於大家瞭解衝突產生的原因,以及對衝突進行處理~

1、案例

首先我們接著上一篇的程式碼,在程式碼中新增一個自定義的LinearLayout:

package com.example.zhy_event03;

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

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

}

繼承LinearLayout,然後複寫了與事件分發機制有關的程式碼,新增上了日誌的列印~

然後看我們的佈局檔案:

<com.example.zhy_event03.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <com.example.zhy_event03.MyButton
        android:id="@+id/id_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="click me" />

</com.example.zhy_event03.MyLinearLayout>

MyLinearLayout中包含一個MyButton,MyButton都上篇部落格中已經出現過,這裡就不再貼程式碼了,不清楚可以去檢視~

然後MainActivity就是直接載入佈局,沒有任何程式碼~~~

直接執行我們的程式碼,然後點選我們的Button,依然是有意的MOVE一下,不然不會觸發MOVE事件,看一下日誌的輸出:

09-06 09:57:27.287: E/MyLinearLayout(959): dispatchTouchEvent ACTION_DOWN
09-06 09:57:27.287: E/MyLinearLayout(959): onInterceptTouchEvent ACTION_DOWN
09-06 09:57:27.287: E/MyButton(959): dispatchTouchEvent ACTION_DOWN
09-06 09:57:27.297: E/MyButton(959): onTouchEvent ACTION_DOWN
09-06 09:57:27.297: E/MyButton(959): onTouchEvent ACTION_MOVE
09-06 09:57:27.327: E/MyLinearLayout(959): dispatchTouchEvent ACTION_MOVE
09-06 09:57:27.327: E/MyLinearLayout(959): onInterceptTouchEvent ACTION_MOVE
09-06 09:57:27.337: E/MyButton(959): dispatchTouchEvent ACTION_MOVE
09-06 09:57:27.337: E/MyButton(959): onTouchEvent ACTION_MOVE
09-06 09:57:27.457: E/MyLinearLayout(959): dispatchTouchEvent ACTION_UP
09-06 09:57:27.457: E/MyLinearLayout(959): onInterceptTouchEvent ACTION_UP
09-06 09:57:27.457: E/MyButton(959): dispatchTouchEvent ACTION_UP
09-06 09:57:27.457: E/MyButton(959): onTouchEvent ACTION_UP

可以看到大體的事件流程為:

 

MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent

可以看出,在View上觸發事件,最先捕獲到事件的為View所在的ViewGroup,然後才會到View自身~

下面我們按照日誌的輸出,進入原始碼~

2、原始碼分析

ViewGroup - dispatchTouchEvent

1、ViewGroup - dispatchTouchEvent – ACTION_DOWN

首先是ViewGroup的dispatchTouchEvent方法:

 @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

程式碼比較長,決定分段貼出,首先貼出的是ACTION_DOWN事件相關的程式碼。

16行:進入ACTION_DOWN的處理

17-23行:將mMotionTarget置為null

26行:進行判斷:if(disallowIntercept || !onInterceptTouchEvent(ev))

兩種可能會進入IF程式碼段

1、當前不允許攔截,即disallowIntercept =true,

2、當前允許攔截但是不攔截,即disallowIntercept =false,但是onInterceptTouchEvent(ev)返回false ;

注:disallowIntercept 可以通過viewGroup.requestDisallowInterceptTouchEvent(boolean);進行設定,後面會詳細說;而onInterceptTouchEvent(ev)可以進行復寫。

36-57行:開始遍歷所有的子View

41行:進行判斷當前的x,y座標是否落在子View身上,如果在,47行,執行child.dispatchTouchEvent(ev),就進入了View的dispatchTouchEvent程式碼中了,如果不瞭解請參考:Android View的事件分發機制,當child.dispatchTouchEvent(ev)返回true,則為mMotionTarget=child;然後return true;

ViewGroup的ACTION_DOWN分析結束,總結一下:

ViewGroup實現捕獲到DOWN事件,如果程式碼中不做TOUCH事件攔截,則開始查詢當前x,y是否在某個子View的區域內,如果在,則把事件分發下去。

按照日誌,接下來到達ACTION_MOVE

2、ViewGroup - dispatchTouchEvent – ACTION_MOVE

首先我們原始碼進行刪減,只留下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);
    }

18行:把ACTION_DOWN時賦值的mMotionTarget,付給target ;

23行:if (!disallowIntercept && onInterceptTouchEvent(ev)) 當前允許攔截且攔截了,才進入IF體,當然了預設是不會攔截的~這裡執行了onInterceptTouchEvent(ev)

28-30行:把座標系統轉化為子View的座標系統

32行:直接return target.dispatchTouchEvent(ev);

可以看到,正常流程下,ACTION_MOVE在檢測完是否攔截以後,直接呼叫了子View.dispatchTouchEvent,事件分發下去;

最後就是ACTION_UP了

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

17行:判斷當前是否是ACTION_UP

21,28行:分別重置攔截標誌位以及將DOWN賦值的mMotionTarget置為null,都UP了,當然置為null,下一次DOWN還會再賦值的~

最後,修改座標系統,然後呼叫target.dispatchTouchEvent(ev);

 

正常情況下,即我們上例整個程式碼的流程我們已經走完了:

1、ACTION_DOWN中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則找到包含當前x,y座標的子View,賦值給mMotionTarget,然後呼叫 mMotionTarget.dispatchTouchEvent

2、ACTION_MOVE中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接呼叫mMotionTarget.dispatchTouchEvent(ev)

3、ACTION_UP中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接呼叫mMotionTarget.dispatchTouchEvent(ev)

當然了在分發之前都會修改下座標系統,把當前的x,y分別減去child.left 和 child.top ,然後傳給child;

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 ;

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是木有辦法的捕獲事件的~~~

4、如果沒有找到合適的子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 ;

5、總結

 

關於程式碼流程上面已經總結過了~

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就可以順利展現出選單欄了。

相關文章