正文  软件开发 > 编程综合 >

十分钟理解 Android 中的嵌套滚动机制

从是什么开始 我们先来看一个动图,直观的感受下什么是嵌套滚动(nested scrolling): 既然是嵌套,就说明是一层套着一层,存在两个滚动行为。在上图中,当...

从是什么开始

我们先来看一个动图,直观的感受下什么是嵌套滚动(nested scrolling):

既然是嵌套,就说明是一层套着一层,存在两个滚动行为。在上图中,当我们滚动下面的UI控件时,先滚动的却是外头的父容器,当父容器滚动到一定程度后,下面的UI控件才开始滚动。这样看来,确实存在着两个滚动行为。

那么嵌套关系体现在哪呢?我们先来看下实现上图效果的布局文件结构:

<ParentView>

  <ImageView />

  <NestedScrollView>
    <WebView />
  </NestedScrollView>

</CoordinatorLayout>

可以看到,ImageView和NestedScrollView都是ParentView的子View,ParentView使我们自定义的一个继承自LinearLayout的布局管理器。实际上,ParentView类实现了NestedScrollingParent接口,NestedScrollView实现了NestedScrollingChild接口。从名字上我们可以做出这样的猜测:嵌套滚动中需要一个子View和一个作为容器的父View,子View需要实现NestedScrollingChild接口,父View需要实现NestedScrollingParent接口。分别实现了上述两个接口的父View把子View套起来,就可以实现所谓的嵌套滚动。

现在,我们知道了嵌套滚动中的两个主角:

  • 一个实现了NestedScrollingParent的父容器,在本文的其余部分,我们简称为nestedParent;

  • 一个实现了NestedScrollingChild的子View,下文简称为nestedChild。

本文接下来的部分会以上图效果为例,讲解嵌套滚动究竟是如何实现的。

NestedScrollingParent接口

我们来看下本文例子中会涉及到的NestedScrollingParent接口中的方法:

  • onStartNestedScroll(View child, View target, int nestedScrollAxes) :当nestedChild想要进行嵌套滚动时,会调用nestedParent的这个方法。这个芳法用于指示是否支持嵌套滚动,比如我们只想支持垂直方向上的嵌套滚动,可以在nestedParent中这样实现这个方法:

@Overridepublic boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {    
  if (nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL) {        
    return true;    
  }    
  return false;
}
  • onNestedPreScroll(View target, int dx, int dy, int[] consumed) :当我们滚动nestedChild时,nestedChild进行实际的滚动前,会先调用nestParent的这个方法。nestedParent在这个方法中可以把子View想要滚动的距离消耗掉一部分或是全部消耗,比如我们的例子中,当我们向上滚动nestedChild时,nestParent会抢在它前头先滚动,直到ImageView完全隐藏,才让nestedChild开始滚动。

在这个例子中,我们自定义的ParentView继承了LinearLayout类,实现了NestedScrollingParent接口,并重写了上面提到的两个方法。相关代码如下:

public class ParentView extends LinearLayout implements NestedScrollingParent{    
  int ivHeight = 300;
  . . . 
  @Override
  public boolean onStartNestedScroll(...) {
    . . .
  }    
  @Override
  public void onNestedPreScroll(...,int dy,int[] consumed) {        
    if ((dy > 0 && getScrollY() < ivHeight) ||
        (dy < 0 && getScrollY() > 0)) {
      consumed[1] = dy;
      scrollBy(dx, dy);
    }
  }

}

onStartNestedScroll()方法的实现上面我们已经介绍过,这里不再赘述。简单说下onNestedPreScroll()方法的实现。不过在这之前我们补下滚动相关的知识。

滚动相关知识补充

先来看一张图:

在上图中,黑色边框代表了View的边框,蓝色边框代表了View的内容的边框。其实我们平时对ListView等控件进行滚动时,实际滚动的是View的内容。比如在上图中,我们向右滚动一个控件,可以看到,实际上是它的内容向右进行滚动了,View的边界线的位置始终是固定的。上图中蓝色右边框和黑色右边框间的距离就是View滚动的距离。

每个View都有名为mScrollX和mScrollY的两个成员变量,前者记录了View在水平方向上滚动的距离,后者记录了View在竖直方向上滚动的距离。mScrollX的绝对值为View的左边框与View的内容的左边框的距离,当View向左滚动时,mScrollX是正的;当View向右滚动时,mScrollX是负的。mScrollY的绝对值为View的上边框与View的内容的上边框的距离,当View向上滚动时,mScrollY是正的;当View向下滚动时,mScrollY是负的。

理解了上面的内容后,让我们看看onNestedPreScroll()为什么要像上面那样实现。

在onNestedPreScroll()方法中,参数dy代表了本次NestedScrollView想要滑动的距离。若我们向上滑动NestedScrollView,dy就是正的,向下就是负的。getScorllY()会返回ParentView的mScrollY参数,为正则表示当前ParentView的内容已经向上滚动了一段距离,否则表示向下滚动过一段距离。ivHeight表示ImageView的高度。理解了上面这些,这个方法的逻辑就很好理解了,这里不再赘述。

NestedScrollingChild接口

我们在布局文件中使用的NestedScrollView就实现了NestedScrollingChild接口。当我们滚动nestedChild时,这个接口的方法会先于nestedParent中的方法被调用。这里我们介绍下本文例子中涉及到的方法:

  • startNestedScroll(int axes) :开始沿着参数中指定的方向(水平 or 垂直)进行嵌套滚动

  • dispatchNestedPreScroll(...) :这个方法会调用nestedParent的onNestedPreScroll()方法。这样就使得nestedParent有机会抢在NestedScroll之前消耗滚动事件。

嵌套滚动工作原理探索

现在相信各位同学都了解了如何实现基本的嵌套滚动,那么大家是否能够猜到它的实现原理呢?实际上,是nestedChild的onTouchEvent()方法中会对发生的Touch事件进行判断,若为DOWN事件则会调用startNestedScroll()方法;若为MOVE事件则会调用dispatchNestedPreScroll()方法。我们来看下NestedScrollView的onTouchEvent()方法:

public boolean onTouchEvent(MotionEvent ev) {
  . . . 
  final int actionMasked = . . .;
  . . . 
  switch (actionMasked) {    
    case MotionEvent.ACTION_DOWN: {
      . . .
      startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);      
      break;
    }    
    case MotionEvent.ACTION_MOVE:
      . . .      
      if (dispatchNestedPreScroll(...) {
        deltaY -= mScrollConsumed[1];
        . . .
      }
  . . .
}

我们可以看到,对于DOWN事件,确实会调用startNestedScroll()方法;在MOVE事件时,调用了dispatchNestedPreScroll()方法,deltaY表示nestedChild实际应该滚动的距离,我们可以看到它的值是本该滚动的距离减去nestedParent已经消耗掉的距离。

到这里,对嵌套滚动的启蒙介绍就完毕了:

来自:http://mp.weixin.qq.com/s?__biz=MzIzMjE1Njg4Mw==&mid=2650117793&idx=1&sn=3d9bc24a0138be98521ad4b952535ff3&chksm=f0980d1dc7ef840b3e6ad3db76be4d7133d79307581164b8f4a31ad4acaebd0318feb3fe98cc