美文网首页
[译] NestScrolling 实践——ScrollView

[译] NestScrolling 实践——ScrollView

作者: developerChenxi | 来源:发表于2018-01-30 19:56 被阅读0次

原文作者: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的值,按需处理滚动就好了。

相关文章

网友评论

      本文标题:[译] NestScrolling 实践——ScrollView

      本文链接:https://www.haomeiwen.com/subject/nzxhzxtx.html