源碼解析---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

相關文章
相關標籤/搜索