引言
Android 事件分發網上有很多資料,大部分都是在dispatchTouchEvent() onInterceptTouchEvent() onTouchEvent()三個方法中列印Log日誌,草草的得出,各個方法中返回true/false會呼叫哪個方法,結論良莠不齊。沒有從根本上理解這一塊的實現機制,實際運用的時候 還是一陣懵逼。從原始碼分析,吃透其中的程式碼邏輯才能靈活運用解決實際問題,廢話就講這麼多,進入主題。
View中的事件分發
系統預設情況下,View作為事件分發消費的終點,我們就先看下原始碼裡view在接受到事件時候怎麼處理的,view中跟事件分發相關的主要是兩個方法,一個是dispatchTouchEvent(),一個是onTouch(),先看View中dispatchTouchEvent()怎麼處理的;
dispatchTouchEvent()、onTouchEvent()返回值作用
- 1、public boolean dispatchTouchEvent():預設情況下,這個方法就是把事件分發給目標view,而且這個目標view可能是自己;如果返回true就表示找到了要消費事件的目標view,而且事件被消費了 如果返回false就表示沒有找到
- 2、onTouchEvent():這個方法是處理消費觸控事件的 如果返回true表示這個事件被消費了 如果返回false 表示沒有被消費
- 3、注意 手指觸碰螢幕一個完整的觸碰動作包含最重要的三個事件:按下、滑動和抬起。View(ViewGroup)在做事件分發的時候,最開始接受到的是按下,當按下(ACTION_DOWN)如果沒有分發成功也就是沒有找到要消費的這個事件的view時,把整個觸碰事件的ACTION_DOW以後的ACTION_MOVE和ACTION_UP就不會做分發了
事不宜遲上程式碼,關鍵的地方加了我騷氣的中文註釋
View中的dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//Flag1:如果設定了OnTouchListener監聽且onTouchListener監聽中的onTouch()方法為true把result設定為true
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//Flag2: 如果上一步判斷的結果為false,則進入執行view自身的onTouchEvent(MotionEvent e)方法,
將result設定為true 如果onTouchEvent(e)方法返回true
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}複製程式碼
認閱讀上面這段程式碼,可以知道;
- 通常情況下當我們給view設定了OnTouchListener也就是觸控監聽的時候會先執行我們的觸控監聽中的onTouch()方法
- 並且當onTouch()方法返回false的時候才去執行view的onTouchEvent(),也就是說當onTouch()方法返回true的時候就onTouchEvent方法就不執行了;
View中的onTouchEvent()
簡單分了View的onDispatchTouchEvent方法之後,我們看你下View的onTouchEvent方法
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
//Flag3: disabled view 並且是clickable的情況下依然會消費這個事件,只是不做處理。
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//Flag4
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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();
}
//Flag4:
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
//Flag5
return true;
}
return false;
}複製程式碼
關鍵點:
- 敲黑白注意Flag4和Flag6處,當view為可點選的狀態下,View直接會返回true
- Flag6處 當為手勢為抬起的時候會發生點選事件,結果View的dispatch方法我們知道,如果View設定了OnTouchListener並且在ontouch()中返回了true view的點選監聽事件就會失效
- 這時候再看Flag3處當View為DISABLED的狀態下,如果View是CLICKABLE或者LONGCLICKABLE 就直接消費返回true ,也就是說當View為DISABLED狀態下設定的OnClickListener點選監聽會失效
- 特別說明:當onTouchEvent返回true的時候就意味著,事件的分發找到了要消費他的地方,也就是本View,所以按下以後的滑動抬起這些動作都統統交到這裡來消費;如果返回false就表示本View大人不消費你,以後就不要來送事件了。
ViewGroup中的事件分發消費
ViewGroup中的dispatchTouchEvent()
程式碼比較長,方便檢視裡面的邏輯,做了適當的刪除,關鍵地方加了註釋
(程式碼來源android-24)
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 首先判斷是否對事件進行攔截 ,也就是給intercepted賦值
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//如果允許攔截,就交給intercepted()判斷
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
//如果不允許攔截,設定為false
} else {
intercepted = false;
}
} else {
// 如果ev不是ACTION_DOWN型別而且也沒有找到消費目標 就直接返回true
intercepted = true;
}
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//如果沒有攔截事件是,就去找要消費這個事件的view,並把它放在newTouchTarget中
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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);
newTouchTarget = getTouchTarget(child);
//找了的事件分發的目標 跳出迴圈
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
//事件分發給child,如果child消費,並把newTouchTarget賦值給,mFirstTouchTarget跳出迴圈
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
//
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
}
}
}
//如果沒有找到消費目標,就把該ViewGroup當做一個View處理,因為ViewGroup是View的子類,
//也就是說執行super.dispatchTouchEvent();也就是View的dispatchTouchEvent();
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;
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;
}
}
return handled;
}複製程式碼
關鍵點
- disallowIntercept 這個引數預設是false 表示允許攔截事件,若果為true表示不需要攔截攔截事件, 可以通過requestDisallowInterceptTouchEvent()進行設定,具體應用:ViewGroup的子view通過呼叫這個方法可以告訴他說大哥這個事件別攔截了交給我做吧
- 是否攔截這個決定權交給ViewGroup自己的onInterceptTouchEvent()做判斷
- 如果攔截了就直接交給 super.dispatchTouchEvent(event)進行消費
- 如果沒有攔截就找到自己要”符合條件”的子View進行事件的分發,如果事件沒有分發出去也就是自己view的dispatchTouchEvent()返回了false 或者沒有找到子View做也會呼叫super.dispatchTouchEvent()處理
- 畫圖
ViewGroup中的InterceptTouchEvent()
pubic boolean onInterceptTouchEvent(){
return false;
}複製程式碼
預設情況下 返回false
Activity中的事件攔截和分發
- Q1:嚴格的講,Activity不是View當然也不是ViewGroup,他的事件分發是消費怎麼執行的呢?
- Q2:為什麼ac沒有onInterceptTouchEvent()?
首先第一個問題:原始碼中找答案
Activity中dispatchTouchEvent():
public boolean dispatchTouchEvent(MotionEvent ev) {
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}複製程式碼
PhoneWindow中superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}複製程式碼
DecorView
private final class DecorView extends FrameLayout{
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}複製程式碼
可見Activity的本質使用的View的dispatchTouchEvent();第二個問題迎刃而解View的dispatchTouchEvent()不需要onInterceptTouchEvent().
應用
經常遇到的問題
- 為什麼只接受到了ACTION_DOWN 後續事件接收不到
示例程式碼
//自定義ViewGroup
class MyViewGroup extends ViewGroup{
public boolean dispatchTouchEvent(MotionEvent e){
Log.d("李不凡","MyViewGroup --- dispatchTouchEvent --- "+e..getAction());
return super.dispatchTouchEvent(e);
}
public boolean onInterceptTouchEvent(MotionEvent e){
Log.d("李不凡","MyViewGroup --- onInterceptTouchEvent --- "+e..getAction());
retrun super.onInterceptTouchEvent(e)
}
public boolean onTouchEvent(MotionEvent e){
Log.d("李不凡","MyViewGroup --- dispatchTouchEvent --- "+e..getAction());
retrun super.onTouchEvent(e)
}
}
//自定義View
class MyViewGroup extends TextView{
public boolean dispatchTouchEvent(MotionEvent e){
return super.dispatchTouchEvent(e);
}
public boolean onTouchEvent(MotionEvent e){
retrun super.onTouchEvent(e)
}
}
//xml佈局
<MyViewGroup
android:background="#ffffff"
android:layout_width="match_parent"
android:layout_height="300dp">
<MyView
//--android:clickable="true"
android:background="#ff0000"
android:text="MyButton"
android:textSize="20sp"
android:textColor="#00ff00"
android:id="@+id/my_view"
android:layout_width="match_parent"
android:layout_height="100dp" />
</MyViewGroup>複製程式碼
現象
執行上面這段程式碼會發現,我們做個點選事件,檢視日誌,在ViewGroup 和View中只接受到了ACTION_DOWN 而沒有接收到後續的事件
原始碼
從上面的原始碼分析我們知道 預設情況下 事件分發過程的如果ViewGroup沒有攔截(也就是onInterceptTouchEvent()返回值為false)就會交給子view去做分發,執行ViewGrop的返回值就交給子view的dispatchTouchEvent()的返回值決定 ,預設情況下,view中的onTouchEvent()返回值為false ,導致ViewGroup交給自己的onTouchEvent去做處理預設也是false。最終的結果就是viewgroup的dispatch返回值為false,這個結果就說明viewgroup自己的和子view都不消費這個事件 所以後續的ACTION_MOVE 、ACTION_UP等事件都接收不到
解決
-
-
MyView中重寫onTouchEvent()方法返回true。如下
public boolean onTouchEvent(MotionEvent e){ //super.ononTouchEvent(e) return true; }複製程式碼
注意:如果這樣操作會雖然達到了接收完整事件的目的但MyView身上設定的點選事件會失效。why?提示:遮蔽了super.ononTouchEvent(e);這行程式碼導致的後果
-
-
- 把MyView設定成onclickable可點選。原始碼依據如下:
//View中onTouchEvent原始碼(有化簡)
public boolean onTouchEvent(){
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
break;
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
retrun false;複製程式碼
需要注意的是:如果我們的View為DISABLED且ONCLICKABLE的時候事件也會被消費但不會做處理
-
- 給MyView設定OnTouchListener監聽並在onTouch()方法中返回true,依據參照上面的View中dispatchTouchEvent()分析,這種情況也會導致onTouchEvent不會執行,所以如果給view設定點選監聽也會失效。
下集預告
- 怎麼解決事件衝突
- 自定義一個簡單的Viewpager
- RefreshSwipLayout怎麼進行事件消費和分發的