關於View的事件分發,實質就是關於MotionEvent時間的分發
再簡單點說就是通過一堆判斷,最後決定這個MotionEvent給誰用的問題。
三巨頭
分發過程中有主要涉及到三個人:
dispatchTouchEvent()
,onInterceptTouchEvent()
,onTouchEvent()
這三者的關係如下public boolean dispatchTouchEvent(MotionEvent ev) { boolean belongToMe=false; if(onInterceptTouchEvent(ev)){ belongToMe=onTouchEvent(ev); }else{ belongToMe=child.dispatchTouchEvent(ev); } return belongToMe; }
但有點擊事件產生的時候, dispatchTouchEvent被調用,然後給onInterceptTouchEvent看下要不要攔截,攔截下來的就調用onTouchEvent處理下。如果不攔截,就傳遞給子view去做,重複這個流程。
不過需要說的是,這個onInterceptTouchEvent()是ViewGroup的,View裏面沒有這個。
另外這個事件還受OnTouchListener
這個的影響,如果我們設置了監聽,且他的onTouch()
事件返回真,那麼事件是不會發到onTouchEvent
裏面去的。即前者有更高的優先級。傳遞順序
事件的傳遞順序是從Activity傳起,最後到我們的各種View裏面去的,即使父傳給子的關係。
如果傳到底部的onTouchEvent
也沒有人出來處理這個MotionEvent的話,最終這個事件會像遞歸一樣,跑回來Activity,然後他的onTouchEvent
函數被調用.
即:MotionEvent—->Activity->widnow->DecorView->ViewGroup->View->ViewGroup->DecorView->window->Activity;
就像下面這樣的:
起航
API:23
我們看下我們的Activity的處理
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
他會調用window的時間分發,把時間分發下去,如果返回的是false,再調用回自己的onTouchEvent()
。
這裏的getWindow()返回的是Windows類,一個抽象類,他的具體實現是PhoneWidnow
看下我們的PhoneWindow裏面寫的內容:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
跑去了mDecor即DecorView裏面去了
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
這DecorView是PhoneWindow裏面的一個內部類,繼承FrameLayout。
我們在Activity裏面通過setContentView(R.layout.activity_main);
來設置我們的界面,而這個函數生成的View,即我們的界面是他的子View。
所以他的分發事件我們看下,是直接調用super的。這樣再去看頂部的那張圖1
,ViewGroup下面一堆的View。就可以知道事件最後會分發到我們的View去。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
好了,我們繼續看下那個ViewGroup裏面的內容
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//1. Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//2. 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;
}
//3.分發事件
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 (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
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);
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();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
...
return handled;
}
這個過程真的挺長的,一百多行,不過在看多了AMS裏面的內容,這個也就一般般的感覺了。
我們慢慢說起,
首先第一步。
我們看下第二個函數裏面的內容private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; }
這裏他會去設置一個
FLAG_DISALLOW_INTERCEPT
的標記,關於他,真的是看得好累啊。
下次補充。可以看下這篇文章看下攔截裏面的內容
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; }
他判斷這個事件是否爲
Action_Down
或者mFirstTouchTarget != null
來進一步選擇是否要攔截。
前面的判斷條件好理解,後面這個mFirstTouchTarget
表示的意思是, 當事件由子View成功處理後,mFirstTouchTarget會被賦值並指向childView,就是說,如果這時間被childView處理了,這標記就不是空,因此ViewGroup不再做攔截,並且事件將繼續默認都交給這個ChildView。因此這個onInterceptTouchEvent()
並不是每次都回被調用,雖然我開頭那樣寫,看起來像每次都要攔截的樣子。事件分發
在事件分發部分的內容,他先看下這個Child是在播動畫,或者這個child的區域在不再這個Event的範圍內的,不在範圍就不發給這個child。if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; }
如果沒播而且在這個範圍內,就發送事件給她
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
具體的內容是調用他的
dispatchTouchEvent()
,就像下面代碼一樣。private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } ... }
如果這個處理返回的handled是 true,那麼我們看到下面的內容:
newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break;
他標記新的touchTarGet,然後退出循環,另外在addTouchTarget()函數裏面
private TouchTarget addTouchTarget(View child, int pointerIdBits) { TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
我們看到了
mFirstTouchTarget = target
這句話,前面我們在攔截的時候,有用到這個作爲一個判斷條件!判斷是否要對事件攔截。
對於循環一圈分發完後,如果都沒人處理的話,即沒有一個ChildView或者ChildView返回了false的情況。// Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }
這時候我們的ViewGroup就自己處理了。這個
dispatchTransformedTouchEvent()
我們前面有提到,因child參數被設成null,我們知道他會調用handled = super.dispatchTouchEvent(event);
這句。
這句跑去調用的就是View的dispatchTouchEvent()去了。
小結:
這裏我們可以做個簡單的總結,當我們ViewGroup在分發事件的過程中,如果自己的childView沒一個處理好了事件,那麼這事件會從ViewGroup轉到View去分發。
前進 —— View的事件處理
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
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;
}
}
...
return result;
}
我們截取重要部分。
他看下我們有沒設置onTouchListener
,如果有調用,並且如果返回的是true,那麼就結束了,不會再去調用onTouchEvent
了,沒有的話纔去調用onTouchEvent。
這個onTouchevent還是挺長的,基本都是對event的Action()做處理,爲何不分割成幾個小函數呢,不就容易看多了。
哎,這裏弄個大概的樣子,方便掌握整體,清楚順序邏輯。
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
我們來看下開頭的
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
這裏說的內容是,當我們設置我們的View是Disabled的狀態,不過還ClickAble的話,就消耗掉事件。
這裏補充一點:
我們平常的LONG_CLICKABLE默認是false,而CLICKABLE就分情況了,例如那個Textview就默認是false。Button默認是true。有時我習慣用Textview來替代Button做一些事,所以老要加這個熟悉的設置….
接下來就到了一句有趣的了,如果我們給View設置了代理,就調用我們的代理 onTouchEvent()
去幹活。
這麼久都沒有設置過view.setTouchDelegate()
有點意思,查了下,可以用來擴大觸摸點擊區域
接着看下面的內容
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
...
}
return true;
}
return false;
我們的View有一個特效,只要是可點擊的狀態,不管你是不是Enable,都能消耗掉MotionEvent!
我們看下其中的一個case情況
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
...
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
...
removeTapCallback();
}
break;
這裏面說了一件重要的事,當我們申起手的時候,會觸發點擊事件。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
好啦,到這裏,我們的事件基本就處理完了,從Activity到最後我們的View的過程。
不過還是有一些內容沒說,下次有空記得再補充吧!
後記
在這個過程看到了關於Touch事件的委託。
Window類的具體實現的PhoneWindow,和裏面的DecorView.
重要的是其中我們熟悉的每次設置界面都調用的函數,看來下次的目標就是PhoeWindow咯。
參考資料:
FLAG_DISALLOW_INTERCEPT: 探究requestDisallowInterceptTouchEvent失效的原因