在上一篇中我們一起分析了事件從手機硬件傳遞到DecroView的過程,接著本文我們一起來分析一下ViewGroup和View是怎么傳遞,處理觸摸事件的。
View的事件分發(fā)機制重要性不言而喻,面試,平時做都是經(jīng)常接觸。平時都是照著代碼寫,但是其實并不知道很多原理。比如為什么onTouch比OnClick先執(zhí)行?為什么onTouch返回true后OnClick就不再執(zhí)行?onTouch和onTouchEvent有什么區(qū)別?這些問題可以說是一直困擾著很多人。今天,我們就通過寫demo,邊看效果邊跟源碼來一個一個問題的分析。本文源碼均來自API24。文末會附加分析觸摸事件從硬件開始到最后View處理的整個流程。
秉承著前人栽樹后人乘涼的想法,首先給出自己所讀總結的精華和大神的總結圖:
在Android中,View的事件分發(fā)主要有3個方法:dispatchTouchEvent()、 onInterceptTouchEvent() 以及 onTouchEvent()。當我們點擊某個子View控件時,首先調用的是這個view的父ViewGroup的dispatchTouchEvent(),dispatchTouchEvent()內部調用onInterceptTouchEvent()來決定是否攔截該事件,如果攔截,則調用ViewGroup的onTouchEvent()進行處理,并且屏蔽掉該事件,不再往下傳遞,所以有可能會產(chǎn)生你點擊某個按鈕,按鈕的處理動作沒有發(fā)生,父控件的處理事件觸發(fā)了的情況。如果父ViewGroup不攔截事件,否則調用子View的dispatchTouchEvent(),可以參考如下圖:
出自http://blog.csdn.net/huachao1001
-------------------------------一條酷炫的分割線-----------------------------------------
總結說完了,說句實話也就只有那些以前研究過回頭來看的大神們想起來了是個什么回事,沒了解過的萌新們看了半天還是一句哈玩意兒?那么接下來我們就寫一個demo,邊看效果邊跟源代碼來分析一下。
------------------------------又是一條酷炫的分割線------------------------------------
ViewGroup
首先我們來定義一個布局,只有一個Button按鈕,外圍是一個我們自定義的Layout,我們取名為TestLayout。暫時只繼承于LineLayout,不加任何代碼。
TestLayout代碼:
public class TestLayout extends LinearLayout {
public TestLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public TestLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
Activity代碼:
public class Main6Activity extends AppCompatActivity {
Button mButton;
TestLayout mTestLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main6);
mButton = (Button) findViewById(R.id.test_bt);
mTestLayout = (TestLayout) findViewById(R.id.test_layout);
//TestLayout注冊onTouchListener
mTestLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("touchEvent", "mTestLayout的觸摸事件執(zhí)行了!執(zhí)行Action為:" + event.getAction());
return false;
}
});
//mButton注冊OnClickListener
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("touchEvent", "button的點擊事件執(zhí)行了!");
}
});
//mButton注冊onTouchListener
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("touchEvent", "button的觸摸事件執(zhí)行了!執(zhí)行Action為:" + event.getAction());
return false;
}
});
}
}
點擊按鈕運行一下看一下日志:
首先我們看到是button的onTouch方法首先執(zhí)行(有手指按下和抬起兩個事件),隨后執(zhí)行button的onClick方法。而父布局testLayout的touch事件沒有被觸發(fā)。
emmm.......
哈玩意兒?不是說好的事件先給viewGroup么?咋這都沒反應呢??那給父布局注冊touch事件干嘛啊。別忙,既然這樣,我們就去看看ViewGroup的dispatchTouchEvent()方法的源代碼(讀者如果親自跟進去就會發(fā)現(xiàn)這個方法的源碼很長很復雜,這里為了分析,分解為各個部分講解):
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...//其他代碼
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
代碼1處---------------------------
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();
}
我們將關鍵代碼貼出來,我們來開始分析一下,在上面代碼1處判斷如果是按下事件,會調用cancelAndClearTouchTargets(ev)方法,這個方法里面會把mFirstTouchTarget置空。這個變量非常重要,在后面會繼續(xù)講解。接著往下看:
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
代碼2處---------------------------
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...//其他代碼
接下來繼續(xù)分析,當如果是按下事件或者 mFirstTouchTarget 不為空的時候會調用代碼2處的onInterceptTouchEvent()方法來判斷是否攔截觸摸事件。我們跟進去看看:
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
在這里我們可以看做這個方法正常情況下是默認返回false。因此這里也說明了ViewGroup是默認不會攔截觸摸事件的。所以在之前的demo效果中自然會出現(xiàn)父布局沒有做任何處理直接將觸摸事件往下傳遞給Button的情況。如果我們重寫父布局的onInterceptTouchEvent()方法返回true。那么表明ViewGroup父布局攔截觸摸事件,事件就不會再繼續(xù)往下傳遞給子View。我們來繼續(xù)改demo看看效果:
在TestLayout中重寫onInterceptTouchEvent()方法返回false:
public class TestLayout extends LinearLayout {
...//其他代碼
//重寫該方法,返回true
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
}
我們再點擊按鈕看看效果:
如上圖,我們可以看到,當TestLayout攔截了觸摸事件時,Button的觸摸和點擊事件都沒有被觸發(fā)。因此我們可以得出結論,事件分發(fā)是從父布局向子控件傳遞的。
回到ViewGroup的代碼我們接著往下看:
代碼3處---------------------------
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) {
...//其他代碼
代碼4處---------------------------
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
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);
...//其他代碼
}
在代碼3處,會定義一個 newTouchTarget變量。接著會判斷事件是否取消并且是否打斷,如果都不。那么執(zhí)行if條件里面的代碼。
在代碼4處,會定義一個int類型的childrenCount來表示該ViewGroup有多少個子View。接下來再判斷childrenCount個數(shù)是否為0(即ViewGroup是否有子View)。如果有,那么就執(zhí)行代碼去尋找事件應該傳遞到哪個集體的子View。
繼續(xù)往下看源碼:
代碼5處---------------------------
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();
代碼6處---------------------------
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
我們可以看到在代碼5處會調用dispatchTransformedTouchEvent()方法,這個是什么方法呢?我們再跟進去看看:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...//其他代碼
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;
}
....//其他代碼
我們在這里就可以看到,當child為空時,表示沒有找到觸摸事件在某個具體子View的范圍內。此時調用super.dispatchTouchEvent()方法,而ViewGroup的父類是View對象。表明此時事件已經(jīng)交給ViewGroup自己處理。而如果child不為空,則調用子View的dispatchTouchEvent()方法來處理。當觸摸事件處理完畢,就會返回一個布爾值handled,該值表示子View是否消耗了事件。怎樣判斷一個子View是否消耗了事件呢?如果說子View的onTouchEvent()返回true,那么就是表明消耗了事件。
接下來在代碼6處,會調用addTouchTarget()方法來賦值給newTouchTarget變量。我們再跟進去看一看:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
我們可以看到,首先是找到具體的child的TouchTarget對象,然后mFirstTouchTarget指向這個child.然后返回target。
繼續(xù)回到ViewGroup的dispatchTouchEvent()方法往下看:
代碼7處---------------------------
// Dispatch to touch targets.
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;
代碼8處---------------------------
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
...//其他代碼
}
}
...//其他代碼
return handled;
}
在代碼7處會判斷mFirstTouchTarget是否為空,很明顯,在剛才的分析里可以看到,如果我們的觸摸事件沒有被攔截,就會在addTouchTarget()方法中為mFirstTouchTarget賦值,因此這段代碼就不會被執(zhí)行。只有在ViewGroup類型不攔截或者點擊了ViewGroup的空白處(沒有點擊任何子控件,事件沒有發(fā)生在任何子View的范圍內)。這兩種情況下mFirstTouchTarget不會被改變依然為null才會執(zhí)行if條件里面的代碼。換句話說反過來也就是如果mFirstTouchTarget == null條件成立執(zhí)行的這段代碼會處理ViewGroup攔截了事件或者所有子View均不消耗事件這兩種情況。那么在這里調用dispatchTransformedTouchEvent()方法交由ViewGroup處理事件。如果mFirstTouchTarget不為空,那么表明有child已經(jīng)處理了ACTION_DOWN觸摸事件,那么執(zhí)行else代碼塊的代碼。
到代碼8處,在上面的分析中可以看到,如果子View消耗了ACTION_DOWN觸摸事件,那么alreadyDispatchedToNewTouchTarget會修改為true并且target == newTouchTarget也是成立的。因此表示這是個ACTION_DOWN事件,如果有一個不成立,表明是ACTION_DOWN之外的其他事件,那么在這里繼續(xù)把其他事件分發(fā)給子View處理。
至此我們就已經(jīng)把ViewGroup的dispatchTouchEvent()方法分析完畢了??偨Y一下:
- 當事件傳遞到我們的ViewGroup時,會調用dispatchTouchEvent()方法,在里面首先會調用onInterceptTouchEvent()方法(默認為false不攔截)來決定ViewGroup是否攔截該事件。結果為兩種:1. 如果方法返回true,即要攔截,那么事件傳遞到此為止,不再傳遞給子View。2.如果放回false,不攔截事件,那么將事件傳遞給子View,由子View去進行處理。
View
上面分析完了ViewGroup的dispatchTouchEvent()源碼,接下來我們來分析分析View的dispatchTouchEvent()方法。
我們先來把demo改一下代碼,首先把之前的Button去掉,重寫一個自定義View繼承自Button重寫onTouchEvent()方法(記得把我們之前的TestLayout的攔截事件方法改為返回false哦,否則事件都被攔截了):
public class TestButton extends Button {
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public void init() {
setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("touchEvent", "button的點擊事件執(zhí)行了!");
}
});
setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("touchEvent", "button的onTouch方法執(zhí)行了!執(zhí)行Action為:" + event.getAction());
return false;
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("touchEvent", "button的onTouchEvent方法執(zhí)行了!執(zhí)行Action為:" + event.getAction());
return super.onTouchEvent(event);
}
}
布局如下很簡單:
<com.demo.dltlayoutlayoutparams.TestLayout
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:id="@+id/test_layout"
tools:context="com.demo.dltlayoutlayoutparams.Main6Activity">
<com.demo.dltlayoutlayoutparams.TestButton
android:id="@+id/test_bt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</com.demo.dltlayoutlayoutparams.TestLayout>
Activity的代碼如下:
public class Main6Activity extends AppCompatActivity {
TestLayout mTestLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main6);
mTestLayout = (TestLayout) findViewById(R.id.test_layout);
mTestLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("touchEvent", "mTestLayout的觸摸事件執(zhí)行了!執(zhí)行Action為:" + event.getAction());
return false;
}
});
}
}
我們運行demo點擊一下按鈕,看一下日志結果:
如上圖結果,我們可以看到顯然是onTouch方法先執(zhí)行,然后OntouchEvent()再執(zhí)行,然后又執(zhí)行了click()方法,因為我們手指按下后又抬起。所以有兩個事件先后發(fā)生處理。
那么為什么onTouch()方法會比onTouchEvent()方法先執(zhí)行呢?onTouch()方法返回類型為布爾值,有什么用呢?(不可能因為onTouch字母數(shù)比onTouchEvent()字母少所以先執(zhí)行把?)帶著這些疑惑,我們來一起分析分析View的dispatchTouchEvent()方法。
在這里先貼出源碼:
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
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;
}
//noinspection SimplifiableIfStatement
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
emmm...首先可以看到源碼其實包含了很多注釋了很多其他代碼來做一些判斷和處理工作,在這里我抽出核心過程代碼來看看:
//ListenerInfo是一個包裝類,里面封裝了View的各種監(jiān)聽器Listener。
//我們設置onTouchListener也是設置給ListenerInfo。
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的OnTouchListener不為空并且我們的View是可以編輯的(處于Enable狀態(tài))并且重寫的onTouch()方法返回true,那么result設置為true。那么接下來就不會再執(zhí)行onTouchEvent()方法。因此onTouch()方法比onTouchEvent()方法優(yōu)先執(zhí)行的原因在這里。我們來設置onTouch()方法返回true來看看效果:
setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("touchEvent", "button的onTouch方法執(zhí)行了!執(zhí)行Action為:" + event.getAction());
//修改前
return false;
//修改后
return true;
}
});
}
運行點擊一下按鈕:
可以看到我們onTouch方法返回true,那么就不會再執(zhí)行onTouchEvent()方法。
誒?突然發(fā)現(xiàn),怎么onClick()方法也沒了呢??我沒有改點擊事件???那么為什么我們的onTouch()方法返回true后,onClick()方法沒有執(zhí)行呢?我們跟進去onTouchEvent()方法一探究竟。
先放上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);
}
// 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
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) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
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();
}
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;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
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;
}
return true;
}
return false;
}
emmm.....好像有點長??看了真是讓人頭大。那么長的代碼。大概的我們也不可能一一去分析,那么我們就過濾掉一些無關代碼,來提取一下精華:
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 & 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();
}
}
}
.......//不重要代碼
return false;
}
經(jīng)過這么一提取,關鍵代碼就出現(xiàn)了。我們可以看到在上面的case MotionEvent.ACTION_UP情況下,我們獲取執(zhí)行performClick()方法。我們繼續(xù)跟進去查看代碼:
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;
}
至此就很清晰了,就是在這里執(zhí)行了我們在代碼中設置的onClickListener的onClick()方法。因此,如果我們的onTouch()方法返回true,那么onTouchEvent()方法也不會執(zhí)行,那么onTouchEvent()方法里的onClick()方法更不能得到執(zhí)行!
那么問題又又又來了....onTouch()返回值影響了onTouchEvent()是否執(zhí)行,那么onTouchEvent()的返回值又是拿來干嘛的?好像這個返回值又不影響點擊事件的執(zhí)行?我們來改一改試成false和true試試:
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("touchEvent", "button的onTouchEvent方法執(zhí)行了!執(zhí)行Action為:" + event.getAction());
//修改前
return super.onTouchEvent(event);
//第一次修改后
return false;
//第二次修改后
return true;
}
我們來看一下效果:
我們看到,首先修改為true或者false后,點擊事件都消失了,這個其實很好理解。因為onClick()方法之前分析過是在View的onTouchEvent()方法調用的。而我們現(xiàn)在重寫onTouchEvent()方法卻沒有調用super.onTouchEvent()方法,因此點擊事件肯定不會被調用。
其次,我們發(fā)現(xiàn),在返回false的時候發(fā)現(xiàn)onTouch和onTouchEvent()都只是處理了按下的動作,并沒有繼續(xù)處理后面的抬起動作。而且TestLayout的onTouch()抬起動作執(zhí)行了,但是也沒處理抬起動作。
其實這是因為onTouchEvent()當我們返回false,表明我們子View不處理事件,于是就會將事件全部回傳給父布局。當down事件不消耗后,后續(xù)的move,up等等事件也不再執(zhí)行onTouch()方法。因此在這里直接將事件交給父布局,而在這里我們的父布局TestLayout在Activity中設置的onTouch()方法也返回false。因此也會將事件繼續(xù)上傳,這里有興趣的同志可以把TestLayout的onTouch()方法返回true來看一下效果(文章篇幅實在太長了。。。)。
而當我們返回true就表示我們要處理事件,因此事件就不會回傳到父布局了。
------------------------------一條假裝很華麗的分割線---------------------------------
額外分析: 具體原因是在文初的ViewGroup源碼的代碼5處調用onTouch()方法,而這個代碼5處是包含在代碼4處上面的if條件里面的,條件是事件為手指按下事件或者其他事件才會執(zhí)行。然后onTouch方法中調用的onTouchEvent()方法,如果onTouchEvent()返回false,那么onTouch()方法也返回false(可看上面View的dispatchTouchEvent()源碼),那么代碼5處的條件判斷不成立,那么mFirstTouchTarget變量不會被修改仍然為null,因此在走到ViewGroup源代碼7處交給父布局處理了。而其他比如手指移動,手指抬起事件不會再走代碼4,5處。因此直接跳到代碼7處,mFirstTouchTarget仍然為null。所以說當子View不處理down事件,所有事件就不會再交給子View來處理了。換言之所以只有onTouchEvent()中第一個down事件處理返回true,才會使得mFirstTouchTarget賦值不為null。那么在代碼7處才會把后續(xù)事件交給子View處理。
------------------------------一條假裝很華麗的結束線---------------------------------
呼...至此總算就分析完了ViewGroup和View的事件分發(fā)的傳遞和回傳過程,也算是完結撒花啦!
最后總結一下View:
View的執(zhí)行順序為onTouch()->onTouchEvent()->onClick()。如果onTouch()返回true表示消耗了事件,就不再傳給onTouchEvent()執(zhí)行。(好比小明和小剛一起去買冰淇淋,小明先吃一口,他可以選擇把冰淇淋給小剛吃還是把冰淇淋留下來)
如果View只消耗down事件,而不消耗其他事件,那么其他事件不會回傳給ViewGroup,而是默默的消逝掉。我們知道,一旦消耗down事件,接下來的該系列所有的事件都會交給這個View,因此,如果不處理down以外的事件,這些事件就會被“遺棄”。
某個View,在onTouchEvent中,如果針對最開始的down事件都返回false,那么接下來的事件系列都不會交給這個View。
View沒有onInterceptTouchEvent方法。因為它不是一個父控件,不需要決定是否攔截。
再ps:本文也參考借鑒了郭霖大神和各位大神的文章:
Android事件分發(fā)機制完全解析,帶你從源碼的角度徹底理解
徹底理解View事件體系!