對於Android開發者來講,自定義View是必須攻克的一關,也是從初級工程師邁向高級的進階關卡,要想經過此階段,除了必須掌握View的測量、繪製、滑動等基礎知識外,更要掌握View的核心知識點:View的事件分發,本篇就一塊兒從源碼的角度分析View和ViewGroup的事件分發機制;bash
在咱們平時的使用或寫自定義View時,都會直接或間接的使用View的事件分發,View的事件分發主要與View源碼中的3個方法有關:併發
下面咱們針對這三個方法從源碼學習和分析事件的分發,一塊兒從本質上掌握View是如何在層層傳遞和消耗事件;ide
public boolean dispatchTouchEvent(MotionEvent 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;
}
}複製代碼
上面代碼是dispatchTouchEvent()中的部分代碼,也是與咱們使用最接近的核心代碼,首先會判斷View是否設置觸摸監聽mOnTouchListener,若是設置則會調用OnTouchListener.onTouch()方法,若是此方法返回true,則dispatchTouchEvent()返回true即攔截事件,若onTouch()返回false,則調用onTouchEvent(),若是onTouchEvent()返回true則事件被消耗,不然事件繼續傳遞;從上面的方法和敘述咱們能夠得出如下結論:源碼分析
1.一、onTouchEvent()源碼分析佈局
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN; // 設置mPrivateFlags3爲FINGER_DOWN標記
}
mHasPerformedLongPress = false; //設置false表示此事還未出髮長按事件
boolean isInScrollingContainer = isInScrollingContainer(); // 調用父容器的shouldDelayChildPressedState(),默認true
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED; // 狀態設置爲中間狀態PFLAG_PREPRESSED
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); // 延時發送執行CheckForTap中的run(),ViewConfiguration.getTapTimeout() = 100ms
} else {
setPressed(true, x, y);
checkForLongClick(0, x, y); // 直接檢測長按事件
}
//CheckForTap中調用檢測長按事件
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);//調用長按檢測方法
}
// checkForLongClick中延時發送CheckForLongPress實例
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset); // getLongPressTimeout()爲 500ms(系統默認的長按時間)
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) { //
mHasPerformedLongPress = true; // 設置標誌表示觸發長按;此標誌是否爲true取決於li.mOnLongClickListener.onLongClick的返回值
}
}
}
//在performLongClick()中代碼會最終調用performLongClickInternal()
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this); //調用長按監聽中的onLongClick();返回值影響mHasPerformedLongPress
}複製代碼
以上代碼是View的onTouchEvent()的ACTION_DIOWN執行邏輯,只粘貼了部分關鍵代碼,所執行邏輯如上面註釋,下面咱們逐步分析如下:post
if (!pointInView(x, y, mTouchSlop)) { //判斷手指是否劃出View範圍
removeTapCallback(); // 移除CheckForTap事件
removeLongPressCallback(); // 移除長按檢測事件
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}複製代碼
在Action_Move事件中,主要根據手指滑動的座標判斷是否移除View的範圍,若移除則取消和移除CheckForTap事件學習
if (!clickable) { // 若是步可點擊移除全部的事件檢測
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { //若是已經出發長按事件,且mHasPerformedLongPress設置爲true則不去執行單擊
if (mPerformClick == null) {
mPerformClick = new PerformClick(); //建立PerformClick檢測單擊事件,最終調用 performClick();
}
if (!post(mPerformClick)) { //發送失敗直接調用performClick()
performClick();
}
}
public boolean performClick() {
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this); // 調用onClick方法
result = true;
} else {
result = false;
}
}複製代碼
在手指擡起時View執行如下操做:ui
public boolean performClick() {
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this); // 調用onClick方法
result = true;
} else {
result = false;
}
}複製代碼
這個方法看起來是否是很面熟,和上面判斷onTouch()的基本一致,首先判斷View是否設置了OnClickListener事件監聽,若設置則調用onClick()方法,此時result返回true表示消耗事件,因此咱們設置的onClick的監聽等級較低,按照事件分發邏輯看,處理咱們觸摸事件的方法按優先級以此爲:onTouch() -> onTouchEvent() -> onClick();this
View的事件傳遞到此就結束了,下面看看比他更復雜的、它的父類ViewGroup的事件分發;spa
前面分析了View的事件分發,但在實際開發過程當中真正要使用View事件分發時,基本都是由於ViewGroup的嵌套致使的內外滑動問題,因此對ViewGroup的事件分發更須要深刻了解,和View的事件分發同樣,ViewGroup事件分發同樣與幾個方法有關:
使用一段僞代碼來表述上面三個方法在ViewGroup事件分發中的做用,代碼以下:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else{
consume = child.dispatchTouchEvent(event);
}
return consume;
}複製代碼
從上面代碼中看出,事件傳遞到ViewGroup時首先傳遞到dispatchTouchEvent(MotionEvent event)中,而後執行如下邏輯,首先在ViewGroup.dispatchTouchEvent() 中調用onInterceptTouchEvent() 方法:
在onInterceptTouchEvent() 返回false時,代表當前ViewGroup不消耗事件,此事件會向下傳遞給子View,此子View多是View也多是ViewGroup,若是是View則按照上面的事件分發消耗事件;
事件的傳遞首先是從手指觸摸屏幕開始,因此咱們先查看dispatchTouchEvent()中的ACTION_DOWN方法,剔除剩餘複雜的邏輯,方法有一段主要的代碼:
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 返回true表示子View設置了父容器不攔截事件
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}複製代碼
上述代碼雖然簡單但ViewGroup的事件分發多半與此處的邏輯有關,裏面的每一個細節都會影響到最終的事件消耗,總結上面代碼執行以下:
在上述代碼中除了MotionEvent.ACTION_DOWN和mFirstTouchTarget != null條件以外,還有一個會影響到onInterceptedTouchEvent()的調用,就是(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0,這裏主要是用於在子View中設置父容器的攔截條件(多用於滑動衝突),先看如下FLAG_DISALLOW_INTERCEPT這個標識爲:
看一下requestDisallowInterceptTouchEvent()方法源碼:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // 狀態相等時無需設定
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT; // mGroupFlags = FLAG_DISALLOW_INTERCEPT
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; // mGroupFlags = 0;
}
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}複製代碼
上面代碼中mGroupFlags初始值爲0,FLAG_DISALLOW_INTERCEPT初始值爲0x80000,在方法中根據參數boolean設置mGroupFlags的值:
當傳入disallowIntercept爲true時,mGroupFlags = mGroupFlags | FLAG_DISALLOW_INTERCEPT = 0x80000;此時在dispatchTouchEvent()中 知足(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 即disallowIntercept = true,因此intercepted 直接返回false,不攔截事件
當傳入disallowIntercept爲false時,mGroupFlags = mGroupFlags & ~FLAG_DISALLOW_INTERCEPT = 0;此時在dispatchTouchEvent()中 不知足(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 即disallowIntercept = false,因此回調onInterceptTouchEvent(),父佈局有機會攔截事件
總結一句話就是在requestDisallowInterceptTouchEvent()中設置true,表示不容許父容器攔截事件,設置爲false,表示容許父容器攔截事件;
既然上面全部的條件都在判斷是否須要調用onInterceptTouchEvent(),說明事件最後的攔截取決於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,即父容器不攔截任何事件
}複製代碼
if (!canceled && !intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) { //循環檢測每一個子View
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
…...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) { //檢測當前座標是否超出View的範圍,若超出跳過此view
ev.setTargetAccessibilityFocus(false);
continue;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //調用dispatchTransformedTouchEvent方法
…...
newTouchTarget = addTouchTarget(child, idBitsToAssign); // addTouchTarget中賦值mFirstTouchTarget指向child
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
//dispatchTransformedTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event); // 若是child == null,直接調用super.dispatchTouchEvent,ViewGroup本身處理
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event); // 若是存在child,調用child.dispatchTouchEvent(event)
event.offsetLocation(-offsetX, -offsetY);
}
}複製代碼
上面代碼爲ViewGroup的dispatchTouchEvent()中的部分代碼,也是控制ViewGroup的事件傳向子View的傳遞,一塊兒來看一下執行邏輯:
在dispatchTransformedTouchEvent()中根據子View判斷執行,若是child == null則直接調用super.dispatchTouchEvent,ViewGroup本身處理,若是存在child,調用child.dispatchTouchEvent(event),則事件傳遞到View,接着剛纔的代碼向下看,當dispatchTransformedTouchEvent()返回true時,代碼會執行到addTouchTarget(child, idBitsToAssign)方法:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}複製代碼
在addTouchTarget()方法中將mFirstTouchTarget指向子View,因此上面的判斷mFirstTouchTarget != null在子View攔截事件時成立;
到View的onTouchEvent()返回true即表示事件被View消耗,事件的分發也到此結束了,可有沒有考慮過最上層的子View的onTouchEvent()若是不攔截事件呢?最終的事件會去哪呢?答案是要被Activity的onTouchEvent()消耗,咱們知道當一個事件產生時最早獲取的是Activity,而後按照Activity -》Window -》ViewGroup -》View這樣的順序傳遞下去,而在ViewGroup中子View的返回值是在dispatchTransformedTouchEvent()中獲取的,查看代碼:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;複製代碼
在dispatchTransformedTouchEvent()中若返回false,程序會執行到如下邏輯:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} 複製代碼
經過上面的學習咱們知道mFirstTouchTarget是指向消耗事件的子View,但當子View不消耗時此時mFirstTouchTarget == null成立,代碼會再次調用dispatchTransformedTouchEvent()方法,此時傳遞的child爲null,經過上面的代碼咱們知道child = null時代碼執行super.dispatchTouchEvent(event),即調用父類的dispatchTouchEvent(event),由於ViewGroup本質上也是繼承View,只不過是包含子View的View,因此事件的傳遞又到了上層View中,在View的dispatchTouchEvent()會詢問onTouch()和onTouchEvent()方法,因此事件又被向上傳遞了;
但若是全部的ViewGroup和子View都不消耗事件,事件會逐層向上傳遞知道事件的開始,也就是Activity層,這時咱們點開Activity的dispatchTouchEvent()方法,
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}複製代碼
從代碼中能夠看出系統調用getWindow().superDispatchTouchEvent(ev)進行事件分發,其實就是向Window和ViewGroup進行事件的傳遞,如有消耗事件的這裏返回true方法結束,若沒有View消耗事件即getWindow().superDispatchTouchEvent(ev)返回false,系統會調用Activity的onTouchEvent()處理事件,因此事件必定會被消耗掉,到此針對View不消耗事件的分析就結束了,咱們也能夠得出如下結論:
關於ViewGroup的事件分發的基本知識和源碼分析到這裏就介紹完了,可能直接理解會比較抽象,下面咱們具體的看一下是如何控制和攔截事件的;
根據上面的View和ViewGroup的事件分發學習,這裏給出幾個View事件傳遞的結論(如下結論針對系統自動分發),並根據學習內容進行逐條分析
事件攔截最經典的使用示例和場景就是滑動衝突,按照View的衝突場景分,滑動衝突能夠分爲3類:
通常處理滑動衝突有兩種攔截方法:外攔截和內攔截
外攔截顧名思義是在View的外部攔截事件,對View來講外部就是其父類容器,即在父容器中攔截事件,經過上面的代碼咱們知道,ViewGroup的事件攔截取決與onInterceptTouchEvent()的返回值,因此咱們在ViewGroup中重寫onInterceptTouchEvent()方法,在父類須要的時候返回true攔截事件,具體須要的場景要按照本身的業務邏輯判斷:
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercept = false
when(ev!!.action){
MotionEvent.ACTION_DOWN ->{intercept = false}
MotionEvent.ACTION_MOVE->{
intercept = if (isNeed()){
true
}else{
false
}
}
MotionEvent.ACTION_UP->{intercept = false}
}
return intercept
}複製代碼
從上面代碼中看出:在onInterceptTouchEvent()的ACTION_DOWN中必須返回false,即不攔截ACTION_DOWN事件,由於若是ACTION_DOWN一但攔截,事件後面的事件都會默認給ViewGroup處理,也不會再調用onInterceptTouchEvent()詢問攔截,那子View將沒有獲取事件的機會;在ACTION_DOWN中,根據本身須要的時候返回true,那此時事件就會被父ViewGroup消耗
內攔截是在View的內部控制父容器是否攔截事件,你可能已經想到了就是使用上面介紹的requestDisallowInterceptTouchEvent(),答案沒錯就是利用這個方法,關於使用這個方法去控制mGroupFlags的值上面已經介紹了,下面咱們分析下爲什麼設置此數據來控制ViewGroup的事件攔截:
由於事件的攔截是在onInterceptTouchEvent()中肯定的,咱們不可能在子View中控制父容器的方法,但從上面的代碼中看出,ViewGroup訪問onInterceptTouchEvent()以前必須經過一段關卡,就是(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 要成立,而若是此條件不成立,那dispatchTouchEvent()會直接返回false,因此咱們在子View中只要控制這個值就能夠了;
到此雖然能夠控制訪問權限,但如何確保只要在容許訪問的時候就會自動攔截呢?那就是onInterceptTouchEvent()要在特定狀態下一直返回true,即默認想攔截事件 ,綜上所述咱們在子View中要想控制父容器必須知足如下條件:
上面的事件分發,其實和公司安排任務同樣,當一項任務來臨時,公司會開會進行任務安排,你可能作好了承擔一切任務的準備,但大領導不詢問你,整個事件就會按照領導的意見進行安排,忽然在某個任務時大領導問了你願不肯意接,這時你提出了確定的答覆,而後事情就歸你了 ,固然幹好幹很差就是你的問題了,攔截的狀況和這個例子同樣,下面看下攔截的代碼:
//在子View中重寫dispatchTouchEvent()方法控制父類的攔截
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
y = event.getY();
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float currentY = event.getY();
int minTouchSlop = 150;
if (Math.abs(currentY - y) >= minTouchSlop) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
//在ViewGroup中攔截除ACTION_DOWN之外的事件
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercept = false
when(ev!!.action){
MotionEvent.ACTION_DOWN ->{intercept = false}
else -> {intercept = true}
}
return intercept
}複製代碼
到此View和ViewGroup的事件分發和事件滑動衝突的處理到此介紹完畢了,雖然很早以前就學習過這部分的內容,但並無很好的整理這部份內容,本身寫一遍會對整個只是點更加詳細的理解,相信在開發過程當中不少人都被滑動衝突困擾過,尤爲對初級開發者,那段痛苦是必須通過的,因此只有熟悉View和ViewGroup的事件分發邏輯,才能從根本上解決實際開發中的問題