Android NestedScrolling机制

317733423 8年前
   <h2><strong>一、概述</strong></h2>    <p>这样一个效果图,我们思考下如何实现</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fa59fd9d3d2e6be58fb76200cb889dd6.gif"></p>    <p style="text-align:center">nestedscrolling.gif</p>    <p>可以看到“Sticky View”滚动到顶部会“固定住”,列表下拉到第一条数据“Sticky View”又会一起往下滚动。</p>    <p>有人说,这个不就是View的事件分发吗?</p>    <p>假设我们按照传统的事件分发去理解,我们滑动的是下面的内容区域View,但是滚动的却是外部的ViewGroup,那么肯定是ViewGroup拦截了子View的事件;但是,上面的效果图,当ViewGroup滑动到一定程度,子View又开始滑动了,而且中间的过程是没有间断的。从正常的事件分发机制来讲这个是不可能的,因为当ViewGroup拦截事件后,是没办法再次交还给子View去处理的(除非你手动干预了事件的分发),关于这一点如果有不清楚的同学,可以先去了解下Android的事件分发机制。</p>    <p>那么有没有其他方案去解决我们的问题呢?答案是,有。</p>    <p>Android在support.v4包中为我们引入两个重要的接口:</p>    <ul>     <li> <p>NestedScrollingParent</p> </li>     <li> <p>NestedScrollingChild</p> </li>    </ul>    <p>有了上面这两个类,我们就可以实现“NestedScrolling(嵌套滚动)”的无缝衔接。</p>    <h2><strong>二、实现</strong></h2>    <p>上述效果图,分为三个部分:顶部布局(ImageView),中间的“Sticky View”(TextView)和底部的列表(RecyclerView)。</p>    <p>RecyclerView已经实现了NestedScrollingChild接口,所以本文的重点是实现NestedScrollingParent接口。</p>    <p>(1) 布局</p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="match_parent">      <com.example.hiphonezhu.nestedscrolling.StickyLayout          android:id="@+id/stickyNavLayout"          android:layout_width="match_parent"          android:layout_height="match_parent"          android:orientation="vertical">          <ImageView              android:id="@+id/iv"              android:layout_width="match_parent"              android:layout_height="100dp"             android:scaleType="centerCrop"              android:src="@drawable/bg" />          <TextView              android:id="@+id/tv_sticky"              android:layout_width="match_parent"              android:layout_height="wrap_content"              android:background="@android:color/holo_green_dark"              android:gravity="center"              android:paddingBottom="10dp"              android:paddingTop="10dp"              android:text="Sticky View"              android:textColor="@android:color/white" />          <android.support.v7.widget.RecyclerView              android:id="@+id/rv"              android:layout_width="match_parent"              android:layout_height="match_parent" />      </com.example.hiphonezhu.nestedscrolling.StickyLayout>  </FrameLayout></code></pre>    <p>StickyLayout是直接继承自LinearLayout,并且实现了NestedScrollingParent接口。</p>    <p>(2) 实现NestedScrollingParent接口</p>    <p>在具体实现之前,我们先看下这个接口的几个方法。</p>    <pre>  <code class="language-java">public interface NestedScrollingParent {      public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);      public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);      public void onStopNestedScroll(View target);      public void onNestedScroll(View target, int dxConsumed, int dyConsumed,        int dxUnconsumed, int dyUnconsumed);      public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);      public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);      public boolean onNestedPreFling(View target, float velocityX, float velocityY);      public int getNestedScrollAxes();  }</code></pre>    <p>我们需要重点关注下面几个方法</p>    <ul>     <li> <p>onStartNestedScroll该方法返回true,代表当前ViewGroup能接受内部View的滑动参数(这个内部View不一定是直接子View),一般情况下建议直接返回true,当然你可以根据nestedScrollAxes:判断垂直或水平方向才返回true。</p> </li>     <li> <p>onNestedPreScroll该方法会传入内部View移动的dx与dy,当前ViewGroup可以消耗掉一定的dx与dy,然后通过最后一个参数consumed传回给子View。例如,当前ViewGroup消耗掉一半dx与dy</p> <pre>  <code class="language-java">scrollBy(dx/2, dy/2);  consumed[0] = dx/2;  consumed[1] = dy/2;</code></pre> </li>     <li> <p>onNestedPreFling你可以捕获对内部View的fling事件,返回true表示拦截掉内部View的事件</p> </li>    </ul>    <p>我们看下具体的代码实现(仅是关键代码):</p>    <pre>  <code class="language-java">public class StickyLayout extends LinearLayout implements NestedScrollingParent {      @Override      public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)      {          return true;      }      @Override      public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)      {          // dy > 0表示子View向上滑动;            // 子View向上滑动且父View的偏移量<ImageView高度          boolean hiddenTop = dy > 0 && getScrollY() < maxScrollY;            // 子View向下滑动(说明此时父View已经往上偏移了)且父View还在屏幕外面, 另外内部View不能在垂直方向往下移动了          /**           * ViewCompat.canScrollVertically(view, int)           * 负数: 顶部是否可以滚动(官方描述: 能否往上滚动, 不太准确吧~)           * 正数: 底部是否可以滚动           */          boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1);            if (hiddenTop || showTop)          {              scrollBy(0, dy);              consumed[1] = dy;          }      }      @Override      public boolean onNestedPreFling(View target, float velocityX, float velocityY)      {          if (velocityY > 0 && getScrollY() < maxScrollY) // 向上滑动, 且当前View还没滑到顶          {              fling((int) velocityY, maxScrollY);              return true;          }          else if (velocityY < 0 && getScrollY() > 0) // 向下滑动, 且当前View部分在屏幕外          {              fling((int) velocityY, 0);              return true;          }          return false;      }  }</code></pre>    <ul>     <li> <p>onNestedPreScroll中,判断子View上滑( dy>0 )并且 StickyLayout 滚动到屏幕外的距离( getScrollY() )< 最大滚动距离 maxScrollY ,则隐藏顶部布局( ImageView );同理,如果子View下滑( dy < 0 )且 StickyLayout 还在屏幕外面( getScrollY() > 0 ),同时内部View不能在垂直方向往下移动了(可以借助 ViewCompat.canScrollVertically 来实现)。</p> <p>ViewCompat.canScrollVertically(view, int) ,第二个int类型参数</p> <p>负数: 顶部是否可以往下滚动</p> <p>正数: 底部是否可以往上滚动</p> <p>官方描述:“Negative to check scrolling up, positive to check scrolling down”,我觉得有误人子弟的嫌疑。</p> </li>     <li> <p>onNestedPreFling中,如果向上滑动( velocityY > 0 )且 ImageView 没有完全隐藏( getScrollY() < maxScrollY ),则使用fling方法,“尝试”(因为滑动距离取决于初始速度)将 ImageView 完全隐藏;同理,如果向下滑动( velocityY < 0 )且 ImageView 部分在屏幕外( getScrollY() > 0 ),则使用fling方法,“尝试”(因为滑动距离取决于初始速度)将 ImageView 完全显示。</p> </li>    </ul>    <p>对于fling方法,我们使用OverScroller的fling方法,另外边界检测,重写了scrollTo方法:</p>    <pre>  <code class="language-java">public void fling(int velocityY, int maxY)  {          mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, maxY);                invalidate();  }    @Override  public void scrollTo(int x, int y)  {      if (y < 0) // 不允许向下滑动      {          y = 0;      }      if (y > maxScrollY) // 防止向上滑动距离大于最大滑动距离      {          y = maxScrollY;      }      if (y != getScrollY())      {          super.scrollTo(x, y);      }  }  @Override  public void computeScroll()  {      if (mScroller.computeScrollOffset())      {          scrollTo(0, mScroller.getCurrY());          invalidate();      }        }</code></pre>    <p>到这里,大家发现其实NestedScrolling机制其实并不复杂:</p>    <p>在滑动的时候,内部View会把滑动的距离(dx与dy)传入给NestedScrollingParent,NestedScrollingParent可以决定对其是否消耗,消耗的值通过consumed[]再传回给子View。</p>    <h3><strong>三、写在最后</strong></h3>    <p>由于本文的效果ImageView和Sticky View(TextView)与“状态栏”有融合的效果,所以具体源码会比这个略微复杂些~</p>    <p>主要思路是:</p>    <p>布局中有一个一模一样的Sticky View(TextView),通过隐藏和显示它来达到最终的效果,如果你有更好的想法可以联系我。</p>    <p> </p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/aff5e82f0174</p>    <p> </p>