標籤: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 }}
- 替換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