勤奮可以彌補聰明的不足,但聰明無法彌補懶惰的缺陷。
最近工作挺忙的,但是感覺不寫博客的話,心里空蕩蕩的,每寫一篇博客心里都踏實很多,也不知道寫博客能堅持多久,但是我會繼續努力認真學習每一個知識點。廢話不多說了,進入正題吧。
在上一篇文章中我們詳細介紹了View的事件分發,在學習ViewGroup的事件分發之前最好先學習一下[](http://blog.csdn.net/dmk877/article/details/48781845)[Android事件分發機制——View(一)](http://blog.csdn.net/dmk877/article/details/48781845),在了解了View的事件分發機制之后來學習
ViewGroup的事件分發就簡單多了,我們一起來探討一下吧,如有謬誤歡迎批評指正,如有疑問歡迎留言。(注:本文采用的是Android 2.3的源碼)
1.ViewGroup相關知識
在探討事件分發機制之前,我們必須明白android兩個基礎控件view和viewgroup,以及它們之間的關系:View是沒有子控件的,像button,textview都是view控件。而viewgroup繼承自view,是可以存在子控件的,像LinearLayout,FrameLayout等。也就是說Viewgroup就是一組View或者是Viewroup的集合,它是所有頁面布局的父類。View在ViewGroup內,ViewGroup也可以在其他ViewGroup內,這時候把內部的ViewGroup當成View來分析。
ViewGroup的繼承關系圖如下

ViewGroup的相關事件有三個:
onInterceptTouchEvent---------負責事件的攔截
dispatchTouchEvent-------------負責事件分發
onTouchEvent--------------負責事件的處理。
2.案例
我們先從一個案例說起這個案例很簡單包含一個自定義的MyLinearLayout在其中有一個自定義的MyButton。
MyLinearLayout的源碼
~~~
package com.example.viewgrouppractice;
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";
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.i(TAG,"MyLinearLayout--dispatchTouchEvent--ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG,"MyLinearLayout--dispatchTouchEvent--ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG,"MyLinearLayout--dispatchTouchEvent--ACTION_UP");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG,"MyLinearLayout--onInterceptTouchEvent--ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG,"MyLinearLayout--onInterceptTouchEvent--ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG,"MyLinearLayout--onInterceptTouchEvent--ACTION_UP");
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG,"MyLinearLayout--onTouchEvent--ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG,"MyLinearLayout--onTouchEvent--ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG,"MyLinearLayout--onTouchEvent--ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
}
~~~
可以看到我們只是添加了日志的打印,其它的都和系統默認的是一樣的。
MyButton的源碼如下
~~~
package com.example.viewgrouppractice;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
public class MyButton extends Button {
private static final String TAG = "MyLinearLayout";
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG,"MyButton--dispatchTouchEvent--ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG,"MyButton--dispatchTouchEvent--ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG,"MyButton--dispatchTouchEvent--ACTION_UP");
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG,"MyButton--onTouchEvent--ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG,"MyButton--onTouchEvent--ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG,"MyButton--onTouchEvent--ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
}
~~~
同樣MyButton也是只添加了日志的打印
MainActivity的布局文件如下
~~~
<com.example.viewgrouppractice.MyLinearLayout
android:id="@+id/ll_main"
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"
android:orientation="vertical" >
<com.example.viewgrouppractice.MyButton
android:id="@+id/mbtn_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="test" />
</com.example.viewgrouppractice.MyLinearLayout>
~~~
清楚的了解了這些后,我們來運行下程序,點擊按鈕并滑動一下會有如下的日志打印

此時點擊MyLinearLayout的空白區域的日志如下

然后將MyLinearLayout的onInterceptTouchEvent方法的返回值直接return true然后點擊按鈕和空白區域發現打印的日志相同如下

