1 從一個細(xì)節(jié)展開
前些日子收藏了@鄭海波-mobctrl的SwipeRefreshLayout,想研究下如何實(shí)現(xiàn)啊鸭。當(dāng)自己動手實(shí)現(xiàn)的時候發(fā)現(xiàn)了一個問題:在listview距離上方還有一定距離的地方開始下拉旁仿,頂住上方內(nèi)容后滑不動了,而SwipeRefreshLayout卻可以繼續(xù)下拉,并觸發(fā)下拉刷新。如圖所示:
經(jīng)過一番排查,發(fā)現(xiàn)我自己實(shí)現(xiàn)的代碼菱父,在onInterceptTouchEvent中能接收到1個ACTION_DOWN颈娜,和2個ACTION_MOVE,之后就再也接受不到ACTION_MOVE事件浙宜,導(dǎo)致無法更新子view是否能下拉,是否在下拉的狀態(tài)蛹磺;而SwipeRefreshLayout可以接收連續(xù)的ACTION_MOVE事件粟瞬。
最后發(fā)現(xiàn),居然是SwipeRefreshLayout中一句不起眼的函數(shù)重寫實(shí)現(xiàn)的萤捆,代碼如下:
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
// Nope.
}
SwipeRefreshLayout繼承自ViewGroup裙品,requestDisallowInterceptTouchEvent覆蓋的是ViewGroup中的下述代碼:
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
為什么一句簡單的重寫,能解決這個問題俗或?
2 Android TouchEvent
Touch事件通過底層接收市怎,傳遞到ViewRootImpl中,分發(fā)給phoneWindow的decorView辛慰,首先回調(diào)給Activity的dispatchTouchEvent處理区匠,隨后回到decorView開始往子view進(jìn)行dispatch,在一個ViewGroup中的傳遞邏輯如下圖所示:
在TouchEvent dispatchTouchEvent到某ViewGroup中時帅腌,會有三步判斷驰弄,如上圖淺綠色所示。
- disallowIntercept?
disallowIntercept的作用
ViewGroup有一個disallowIntercept開關(guān)速客,可以設(shè)置此ViewGroup是否屏蔽onInterceptTouchEvent事件戚篙。如果開啟此開關(guān),則此ViewGroup跳過自身的onInterceptTouchEvent事件溺职,直接dispatchTouchEvent到子View岔擂。
重置disallowIntercept
disallowIntercept位喂,會在每次ACTION_DOWN被重置,默認(rèn)為允許調(diào)用onInterceptTouchEvent乱灵。
//ViewGroup.dispatchTouchEvent
@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.
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();
}
...
}
...
}
/**
*
* Resets all touch state in preparation for a new cycle.
*/
//ViewGroup.resetTouchState
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
每次用戶的按下滑動抬起操作為一組完整的操作塑崖。新一組操作開始,即當(dāng)用戶開始點(diǎn)擊屏幕的時候阔蛉,ViewGroup會重置當(dāng)前的disallowIntercept開關(guān)弃舒,恢復(fù)到允許調(diào)用onInterceptTouchEvent狀態(tài)。
intercept?
onInterceptTouchEvent返回值為true
當(dāng)調(diào)用ViewGroup的onInterceptTouchEvent后返回值為true状原,則表示當(dāng)前ViewGroup攔截了此TouchEvent事件聋呢,此ViewGroup的onTouchEvent會收到回調(diào);
onInterceptTouchEvent返回值為false
如果返回值為false颠区,則調(diào)用dispatchTransformedTouchEvent削锰,去尋找此Point上hit到的子View,如果尋找到子View毕莱,則調(diào)用子View的dispatchTouchEvent事件器贩,否則就調(diào)用super.dispatchTouchEvent,即調(diào)用View的dispatchTouchEvent實(shí)現(xiàn)朋截,在此會調(diào)用到onTouchEvent函數(shù)去處理此TouchEvent事件蛹稍。
onInterceptTouchEvent總結(jié)
onInterceptTouchEvent流程為父ViewGroup->子ViewGroup->孫ViewGruop,如果其中一個ViewGroup攔截了事件部服,則此ViewGroup唆姐,則此ViewGroup直接處理OnTouchEvent事件,且TouchEvent不在往下dispatch廓八,而是開始return奉芦。handled?
onTouchEvent返回值為true
如果返回值為true,則此TouchEvent被處理完畢
onTouchEvent返回值為false
如果為false剧蹂,則return給父ViewGroup声功,父ViewGroup會繼續(xù)交給此ViewGroup的兄弟View處理。
3 requestDisallowInterceptTouchEvent
子View在onInterceptTouchEvent的ACTION_DOWN之后調(diào)用requestDisallowInterceptTouchEvent(true)宠叼,則此子View的所有父ViewGroup會跳過onInterceptTouchEvent回調(diào)先巴,即文章中開頭出現(xiàn)的情況:ACTION_MOVE開始后,父ViewGroup的后幾個ACTION_MOVE事件接收不到了车吹。那么可以斷定筹裕,ScrollView、ListView等子View在判斷開始滑動并攔截事件后窄驹,調(diào)用了requestDisallowInterceptTouchEvent(true)朝卒,致使所有父ViewGroup跳過onInterceptTouchEvent回調(diào),直接dispatchTransformedTouchEvent到ScrollView或者ListView乐埠,實(shí)現(xiàn)代碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
...
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
...
}
}
return mIsBeingDragged;
}
如果滑動超過mTouchSlop闕值抗斤,則判斷為ScrollView正在滑動囚企,所以開始屏蔽掉父ViewGroup的onInterceptTouchEvent回調(diào)。所以如果在此ScrollView的父ViewGroup中覆蓋了requestDisallowInterceptTouchEvent瑞眼,并且什么都不做龙宏,那么ScrollView無法屏蔽掉父ViewGroup的onInterceptTouchEvent回調(diào),那么ScrollView開始處理滑動后的ACTION_MOVE也可以被父ViewGroup所接收到伤疙,也就解決了這個問題银酗。
4 應(yīng)用
在chrisbanes的Android-PullToRefresh項目中也存在這個問題,只需要以下2步即可修復(fù):
- 新建個RefreshableViewWrapperLayout.java
package com.handmark.pulltorefresh.library;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* Created by Asha on 15-8-28.
* Asha ashqalcn@gmail.com
*/
public class RefreshableViewWrapperLayout extends FrameLayout {
public RefreshableViewWrapperLayout(Context context) {
super(context);
}
public RefreshableViewWrapperLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RefreshableViewWrapperLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public RefreshableViewWrapperLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
//do nothing
}
}
- 替換PullToRefreshBase中addRefreshableView的實(shí)現(xiàn)
private void addRefreshableView(Context context, T refreshableView) {
//mRefreshableViewWrapper = new FrameLayout(context);
//替換為
mRefreshableViewWrapper = new RefreshableViewWrapperLayout(context);
mRefreshableViewWrapper.addView(refreshableView, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
addViewInternal(mRefreshableViewWrapper, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
}
5 形象的注釋
在AbsListView的ACTION_MOVE開始后調(diào)用了startScrollIfNeeded函數(shù)徒像,函數(shù)中有一句注釋:
Time to start stealing events! Once we've stolen them, don't let anyone steal from us
哈哈哈黍特,我的事件,誰都別想從我這偷走锯蛀!
6 SwipeRefreshLayout實(shí)現(xiàn)中另外的小細(xì)節(jié)
- 判斷child是否還可以往上滑動
如果可以滑動灭衷,則讓子View處理滑動
ViewCompat.canScrollVertically(child,-1);
- 標(biāo)準(zhǔn)的滑動開始閾值
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
mTouchSlop = configuration.getScaledTouchSlop();
- View的同步位移方法
相比異步的requestLayout,這個方法是同步執(zhí)行的
child.offsetTopAndBottom(offset);
7 疑問
ListView和ScrollView為什么要屏蔽調(diào)這些事件不讓父ViewGroup回調(diào)onInterceptTouchEvent旁涤?出于效率的考慮翔曲,還是簡化邏輯避免滑動出錯,期待高手解答劈愚。
8 reference
SwipeRefreshLayout源代碼
Android SDK 22源代碼
探究requestDisallowInterceptTouchEvent失效的原因