[譯] NestScrolling 實踐——ScrollView與RecyclerView的完美銜接

原文作者:Alex Lockwood

原文地址: Experimenting with Nested Scrolling

Demo: https://github.com/alexjlockwood/adp-nested-scrolling

從API 21開始,support庫提供了一套處理嵌套滑動的API(以下簡稱NS),用于可滑動的父布局可以嵌套可滑動的子View,從而實現(xiàn) Material Design提供的一些列滑動效果(效果集合傳送門)。如圖1效果,就是使用了CoordinatorLayout和NestedScrollView,

圖1

如果沒有nested scrolling,NestedScrollView的滑動將不能和其他空間的效果融為一體;使用nested scrolling,CoordinatorLayout和NestedScrollView輪流攔截和消費滑動事件,也使得‘collapsing toolbar’ 的效果看起來更加連貫, 如圖2。

圖2

那么,NS是如何工作的呢?首先,父布局需要實現(xiàn)NestedScrollingParent,子View需要實現(xiàn)NestedScrollChild,如圖3所示,以NestedScrollView(以下簡稱NSV)和RecyclerView(以下簡稱RV)為例:

圖3

NSV嵌套RV,如果沒有嵌套滑動,RV會攔截并消費掉滑動事件,這顯然不是我們想要的,我們希望一次滑動事件能同時作用于兩個View,也就是說

  • 如果RV滑動到最頂部即沒有滑動的初始狀態(tài),那么RV的向上的滑動事件要作用于NSV,使NSV向上滑動。
  • 如果NSV沒有滑動到底部,那么RV向下的滑動事件要作用于NSV,使NSV向下滑動。

NS提供了一種方式,讓NSV和RV之間可以傳遞所有的滑動事件,每一個View自己來決定是否消費滑動事件,當需要處理一系列的MotionEvents和復(fù)雜的用戶場景時,使用NS更加清晰簡單。

NS的工作過程:

  1. RV的 onTouchEvent(ACTINON_MOVE)會被調(diào)用
  2. RV調(diào)用dispatchNestedPreScroll(),通知NSV即將要消費一部分滑動事件
  3. NSV的onNestedPreScroll會被調(diào)用,使得NSV有機會在RV消費掉滑動事件之前對該事件作出響應(yīng)。
  4. RV消費剩余的滑動事件,NSV消費了整個事件的話,RV將不做處理
  5. RV調(diào)用自身的dispatchNestedScroll()方法,通知NSV它消費了一部分滑動事件
  6. NSV的onNestedScroll()方法被調(diào)用,NSV有機會去消費剩余未被消費的滑動事件
  7. RV的onTouchEvent(ACTINON_MOVE) return true,消費掉touch事件

然鵝,但是,Unfortunately,簡單的使用NSV和RV并不能滿足我們的需求,如圖4所示,簡單使用NSV和RV存在兩個問題:

  • 左邊的RV在不應(yīng)當消費滑動事件的時候消費了滑動事件,NSV還沒有滑動到底部,RV就開始滑動了。
  • 右邊RV的fling事件沒有繼續(xù)傳遞給父控件,使得頂部的空間展開和折疊非常生硬。
圖4

我們在了解了NestScrolling是如何工作的以后,修復(fù)這兩個問題就比較簡單了。我們只需要創(chuàng)建一個CustomNestedScrollView通過重寫onNestedPreScroll()和onNestedPreFling()方法來修正滑動效果。

/**
 * A NestedScrollView with our custom nested scrolling behavior.
 */
public class CustomNestedScrollView extends NestedScrollView {

  /* NestedScrollView 在一下兩種情況中將攔截scroll/fling事件:
  (1) RecyclerView已經(jīng)滑動到頂部,用戶手指繼續(xù)向下滑動
  (2) NestedScrollView已經(jīng)滑動到底部,用戶手指繼續(xù)向上滑動*/

  @Override
  public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    final RecyclerView rv = (RecyclerView) target;
    if ((dy < 0 && isRvScrolledToTop(rv)) || (dy > 0 && !isNsvScrolledToBottom(this))) {
      // 滑動NestedScrollView并且標記滑動距離,
      // 這樣RecyclerView就可以知道有多少滑動距離是不用去處理的
      scrollBy(0, dy);
      // consumed[0]表示橫向滑動, consumed[1]表示縱向滑動
      consumed[1] = dy;
      return;
    }
    super.onNestedPreScroll(target, dx, dy, consumed);
  }

  @Override
  public boolean onNestedPreFling(View target, float velX, float velY) {
    final RecyclerView rv = (RecyclerView) target;
    if ((velY < 0 && isRvScrolledToTop(rv)) || (velY > 0 && !isNsvScrolledToBottom(this))) {
      // 處理NestedScrollView的fling,并return true,
      同樣的RecyclerView也會收到通知,不用處理這次的Fling事件了 
      fling((int) velY);
      return true;
    }
    return super.onNestedPreFling(target, velX, velY);
  }

  /**
   * 判斷NestedScrollView是否滑動到底部。
   * 
   * @return NestedScrollView 滑動到底部的時候return true
   * 即RecyclerView完全可見的時候return true
   */
  private static boolean isNsvScrolledToBottom(NestedScrollView nsv) {
    return !nsv.canScrollVertically(1);
  }

  /**
   * 判斷RecyclerView是否滑動到頂部
   * 
   * @return RecyclerView 滑動到頂部的的時候return true,
   * 即RecyclerView的第一個item完全可見的時候return true。
   */
  private static boolean isRvScrolledToTop(RecyclerView rv) {
    final LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
    return lm.findFirstVisibleItemPosition() == 0
        && lm.findViewByPosition(0).getTop() == 0;
  }
}

