Android TouchEvent之requestDisallowInterceptTouchEvent

1 從一個細(xì)節(jié)展開

前些日子收藏了@鄭海波-mobctrlSwipeRefreshLayout,想研究下如何實(shí)現(xiàn)啊鸭。當(dāng)自己動手實(shí)現(xiàn)的時候發(fā)現(xiàn)了一個問題:在listview距離上方還有一定距離的地方開始下拉旁仿,頂住上方內(nèi)容后滑不動了,而SwipeRefreshLayout卻可以繼續(xù)下拉,并觸發(fā)下拉刷新。如圖所示:

左圖開始滑動擂煞,右圖拉到頂無法繼續(xù)下拉

經(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流程

在TouchEvent dispatchTouchEvent到某ViewGroup中時帅腌,會有三步判斷驰弄,如上圖淺綠色所示。

  1. 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)。

  1. 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奉芦。

  2. 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ù):

  1. 新建個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
    }
}
  1. 替換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失效的原因

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瞳遍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子菌羽,更是在濱河造成了極大的恐慌傅蹂,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件算凿,死亡現(xiàn)場離奇詭異,居然都是意外死亡犁功,警方通過查閱死者的電腦和手機(jī)氓轰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浸卦,“玉大人署鸡,你說我怎么就攤上這事∠尴樱” “怎么了靴庆?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長怒医。 經(jīng)常有香客問我炉抒,道長,這世上最難降的妖魔是什么稚叹? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任焰薄,我火速辦了婚禮拿诸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘塞茅。我一直安慰自己亩码,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布野瘦。 她就那樣靜靜地躺著描沟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鞭光。 梳的紋絲不亂的頭發(fā)上吏廉,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機(jī)與錄音衰猛,去河邊找鬼迟蜜。 笑死,一個胖子當(dāng)著我的面吹牛啡省,可吹牛的內(nèi)容都是我干的娜睛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼卦睹,長吁一口氣:“原來是場噩夢啊……” “哼畦戒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起结序,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤障斋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后徐鹤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體垃环,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年返敬,在試婚紗的時候發(fā)現(xiàn)自己被綠了遂庄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡劲赠,死狀恐怖涛目,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凛澎,我是刑警寧澤霹肝,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站塑煎,受9級特大地震影響沫换,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜轧叽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一苗沧、第九天 我趴在偏房一處隱蔽的房頂上張望刊棕。 院中可真熱鬧,春花似錦待逞、人聲如沸甥角。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗤无。三九已至,卻和暖如春怜庸,著一層夾襖步出監(jiān)牢的瞬間当犯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工割疾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嚎卫,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓宏榕,卻偏偏與公主長得像拓诸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子麻昼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容