CoordinatorLayout与快速返回的实现

英文原文:Quick return with CoordinatorLayout 

本文地址:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0818/3315.html 转载注明出处。

一个可以添加到滚动视图中的漂亮UI元素就是quick return view(快速返回视图)-一个可以在用户向某个方向滚动的时候消失,然后向另外方向滚动时显示的元素。

换句话说就是Google+  app中浮动操作按钮所做的事情。

1-uckFJ1qZy5_aoCpRlu4_-Q.gif

我现在处于从ListView到RecyclerView的转换过程当中,其中一个需要重新实现的效果就是quick return view之类的效果。

有相当多的热门库通过ListView实现了这个功能,比如 Lars Werkman的。Nick Butcher 和Roman Nurik的滚动技巧 启发了很多这样的库。在design support library发布之前,Makovastar的 FAB 库 是许多需要FAB的应用的选择。这个库提供了连接到滚动视图的功能,让FAB可以在列表向下滚动的时候自动消失。

但是我们的快速返回视图并不是一个FAB,而且我感觉可以不需要像现有解决办法那样的复杂。

剧透:答案是 CoordinatorLayout

我想起了AppBarLayout是如何利用CoordinatorLayout的 Behavior机制来在nested scroll view滚动的时候折叠Toolbar的。不过我被吓到了,CoordinatorLayout是崭新的东西,而且我以为Behavior很复杂。

但其实Behavior并不复杂。我将演示如何创建一个提供quick return功能的CoordinatorLayout Behavior。

CoordinatorLayout

首先,我们需要一个布局。我们的布局非常简单,只是在一个CoordinatorLayout中包含一个RecyclerView和footer view。

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:text="QuickReturn Footer"/>
</android.support.design.widget.CoordinatorLayout>

接下来,让我们看看CoordinatorLayout.Behavior类。它提供了很多回调方法来接收来自于CoordinatorLayout其它view的事件,好吧,其实是那些你想与之协作的view。一个Behavior与一个特定的View相关-回调中通过“child”引用这个view。许多回调还会传递一个“target” 或者一个 触发该回调的view:“dependency”。

有些方法是为了让CoordinatorLayout知道你的Behavior关心的是什么View,比如,如果你想让一个View基于一个ImageView的表现作什么事情,你可以使用下面的代码重写:

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof ImageView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    // Adjust the child View accordingly
    return true;
}

Scrolling Behavior

但是,我们的Behavior只关心滚动事件。一个Behavior会自动接收某些事件,而不需要我们去声明任何依赖。

不过,CoordinatorLayout会让Behavior知道它里面任意一个子view滚动何时开始,但是要了解更多的滚动信息的话,我们需要让CoordinatorLayout知道我们关注的是什么:

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent,
        View child, View target, View target,int scrollAxes) {
    return (scrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

我只关心RecyclerView中的垂直滚动,因此只在滚动事件包括垂直信息的时候才返回true。

接下来我们将响应滚动事件。我们有两个回调可以利用:onNestedScroll() 和 onNestedPreScroll()。存在着两个方法的原因是一些Behaviors(比如和AppBarLayout使用的)可能会消费掉部分滚动事件。既然AppBarLayout允许toolbar随着内容的滚动而滚动出去,它就会消费掉任意多的滚动距离向其他view暗示我已经计算了一段滚动距离了。

我想让quick return view的显示和隐藏不受其它view的影响,因此我只需要使用onNestedPreScroll()获取用户滚动的距离就好了。

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int\[\] consumed) {
    if (dy > 0 && mDySinceDirectionChange < 0
            || dy < 0 && mDySinceDirectionChange > 0) {
        mDySinceDirectionChange = 0;
    }
    mDySinceDirectionChange += dy;
    if (mDySinceDirectionChange > child.getHeight() 
            && child.getVisibility() == View.VISIBLE) {
        hide(child);
    } else if (mDySinceDirectionChange < 0 
            && child.getVisibility() == View.GONE) {
        show(child);
    }
}

这里的代码有点多,我们慢慢道来。

首先,我们的Behavior跟踪和计算自从用户上次改变滚动方向之后,目标view滚动了多少距离。这可以让behavior在用户改变了滑动方向但手指并没有离开的情况下也能正确响应。因为Behavior跟踪的是用户滚动的累加距离,我们可以在隐藏view之前先等待距离超过一个设定的值。在这里这个值就是quick return view的高度。

同时,我还在显示或者隐藏之前检查了child是否可见-一会儿你会知道为什么。

quick return view的显示与隐藏

直接设置quick return的visibility为GONE是可以的,但是我不希望它就像变了戏法似的突然消失了,我想它进入和退出时都有动画效果。

ViewPropertyAnimator可以轻易做到这点:

private void hide(final View view) {
    view.animate()
            .translationY(view.getHeight())
            .setInterpolator(INTERPOLATOR)
            .setDuration(200)
            .start();
}
private void show(final View view) {
    view.animate()
            .translationY(0)
            .setInterpolator(INTERPOLATOR)
            .setDuration(200)
            .start();
}

这就是需要做的全部工作!

不过,还有更多的细节需要处理。我告诉过我只想在view可见的时候才去隐藏view,在隐藏的时候才去显示。

if (mDySinceDirectionChange > child.getHeight() && child.getVisibility() == View.VISIBLE) {
    hide(child);
} else if (mDySinceDirectionChange < 0 && child.getVisibility() == View.GONE) {
    show(child);
}

另一个问题是在动画期间如果用户改变滚动方向Behavior的响应不太正确。为此我们需要在方向改变的时候取消动画:

if (dy > 0 && mDySinceDirectionChange < 0
        || dy < 0 && mDySinceDirectionChange > 0) {
    // We detected a direction change- cancel existing animations and reset our cumulative delta Y
    child.animate().cancel();
    mDySinceDirectionChange = 0;
}

最后,一旦view离开了屏幕,我们可以通过把view的visibility设置成GONE来优化下布局的效果。

为此,我们为animation添加一个AnimatorListener,在animation完成的时候更新view的visibility:

animator.setListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animator) {}
    @Override
    public void onAnimationEnd(Animator animator) {
        // Prevent drawing the View after it is gone
        view.setVisibility(View.GONE);
    }
    @Override
    public void onAnimationCancel(Animator animator) {
        // Canceling a hide should show the view
        show(view);
    }
    @Override
    public void onAnimationRepeat(Animator animator) {}
});

这里是hide() 方法中的animator listener 。show() 方法中类似。

使用 behavior

在quick return behavior发挥作用之前还有最后一个任务!我们需要把Behavior和quick return view关联。

最简单的办法就是在布局中使用app:layout_behavior属性。

<TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/quickreturn_background"
        android:gravity="center"
        android:padding="16dp"
        android:layout_gravity="bottom"
        android:textColor="@android:color/white"
        android:text="QuickReturn Footer"
        app:layout_behavior=".QuickReturnFooterBehavior"/>

结束

CoordinatorLayout Behavior可以做如此多的事情-Material风格的折叠式toolbar,FloatingActionButton自动为Snackbar让出空间,等等。我强烈推荐熟悉它以便我们自己去创建这些美好的交互。

代码

如果你想试试Behavior或者查看整个工作代码,这里是我在github上的例子.。

最后,如果你认证看了这篇文章,你还可以阅读这篇codepath教程:浮动操作按钮详解 中的“使用CoordinatorLayout”一节,里面的自定义浮动操作按钮Behavior的内容对本文是很好的补充。

--译者注。