在上一篇文章中咱們介紹了 View 的基礎知識以及 View 滑動的實現,本篇將爲你們帶來 View 的一個核心知識點 事件分發機制。事件分發機制不只僅是核心知識點也是 Android 中的一個難點,下面咱們就從源碼的角度來分析事件的傳遞還有最後是如何解決滑動衝突的。java
在介紹事件傳遞規則以前,首先咱們要明白要分析的對象就是 MotionEvent , 關於 MotionEvent 在上一篇文章介紹滑動的時候我們已經用過了。其實所謂的點擊事件分發就是對 MotionEvent 事件的分發過程,點擊事件的分發過程由三個很重要的方法共同完成,以下:android
1. dispatchTouchEvent(MotionEvent ev)git
用來進行事件的分發。若是事件可以傳遞給當前的 View ,那麼此方法必定會被調用,返回結果受當前 View 的 onTouchEvent 和下級 View 的 dispatchTouchEvent 方法的影響,表示是否消耗當前事件。github
2. onInterceptTouchEvent(MotionEvent ev)ide
在上述內部方法調用,用來判斷是否攔截某個事件,若是當前 View 攔截了某個事件,那麼在同一個事件序列中,此方法不會被再次調用,返回結果表示是否攔截當前事件。函數
3. onTouchEvent(MotionEvent ev)源碼分析
在第一個方法中調用,用來處理點擊事件,返回結果表示是否消耗此事件,若是不消耗,當前 View 就沒法再次接收到事件。post
下面我畫了一個圖來具體說明下上面 3 個方法之間的關係動畫
也能夠用一段僞代碼來講明,以下:ui
fun dispatchTouchEvent(MotionEvent ev):Boolean{
var consume = false
//父類是否攔截
if(onInterceptTouchEvent(ev)){
//若是攔截將執行自身的 onTouchEvent 方法
consume = onTouchEvent(ev)
}else{
//若是事件在父類不攔截,將繼續分發給子類
consume = child.dispatchTouchEvent(ev)
}
reture consume
}
複製代碼
上圖跟僞代碼意思同樣,特別是僞代碼已經將它們三者的關係表現得很是到位,經過上面僞代碼咱們能夠大體瞭解到點擊事件的一個傳遞規則,對應一個根 ViewGroup 來講,點擊事件產生後,首先會傳遞給它, 這時它的 dispatchTouchEvent 就會被調用,若是這個 ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要攔截當前事件,接着事件就會交給這個 ViewGroup 處理,即它的onTouchEvent 方法就會被調用;若是這個 ViewGroup 的 onInterceptTouchEvent 返回 false 就表示它不攔截當前事件,這時當前事件就會傳遞給它的子元素,接着子元素的 dispatchTouchEvent 方法就會被調用,如此反覆直到事件被最終處理。
當一個 View 須要處理事件時的調用規則,以下僞代碼:
fun dispatchTouchEvent(MotionEvent event): boolean{
//1. 若是當前 View 設置 onTouchListener
if(onTouchListener != null){
//2. 那麼自身的 onTouch 就會被調用,若是返回 false 其自身的 onTouchEvent 被調用
if(!onTouchListener.onTouch(v: View?, event: MotionEvent?)){
//3. onTouch 返回了 false ,onTouchEvent 調用,而且會調用內部的 onClick 事件
if(!onTouchEvent(event)){
//4. 若是也設置了 onClickListener 那麼 onClick 也會被調用
onClickListener.onClick()
}
}
}
}
複製代碼
上面的僞代碼的邏輯總結一下就是若是當前 View 設置了 onTouchListener 那麼自身的 onTouch 就會執行,若是 onTouch 返回值是 false ,其自身的 onTouchEvent 會被調用。若是 onTouchEvent 返回也爲 false 那麼 onClick 就會執行 。優先級爲 onTouch > onTouchEvent > onClick。
當一個點擊事件產生後,它的傳遞過程遵循以下順序:Activity -> Window -> View ,即事件老是先傳遞給 Activity , Activity 再傳遞給 Window , 最後 Window 再傳遞給頂級 View 。頂級 View 接收到事件後,就會按照事件分發機制去分發事件。考慮一種狀況,若是一個 View 的 onTouchEvent 返回 false ,那麼它的父容器的 onTouchEvent 將會被調用,依次類推。若是全部的元素都不處理這個事件,那麼這個事件將會最終傳遞給 Activity 處理,即 Activity 的 onTouchEvent 方法會被調用。下面咱們就以一段代碼示例來演示一下這種場景,代碼以下:
重寫 Activity dispatchTouchEvent 分發和 onTouchEvent 事件處理
class MainActivity : AppCompatActivity() {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN)
println("事件分發機制開始分發 ----> Activity dispatchTouchEvent")
return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event.action == MotionEvent.ACTION_DOWN)
println("事件分發機制處理 ----> Activity onTouchEvent 執行")
return super.onTouchEvent(event)
}
}
複製代碼
重寫根 ViewGroup dispatchTouchEvent 分發和 onTouchEvent 事件處理
public class GustomLIn(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) {
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event.action == MotionEvent.ACTION_DOWN)
println("事件分發機制處理 ----> 父容器 LinearLayout onTouchEvent")
return false
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN)
println("事件分發機制開始分發 ----> 父容器 dispatchTouchEvent")
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN)
println("事件分發機制開始分發 ----> 父容器是否攔截 onInterceptTouchEvent")
return super.onInterceptTouchEvent(ev)
}
}
複製代碼
重寫子 View dispatchTouchEvent 分發和 onTouchEvent 事件處理
public class Button(context: Context?, attrs: AttributeSet?) : AppCompatButton(context, attrs) {
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
if (event.action == MotionEvent.ACTION_DOWN)
println("事件分發機制開始分發 ----> 子View dispatchTouchEvent")
return super.dispatchTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event.action == MotionEvent.ACTION_DOWN)
println("事件分發機制處理 ----> 子View onTouchEvent")
return false
}
}
複製代碼
輸出:
System.out: 事件分發機制開始分發 ----> Activity dispatchTouchEvent
System.out: 事件分發機制開始分發 ----> 父容器 dispatchTouchEvent
System.out: 事件分發機制開始分發 ----> 父容器是否攔截 onInterceptTouchEvent
System.out: 事件分發機制開始分發 ----> 子View dispatchTouchEvent
System.out: 事件分發機制開始處理 ----> 子View onTouchEvent
System.out: 事件分發機制開始處理 ----> 父容器 LinearLayout onTouchEvent
System.out: 事件分發機制開始處理 ----> Activity onTouchEvent 執行
複製代碼
得出的結論跟以前的描述徹底一致,這就說明了若是子 View ,父 ViewGroup 都不處理事件的話,最後交於 Activity 的 onTouchEvent 方法。也能夠從上面的結果看出來事件傳遞是由外向內傳遞的,即事件老是先傳遞給父元素,而後再由父元素分發給子 View 。
上一小節咱們分析了 View 的事件分發機制,本節將從源碼的角度進一步去分析。
Activity 對點擊事件的分發過程
點擊事件用 MotionEvent 來表示,當一個點擊操做發生時,事件最早傳遞給當前 Activity ,由 Activity 的 dispatchTouchEvent 來進行事件派發,具體的工做是由 Activity 內部的 Window 來完成的。Window 會將事件傳遞給 DecorView ,DecorView 通常就是當前界面的底層容器也就是setContentView 所設置的父容器,它繼承自 FrameLayout ,它在 Activity 中能夠經過 getWindow().getDecorView()得到 ,因爲事件最早由 Activity 開始進行分發,那麼咱們就直接看它的 dispactchTouchEvent 方法,代碼以下:
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
/** * 首先按下的觸發的是 ACTION_DOWN 事件 */
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
/** * 拿到當前 Window 調用 superDispatchTouchEvent 方法 */
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
/** * 若是全部的 View 都沒有處理,那麼最終會執行到 Activity onTouchEvent 方法中。 */
return onTouchEvent(ev);
}
複製代碼
經過上面的代碼咱們知道首先執行的是 ACTION_DOWN 按下事件執行 onUserInteraction 空方法,而後調用 getWindow() 的 superDispatchTouchEvent 方法,這裏的 getWindow 其實就是它的惟一子類 PhoneWindow 咱們看它的具體調用實現,代碼以下:
//PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
...
}
複製代碼
在 PhoneWindow 的 superDispatchTouchEvent 函數中又交於了 DecorView 來處理,那麼 DecorView 是什麼呢?
//DecorView.java
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
...
DecorView(Context context, int featureId, PhoneWindow window,
WindowManager.LayoutParams params) {
super(context);
...
@Override
public final View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
}
...
}
複製代碼
咱們看到 DecorView 它其實就是繼承的 FrameLayout ,咱們知道在 Activity 中咱們能夠經過 getWindow().getDecorView().findViewById() 拿到對應在 XML 中的 View 對象 , 那麼 DecorView 又是何時進行實例化呢?還有 PhoneWindow 又是什麼時候進行實例化的呢?由於這些不是我們今天講解的主要內容,感興趣的能夠看我以前對 Activity 啓動源碼分析該篇中有講過 它們其實都是在 Activity 啓動的時候進行各自的實例化。好了,DecorView 實例化就將到這裏。目前事件傳遞到了 DecorView 這裏,咱們看它的內部源碼實現,代碼以下:
// DecorView.java
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
複製代碼
咱們看到內部又調用了父類 dispatchTouchEvent 方法, 因此最終是交給 ViewGroup 頂級 View 來處理分發了。
頂級 View 對點擊事件的分發過程
在上一小節中咱們知道了一個事件的傳遞流程,這裏咱們就大體在回顧一下。首先點擊事件到達頂級 ViewGroup 以後,會調用自身的 dispatchTouchEvent 方法,以後若是自身的攔截方法 onInterceptTouchEvent 返回 true ,則事件不會繼續下發給子類,若是自身設置了 mOnTouchListener 監聽,則 onTouch 會被調用,不然 onTouchEvent 會被調用,若是 onTouchEvent 中設置了 mOnClickListener 那麼 onClick 會調用。若是 ViewGroup 的 onInterceptTouchEvent 返回 false,則事件會傳遞到所點擊的子 View 中,這時子 View 的 dispatchTouchEvent 會被調用。到此爲止,事件已經從頂級 View 傳遞給了下一層 View ,接下來的傳遞過程和頂級 ViewGroup 同樣,如此循環就完成了整個事件的分發。
在該小節的第一點中咱們知道,在 DecorView 中的 superDispatchTouchEvent 方法內部調用了父類的 dispatchTouchEvent 方法,咱們看它的實現,代碼以下:
//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (actionMasked == MotionEvent.ACTION_DOWN) {
//這裏主要是在新事件開始時處理完上一個事件
cancelAndClearTouchTargets(ev);
resetTouchState();
}
/** 檢查事件攔截,表示事件是否攔截*/
final boolean intercepted;
/** * 1. 判斷當前是不是按下 */
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//2. 子類能夠經過 requestDisallowInterceptTouchEvent 方法來設置父類不要攔截
if (!disallowIntercept) {
//3
intercepted = onInterceptTouchEvent(ev);
//恢復事件防止其改變
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
}
複製代碼
從上面代碼咱們能夠看出若是 actionMasked == MotionEvent.ACTION_DOWN 或者 mFirstTouchTarget != null 成立的話會執行註釋 2 的判斷(mFirstTouchTarget 的意思若是當前事件被子類消費了,就不成立,後面會提升),disallowIntercept 能夠在子類中經過調用父類的 requestDisallowInterceptTouchEvent(true) 請求父類不要攔截分發事件,也就是阻止執行註釋 3 的攔截子類接收按下的事件,反之執行 onInterceptTouchEvent(ev); 若是返回 true 說明攔截了事件 。
上面介紹了註釋 1,2,3 onInterceptTouchEvent 返回 true 的狀況,說明攔截了事件,下面咱們來說解 intercepted = false 當前 ViewGroup 不攔截事件的時候,事件會下發給它的子 View 進行處理,下面看子 View 處理的源碼,代碼以下:
//ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (!canceled && !intercepted) {
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);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
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;
}
ev.setTargetAccessibilityFocus(false);
}
}
...
}
複製代碼
上面這段代碼也很好理解,首先遍歷 ViewGroup 子孩子,而後判斷子元素是否在播放動畫和點擊事件是否落在了子元素的區域內。若是某個子元素知足這 2 個條件,那麼事件就會傳遞給該子類來處理,能夠看到 ,dispatchTransformedTouchEvent 實際上調用的就是子類的 dispatchTouchEvent 方法,在它的內部有以下一段內容,而在上面的代碼中 child 傳遞不是 null ,所以它會直接調用子元素的 dispatchTouchEvent 方法,這樣事件就交由子元素處理,從而完成了一輪事件分發。
//ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
...
}
複製代碼
這裏若是 child.dispatchTouchEvent(event) 返回 true , 那麼 mFirstTouchTarget 就會被賦值同時跳出 for 循環,以下所示:
//ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
...
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
//這個時候 mFirstTouchTarget 就表明子 View 成功處理了事件
mFirstTouchTarget = target;
return target;
}
複製代碼
這幾行代碼完成了 mFirstTouchTarget 的賦值並終止了對子元素的遍歷。若是子元素的 dispatchTouchEvent 返回 false ,ViewGroup 會繼續遍歷進行事件分發給下一個子元素。
若是遍歷全部的子元素後事件都沒有被處理的時候,那麼 ViewGroup 就會本身處理點擊事件,這裏包含 2 種狀況下 ViewGroup 會本身處理事件 (其一: ViewGroup 沒有子元素,其二:子元素處理了點擊事件,可是在 dispatchTouchEvent 中返回了false,這通常是在子元素的 onTouchEvent 中返回了 false )
代碼以下:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
...
}
複製代碼
能夠看到若是 mFirstTouchTarget == null 的時候,那麼就是表明 ViewGroup 的子 View 沒有被消費點擊事件,將調用自身的 dispatchTransformedTouchEvent 方法。注意上面這段代碼這裏的第三個參數 child 爲 null ,從前面的分析能夠知道,它會調用 super.dispatchTouchEvent(event) ,顯然,這裏就會調用父類 View 的 dispatchTouchEvent 方法,即點擊事件開始交由 View 處理,請看下面的分析:
View 對點擊事件的處理過程
其實 View 對點擊事件的處理過程稍微簡單一些,注意這裏的 View 不包含 ViewGroup 。先看它的 dispatchTouchEvent 方法,代碼以下:
//View.java
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
//1.
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//2.
if (!result && onTouchEvent(event)) {
result = true;
}
}
....
return result;
}
複製代碼
View 中的事件處理邏輯比較簡單,咱們先看註釋 1 處,若是咱們外部設置了 mOnTouchListener 點擊事件,那麼就會執行 onTouch 回調,若是該回調的返回值爲 false ,那麼纔會執行 onTouchEvent 方法,可見onTouchListener 優先級高於 onTouchEvent 方法,下面咱們來分析 onTouchEvent 方法實現,代碼以下:
//View.java
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
/** * 1. View 處於不可用狀態下的點擊事件的處理過程 */
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
/** * 2. 若是 View 設置了代理,那麼還會執行 TouchDelegate 的 onTouchEvent 方法。 */
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
/** * 3. 若是 clickable 或 (viewFlags & TOOLTIP) == TOOLTIP 有一個成立那麼就會處理該事件 */
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
// 用於識別快速按下
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) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
/** * 若是設置了點擊事件 mOnClickListener 就會執行內部回調 */
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:
...
//判斷是不是在滾動容器中
boolean isInScrollingContainer = isInScrollingContainer();
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:
if (clickable) {
setPressed(false);
}
//移除一些回調好比長按事件
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
if (!pointInView(x, y, mTouchSlop)) {
//移除一些回調好比長按事件
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
複製代碼
上面代碼雖然比較多,可是邏輯仍是很清楚的,咱們來分析一下
到這裏點擊事件的分發機制源碼實現已經分析完了,結合以前分析的傳遞規則和下面這張圖,而後結合源碼相信你應該理解了事件分發跟事件處理機制了。
本小節將介紹 View 體系中一個很是重要的知識點滑動衝突,相信在開發中特別是作一些滑動效果處理的時候並且還不止一層滑動,又的是嵌套好幾層的滑動,那麼它們之間若是不解決滑動衝突一定是不可行的,下面咱們先來看看形成滑動衝突的場景。
1. 外部滑動方向和內部滑動方向不一致
主要是將 ViewPager 和 Fragment 配合使用所組成的頁面滑動效果,主流應用幾乎都會使用這個效果。在這種效果中,能夠經過左右滑動來切換頁面,而每一個頁面內部每每是一個 RecyclerView 。原本這種狀況下是有滑動衝突的,可是 ViewPager 內部處理了這種滑動衝突,所以採用 ViewPager 時咱們無須關注這個問題,可是若是咱們採用的是 ScrollView 等滑動控件,那就必須手動處理滑動衝突了,不然形成的後果就是內外兩層只能由一層可以滑動,這是由於二者之間的滑動事件有衝突。
它的處理規則是:
當用戶左右滑動時,須要讓外部的 View 攔截點擊事件,當用戶上下滑動的時候,須要讓內部的 View 攔截點擊事件。這個時候咱們就能夠根據他們的特徵來解決滑動衝突。具體來講就是能夠經過判斷滑動手勢是水平方向仍是豎直方向具體來對應攔截事件。
2. 外部滑動方向和內部滑動方向一致
這種狀況就稍微複雜一些,當內外兩層都在同一個方向能夠滑動的時候,顯然存在邏輯問題。由於當手指開始滑動的時候,系統沒法知道用戶究竟是想讓那一層滑動,因此當手指滑動的時候就會出現問題,要麼只有一層能滑動,要麼就是內外兩層都滑動得很卡頓。在實際的開發中,這種場景主要是指內外兩層同時能上下滑動或者內外兩層同時能左右滑動。
它的處理規則是:
這種事比較特殊的,由於它沒法根據滑動的角度、距離差以及速度差來作判斷,可是這個時候通常都能在業務上找到突破點,好比業務有規定,當處理某種狀態的時候須要外部 View 響應用戶的滑動,而處於另一種狀態時則須要內部 View 來響應 View 的滑動,根據這種業務上的需求咱們也能得出相應的處理規則,有了處理規則一樣能夠進行下一步處理。這種場景經過文字描述可能比較抽象,在下一小節中咱們會經過實際例子來演示這種狀況。
3. 1 + 2 場景的嵌套
場景三是場景一和場景二兩種狀況的嵌套,所以場景三的滑動衝突看起來就更加複雜了。好比在許多應用中會有這麼一個效果:內層有一個場景 1 中的滑動效果,而後外層又有一個場景 2 中的滑動效果。雖說場景三的滑動衝突看起來是比較複雜的,可是它是幾個單一的滑動衝突的疊加,因此只須要分別處理內中外層之間的衝突就好了,處理方式跟場景 1 和 2 一致。
下面咱們就來看一下滑動衝突的處理規則。
它的處理規則是:
它的滑動規則就更復雜了,和場景 2 同樣,它也沒法直接根據滑動的角度、距離以及速度差來作判斷,一樣仍是隻能從業務員上找到突破點,具體方法和場景 2 同樣,都是從業務的需求上得出相應的處理規則,在下一節中一樣會給出代碼示例來進行演示。
上面說過針對場景 1 中的滑動,咱們能夠根據滑動的距離差來進行判斷,這個距離差就是所謂的滑動規則。若是用 ViewPager 去實現場景 1 中的效果,咱們不須要手動處理滑動衝突,由於 ViewPager 已經幫咱們作了,可是這裏爲了更好的演示滑動衝突解決思想,沒有采用 ViewPager 。其實在滑動過程當中獲得滑動的角度這個是至關簡單的,可是到底要怎麼作才能將點擊事件交給合適的 View 去處理呢?這時就要用到 3.4 節所講述的事件分發機制了。針對滑動衝突,這裏給出 2 種解決滑動衝突的方式,外部攔截和內部攔截髮。
外部攔截法
所謂外部攔截就是指點擊事件先通過父容器的攔截處理,若是父容器須要此事件就攔截,若是不須要此事件就不攔截,這樣就能夠解決滑動衝突的問題,這種方法比較符合點擊事件的分發機制。外部攔截法須要重寫 onInterceptTouchEvent方法,在內部作響應的攔截便可,能夠參考下面代碼:
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
isIntercepted = false
}
MotionEvent.ACTION_MOVE -> {
//攔截子類的移動事件
if (true) {
println("事件分發機制開始分發 ----> 攔截子類的移動事件 onInterceptTouchEvent")
isIntercepted = true
} else {
isIntercepted = false
}
}
MotionEvent.ACTION_UP -> {
isIntercepted = false
}
}
return isIntercepted
}
複製代碼
上述代碼是外部攔截的典型邏輯,針對不一樣的滑動衝突只須要修改父容器須要當前點擊事件這個條件便可,其它均不作修改也不能修改。這裏對上述代碼再描述一下,在 onInterceptTouchEvent 方法中,首先是 ACTION_DOWN 這個事件,父容器必須返回 false 。既不攔截 ACTION_DOWN 事件,這是由於一旦父容器攔截了 ACTION_DOWN , 這是由於一旦父容器攔截 ACTION_DOWN, 那麼後續的 ACTION_DOWN, 那麼後續的 ACTION_MOVE 和 ACTION_UP 事件都會直接交由父容器處理,這個時候事件無法再傳遞給子元素了;其次是 ACTION_MOVE 事件,這個事件能夠根據須要來決定是否攔截,若是是 ACTION_UP 事件,這裏必需要返回 false , 由於 ACTION_UP 事件自己沒有太多意義。
考慮一種狀況,假設事件交由子元素處理,若是父容器在 ACTION_UP 時返回了 true ,就會致使子元素沒法接收到 ACTION_UP 事件,這個時候子元素中的 onClick 事件就沒法觸發,可是父容器比較特殊,一旦它開始攔截任何一個事件,那麼後續的事件都會交給它來處理,而 ACTION_UP 做爲最後一個事件也一定能夠傳遞給父容器,即使父容器的 onInterceptTouchEvent 方法在 ACTION_UP 時返回了 false.
內部攔截法
內部攔截法是指父容器不攔截任何事件,全部的事件都傳遞給子元素,若是子元素須要此事件就直接消耗掉,不然就交由父容器進行處理,這種方法和 Android 中的事件分發機制不一致,在講解源碼的時候,咱們講解了 ,能夠經過 requestDisalloWInterceptTouchEvent 方法才能正常工做,使用起來較外部攔截法稍顯複雜,咱們須要重寫子元素的 dispatchTouchEvent 方法
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
println("事件分發機制開始分發 ----> 子View dispatchTouchEvent ACTION_DOWN")
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
println("事件分發機制開始分發 ----> 子View dispatchTouchEvent ACTION_MOVE")
if (true){
parent.requestDisallowInterceptTouchEvent(false)
}
}
MotionEvent.ACTION_UP -> {
println("事件分發機制開始分發 ----> 子View dispatchTouchEvent ACTION_UP")
}
}
return super.dispatchTouchEvent(event)
}
複製代碼
上述代碼是內部攔截法的典型代碼,當面對不一樣的滑動策略時只須要修改裏面的條件便可,其它不須要作改動並且也不能有改動,除了子元素須要作處理之外,父元素也要默認攔截除了 ACTION_DOWN 之外的其它事件,這樣當子元素調用 parent.requestDisallowInterceptTouchEvent(false) ,父元素才能繼續攔截所需的事件。
下面就以實戰的 demo 具體來講明一下。
場景一 滑動衝突案例
咱們自定義一個 ViewPager + RecyclerView 包含左右 + 上下滑動,這樣就知足了咱們場景一的滑動衝突,咱們先來看一下完整的效果圖:
上面錄屏的效果解決了上下滑動跟左右滑動衝突,實現方式就是自定義 ViewGroup 利用 Scroller 達到像 ViewPager 同樣絲滑般的感受 ,而後內部添加了 3 個 RecyclerView 。
咱們看一下自定義 ViewGroup 實現:
class ScrollerViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
/** * 定義 Scroller 實例 */
private var mScroller = Scroller(context)
/** * 判斷拖動的最小移動像素點 */
private var mTouchSlop = 0
/** * 手指按下屏幕的 x 座標 */
private var mDownX = 0f
/** * 手指當前所在的座標 */
private var mMoveX = 0f
/** * 記錄上一次觸發 按下是的座標 */
private var mLastMoveX = 0f
/** * 界面能夠滾動的左邊界 */
private var mLeftBorder = 0
/** * 界面能夠滾動的右邊界 */
private var mRightBorder = 0
/** * 記錄下一次攔截的 X,y */
private var mLastXIntercept = 0
private var mLastYIntercept = 0
/** * 是否攔截 */
private var interceptor = false
init {
init()
}
constructor(context: Context?) : this(context, null) {
}
private fun init() {
/** * 經過 ViewConfiguration 拿到認爲手指滑動的最短的移動 px 值 */
mTouchSlop = ViewConfiguration.get(context).scaledPagingTouchSlop
}
/** * 測量 child 寬高 */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//拿到子View 個數
val childCount = childCount
for (index in 0..childCount - 1) {
val childView = getChildAt(index)
//爲 ScrollerViewPager 中的每個子控件測量大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec)
}
}
/** * 測量完以後,拿到 child 的大小而後開始對號入座 */
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
if (changed) {
val childCount = childCount
for (child in 0..childCount - 1) {
//拿到子View
val childView = getChildAt(child)
//開始對號入座
childView.layout(
child * childView.measuredWidth, 0,
(child + 1) * childView.measuredWidth, childView.measuredHeight
)
}
//初始化左右邊界
mLeftBorder = getChildAt(0).left
mRightBorder = getChildAt(childCount - 1).right
}
}
/** * 外部解決 1. 根據垂直或水平的距離來判斷 */
// override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
// interceptor = false
// var x = ev.x.toInt()
// var y = ev.y.toInt()
// when (ev.action) {
// MotionEvent.ACTION_DOWN -> {
// interceptor = false
// }
// MotionEvent.ACTION_MOVE -> {
// var deltaX = x - mLastXIntercept
// var deltaY = y - mLastYIntercept
// interceptor = Math.abs(deltaX) > Math.abs(deltaY)
// if (interceptor) {
// mMoveX = ev.getRawX()
// mLastMoveX = mMoveX
// }
// }
// MotionEvent.ACTION_UP -> {
// //拿到當前移動的 x 座標
// interceptor = false
// println("onInterceptTouchEvent---ACTION_UP")
//
// }
// }
// mLastXIntercept = x
// mLastYIntercept = y
// return interceptor
// }
/** * 外部解決 2. 根據第二點座標 - 第一點座標 若是差值大於 TouchSlop 就認爲是在左右滑動 */
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
interceptor = false
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
//拿到手指按下至關於屏幕的座標
mDownX = ev.getRawX()
mLastMoveX = mDownX
interceptor = false
}
MotionEvent.ACTION_MOVE -> {
//拿到當前移動的 x 座標
mMoveX = ev.getRawX()
//拿到差值
val absDiff = Math.abs(mMoveX - mDownX)
mLastMoveX = mMoveX
//當手指拖動值大於 TouchSlop 值時,就認爲是在滑動,攔截子控件的觸摸事件
if (absDiff > mTouchSlop)
interceptor = true
}
}
return interceptor
}
/** * 父容器沒有攔截事件,這裏就會接收到用戶的觸摸事件 */
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE -> {
println("onInterceptTouchEvent---onTouchEvent--ACTION_MOVE ")
mLastMoveX = mMoveX
//拿到當前滑動的相對於屏幕左上角的座標
mMoveX = event.getRawX()
var scrolledX = (mLastMoveX - mMoveX).toInt()
if (scrollX + scrolledX < mLeftBorder) {
scrollTo(mLeftBorder, 0)
return true
} else if (scrollX + width + scrolledX > mRightBorder) {
scrollTo(mRightBorder - width, 0)
return true
}
scrollBy(scrolledX, 0)
mLastMoveX = mMoveX
}
MotionEvent.ACTION_UP -> {
//當手指擡起是,根據當前滾動值來斷定應該回滾到哪一個子控件的界面上
var targetIndex = (scrollX + width / 2) / width
var dx = targetIndex * width - scrollX
/** 第二步 調用 startScroll 方法彈性回滾並刷新頁面*/
mScroller.startScroll(scrollX, 0, dx, 0)
invalidate()
}
}
return super.onTouchEvent(event)
}
override fun computeScroll() {
super.computeScroll()
/** * 第三步 重寫 computeScroll 方法,並在其內部完成平滑滾動的邏輯 */
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.currX, mScroller.currY)
postInvalidate()
}
}
}
複製代碼
上面代碼很簡單,經過 2 種方式處理了外部攔截法衝突,分別是:
固然咱們也能夠用內部攔截法來解決,按照咱們前面對內部攔截法的分析,咱們只須要修改自定義 RecylerView 的 分發事件 dispatchTouchEvent 方法中的父容器的攔截邏輯,下面請看代碼實現:
class MyRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs) {
/** * 分別記錄咱們上次滑動的座標 */
private var mLastX = 0;
private var mLastY = 0;
constructor(context: Context) : this(context, null)
/** * 重寫分發事件 */
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
val x = ev.getX().toInt()
val y = ev.getY().toInt()
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
var par = parent as ScrollerViewPager
//請求父類不要攔截事件
par.requestDisallowInterceptTouchEvent(true)
Log.d("dispatchTouchEvent", "---》子ACTION_DOWN");
}
MotionEvent.ACTION_MOVE -> {
val deltaX = x - mLastX
val deltaY = y - mLastY
if (Math.abs(deltaX) > Math.abs(deltaY)){
var par = parent as ScrollerViewPager
Log.d("dispatchTouchEvent", "dx:" + deltaX + " dy:" + deltaY);
//交於父類來處理
par.requestDisallowInterceptTouchEvent(false)
}
}
MotionEvent.ACTION_UP -> {
}
}
mLastX = x
mLastY = y
return super.dispatchTouchEvent(ev)
}
}
複製代碼
還須要改父類 onInterceptTouchEvent 方法
/** * 子類請求父類也叫作內部攔截法 */
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
interceptor = false
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
//拿到手指按下至關於屏幕的座標
mDownX = ev.getRawX()
mLastMoveX = mDownX
if (!mScroller.isFinished) {
mScroller.abortAnimation()
interceptor = true
}
Log.d("dispatchTouchEvent", "--->onInterceptTouchEvent, ACTION_DOWN" );
}
MotionEvent.ACTION_MOVE -> {
//拿到當前移動的 x 座標
mMoveX = ev.getRawX()
//拿到差值
mLastMoveX = mMoveX
//父類消耗移動事件,那麼自身 onTouchEvent 會被調用
interceptor = true
Log.d("dispatchTouchEvent", "--->onInterceptTouchEvent, ACTION_MOVE" );
}
}
return interceptor
}
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<com.devyk.customview.sample_1.ScrollerViewPager //父節點
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
//子節點
<com.devyk.customview.sample_1.MyRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.devyk.customview.sample_1.MyRecyclerView>
<com.devyk.customview.sample_1.MyRecyclerView
android:id="@+id/recyclerView2"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.devyk.customview.sample_1.MyRecyclerView>
<com.devyk.customview.sample_1.MyRecyclerView
android:id="@+id/recyclerView3"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.devyk.customview.sample_1.MyRecyclerView>
</com.devyk.customview.sample_1.ScrollerViewPager>
複製代碼
這裏解釋一下上面代碼的含義,首先在 MyRecylerView 中重寫 dispatchTouchEvent 事件分發事件,分別對 DOWN, MOVE 作處理。
**DOWN: **當咱們手指按下的時候會執行到ViewGroup 的 dispatchTouchEvent 方法,而且會執行 ViewGroup 的 onInterceptTouchEvent 攔截事件方法,因爲在 ScrollerViewPager 中重寫了 onInterceptTouchEvent 事件,能夠看到上面 DOWN 只有再滑動沒有結束的狀況下事件會由父類攔截,那麼通常狀況下返回的就是 false 父類不攔截,當父類不攔截 DOWN 事件的時候,子節點 MyRecylerView 的 dispatchTouchEvent 的 DOWN 事件就會被觸發,你們注意看,在 DOWN 事件中,我調用了當前根節點 ScrollerViewPager 的 requestDisallowInterceptTouchEvent(true) 方法,其意思就是不讓父類執行 onInterceptTouchEvent 方法。
MOVE: 當咱們手指滑動的時候因爲咱們請求父類不攔截子節點事件,ViewGroup 的 onInterceptTouchEvent 就不會執行,如今就執行到子節點的 MOVE 方法,若是當前按下的 x,y 座標減去上一次 x,y 座標 只要 deltaX 的絕對值 > deltaY 那麼就認爲是在 左右滑動,如今就要攔截子節點 MOVE 事件交於父節點來處理,從而在 ScrollerViewPager 就能夠了左右滑動。反之就認爲在上下滑動,子節點來處理。
能夠看到內部攔截法比較複雜,不只要修改子節點內部代碼,還要修改父節點方法,其穩定和可維護性明顯不如外部攔截法,因此仍是推薦你們使用外部攔截法來解決時間衝突。
下面看一個 APP 經常使用功能,側滑刪除實現,通常側滑是由一個 RecyclerView + 側滑自定義 ViewGroup 來實現:
實戰
參考該 Demo 中的實現,從中你能夠學到自定義 ViewGroup 、滑動衝突解決等技術。