玩转Android嵌套滚动

Darla2301 7年前
   <p>在Android UI开发过程中,经常会遇到嵌套滚动的需求,所谓嵌套滚动,就是父view可以滚动的情况下子view也可以滚动,例如下拉刷新(PullToRefresh)。在微信读书之前的版本中,书籍讨论圈有一个比较复杂的嵌套滚动的例子,我把它抽取出来作为今天讲解的例子:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5ed8d4d7b7c3710b45dfb37f0cfc22c1.gif"></p>    <p>这个例子的嵌套比较复杂,上方的header为书籍封面,下方是一个ViewPager+TabLayout组成的容器(下文简称VT容器),ViewPager中的三个item为三个列表,也是可以滚动的。业务需求是:</p>    <ol>     <li>VT容器可以滚动;</li>     <li>书籍封面可以滚动,并且有视差;</li>     <li>当VT容器滚动到顶部时,滚动列表,并且滚动可以衔接。</li>     <li>当列表滚动到顶部时,可以滚动书籍封面以及VT容器,并且滚动可以衔接</li>    </ol>    <p>逻辑清楚了,接下来就看如何实现了。在android5以前,对于这种滚动,我们只能选择自己去拦截事件并处理,但在后面的某个版本,android推出了NestingScroll机制,开发者的日子就好过多了,并且android提供了一个非常好的容器类:CoordinatorLayout,极大的简化了开发者的工作。当然我们也需要投入精力去学习并运用这些新的Api了。</p>    <p>当然,我们也要知道如果没有这些API,我们应当如何去实现这些效果。因此本文会用三种方式去实现这个效果:</p>    <ol>     <li>纯事件拦截与派发方案</li>     <li>基于NestingScroll机制的实现方案</li>     <li>基于CoordinatorLayout与Behavior方案的实现</li>    </ol>    <p>示例代码放在Github上,可以 clone下来结合文章观看</p>    <h3>纯事件拦截与派发方案</h3>    <p>这是最为原始的方案,当然也灵活性最高的了。其它的方案原理上都是系统基于它提供的封装。使用这种方案时,我们需要解决以下几个问题:</p>    <ol>     <li>view的滚动(Scroller);</li>     <li>view的速度追踪(VelocityTracker);</li>     <li>当VT容器滚动到顶部时,我们如何将事件传递给ListView?</li>     <li>当ListView滚动到顶部时,VT容器如何拦截到事件?</li>    </ol>    <p>1、2两点属于滚动的基础知识,这里不会做细致的讲解。而第3点为何会出现呢?因为android系统在事件派发时,如果事件被拦截,那么之后的事件都将不会传递给子view了。其解决方案也很简单:在滚动到顶部时主动派发一次Down事件:</p>    <pre>  <code class="language-java">if (mTargetCurrentOffset + dy <= mTargetEndOffset) {      moveTargetView(dy);      // 重新dispatch一次down事件,使得列表可以继续滚动      int oldAction = ev.getAction();      ev.setAction(MotionEvent.ACTION_DOWN);      dispatchTouchEvent(ev);      ev.setAction(oldAction);  } else {      moveTargetView(dy);  }</code></pre>    <p>那么第4点是什么问题呢?这里就需要清楚一个坑点了:不是所用的事件都会走入onInterceptTouchEvent。有一种情况是子View主动调用 parent.requestDisallowInterceptTouchEvent(true) 来告诉系统说:这个事件我要了,父View不要拦截了。这就是所谓的内部拦截法。在ListView的某些时刻它会去调用这个方法。因此一旦事件传递给了ListView,外部容器就拿不到这个事件了。因此我们要打破它的内部拦截:</p>    <pre>  <code class="language-java">@Override  public void requestDisallowInterceptTouchEvent(boolean b) {      // 去掉默认行为,使得每个事件都会经过这个Layout  }</code></pre>    <p>方法如上,把requestDisallowInterceptTouchEvent的实现干掉就可以了。</p>    <p>主要的技术点已近提出来了。那么下面就看具体实现,首先看使用xml:</p>    <pre>  <code class="language-java"><org.cgspine.nestscroll.one.EventDispatchPlanLayout      android:id="@+id/scrollLayout"      android:layout_marginTop="?attr/actionBarSize"      android:layout_width="match_parent"      android:layout_height="match_parent"      app:header_view="@+id/book_header"      app:target_view="@+id/scroll_view"      app:header_init_offset="30dp"      app:target_init_offset="70dp">      <View          android:id="@id/book_header"          android:layout_width="120dp"          android:layout_height="150dp"          android:background="@color/gray"/>      <org.cgspine.nestscroll.one.EventDispatchTargetLayout          android:id="@id/scroll_view"          android:layout_width="match_parent"          android:layout_height="match_parent"          android:orientation="vertical"          android:background="@color/white">          <android.support.design.widget.TabLayout              android:id="@+id/tab_layout"              android:background="@drawable/list_item_bg_with_border_top_bottom"              android:layout_width="match_parent"              android:layout_height="@dimen/tab_layout_height"              android:fillViewport="true"/>          <android.support.v4.view.ViewPager              android:id="@+id/viewpager"              android:layout_width="match_parent"              android:layout_height="0dp"              android:layout_weight="1"/>      </org.cgspine.nestscroll.one.EventDispatchTargetLayout>  </org.cgspine.nestscroll.one.EventDispatchPlanLayout></code></pre>    <p>EventDispatchTargetLayout 实现了自定义接口 ITargetView :</p>    <pre>  <code class="language-java">public interface ITargetView {      boolean canChildScrollUp();      void fling(float vy);  }</code></pre>    <p>这是因为与具体业务抽离,我并不清楚内层盒子是怎样的(有可能就是ListView了,也有可能是ViewPager包裹ListView)</p>    <p>主要的实现在EventDispatchPlanLayout,使用时在xml中指定 header_init_offset 、 target_init_offset 等变量就可以了,基本上与业务逻辑独立。</p>    <p>其重点实现逻辑在 onInterceptTouchEvent 与 onTouchEvent 中了。个人不是很建议去动 dispatchTouchEvent ,虽然所有事件都会经过这里,但是这也明显会增加代码处理复杂度:</p>    <pre>  <code class="language-java">public boolean onInterceptTouchEvent(MotionEvent ev) {      ensureHeaderViewAndScrollView();      final int action = MotionEventCompat.getActionMasked(ev);      int pointerIndex;        // 不阻断事件的快路径:如果目标view可以往上滚动或者`EventDispatchPlanLayout`不是enabled      if (!isEnabled() || mTarget.canChildScrollUp()) {          Log.d(TAG, "fast end onIntercept: isEnabled = " + isEnabled() + "; canChildScrollUp = "                  + mTarget.canChildScrollUp());          return false;      }      switch (action) {          case MotionEvent.ACTION_DOWN:              mActivePointerId = ev.getPointerId(0);              mIsDragging = false;              pointerIndex = ev.findPointerIndex(mActivePointerId);              if (pointerIndex < 0) {                  return false;              }              // 在down的时候记录初始的y值              mInitialDownY = ev.getY(pointerIndex);              break;            case MotionEvent.ACTION_MOVE:              pointerIndex = ev.findPointerIndex(mActivePointerId);              if (pointerIndex < 0) {                  Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");                  return false;              }                final float y = ev.getY(pointerIndex);              // 判断是否dragging              startDragging(y);              break;            case MotionEventCompat.ACTION_POINTER_UP:              // 双指逻辑处理              onSecondaryPointerUp(ev);              break;            case MotionEvent.ACTION_UP:          case MotionEvent.ACTION_CANCEL:              mIsDragging = false;              mActivePointerId = INVALID_POINTER;              break;      }        return mIsDragging;  }</code></pre>    <p>代码逻辑很清晰,应该不用多说。接下来看 onTouchEvent 的处理逻辑。</p>    <pre>  <code class="language-java">public boolean onTouchEvent(MotionEvent ev) {      final int action = MotionEventCompat.getActionMasked(ev);      int pointerIndex;        if (!isEnabled() || mTarget.canChildScrollUp()) {          Log.d(TAG, "fast end onTouchEvent: isEnabled = " + isEnabled() + "; canChildScrollUp = "                  + mTarget.canChildScrollUp());          return false;      }     // 速度追踪     acquireVelocityTracker(ev);        switch (action) {          case MotionEvent.ACTION_DOWN:              mActivePointerId = ev.getPointerId(0);              mIsDragging = false;              break;            case MotionEvent.ACTION_MOVE: {              pointerIndex = ev.findPointerIndex(mActivePointerId);              if (pointerIndex < 0) {                  Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");                  return false;              }              final float y = ev.getY(pointerIndex);              startDragging(y);                if (mIsDragging) {                  float dy = y - mLastMotionY;                  if (dy >= 0) {                      moveTargetView(dy);                  } else {                      if (mTargetCurrentOffset + dy <= mTargetEndOffset) {                          moveTargetView(dy);                          // 重新dispatch一次down事件,使得列表可以继续滚动                          int oldAction = ev.getAction();                          ev.setAction(MotionEvent.ACTION_DOWN);                          dispatchTouchEvent(ev);                          ev.setAction(oldAction);                      } else {                          moveTargetView(dy);                      }                  }                  mLastMotionY = y;              }              break;          }          case MotionEventCompat.ACTION_POINTER_DOWN: {              pointerIndex = MotionEventCompat.getActionIndex(ev);              if (pointerIndex < 0) {                  Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");                  return false;              }              mActivePointerId = ev.getPointerId(pointerIndex);              break;          }            case MotionEventCompat.ACTION_POINTER_UP:              onSecondaryPointerUp(ev);              break;            case MotionEvent.ACTION_UP: {              pointerIndex = ev.findPointerIndex(mActivePointerId);              if (pointerIndex < 0) {                  Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");                  return false;              }                if (mIsDragging) {                  mIsDragging = false;                  // 获取瞬时速度                  mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);                  final float vy = mVelocityTracker.getYVelocity(mActivePointerId);                  finishDrag((int) vy);              }              mActivePointerId = INVALID_POINTER;              //释放速度追踪              releaseVelocityTracker();              return false;          }          case MotionEvent.ACTION_CANCEL:              releaseVelocityTracker();              return false;      }        return mIsDragging;  }</code></pre>    <p>或许有人会说:为何与 onInterceptTouchEvent 与有很多重复代码?这是因为如果事件不打断,并且子类不处理,就会走进 onTouchEvent 逻辑,所以这些重复处理是有意义的(其实是抄 SwipeRefreshLayout 的)。里面主要的逻辑就是两个:</p>    <ol>     <li>滚动容器</li>     <li>TouchUp时滚动到特定位置以及fling传递</li>    </ol>    <p>滚动容器的逻辑:</p>    <pre>  <code class="language-java">private void moveTargetViewTo(int target) {      target = Math.max(target, mTargetEndOffset);      // 用offsetTopAndBottom来偏移view      ViewCompat.offsetTopAndBottom(mTargetView, target - mTargetCurrentOffset);      mTargetCurrentOffset = target;        // 滚动书籍封面view,根据TargetView进行定位      int headerTarget;      if (mTargetCurrentOffset >= mTargetInitOffset) {          headerTarget = mHeaderInitOffset;      } else if (mTargetCurrentOffset <= mTargetEndOffset) {          headerTarget = mHeaderEndOffset;      } else {          float percent = (mTargetCurrentOffset - mTargetEndOffset) * 1.0f / mTargetInitOffset - mTargetEndOffset;          headerTarget = (int) (mHeaderEndOffset + percent * (mHeaderInitOffset - mHeaderEndOffset));      }      ViewCompat.offsetTopAndBottom(mHeaderView, headerTarget - mHeaderCurrentOffset);      mHeaderCurrentOffset = headerTarget;  }</code></pre>    <p>TouchUp的滚动逻辑:</p>    <pre>  <code class="language-java">private void finishDrag(int vy) {      Log.i(TAG, "TouchUp: vy = " + vy);      if (vy > 0) {          // 向下触发fling,需要滚动到Init位置          mNeedScrollToInitPos = true;          mScroller.fling(0, mTargetCurrentOffset, 0, vy,                  0, 0, mTargetEndOffset, Integer.MAX_VALUE);          invalidate();      } else if (vy < 0) {         // 向上触发fling,需要滚动到End位置          mNeedScrollToEndPos = true;          mScroller.fling(0, mTargetCurrentOffset, 0, vy,                  0, 0, mTargetEndOffset, Integer.MAX_VALUE);          invalidate();      } else {          // 没有触发fling,就近原则          if (mTargetCurrentOffset <= (mTargetEndOffset + mTargetInitOffset) / 2) {              mNeedScrollToEndPos = true;          } else {              mNeedScrollToInitPos = true;          }          invalidate();      }  }</code></pre>    <p>当然这里会打上一些标志位,具体实现是在 computeScroll 中,这属于Scroller的功能,这里就不展开了。</p>    <p>这样大体逻辑就讲述清楚了,其它细节就请看官直接看源码了。</p>    <h3>基于NestingScroll机制的实现方案</h3>    <p>NestingScroll机制是在某个版本support包加入的,不过外界极少有文章介绍,所以应该大多数人并不知道这个机制。NestingScroll主要有两个接口:</p>    <ul>     <li>NestedScrollingParent</li>     <li>NestedScrollingChild</li>    </ul>    <p>当我们需要使用NestingScroll特性时,我们去实现这两个接口就好了。NestingScroll本质是内部拦截发然后将相应的接口开给外界。因此实现NestedScrollingChild接口是有难度的,不过像RecyclerView这些控件,官方已经帮我们实现好了NestedScrollingChild,要完成我们的需求,我们直接拿来用就好了(ListView就没办法使用了,当然你也可以去实现NestedScrollingChild接口)。并且 NestedScrollingChild 与 NestedScrollingParent 只要有嵌套关系就行了,并不一定 NestedScrollingChild 是直接的子View。</p>    <p>我们来来看看 NestedScrollingParent 的定义:</p>    <pre>  <code class="language-java">public interface NestedScrollingParent {      // 是否接受NestingScroll      public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);      // 接受NestingScroll的Hook钩子      public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);      // NestingScroll结束      public void onStopNestedScroll(View target);      // NestingScroll进行中。重要参数dxUnconsumed, dyUnconsumed: 用于表示没有被消耗的滚动量,一般是列表滚动到头了,就会产生未消耗量      public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);      // NestingScroll滚动之前。重要参数consumed: 是用于告诉子View我消耗了多少。如果位全部消耗dy,那么子view就可以消耗了。      public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);      // fling时      public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);      // fling之前:可以由父元素消耗这次fling事件      public boolean onNestedPreFling(View target, float velocityX, float velocityY);     // 获取滚动轴: x轴或y轴     public int getNestedScrollAxes();  }</code></pre>    <p>接口是非常丰富的。有一个很重要的概念: 消耗量 。 比如我滑动了10dp,那么父元素先看看可以消耗多少(例如4dp),然后会把未消耗量传递给子View(6dp)。这就把嵌套滚动的问题转换为资源分配的问题了。非常机智。除此以外,官方提供了 NestedScrollingParentHelper 类帮我实现了一些公共方法并做好了低版本兼容,我们应当拿来用。</p>    <p>现在来看看Demo项目的实现。先来看看基于NestingScroll的实现的方案的滚动的使用xml:</p>    <pre>  <code class="language-java"><org.cgspine.nestscroll.two.NestingScrollPlanLayout      android:id="@+id/scrollLayout"      android:layout_marginTop="?attr/actionBarSize"      android:layout_width="match_parent"      android:layout_height="match_parent"      app:header_view="@+id/book_header"      app:target_view="@+id/scroll_view"      app:header_init_offset="30dp"      app:target_init_offset="70dp">      <View          android:id="@id/book_header"          android:layout_width="120dp"          android:layout_height="150dp"          android:background="@color/gray"/>      <LinearLayout          android:id="@id/scroll_view"          android:layout_width="match_parent"          android:layout_height="match_parent"          android:orientation="vertical"          android:background="@color/white">          <android.support.design.widget.TabLayout              android:id="@+id/tab_layout"              android:background="@drawable/list_item_bg_with_border_top_bottom"              android:layout_width="match_parent"              android:layout_height="@dimen/tab_layout_height"              android:fillViewport="true"/>          <android.support.v4.view.ViewPager              android:id="@+id/viewpager"              android:layout_width="match_parent"              android:layout_height="0dp"              android:layout_weight="1"/>      </LinearLayout>    </org.cgspine.nestscroll.two.NestingScrollPlanLayout></code></pre>    <p>可以看到大体上与第一种方式的使用相同,并且我们不用再额外封装一个内部Layout了。集中在 NestingScrollPlanLayout 就好了。</p>    <p>它是作为NestingScroll父元素存在,因此实现了 NestedScrollingParent 接口:</p>    <pre>  <code class="language-java">public class NestingScrollPlanLayout extends ViewGroup implements NestedScrollingParent{...}</code></pre>    <p>其几个实现方法为:</p>    <pre>  <code class="language-java">@Override  public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {      Log.i(TAG, "onStartNestedScroll: nestedScrollAxes = " + nestedScrollAxes);      // 接受纵向滚动      return isEnabled() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;  }    @Override  public void onNestedScrollAccepted(View child, View target, int axes) {      Log.i(TAG, "onNestedScrollAccepted: axes = " + axes);      // 这一步需要交给NestedScrollingParentHelper去记录相关变量      mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);  }    @Override  public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {      // NestingScroll滚动前,我们要先看看自己能不能消耗,消耗量记录在consumed     // 往上滑动时我们先看看自己可以消耗多少(因为上滑时自己的消耗量可以出现上限),往下滑动时我们看看子元素可以消耗多少(因为下滑时子View的消耗量可以出现上限)     // 基于上一点,我们这里只处理上滑的情况      Log.i(TAG, "onNestedPreScroll: dx = " + dx + " ; dy = " + dy);      if (canViewScrollUp(target)) {          return;      }      if (dy > 0) {          // 往上滑          int parentCanConsume = mTargetCurrentOffset - mTargetEndOffset;          if (parentCanConsume > 0) {              if (dy > parentCanConsume) {                 // 自己消耗不完,会余下部分给子view                  consumed[1] = parentCanConsume;                  moveTargetViewTo(mTargetEndOffset);              } else {                  // 自己全部消耗                  consumed[1] = dy;                  moveTargetView(-dy);              }          }      }  }    @Override  public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {      // NestingScroll时,我们只处理往下滑的情况,如果有未消耗的量,则滚动父View      Log.i(TAG, "onNestedScroll: dxConsumed = " + dxConsumed + " ; dyConsumed = " + dyConsumed +              " ; dxUnconsumed = " + dxUnconsumed + " ; dyUnconsumed = " + dyUnconsumed);      if (dyUnconsumed < 0 && !(canViewScrollUp(target))) {          int dy = -dyUnconsumed;          moveTargetView(dy);      }  }    @Override  public int getNestedScrollAxes() {      return mNestedScrollingParentHelper.getNestedScrollAxes();  }    @Override  public void onStopNestedScroll(View child) {      Log.i(TAG, "onStopNestedScroll");      mNestedScrollingParentHelper.onStopNestedScroll(child);      // 结束滚动:因为不管有没有出现fling,都会走近这里,所以我这里有一个标志位,如果有fling,则在fling中处理最终定位,否则在结束时处理最终定位      if (mHasFling) {          mHasFling = false;      } else {          if (mTargetCurrentOffset <= (mTargetEndOffset + mTargetInitOffset) / 2) {              mNeedScrollToEndPos = true;          } else {              mNeedScrollToInitPos = true;          }          invalidate();      }  }    @Override  public boolean onNestedPreFling(View target, float velocityX, float velocityY) {      // fling前回调,我们会主动将其滚动到特定位置,如果向上fling时,会return false表示并不阻断子view的fling      super.onNestedPreFling(target, velocityX, velocityY);      Log.i(TAG, "onNestedPreFling: mTargetCurrentOffset = " + mTargetCurrentOffset +              " ; velocityX = " + velocityX + " ; velocityY = " + velocityY);      mHasFling = true;      int vy = (int) -velocityY;      if (velocityY < 0) {          // 向下          if (canViewScrollUp(target)) {              return false;          }          mNeedScrollToInitPos = true;          mScroller.fling(0, mTargetCurrentOffset, 0, vy,                  0, 0, mTargetEndOffset, Integer.MAX_VALUE);          invalidate();          return true;      } else {          // 向上          if (mTargetCurrentOffset <= mTargetEndOffset) {              return false;          }          mNeedScrollToEndPos = true;          mScroller.fling(0, mTargetCurrentOffset, 0, vy,                  0, 0, mTargetEndOffset, Integer.MAX_VALUE);          invalidate();      }      return false;  }</code></pre>    <p>在NestingScroll机制的帮助下,程序员们终于不需要亲自去处理事件拦截与处理了,只需要在各个回调中加上我们的逻辑,就可以跑起来了,堪称完美。</p>    <p>除此之外需要说明一点:NestingScroll机制下的各种回调的参数如dx、dy、velocityX、velocityY与我们第一种方案自己所计算的值正负是相反的,需要我们留意一下。</p>    <h3>基于CoordinatorLayout与Behavior方案的实现</h3>    <p>CoordinatorLayout 是一个非常牛逼的控件,其本质也是基于NestingScroll机制的一种实现。在网上经常有 CoordinatorLayout 配合 AppBarLayout 、 FloatingActionButton 实现非常漂亮的MD风格,所以学会 CoordinatorLayout 的使用也是很必要的。</p>    <p>CoordinatorLayout 只是提供了一个环境,想要使用 CoordinatorLayout 实现一些特效则需要依赖官方提供的另外一个抽象类 Behavior 。像 AppBarLayout 这种控件是系统提供了内置的 Behavior 实现,所以我们拿来就可以用。但如果我们想要特殊行为,就需要自己去实现自己的 Behavior 。</p>    <p>Behavior 翻译过来则是 行为 。将 Behavior 运用到View上则大体上会有两类:</p>    <ol>     <li>View自身的变化依赖于其它View的变化(例如Demo的书籍封面)</li>     <li>外部事件驱动View的变化(例如Demo的VT容器)</li>    </ol>    <p>以Demo项目为例,书籍封面的位置移动是依赖于VT容器。只要后者位置变化,那么它就改变自己的位置,我们可以用 Behavior 来描述这种依赖关系:</p>    <pre>  <code class="language-java">public class CoverBehavior extends CoordinatorLayout.Behavior<View> {      //...        @Override      public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {          // 这里绝对依赖于谁?CoordinatorLayout会一个个询问child的兄弟元素,看是否依赖于它          // demo中我就让它依赖于拥有TargetBehavior的view          Log.i(TAG, "layoutDependsOn");          CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) dependency.getLayoutParams();          if (lp.getBehavior() instanceof TargetBehavior) {              return true;          }          return super.layoutDependsOn(parent, child, dependency);      }        @Override      public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {          // 当依赖View发生变化时,child就可以相应做出一些改变          CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) dependency.getLayoutParams();          if (lp.getBehavior() instanceof TargetBehavior) {              TargetBehavior behavior = (TargetBehavior) lp.getBehavior();              moveHeaderView(behavior, child);              return true;          }          return super.onDependentViewChanged(parent, child, dependency);      }  }</code></pre>    <p>而另外一种行为就是手指移动驱使View滚动。也就是ViewPager+TabLayout容器,实现还是基于NestingScroll:</p>    <pre>  <code class="language-java">public class TargetBehavior extends CoordinatorLayout.Behavior<View> {      //...      @Override      public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {           //...      }        @Override      public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,                                    int dx, int dy, int[] consumed) {           //...      }        @Override      public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,                                 int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {           //...      }        @Override      public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,                                      float velocityX, float velocityY) {           //...      }        @Override      public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {          super.onStopNestedScroll(coordinatorLayout, child, target);           //...      }  }</code></pre>    <p>我们可以看到,这实现基本上还是NestingScroll那一套,并且调用时机想仿,或许看完代码后大家会有一个疑问:这里并不是View,而一般利用Scroller滚动需要借助View的 computeScroll 方法,那我们这里应该怎么做呢?其实利用 computeScroll 方法只是利用了view每次 invalidate 会调用这个方法的特性,所以我们可以用 ViewCompat.postOnAnimation(View, Runnable) 仿造这一行为。它的传参需要实现Runnable接口,我的实现如下:</p>    <pre>  <code class="language-java">private class ScrollAction implements Runnable {      private View mView;        public ScrollAction(View view) {          mView = view;      }        @Override      public void run() {          if (mScroller.computeScrollOffset()) {              int offsetY = mScroller.getCurrY();              moveTargetViewTo(mView, offsetY);              ViewCompat.postOnAnimation(mView, new ScrollAction(mView));          } else if (mNeedScrollToInitPos) {              mNeedScrollToInitPos = false;              if (mTargetCurrentOffset == mTargetInitOffset) {                  return;              }              mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetInitOffset - mTargetCurrentOffset);              ViewCompat.postOnAnimation(mView, new ScrollAction(mView));          } else if (mNeedScrollToEndPos) {              mNeedScrollToEndPos = false;              if (mTargetCurrentOffset == mTargetEndOffset) {                  return;              }              mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetEndOffset - mTargetCurrentOffset);              ViewCompat.postOnAnimation(mView, new ScrollAction(mView));          }      }  }</code></pre>    <p>其实Behavior可以做到更多,它可以接管view的onMeasure、onLayout、onInterceptTouchEvent、onTouchEvent等方法。在 CoordinatorLayout 环境下,每一个子View提供自己的特殊行为, CoordinatorLayout 则负责协调这些行为,使得整个系统可以有机结合起来。</p>    <p>最后看一下如何使用Behavior。Behavior提供两种方式,一种是在xml用 layout_behavior 的方式,传入字符串,在编译时通过反射生成对象。另一种就是在java代码里面赋值了,本demo采取的直接在Java代码里赋值:</p>    <pre>  <code class="language-java">mHeaderView = findViewById(R.id.book_header);  CoordinatorLayout.LayoutParams headerLp = (CoordinatorLayout.LayoutParams) mHeaderView          .getLayoutParams();  headerLp.setBehavior(new CoverBehavior(Util.dp2px(this, 30), 0));    mTargetLayout = (LinearLayout) findViewById(R.id.scroll_view);  CoordinatorLayout.LayoutParams targetLp = (CoordinatorLayout.LayoutParams) mTargetLayout          .getLayoutParams();  targetLp.setBehavior(new TargetBehavior(this, Util.dp2px(this, 70), 0));</code></pre>    <h3>写在最后</h3>    <p>虽然google提供了很多新颖好玩的接口。但这需要花费部分精力去实践这些新技术。这是非常有意义的投入。多看、多写,才能帮助我们用更少的时间写更好的代码。</p>    <h3>参考文章:</h3>    <p><a href="/misc/goto?guid=4959734132375848904" rel="nofollow,noindex">Android NestedScrolling机制完全解析 带你玩转嵌套滑动</a></p>    <p><a href="/misc/goto?guid=4959734132466397321" rel="nofollow,noindex">Material Design 之 Behavior 的使用和自定义 Behavior</a></p>    <p> </p>    <p>来自:http://www.androidchina.net/6270.html</p>    <p> </p>