在上面的日志打印中第一張圖我們對事件的傳遞沒有做任何的人為的改變,因此它也是系統默認的打印,認真的看日志我們會發現事件的執行順序是:
MyLinearLayout的dispatchTouchEvent---->MyLinearLayout的onInterceptTouchEvent--->MyButton的dispatchTouchEvent---->MyButton的onTouchEvent
為什么日志會這樣打印呢?ViewGroup的事件到底是怎么分發的呢?唯有源碼更具有說服力,說到源碼我們首先想到的應該是ViewGroup的dispatchTouchEvent方法,好下面我們就來分析分析ViewGroup的事件分發的相關源碼。
3.分析源碼
ViewGroup的dispatchTouchEvent源碼
~~~
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;
/**
* disallowIntercept表示是否允許事件攔截,默認是false,即不攔截事件
* 此值可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法進行設置
*/
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
/**
* 因為從ACTION_DOWN開始要開啟新一輪的事件分發,所有要將mMotionTarget(目標)置為空
*/
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
/**
* 當disallowIntercept(默認是false)為true,或者onInterceptTouchEvent(ev)(默認返回為false)方法的返回值
* 為false,取反則為true,則判斷條件成立
*/
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;
/**
* 遍歷當前ViewGroup的所有子View
*/
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
/**
* 如果當前的View是VISIBLE的或者有動畫執行
*/
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
/**
* 如果子View包含當前觸摸的點
*/
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.
/**
* 如果子View的dispatchTouchEvent方法的返回值為true,則表示子View已經消費了事件
* 此時將子View賦值給mMotionTarget
*/
mMotionTarget = child;
/**
* 直接返回true,表示down事件被消費掉了
*/
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
/**
* 如果是ACTION_UP或者ACTION_CANCEL, 將disallowIntercept設置為默認的false
* 因為ACTION_UP或者ACTION_CANCEL表示事件執行完,要將前面設置的值復位
*/
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
/**
* 當mMotionTarget為空表示沒有找到消費事件的View,此時需要調用ViewGroup父類的dispatchTouchEvent方法,
* ViewGroup的父類即為View
*/
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);
}
// if have a target, see if we're allowed to and want to intercept its
// events
/**
* 如果執行到此說明target!=null,然后判斷是否允許攔截和是否想要攔截
* 如果允許攔截(!disallowIntercept=true),并且想要攔截(onInterceptTouchEvent(ev)返回值為true)
* 則條件成立
*/
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
/**
* 設置動作為ACTION_CANCEL,此處與ev.getAction相對應
*/
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;
}
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);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
/**
* 將事件分發給target返回target.dispatchTouchEvent(ev)的返回值
*/
return target.dispatchTouchEvent(ev);
}
~~~
上面的注釋已經很詳細了,但是為了能夠理解的更加清楚,我們再來詳細的分析一遍,我們首先分析下ViewGroup的事件分發的流程首先第14行會
進入第一個事件ACTION_DOWN的判斷在這判斷的內部首先會將mMotionTarget置為空,然后進入?if (disallowIntercept ||
?!onInterceptTouchEvent(ev))這個判斷,這個判斷的條件有兩個
①disallowIntercept:表示當前不允許攔截,如果為false就雙重否定等于肯定,即允許攔截,如果為true就表示不允許攔截。它的默認值為false,它的值可以通過viewGroup.requestDisallowInterceptTouchEvent(boolean);進行設置
②!onInterceptTouchEvent:對onInterceptTouchEvent的返回值進行取反操作,它的值我們可以通過在ViewGroup中復寫onInterceptTouchEvent這個方法進行改變,就像上面的案例那樣
因為disallowIntercept的默認值為false所以決定是否進入if語句的重任就落在了onInterceptTouchEvent返回值的身上有兩種情況
1.如果onInterceptTouchEvent的返回值為false那么取反后就為true就會進入到if語句內部
2.如果onInterceptTouchEvent的返回值為true那么取反后就為false,就會跳出這個if判斷。
我們首先看下第一種情況if語句的內部是怎么實現的,第43行遍歷所有的子View,然后判斷當前點擊的“點”是否在當前所遍歷到的
子View內,這里的子View有兩種可能
①子View是ViewGroup則遞歸的去遍歷。
②子View是View如果在當前所遍歷的子View內,那么調用View的dispatchTouchEvent方法,之后就進入了View的事件分發,[參考View的事件分發機制](http://blog.csdn.net/dmk877/article/details/48781845)
在調用View的dispatchTouchEvent方法時如果此方法返回true則表示已經找到消費事件的View,將mMotionTarget = child然后return true;表示
down事件已經消費了。ACTION_DOWN后面的代碼也就無法執行了。如果此方法返回false則mMotionTarget ==null,此時會進入到102行調用View的
dispatchTouchEvent方法到這里ACTION_DOWN的邏輯就走完了。
在ACTION_DOWN中如果我們找到了消費事件的View就會執行mMotionTarget = child此時mMotionTarget !=null,我們進行Move操作
它會怎么執行呢?它會進入到第120行的邏輯為了便于查看我們將代碼復制到下面
~~~
/**
* 如果執行到此說明target!=null,然后判斷是否允許攔截和是否想要攔截
* 如果允許攔截(!disallowIntercept=true),并且想要攔截(onInterceptTouchEvent(ev)返回值為true)
* 則條件成立
*/
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
/**
* 設置動作為ACTION_CANCEL,此處與ev.getAction相對應
*/
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;
}
~~~
這個if判斷語句有兩個條件
①!disallowIntercept:對disallowIntercept取反,如果disallowIntercept=false(允許攔截)則!disallowIntercept=true
②onInterceptTouchEvent:ViewGroup中onInterceptTouchEvent的返回值,onInterceptTouchEvent=true表示我要攔截
這兩個條件是與的關系即當ViewGroup允許攔截并且我要攔截時會進入if語句的內部進行事件的攔截,它攔截的方式就是將mMotionTarget置為null然后返回true,因為mMotionTarget==null所以事件就不會分發給子View了。如果沒有進行攔截,則它會執行165行的target.dispatchTouchEvent最后當為ACTION_UP時進入147行將mMotionTarget置為null即復位,準備下次事件的觸發。到這里ViewGroup的事件分發就分析完了,我們通過一張圖來把上面的流程描繪出來

