[译] NestScrolling 实践——ScrollView与RecyclerView的完美衔接

原文作者:Alex Lockwood

原文地址: Experimenting with Nested Scrolling

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

从API 21开始,support库提供了一套处理嵌套滑动的API(以下简称NS),用于可滑动的父布局可以嵌套可滑动的子View,从而实现 Material Design提供的一些列滑动效果(效果集合传送门)。如图1效果,就是使用了CoordinatorLayout和NestedScrollView,

图1

如果没有nested scrolling,NestedScrollView的滑动将不能和其他空间的效果融为一体;使用nested scrolling,CoordinatorLayout和NestedScrollView轮流拦截和消费滑动事件,也使得‘collapsing toolbar’ 的效果看起来更加连贯, 如图2。

图2

那么,NS是如何工作的呢?首先,父布局需要实现NestedScrollingParent,子View需要实现NestedScrollChild,如图3所示,以NestedScrollView(以下简称NSV)和RecyclerView(以下简称RV)为例:

图3

NSV嵌套RV,如果没有嵌套滑动,RV会拦截并消费掉滑动事件,这显然不是我们想要的,我们希望一次滑动事件能同时作用于两个View,也就是说

  • 如果RV滑动到最顶部即没有滑动的初始状态,那么RV的向上的滑动事件要作用于NSV,使NSV向上滑动。
  • 如果NSV没有滑动到底部,那么RV向下的滑动事件要作用于NSV,使NSV向下滑动。

NS提供了一种方式,让NSV和RV之间可以传递所有的滑动事件,每一个View自己来决定是否消费滑动事件,当需要处理一系列的MotionEvents和复杂的用户场景时,使用NS更加清晰简单。

NS的工作过程:

  1. RV的 onTouchEvent(ACTINON_MOVE)会被调用
  2. RV调用dispatchNestedPreScroll(),通知NSV即将要消费一部分滑动事件
  3. NSV的onNestedPreScroll会被调用,使得NSV有机会在RV消费掉滑动事件之前对该事件作出响应。
  4. RV消费剩余的滑动事件,NSV消费了整个事件的话,RV将不做处理
  5. RV调用自身的dispatchNestedScroll()方法,通知NSV它消费了一部分滑动事件
  6. NSV的onNestedScroll()方法被调用,NSV有机会去消费剩余未被消费的滑动事件
  7. RV的onTouchEvent(ACTINON_MOVE) return true,消费掉touch事件

然鹅,但是,Unfortunately,简单的使用NSV和RV并不能满足我们的需求,如图4所示,简单使用NSV和RV存在两个问题:

  • 左边的RV在不应当消费滑动事件的时候消费了滑动事件,NSV还没有滑动到底部,RV就开始滑动了。
  • 右边RV的fling事件没有继续传递给父控件,使得顶部的空间展开和折叠非常生硬。
图4

我们在了解了NestScrolling是如何工作的以后,修复这两个问题就比较简单了。我们只需要创建一个CustomNestedScrollView通过重写onNestedPreScroll()和onNestedPreFling()方法来修正滑动效果。

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

  /* NestedScrollView 在一下两种情况中将拦截scroll/fling事件:
  (1) RecyclerView已经滑动到顶部,用户手指继续向下滑动
  (2) NestedScrollView已经滑动到底部,用户手指继续向上滑动*/

  @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,这里又出现了一个新的bug如图5所示:左边部分RecyclerView fling到顶部的时候的fling事件被中断了,我们想要的是右边的效果,可以顺畅的fling下来。

图5

问题的关键在于,support库中并没有提供方法,能让NestedScrolling中的子View把剩余的fling的速率传递给父布局。这个问题Chris Banes已经给出了详细的解释并给出了解决方案,博客传送门,这里就不再赘述了。总的来说,我们需要让我们的父布局和子View去实现新的接口—— NestedScrollingParent2 和 NestedScrollingChild2,这两个接口在v26的support库中添加。由于NestedScrollView依然是实现的NestedScrollingParent,我们需要继承NestedScrollView2并实现 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已经默认帮我们实现了我们想要的效果。
  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小结

按照时间线对Nest Scrolling 进行一个小结(v25):

(按在子View上)

  1. 用户接触屏幕,产生ACTIION_DOWN事件,子View会调用所有的父布局的 startNestedScroll()方法,直到某一个父布局的改方法返回了true;如过所有的度不去都返回false,子View就正常该干嘛干嘛了,不再分发滑动事件。接下的内容,我们都假定父布局的startNestedScroll()方法返回了true
  2. 用户手指移动,产生ACTION_MOVE事件 dispatchNestedPreScroll() 方法会被调用,父布局在这个方法中去决定此次滑动事件消费不消费,消费多少,刷卡还是现金,,如果父布局没有消费掉所有的滑动动作,那么子View会获取到剩余的滑动动作,并把该值传入 dispatchNestedScroll() 方法,调用此方法来消费滑动剩余价值。
  3. 用户手指离开屏幕,产生ACTION_UP事件 子View 计算是否需要 fling ,如果需要 fling,则调用 dispatchNestedPreFling() ,先询问父布局是否要处理,然后调用 dispatchNestedFling(), 如果父类返回 true 那么父布局就消费掉此次事件,子View不再做任何事。否则,子View将fling,然后立即调用 dispatchNestedFling()。接下来,即使子View还在fling,也会立即调用 stopNestedScroll(),标记嵌套滑动已完成。

最后一点是关键,其实父布局有时候并不想消费掉整个fling事件,也想想分发scroll一样,分发掉fling,但v25及以下的的support库中并不支持。

Nested Scrolling 加强版(v26):

新的api已经修复了上述问题:在新的api中在每一个方法中增加了一个type参数,type有两个值:ViewCompat.TYPE_TOUCH 和 ViewCompat.TYPE_NON_TOUCH, 根据 type 的值,我们可以对不同的行为做出不同的处理。

实际上我们大多数时候不需要关心这个type的值,按需处理滚动就好了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容