NESTED SCROLLING WITH COORDINATORLAYOUT ON ANDROID

December 9, 2015

原文:https://lab.getbase.com/nested-scrolling-with-coordinatorlayout-on-android/

上一篇 文章中 我解释了什么是CoordinatorLayout以及如何自定义一个Behavior。有几个读者评论问如何写自定义的可滚动的View来和CoordinatorLayout以及AppBarLayout很好地进行交互。 有些读者还对为什么AppBarLayout只和RecyclerView交互,换成ListView却没有用。

我们来看看滑动RecyclerView时自动显示/隐藏AppbarLayout的效果吧:

https://lab.getbase.com/wp-content/uploads/2015/10/demo3-1.gif

源代码

查看一下AppBarLayout代码,我们会发现默认的Behavior的注解 @DefaultBehavior(AppBarLayout.Behavior.class)

AppBarLayout.Behavior实现了以下几个方法,来对滑动事件作出响应:

void    onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed)
void    onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
void    onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes)
boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes)
void    onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target)

这就解释了为什么AppBarLayout放在一个CoordinatorLayout里面时会发生位置变动。 现在的问题是为什么CoordinatorLayout能够再它的子视图的滑动事件发生时得到通知。 我们来仔细看看RecyclerView类,也许能找到答案。

SAY HELLO TO NESTEDSCROLLINGCHILD AND NESTEDSCROLLINGPARENT INTERFACES.

我们看到RecyclerView实现了NestedScrollingChild接口,Doc是这么写的:

This interface should be implemented by View subclasses that wish to support dispatching nested scrolling operations to a cooperating parent ViewGroup.

大概意思就是说如果View的子类要实现分发嵌套(Nested)的事件给父容器,就实现这个接口。

要接收嵌套(Nested)的滑动操作,CoordinatorLayout需要实现NestedScrollingParent接口。

This interface should be implemented by ViewGroup subclasses that wish to support scrolling operations delegated by a nested child view.

我们再深入进入NestedScrollingChildNestedScrollingParent的通信过程。

当子View将要开始滑动时(会对应到onTouch的down事件),它就会执行onScrollStarted方法,在滑动的每一步都会执行两个方法dispatchPreScrolldispatchScroll, 第一个方法为父容器在子View消费滑动操作之前去消费其中的一部分或者全部。如果父容器返回true,那么子View应该减少它的滑动量,这个量是由父容器消费的。dispatchNestedScroll方法报告滑动进度给父容器,包括已消费的和未消费的滑动量。父容器可以根据情况处理:

An implementation may choose to use the consumed portion to match or chase scroll position of multiple child elements, for example. The unconsumed portion may be used to allow continuous dragging of multiple scrolling or draggable elements, such as scrolling a list within a vertical drawer where the drawer begins dragging once the edge of inner scrolling content is reached.

CoordinatorLayout就扮演了一个代理。它接收到从子View来的回调,并转发给它的Behavior类。

当滑动结束时,子View会调用onScrollStoped

IMPLEMENTING CUSTOM NESTEDSCROLLINGCHILD VIEW.

我们既然知道它是怎么工作的了,我们就可以实现一个示例程序,其中包含一个自定义View,它会检测滑动,并转发给它的CoordinatorLayout

Let’s start with the View public class NestedScrollingChildView extends View implements NestedScrollingChild, OnGestureListener

开始代码:

public class NestedScrollingChildView extends View implements NestedScrollingChild, OnGestureListener

Android团队里的好人们想让这个工作稍微简单一点,所以它们做了一个NestedScrollingChildHelper.

Helper class for implementing nested scrolling child views compatible with Android platform versions earlier than Android 5.0 Lollipop (API 21).

我们需要做的就是创建一个helper的示例,并实现适当的方法。 默认的nested滑动是关闭的,我们可以在构造函数里启用它。

setNestedScrollingEnabled(true);

现在我们要检测滑动,我们用GestureDetectorCompat来减少代码量

@Override
public boolean onTouchEvent(MotionEvent event){
  return mDetector.onTouchEvent(event);
}

onDown方法里,我们要调用onStartScrolling,然后传递垂直轴的flag作为参数。

@Override
public boolean onDown(MotionEvent e) {
  startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
  return true;
}

onScroll方法里,我们可以调用dispatchNestedPreScroll,传递计算好的distanceY,然后调用dispatchNestedScroll。由于我们的View其实并不会滑动,因此,我们0作为我们消费和未消费的滑动值。

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
  dispatchNestedPreScroll(0, (int) distanceY, null, null);
  dispatchNestedScroll(0, 0, 0, 0, null);
  return true;
}

最后一部分是当滑动过程结束时,调用stopNestedScroll。由于GestureDetectorCompat没有提供合适的回调,我们要在onTouchEvent函数中自定义一个实现。

@Override
public boolean onTouchEvent(MotionEvent event){
  final boolean handled = mDetector.onTouchEvent(event);
  if (!handled && event.getAction() == MotionEvent.ACTION_UP) {
    stopNestedScroll();
  }
  return true;
}

为了保持简单,本例中不处理Fling操作(快速滑动后的惯性)。

And voilà

https://lab.getbase.com/wp-content/uploads/2015/10/demo.gif

完整的源码在这里:https://github.com/ggajews/nestedscrollingchildviewdemo 如果你还希望让ListView也和CoordinatorLayout配合,你要自定义 ListView实现NestedScrollingChild接口。