Android View 事件分發原始碼分析

parting_soul發表於2019-03-13

一. 概述

Android的事件分發主要有這幾個角色:Activity、Window、ViewGroup和View。當Activity接收到事件時,會將事件傳遞給Window,然後Window將事件傳遞給頂層容器DecorView(繼承自FrameLayout),事件分發由此開始。

這邊我將對DOWN、MOVE和UP事件結合原始碼單獨分析。

二. 原始碼分析

2.1 前言

首先先明確幾個概念:

  1. 同一事件序列: 由一個DOWN事件,若干個MOVE事件,一個UP事件組成
  2. 新的一個事件序列開始前會重置所有的點選狀態

當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);
}
複製程式碼

從程式碼中可以看到,Activity收到事件後將事件交由Windows處理

PhoneWindow.java

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
複製程式碼

Window會將事件交由DecorView處理

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
	 ...

	 public boolean superDispatchTouchEvent(MotionEvent event) {
	        return super.dispatchTouchEvent(event);
	 }

}
複製程式碼

由此可以看到事件傳遞給了ViewGroup,View事件分發由此開始。

2.2 DOWN事件

首先分析DOWN事件,當我們觸控手機螢幕的一瞬間,Activity接收到DOWN事件,事件由Activity傳遞到Window,再到DecorView。當DecorView接收到事件,會呼叫ViewGroup的dispatchTouchEvent方法。

由於是DOWN事件傳遞到ViewGroup,在dispatchTouchEvent方法中首先會重置觸控狀態,包括清除儲存處理事件View的單連結串列,因此每次DOWN事件代表一個新的事件序列的開始,這點之後會具體分析。

if (actionMasked == MotionEvent.ACTION_DOWN) {
    //重置所有的觸控狀態
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}
複製程式碼

由於是DOWN事件,則先會去判斷當前容器是否禁止攔截事件。預設情況下,父容器可以攔截事件,此時會呼叫onInterceptTouchEvent方法,該方法預設返回false;若父容器被禁止攔截事件,則不會呼叫onInterceptTouchEvent方法

ViewGroup#dispatchTouchEvent

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 {
    //攔截事件
    intercepted = true;
}
複製程式碼

2.2.1 父容器不攔截事件

這裡先看預設情況,事件沒有被父容器攔截,即intercepted為false。此時會去遍歷該ViewGroup的子View,尋找發生DOWN事件的View。若找到發生DOWN事件的View,將事件分發給對應的子View,若View能夠處理事件,也就是子View的dispatchTouchEvent方法返回true,則將該處理事件的View加入mFirstTouchTarget這個連結串列中,並標記當前DOWN事件已被處理。

ViewGroup#dispatchTouchEvent

 TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            //未取消未攔截

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {


                //遍歷所有的子View,尋找處理事件的View
                final View[] children = mChildren;
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = customOrder
                            ? getChildDrawingOrder(childrenCount, i) : i;
                    final View child = (preorderedList == null)
                            ? children[childIndex] : preorderedList.get(childIndex);

                        ...

                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

                        //找到處理事件的子View,儲存該子View
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        //事件已經被子View處理
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }

                }
            }
        }
複製程式碼

父ViewGroup通過呼叫dispatchTransformedTouchEvent 將事件分發給對應的子View。子View處理了DOWN事件,也就是子View的dispatchTouchEvent方法返回true,從而會使得dispatchTransformedTouchEvent方法返回true。

ViewGroup.java

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
  ...
    
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}
複製程式碼

若沒有找到能夠處理事件的子View,此時事件會交給當前ViewGroup來處理。沒有找到處理DOWN事件的子View,也就是mFirstTouchTarget這個連結串列沒有被賦值,此時為null。此時通過dispatchTransformedTouchEvent將事件傳遞給當前ViewGroup的父類,呼叫View的dispatchTouchEvent方法進行事件處理。

ViewGroup#dispatchTouchEvent

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}
複製程式碼

