Android TouchEvent之requestDisallowInterceptTouchEvent

來源:互聯網
上載者:User

標籤:android   listview   下拉重新整理   

1 從一個細節展開

前些日子收藏了@鄭海波-mobctrl的SwipeRefreshLayout,想研究下如何?。當自己動手實現的時候發現了一個問題:在listview距離上方還有一定距離的地方開始下拉,頂住上方內容後滑不動了,而SwipeRefreshLayout卻可以繼續下拉,並觸發下拉重新整理。:

左圖開始滑動,右圖拉到頂無法繼續下拉

經過一番排查,發現我自己實現的代碼,在onInterceptTouchEvent中能接收到1個ACTION_DOWN,和2個ACTION_MOVE,之後就再也接受不到ACTION_MOVE事件,導致無法更新子view是否能下拉,是否在下拉的狀態;而SwipeRefreshLayout可以接收連續的ACTION_MOVE事件。

最後發現,居然是SwipeRefreshLayout中一句不起眼的函數重寫實現的,代碼如下:

@Overridepublic 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中,分發給phoneWindow的decorView,首先回調給Activity的dispatchTouchEvent處理,隨後回到decorView開始往子view進行dispatch,在一個ViewGroup中的傳遞邏輯如所示:

TouchEvent dispatchTouchEvent流程

在TouchEvent dispatchTouchEvent到某ViewGroup中時,會有三步判斷,如淺綠色所示。

  • disallowIntercept?
    disallowIntercept的作用
    ViewGroup有一個disallowIntercept開關,可以設定此ViewGroup是否屏蔽onInterceptTouchEvent事件。如果開啟此開關,則此ViewGroup跳過自身的onInterceptTouchEvent事件,直接dispatchTouchEvent到子View。
    重設disallowIntercept
    disallowIntercept,會在每次ACTION_DOWN被重設,預設為允許調用onInterceptTouchEvent。
//ViewGroup.dispatchTouchEvent@Overridepublic 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.resetTouchStateprivate void resetTouchState() {    clearTouchTargets();    resetCancelNextUpFlag(this);    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;    mNestedScrollAxes = SCROLL_AXIS_NONE;}

每次使用者的按下滑動抬起操作為一組完整的操作。新一組操作開始,即當使用者開始點擊螢幕的時候,ViewGroup會重設當前的disallowIntercept開關,恢複到允許調用onInterceptTouchEvent狀態。

  • intercept?
    onInterceptTouchEvent傳回值為true
    當調用ViewGroup的onInterceptTouchEvent後傳回值為true,則表示當前ViewGroup攔截了此TouchEvent事件,此ViewGroup的onTouchEvent會收到回調;
    onInterceptTouchEvent傳回值為false
    如果傳回值為false,則調用dispatchTransformedTouchEvent,去尋找此Point上hit到的子View,如果尋找到子View,則調用子View的dispatchTouchEvent事件,否則就調用super.dispatchTouchEvent,即調用View的dispatchTouchEvent實現,在此會調用到onTouchEvent函數去處理此TouchEvent事件。
    onInterceptTouchEvent總結
    onInterceptTouchEvent流程為父ViewGroup->子ViewGroup->孫ViewGruop,如果其中一個ViewGroup攔截了事件,則此ViewGroup,則此ViewGroup直接處理OnTouchEvent事件,且TouchEvent不在往下dispatch,而是開始return。

  • handled?
    onTouchEvent傳回值為true
    如果傳回值為true,則此TouchEvent被處理完畢
    onTouchEvent傳回值為false
    如果為false,則return給父ViewGroup,父ViewGroup會繼續交給此ViewGroup的兄弟View處理。

3 requestDisallowInterceptTouchEvent

子View在onInterceptTouchEvent的ACTION_DOWN之後調用requestDisallowInterceptTouchEvent(true),則此子View的所有父ViewGroup會跳過onInterceptTouchEvent回調,即文章中開頭出現的情況:ACTION_MOVE開始後,父ViewGroup的後幾個ACTION_MOVE事件接收不到了。那麼可以斷定,ScrollView、ListView等子View在判斷開始滑動並攔截事件後,調用了requestDisallowInterceptTouchEvent(true),致使所有父ViewGroup跳過onInterceptTouchEvent回調,直接dispatchTransformedTouchEvent到ScrollView或者ListView,實現代碼如下:

 @Overridepublic 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回調。所以如果在此ScrollView的父ViewGroup中覆蓋了requestDisallowInterceptTouchEvent,並且什麼都不做,那麼ScrollView無法屏蔽掉父ViewGroup的onInterceptTouchEvent回調,那麼ScrollView開始處理滑動後的ACTION_MOVE也可以被父ViewGroup所接收到,也就解決了這個問題。

4 應用

在chrisbanes的Android-PullToRefresh項目中也存在這個問題,只需要以下2步即可修複:
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 [email protected] */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的實現
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開始後調用了startScrollIfNeeded函數,函數中有一句注釋:

Time to start stealing events! Once we’ve stolen them, don’t let anyone steal from us

哈哈哈,我的事件,誰都別想從我這偷走!

6 SwipeRefreshLayout實現中另外的小細節
  • 判斷child是否還可以往上滑動
    如果可以滑動,則讓子View處理滑動
ViewCompat.canScrollVertically(child,-1);
  • 標準的滑動開始閾值
final ViewConfiguration configuration = ViewConfiguration.get(mContext);mTouchSlop = configuration.getScaledTouchSlop();
  • View的同步位移方法
    相比非同步requestLayout,這個方法是同步執行的
child.offsetTopAndBottom(offset);
7 疑問

ListView和ScrollView為什麼要屏蔽調這些事件不讓父ViewGroup回調onInterceptTouchEvent?出於效率的考慮,還是簡化邏輯避免滑動出錯,期待高手解答。

8 reference

SwipeRefreshLayout原始碼
Android SDK 22原始碼
探究requestDisallowInterceptTouchEvent失效的原因

著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

Android TouchEvent之requestDisallowInterceptTouchEvent

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.