天天看點

Android ViewGroup事件分發機制 1、案例 2、源碼分析 3、關于攔截 4、如果沒有找到合适的子View 5、總結

首先我們接着上一篇的代碼,在代碼中添加一個自定義的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");  

        case MotionEvent.ACTION_UP:  

            Log.e(TAG, "dispatchTouchEvent ACTION_UP");  

        default:  

        }  

        return super.dispatchTouchEvent(ev);  

    public boolean onTouchEvent(MotionEvent event)  

        int action = event.getAction();  

            Log.e(TAG, "onTouchEvent ACTION_DOWN");  

            Log.e(TAG, "onTouchEvent ACTION_MOVE");  

            Log.e(TAG, "onTouchEvent ACTION_UP");  

        return super.onTouchEvent(event);  

    public boolean onInterceptTouchEvent(MotionEvent ev)  

            Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");  

            Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");  

            Log.e(TAG, "onInterceptTouchEvent ACTION_UP");  

        return super.onInterceptTouchEvent(ev);  

    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自身~

下面我們按照日志的輸出,進入源碼~

ViewGroup - dispatchTouchEvent

首先是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

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

ViewGroup實作捕獲到DOWN事件,如果代碼中不做TOUCH事件攔截,則開始查找目前x,y是否在某個子View的區域内,如果在,則把事件分發下去。

按照日志,接下來到達ACTION_MOVE

首先我們源碼進行删減,隻留下MOVE相關的代碼:

      //...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了

public boolean dispatchTouchEvent(MotionEvent ev) {  

       if (action == MotionEvent.ACTION_DOWN) {...}  

boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  

               (action == MotionEvent.ACTION_CANCEL);  

if (isUpOrCancel) {  

           mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  

if(target ==null ){...}  

if (!disallowIntercept && onInterceptTouchEvent(ev)) {...}  

       if (isUpOrCancel) {  

           mMotionTarget = null;  

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;

上面的總結都是基于:如果沒有攔截;那麼如何攔截呢?

複寫ViewGroup的onInterceptTouchEvent方法:

            //如果你覺得需要攔截  

            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 ; 

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

此時子View希望依然能夠響應MOVE和UP時該咋辦呢?

    public boolean dispatchTouchEvent(MotionEvent event)  

        getParent().requestDisallowInterceptTouchEvent(true);    

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

我們的執行個體,直接點選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);

源碼是這樣的:

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

關于代碼流程上面已經總結過了~

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://www.cnblogs.com/ldq2016/p/6879428.html,如需轉載請自行聯系原作者

繼續閱讀