Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android ViewGroup事件分發機制

Android ViewGroup事件分發機制

編輯:關於Android編程

 

上一篇已經完整的解析了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,然後復寫了與事件分發機制有關的代碼,添加上了日志的打印~

 

然後看我們的布局文件:

 



    



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事件進行攔截;

 

 

 

 

 

 

 

 


 

 

 

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved