在上篇文章中咱們分析了view的事件分發機制《Android 事件分發機制源碼解析-view層》,在本篇文章中咱們繼續分析另外一層viewGroup的事件分發,viewGroup本質上是一組view的集合,它的裏面包含了view和另外一組viewGroup,咱們日常使用的各類佈局如LinearLayout、RelativeLayout、FrameLayout等等都是繼承的viewGroup,對於viewgroup與view以前的關係,咱們能夠用一張圖來描述一下:javascript
ViewGroup和View組成了一棵樹形結構,最頂層爲Activity的ViewGroup,下面有若干的ViewGroup節點,每一個節點之下又有若干的ViewGroup節點或者View節點,依次類推。html
接下來咱們就開始進入正題分析viewGroup的事件分發。java
//Android源碼環境
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
}
//分析工具
Android Studio 2.2.3
Build #AI-145.3537739, built on December 2, 2016
JRE: 1.8.0_112-release-b05 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o複製代碼
對於view事件分發,主要用到兩個方法,而對於viewGroup來講,比view多了一個方法,android
onInterceptTouchEvent(MotionEvent ev)複製代碼
表示是否用於攔截當前事件,返回true表示攔截,若是攔截了事件,那麼將不會分發給子View。好比說:ViewGroup攔截了這個事件,那麼全部事件都由該ViewGroup處理,它內部的子View將不會得到事件的傳遞。(可是ViewGroup是默認不攔截事件的,這個下面會解釋。)注意:View是沒有這個方法的,也便是說,繼承自View的一個子View不能重寫該方法,也無需攔截事件,由於它下面沒有View了,它要麼處理事件要麼不處理事件,因此最底層的子View不能攔截事件。git
另外兩個方法分別是:github
//該方法用來進行事件的分發,即不管ViewGroup或者View的事件,都是從這個方法開始的。
public boolean dispatchTouchEvent(MotionEvent ev)複製代碼
和ide
//這個方法表示對事件進行處理,在dispatchTouchEvent方法內部調用,若是返回true表示消耗當前事件,若是返回false表示不消耗當前事件。
public boolean onTouchEvent(MotionEvent ev)複製代碼
就是這三個方法決定着viewGroup層的事件分發,它們主要的做用能夠經過如下的僞代碼來表示:工具
public boolean dispatchTouchEvent(MotionEvent ev){
boolean handle = false;
if(onInterceptTouchEvent(ev)){
handle = onTouchEvent(ev);
}else{
handle = child.dispatchTouchEvent(ev);
}
return handle;
}複製代碼
上面這段代碼表示什麼意思呢?若是一個事件傳遞到了ViewGroup處,首先會判斷當前ViewGroup是否要攔截事件,即調用onInterceptTouchEvent()方法;若是返回true,則表示ViewGroup攔截事件,那麼ViewGroup就會調用自身的onTouchEvent來處理事件;若是返回false,表示ViewGroup不攔截事件,此時事件會分發到它的子View處,即調用子View的dispatchTouchEvent方法,如此反覆直到事件被消耗掉。接下來,咱們將從源碼的角度來分析整個ViewGroup事件分發的流程是怎樣的。源碼分析
當一個點擊事件產生後,它的傳遞過程將遵循以下順序:佈局
Activity -> Window -> View複製代碼
事件老是會傳遞給Activity,以後Activity再傳遞給Window,最後Window再傳遞給頂級的View,頂級的View在接收到事件後就會按照事件分發機制去分發事件。若是一個View的onTouchEvent返回了FALSE,那麼它的父容器的onTouchEvent將會被調用,依次類推,若是全部都不處理這個事件的話,那麼Activity將會處理這個事件。
因爲事件老是會先傳遞到Activity,因此咱們就從Activity裏面的事件分發開始分析。首先來看下Activity的dispatchTouchEvent的代碼。
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}複製代碼
咱們看到第二個if判斷,getWindow返回的是Window的實現類PhoneWindow,因此這個判斷的意思就是,Activity的事件會交給它所屬的Window進行分發,若是它返回了TRUE,就表明整個事件就結束了,若是返回了FALSE的話就表明事件沒有人處理,那麼它終將會被Activity本身所處理,即會調用本身的onTouchEvent方法。
接下來咱們就來分析一下Window是如何將事件分給ViewGroup的。Window是個抽象類,因此咱們來看下的實現類PhoneWindow的dispatchTouchEvent的源碼,看看裏面是如何進行分發的。
//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}複製代碼
咱們能夠發現,代碼很短,PhoneWindow直接將事件傳遞給了DecorView,關於DecorView的理解,能夠閱讀我寫的另外一篇文章《Android view 部分 setContentView 的來龍去脈》,接着分析,從這裏面咱們能夠知道,這個DecorView便是頂級view了,因此事件就已經傳到了頂級view這裏,通常狀況下,頂級view是ViewGroup。因此從下面開始咱們進入到ViewGroup的事件分發階段。
對於ViewGroup的事件分發過程,大概是這樣的:若是頂級的ViewGroup攔截事件即onInterceptTouchEvent返回true的話,則事件會交給ViewGroup處理,若是ViewGroup的onTouchListener被設置的話,則onTouch將會被調用,不然的話onTouchEvent將會被調用,也就是說:二者都設置的話,onTouch將會屏蔽掉onTouchEvent,在onTouchEvent中,若是設置了onClickerListener的話,那麼onClick將會被調用。若是頂級ViewGroup不攔截的話,那麼事件將會被傳遞給它所在的點擊事件的子view,這時候子view的dispatchTouchEvent將會被調用,從這開始就進入了view的事件分發過程(能夠參考《Android 事件分發機制源碼解析-view層》),這就是整個事件的分發過程。
首先,咱們來看下ViewGroup的事件分發過程,進入到dispatchTouchEvent方法裏,因爲這個方法比較長,因此咱們對重要代碼進行分析,其餘的省略,能夠自行去看看。
//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 處理初始狀態
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//...省略無關代碼
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
//...省略無關代碼
}複製代碼
首先這裏先判斷事件是否爲DOWN事件,若是是,則初始化,把mFirstTouchTarget置爲null。因爲一個完整的事件序列是以DOWN開始,以UP結束,因此若是是DOWN事件,那麼說明是一個新的事件序列,因此須要初始化以前的狀態。這裏的mFirstTouchTarget很是重要,後面會說到當ViewGroup的子元素成功處理事件的時候,mFirstTouchTarget會指向子元素,接着咱們來看下ViewGroup何時會進行攔截呢?從上面那個if判斷能夠知道,在兩種狀況下,ViewGroup纔會攔截事件,第一種是:事件類型是ACTION_DOWN,第二種就是mFirstTouchTarget != null,第一種好理解,可是第二種什麼意思呢?咱們經過後面的代碼分析能夠知道這個mFirstTouchTarget的意思就是,若是ViewGroup裏面的子元素view可以處理事件的話,那麼這個mFirstTouchTarget就會指向這個子元素view。
在上面代碼裏面有個方法,onInterceptTouchEvent(),咱們進入裏面查看一下
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}複製代碼
默認返回false,也就是說,ViewGroup默認是不攔截事件的,若是想讓ViewGroup攔截事件的話,須要重寫該方法。接着往下看,裏面有個變量FLAG_DISALLOW_INTERCEPT,這個標誌位的做用是禁止ViewGroup攔截除了DOWN以外的事件,通常經過子View的requestDisallowInterceptTouchEvent來設置。因此,當ViewGroup要攔截事件的時候,那麼後續的事件序列都將交給它處理,而不用再調用onInterceptTouchEvent()方法了,因此該方法並非每次事件都會調用的。
判斷是否攔截後,咱們來看看ViewGroup不攔截的狀況,ViewGroup不攔截的話那麼它將會把事件交給它的子view來處理。
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//判斷觸摸點位置是否在子View的範圍內或者子View是否在播放動畫,若是均不符合則continue
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//若是子view消耗了事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//若是子View消耗掉了事件,那麼mFirstTouchTarget就會指向子View。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}複製代碼
上面代碼比較清楚,首先就是遍歷ViewGroup的全部子元素,而後判斷子元素是否可以接收到點擊事件(一、是否正在播放動畫,二、點擊事件的座標是否在子元素的區域內),這裏面有個方法private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)
實際上就是調用子元素的dispatchTouchEvent方法的,方法內部有個判斷,判斷child是否爲null,若是不爲null的話,直接調用子元素的dispatchTouchEvent方法,這樣事件就交給子元素處理。完成了ViewGroup到子View的事件傳遞,當事件處理完畢,就會返回一個布爾值handled,該值表示子View是否消耗了事件。怎樣判斷一個子View是否消耗了事件呢?若是說子View的onTouchEvent()返回true,那麼就是消耗了事件。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}複製代碼
若是子元素的dispatchTouchEvent返回了true的話,那麼這個變量mFirstTouchTarget就會被賦值
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}複製代碼
上面這段代碼就是給mFirstTouchTarget賦值的,能夠看出來,這個mFirstTouchTarget實際上是一個單鏈表結構,這個值直接影響ViewGroup是否對事件的攔截,若是爲null的話,那麼ViewGroup將會默認攔截同一序列中全部的點擊事件,若是遍歷全部的元素後發現事件沒有被處理的話,那麼只有兩種狀況,一是ViewGroup沒有子元素,二是子元素處理了點擊事件,可是在dispatchTouchEvent事件中返回了false,也就是在onTouchEvent事件中返回了false,在這種狀況下,ViewGroup只能本身處理事件了。即繼續往下分析代碼能夠看出來:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}複製代碼
在上面,若是mFirstTouchTarget==null的話,就說明子view不處理該事件,那麼該事件將交給ViewGroup來處理。而若是在上面已經找到一個子View來消耗事件了,那麼這裏的mFirstTouchTarget不爲空,接着會往下執行。接着有一個if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget)判斷,這裏就是區分了ACTION_DOWN事件和別的事件,由於在在上面咱們知道,若是子View消耗了ACTION_DOWN事件,那麼alreadyDispatchedToNewTouchTarget和newTouchTarget已經有值了,因此就直接置handled爲true並返回;那麼若是alreadyDispatchedToNewTouchTarget和newTouchTarget值爲null,那麼就不是ACTION_DOWN事件,便是ACTION_MOVE、ACTION_UP等別的事件的話,就會調用下面代碼,把這些事件分發給子View。
if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
handled = true;
}複製代碼
上面這段代碼處理除了ACTION_DOWN事件以外的其餘事件,若是ViewGroup攔截了事件或者全部子View均不消耗事件那麼在這裏交由ViewGroup處理事件;若是有子View已經消耗了ACTION_DOWN事件,那麼在這裏繼續把其餘事件分發給子View處理。
以上基本就是ViewGroup的事件分發過程。看到這裏估計上面的也忘了,因此咱們就用流程圖來總結一下這個ViewGroup的事件分發過程。
借用網上的一張圖來講明一下:
ViewGroup默認不攔截任何事件,因此事件能正常分發到子View處(若是子View符合條件的話),這時候子view的dispatchTouchEvent將會被調用,從這開始就進入了view的事件分發過程。若是沒有合適的子View或者子View不消耗ACTION_DOWN事件,那麼接着事件會交由ViewGroup處理,而且同一事件序列以後的事件不會再分發給子View了。若是ViewGroup的onTouchEvent也返回false,即ViewGroup也不消耗事件的話,那麼最後事件會交由Activity處理。
若是頂級的ViewGroup攔截事件即onInterceptTouchEvent返回true的話,則事件會交給ViewGroup處理,若是ViewGroup的onTouchListener被設置的話,則onTouch將會被調用,不然的話onTouchEvent將會被調用,也就是說:二者都設置的話,onTouch將會屏蔽掉onTouchEvent,在onTouchEvent中,若是設置了onClickerListener的話,那麼onClick將會被調用。這就是ViewGroup的事件分發過程。
github: github.com/crazyandcod…
博客: crazyandcoder.github.io