哎呀,好像解決了!然鵝,但是,Unfortunately,這里又出現(xiàn)了一個新的bug如圖5所示:左邊部分RecyclerView fling到頂部的時候的fling事件被中斷了,我們想要的是右邊的效果,可以順暢的fling下來。

圖5

問題的關(guān)鍵在于,support庫中并沒有提供方法,能讓NestedScrolling中的子View把剩余的fling的速率傳遞給父布局。這個問題Chris Banes已經(jīng)給出了詳細的解釋并給出了解決方案,博客傳送門,這里就不再贅述了??偟膩碚f,我們需要讓我們的父布局和子View去實現(xiàn)新的接口—— NestedScrollingParent2 和 NestedScrollingChild2,這兩個接口在v26的support庫中添加。由于NestedScrollView依然是實現(xiàn)的NestedScrollingParent,我們需要繼承NestedScrollView2并實現(xiàn) NestedScrollingParent2 ,代碼如下:

public class CustomNestedScrollView2 extends NestedScrollView2 {

  @Override
  public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
    final RecyclerView rv = (RecyclerView) target;
    if ((dy < 0 && isRvScrolledToTop(rv)) || (dy > 0 && !isNsvScrolledToBottom(this))) {
      scrollBy(0, dy);
      consumed[1] = dy;
      return;
    }
    super.onNestedPreScroll(target, dx, dy, consumed, type);
  }

  // 我們不需要重寫 onNestedPreFling() ,新的API已經(jīng)默認幫我們實現(xiàn)了我們想要的效果。
  private static boolean isNsvScrolledToBottom(NestedScrollView nsv) {
    return !nsv.canScrollVertically(1);
  }

  private static boolean isRvScrolledToTop(RecyclerView rv) {
    final LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
    return lm.findFirstVisibleItemPosition() == 0
        && lm.findViewByPosition(0).getTop() == 0;
  }
}

chenxi小結(jié)

按照時間線對Nest Scrolling 進行一個小結(jié)(v25):

(按在子View上)

  1. 用戶接觸屏幕,產(chǎn)生ACTIION_DOWN事件,子View會調(diào)用所有的父布局的 startNestedScroll()方法,直到某一個父布局的改方法返回了true;如過所有的度不去都返回false,子View就正常該干嘛干嘛了,不再分發(fā)滑動事件。接下的內(nèi)容,我們都假定父布局的startNestedScroll()方法返回了true
  2. 用戶手指移動,產(chǎn)生ACTION_MOVE事件 dispatchNestedPreScroll() 方法會被調(diào)用,父布局在這個方法中去決定此次滑動事件消費不消費,消費多少,刷卡還是現(xiàn)金,,如果父布局沒有消費掉所有的滑動動作,那么子View會獲取到剩余的滑動動作,并把該值傳入 dispatchNestedScroll() 方法,調(diào)用此方法來消費滑動剩余價值。
  3. 用戶手指離開屏幕,產(chǎn)生ACTION_UP事件 子View 計算是否需要 fling ,如果需要 fling,則調(diào)用 dispatchNestedPreFling() ,先詢問父布局是否要處理,然后調(diào)用 dispatchNestedFling(), 如果父類返回 true 那么父布局就消費掉此次事件,子View不再做任何事。否則,子View將fling,然后立即調(diào)用 dispatchNestedFling()。接下來,即使子View還在fling,也會立即調(diào)用 stopNestedScroll(),標記嵌套滑動已完成。

最后一點是關(guān)鍵,其實父布局有時候并不想消費掉整個fling事件,也想想分發(fā)scroll一樣,分發(fā)掉fling,但v25及以下的的support庫中并不支持。

Nested Scrolling 加強版(v26):

新的api已經(jīng)修復(fù)了上述問題:在新的api中在每一個方法中增加了一個type參數(shù),type有兩個值:ViewCompat.TYPE_TOUCH 和 ViewCompat.TYPE_NON_TOUCH, 根據(jù) type 的值,我們可以對不同的行為做出不同的處理。

實際上我們大多數(shù)時候不需要關(guān)心這個type的值,按需處理滾動就好了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容