接下來事件傳遞到了View,下方是View的dispatchTouchEvent方法。

View.java

public boolean dispatchTouchEvent(MotionEvent ev){

        ...

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
複製程式碼

事件到了View的dispatchTouchEvent方法,先會去判斷事件是否由OnTouchListener消費掉並且View是否可用,若OnTouchListener返回true且View處於可用狀態,則表示該DOWN事件被消費掉,該DOWN事件處理結束;若事件未被OnTouchListener消費掉或者View處於不可用狀態,則將事件交由View的onTouchEvent方法

View.java

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
        	//當前View不可用
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            //若View可點選或者可長按,則事件被消費
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

    	...
        
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
                //若View可點選或者長按,則事件被消費
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
   					...
                    break;

            }

            return true;
        }

        return false;
    }
}
複製程式碼
  • 若當前View處於不可用狀態,但是View可以被點選或者長按,則該事件被消費,反之事件未被當前View消費,則將事件交由父容器處理。
  • 若當前View處於可用狀態,並且View可以被長按或者點選,事件被消費,返回事件交由父容器處理。

2.2.2 父容器攔截了事件

上述2.1.1的前提是父容器沒有攔截事件,也就是intercepted的值為false。若此時intercepted值未true(當onInterceptToucnEvent方法返回true)。此時不會去尋找處理事件的子View,也就是mFirstTouchTarget為null,同樣事件會交由ViewGroup父類的dispatchTouchEvent方法處理。

2.2.3 DOWN事件總結

Android View 事件分發原始碼分析

DOWN事件的分發流程如圖所示,總的來說可以歸納一下幾點:

  1. Activity接收DOWN事件後,將事件傳遞給Window,Window將事件分發給ViewGroup
  2. ViewGroup接收到DOWN事件,預設情況下ViewGroup會遍歷所有子View,尋找發生觸控事件的子View,若找到子View且子View消費了DOWN事件,則ViewGroup會儲存該處理事件的子View。
  3. 若ViewGroup攔截了事件,事件交由ViewGroup自己去處理,此時會呼叫ViewGroup父類的dispatchTouchEvent方法。在View處理事件時,若View可用並且OnTouchListener處理了事件,則DOWN事件被消費,DOWN事件分發結束;反之,則交由onTouchEvent方法處理DOWN事件
  4. 事件傳遞到了View的onTouchEvent方法中,只要View可點選或者長按,則事件一定被消費,反之,ViewGroup沒有處理事件,事件交由父容器處理。

2.3 UP事件

上述2.2分析了DOWN事件的分發,接下來先分析UP事件的分發。 當手機抬起螢幕的一瞬間,Activity會接收到UP事件,Activity將UP事件傳遞給Window,Window將事件傳遞給DecorView,DecorView父類ViewGroup的dispatchTouchEvent方法被呼叫。

由於dispatchTouchEvent方法接收到的是UP事件,若mFirstTouchTarget不為空,此時代表存在處理DOWN和MOVE事件的子View。mFirstTouchTarget是一個連結串列,用於儲存處理事件的子View,在DOWN事件被子View處理後在子View的父容器內被賦值,至於要用一個連結串列的原因是存在多點觸控的情況,這裡只考慮單點觸控的事件分發。

2.3.1 存在處理DOWN和MOVE事件的子View

存在處理DOWN和MOVE事件的子View,也就是mFirstTouchTarget不為空。在mFirstTouchTarget不為空的情況下,會去判斷當前容器是否禁止攔截事件。預設情況下為不攔截事件,此時會呼叫onInterceptTouchEvent方法,該方法預設返回false;

ViewGroup#dispatchTouchEvent

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 {
    //攔截事件
    intercepted = true;
}
複製程式碼
2.3.1.1 當前容器不攔截UP事件

當intercepted為false時,此時為預設情況,代表當前容器不攔截UP事件,事件被分發給儲存在mFirstTouchTarget的子View。

ViewGroup#dispatchToucnEvent

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    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;
            //將事件分發給子View
            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;
    }
}
複製程式碼