現在你能分析我們剛開始的案例的日志打印嗎?
案例的分析:
在ViewGroup的事件分發過程中首先會調用ViewGroup的dispatchTouchEvent方法然后在ACTION_DOWN中會調用onInterceptTouchEvent由于默認情況
下onInterceptTouchEvent的返回值是false所以進入if語句在if語句里遍歷所有子View找到消費事件的View,接著就進入了View的
dispatchTouchEvent。所以案例中的第一張圖會那樣打印
? ?而點擊除Button之外的空白區域由于沒有找到消費事件的View,mMontionTarget==null,所以會調用super.dispatchTouchEvent,由
于ViewGroup的父類是View所以此時的MyLinearLayout就相當于一個View,調用調用super.dispatchTouchEvent也就相當于調用
View.dispatchTouchEvent,而MyLinearLayout是不可點擊的根據我們上篇的View的事件分發機制可以知道此時只會觸發Down事件所
以第二張圖會那樣打印。
? ? 而當我們將onInterceptTouchEvent的返回值置為true時,mMontionTarget始終為null此時就會調用super.dispatchTouchEvent此時的
super.dispatchTouchEvent就是View的dispatchTouchEvent原理和上面說的一樣。
4.開發中常遇到的問題
在實際的開發中我們有可能會遇到這樣的問題
如果ViewGroup的onInterceptTouchEvent(ev) 當ACTION_MOVE時return true ,即攔截子View的MOVE以及UP事件;
此時子View希望依然能夠響應MOVE和UP時我們應該怎么辦呢?
在上面我們也提到過可以調用ViewGroup的requestDisallowInterceptTouchEvent方法我們可以采用如下的形式
~~~
@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
getParent().requestDisallowInterceptTouchEvent(true);
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
~~~
getParent().requestDisallowInterceptTouchEvent(true); ?這樣即使ViewGroup在MOVE的時候return true,子View依然可以捕獲到MOVE以及UP事件。
這一點可以從我們上面分析的ViewGroup的攔截的代碼中看出,也就是120-145行,我們會看到這兩個條件是與的關系只要有一個為
false則會跳過攔截事件的代碼,
5.總結
1.Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View的。
2.在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件
? 繼續向子View傳遞,返回false代表不對事件進行攔截,默認返回false。
3.子View可以通過調用getParent().requestDisallowInterceptTouchEvent(true); ?阻止ViewGroup對其MOVE或者UP事件進行攔
?截;
4. 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。
5.假如我們在某個ViewGroup的onInterceptTouchEvent中,將Action為Down的Touch事件返回true,那便表示將該ViewGroup的所有
? 下發操作攔截掉,這種情況下,mTarget會一直為null,因為mTarget是在Down事件中賦值的。由于mTarge為null,該ViewGroup的
? onTouchEvent事件被執行。這種情況下可以把這個ViewGroup直接當成View來對待。
6.當某個子View返回true時,會中止Down事件的分發,同時在ViewGroup中記錄該子View。接下去的Move和Up事件將由該子View直接
? 進行處理。由于子View是保存在ViewGroup中的,多層ViewGroup的節點結構時,上級ViewGroup保存的會是真實處理事件的View所在
? 的ViewGroup對象:如ViewGroup0-ViewGroup1-TextView的結構中,TextView返回了true,它將被保存在ViewGroup1中,ViewGroup1
? 也會返回true,被保存在ViewGroup0中。當Move和UP事件來時,會先從ViewGroup0傳遞至ViewGroup1,再由ViewGroup1傳遞至
? TextView。