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

dztc6335 8年前
   <p><strong>从是什么开始 </strong></p>    <p>我们先来看一个动图,直观的感受下什么是嵌套滚动(nested scrolling):</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c0c361b94f3f3b9b73c3d4112aad2037.gif"></p>    <p>既然是嵌套,就说明是一层套着一层,存在两个滚动行为。在上图中,当我们滚动下面的UI控件时,先滚动的却是外头的父容器,当父容器滚动到一定程度后,下面的UI控件才开始滚动。这样看来,确实存在着两个滚动行为。</p>    <p>那么嵌套关系体现在哪呢?我们先来看下实现上图效果的布局文件结构:</p>    <pre>  <code class="language-java"><ParentView>      <ImageView />      <NestedScrollView>      <WebView />    </NestedScrollView>    </CoordinatorLayout></code></pre>    <p>可以看到,ImageView和NestedScrollView都是ParentView的子View,ParentView使我们自定义的一个继承自LinearLayout的布局管理器。实际上,ParentView类实现了NestedScrollingParent接口,NestedScrollView实现了NestedScrollingChild接口。从名字上我们可以做出这样的猜测:嵌套滚动中需要一个子View和一个作为容器的父View,子View需要实现NestedScrollingChild接口,父View需要实现NestedScrollingParent接口。分别实现了上述两个接口的父View把子View套起来,就可以实现所谓的嵌套滚动。</p>    <p>现在,我们知道了嵌套滚动中的两个主角:</p>    <ul>     <li> <p>一个实现了NestedScrollingParent的父容器,在本文的其余部分,我们简称为nestedParent;</p> </li>     <li> <p>一个实现了NestedScrollingChild的子View,下文简称为nestedChild。</p> </li>    </ul>    <p>本文接下来的部分会以上图效果为例,讲解嵌套滚动究竟是如何实现的。</p>    <h3><strong>NestedScrollingParent接口 </strong> <strong> </strong></h3>    <p>我们来看下本文例子中会涉及到的NestedScrollingParent接口中的方法:</p>    <ul>     <li> <p>onStartNestedScroll(View child, View target, int nestedScrollAxes) :当nestedChild想要进行嵌套滚动时,会调用nestedParent的这个方法。这个芳法用于指示是否支持嵌套滚动,比如我们只想支持垂直方向上的嵌套滚动,可以在nestedParent中这样实现这个方法:</p> </li>    </ul>    <pre>  <code class="language-java">@Overridepublic boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {        if (nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL) {              return true;        }        return false;  }</code></pre>    <ul>     <li> <p>onNestedPreScroll(View target, int dx, int dy, int[] consumed) :当我们滚动nestedChild时,nestedChild进行实际的滚动前,会先调用nestParent的这个方法。nestedParent在这个方法中可以把子View想要滚动的距离消耗掉一部分或是全部消耗,比如我们的例子中,当我们向上滚动nestedChild时,nestParent会抢在它前头先滚动,直到ImageView完全隐藏,才让nestedChild开始滚动。</p> </li>    </ul>    <p>在这个例子中,我们自定义的ParentView继承了LinearLayout类,实现了NestedScrollingParent接口,并重写了上面提到的两个方法。相关代码如下:</p>    <pre>  <code class="language-java">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);      }    }    }</code></pre>    <p>onStartNestedScroll()方法的实现上面我们已经介绍过,这里不再赘述。简单说下onNestedPreScroll()方法的实现。不过在这之前我们补下滚动相关的知识。</p>    <h3><strong>滚动相关知识补充 </strong></h3>    <p>先来看一张图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d102622d3aaa20e05dcf5cf17ba2168d.png"></p>    <p>在上图中,黑色边框代表了View的边框,蓝色边框代表了View的内容的边框。其实我们平时对ListView等控件进行滚动时,实际滚动的是View的内容。比如在上图中,我们向右滚动一个控件,可以看到,实际上是它的内容向右进行滚动了,View的边界线的位置始终是固定的。上图中蓝色右边框和黑色右边框间的距离就是View滚动的距离。</p>    <p>每个View都有名为mScrollX和mScrollY的两个成员变量,前者记录了View在水平方向上滚动的距离,后者记录了View在竖直方向上滚动的距离。mScrollX的绝对值为View的左边框与View的内容的左边框的距离,当View向左滚动时,mScrollX是正的;当View向右滚动时,mScrollX是负的。mScrollY的绝对值为View的上边框与View的内容的上边框的距离,当View向上滚动时,mScrollY是正的;当View向下滚动时,mScrollY是负的。</p>    <p>理解了上面的内容后,让我们看看onNestedPreScroll()为什么要像上面那样实现。</p>    <p>在onNestedPreScroll()方法中,参数dy代表了本次NestedScrollView想要滑动的距离。若我们向上滑动NestedScrollView,dy就是正的,向下就是负的。getScorllY()会返回ParentView的mScrollY参数,为正则表示当前ParentView的内容已经向上滚动了一段距离,否则表示向下滚动过一段距离。ivHeight表示ImageView的高度。理解了上面这些,这个方法的逻辑就很好理解了,这里不再赘述。</p>    <h3><strong>NestedScrollingChild接口 </strong></h3>    <p>我们在布局文件中使用的NestedScrollView就实现了NestedScrollingChild接口。当我们滚动nestedChild时,这个接口的方法会先于nestedParent中的方法被调用。这里我们介绍下本文例子中涉及到的方法:</p>    <ul>     <li> <p>startNestedScroll(int axes) :开始沿着参数中指定的方向(水平 or 垂直)进行嵌套滚动</p> </li>     <li> <p><strong>dispatchNestedPreScroll(...)</strong> :这个方法会调用nestedParent的onNestedPreScroll()方法。这样就使得nestedParent有机会抢在NestedScroll之前消耗滚动事件。</p> </li>    </ul>    <h2>嵌套滚动工作原理探索</h2>    <p>现在相信各位同学都了解了如何实现基本的嵌套滚动,那么大家是否能够猜到它的实现原理呢?实际上,是nestedChild的onTouchEvent()方法中会对发生的Touch事件进行判断,若为DOWN事件则会调用startNestedScroll()方法;若为MOVE事件则会调用dispatchNestedPreScroll()方法。我们来看下NestedScrollView的onTouchEvent()方法:</p>    <pre>  <code class="language-java">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];          . . .        }    . . .  }</code></pre>    <p>我们可以看到,对于DOWN事件,确实会调用startNestedScroll()方法;在MOVE事件时,调用了dispatchNestedPreScroll()方法,deltaY表示nestedChild实际应该滚动的距离,我们可以看到它的值是本该滚动的距离减去nestedParent已经消耗掉的距离。</p>    <p>到这里,对嵌套滚动的启蒙介绍就完毕了:</p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s?__biz=MzIzMjE1Njg4Mw==&mid=2650117793&idx=1&sn=3d9bc24a0138be98521ad4b952535ff3&chksm=f0980d1dc7ef840b3e6ad3db76be4d7133d79307581164b8f4a31ad4acaebd0318feb3fe98cc</p>    <p> </p>