可以看到事件傳遞到了dispatchTransformedTouchEvent內部,由於child不為null,則會呼叫child的dispatchTouchEvent方法將事件分發給子View。

ViewGroup.java

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
  ...
    
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}
複製程式碼
2.3.1.2 當前容器攔截UP事件

當intercepted的值未true時,代表當前UP事件被當前容器攔截。UP事件被當前容器攔截,但是之前的DOWN和MOVE事件都被子View處理了,此時mFirstTouchTarget不為空,所以此時走else分支,取消當前的UP事件,變為CANCEL事件,往下分發或者交由自己處理,並且此時會在遍歷時清空儲存在mFirstTouchTarget中處理事件的子View,最終mFirstTouchTarget的值為空。

ViewGroup#dispatchTouchEvent

if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                //由於事件被攔截,intercepted為true,cancelChild為true,代表取消事件
                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;
        }
}
複製程式碼

由於是UP事件,最終會清除View的觸控狀態

ViewGroup#dispatchTouchEvent

if (canceled
        || actionMasked == MotionEvent.ACTION_UP
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    resetTouchState();
}
複製程式碼

2.3.2 不存在處理UP事件的子View

當mFirstTouchTarget為空時,不存在處理事件的子View,此時容器父類View的dispatchTouchEvent方法會接收到事件。

if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
    ...
}
複製程式碼

事件傳遞到View的dispatchTouchEvent方法,同樣先會去判斷事件是否由OnTouchListener消費掉,若事件被消費且View可用,則該UP事件處理結束。若事件未被OnTouchListener消費掉或者View不可用,則將事件交由View的onTouchEvent方法。

View.java

public boolean dispatchTouchEvent(MotionEvent ev){

        ...

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
複製程式碼

事件傳遞到View的onTouchEvent中,可以看到在UP的時候在條件滿足的情況下會執行單擊事件,同時事件被消費。

View.java

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        //View不可用,但View可單擊或者長按,同樣可以消費事件
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        //View 可點選或者長按
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    .. 

                    if (!mHasPerformedLongPress) {
                        removeLongPressCallback();
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            //執行單擊事件
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }

                }
                break;

        }

        return true;
    }

    return false;
}
複製程式碼

UP事件交給ViewGroup自己處理時,除了在UP事件會在條件滿足下觸發單擊事件和事件未被消費時會交給Activity處理,其餘的流程和ViewGroup處理DOWN事件類似。

2.3.3 UP事件總結

Android View 事件分發原始碼分析
UP事件的分發流程如圖所示,可以歸納為以下幾點:

  1. Activity收到UP事件後,會將UP事件傳遞給Window,然後Window會將事件分發給ViewGroup。
  2. ViewGroup接收到UP事件後,若存在處理DOWN和MOVE事件的子View,則會去判斷當前ViewGroup是否要攔截UP事件,預設情況下為不攔截;若不存在處理事件的子View,則表示該事件由ViewGroup自己處理。
  3. 當存在處理事件的View時,也就是mFirstTouchTarget這個連結串列不為空,則從中取出儲存的View,將事件分發給該子View。
  4. 當不存在處理事件的View時,事件會由ViewGroup自己處理。當父類View處理事件時,若View可用且OnTouchListener消費了事件,則UP事件被消費;反之將UP事件交由onTouchEvent方法處理。
  5. 在onTouchEvent方法中,只要當前View是可單擊或者可長按,則UP事件一定會被消費;反之,雖然事件往上傳遞,但父容器不會去處理事件,事件會交由Activity處理。
  6. 在onTouchEvent消費UP事件之前,在條件滿足的情況下會觸發單擊事件。

2.4 MOVE事件

Android View 事件分發原始碼分析
MOVE事件和UP事件流程類似,略微有些小差別,這邊不再過多闡述,流程如圖。好了,View事件分發機制的原始碼分析到這裡就結束了,下一篇將介紹滑動衝突解決方式以及原理。

相關文章