Android嵌套滑动简介——手把手教你打造黏性头部控件

Android嵌套滑动简介——手把手教你打造黏性头部控件

    • 问题的引入
    • 接口简介
    • 嵌套滑动实战
    • 关于嵌套滑动踩过的坑以及误区

问题的引入

首先让我们看一个效果:
Android嵌套滑动简介——手把手教你打造黏性头部控件_第1张图片
  在很早之前, 我们想实现上面这个效果的通常做法是自己写一个ViewGroup, 拦截下触摸事件, 控制里面滑动事件的分发. 如果第一个view已经滑出屏幕,则把剩下的事件交给recyclerview处理.
  处理过的同学们都知道, 分发复杂,还要考虑fling这种操作怎么处理,做起来踩坑不断. 好在Android在之后的版本推出了很多替代的方案: 首先看到这个布局, 首选是用CoordinatorLayout来处理, 官方提供的十分好用,不太熟悉的同学可以查一下. 如果说我不想用CoordinatoLayout,想自己来解决嵌套滑动可不可以呢?答案是肯定的, 谷歌为我们提供了下面两个接口:
    NestedScrollingParent(这个接口负责处理嵌套滑动)
    NestedScrollingChild(这个接口负责将滑动事件分发给实现了NestedScrollingParent接口的类)

接口简介

  这里我们先看一下接口中都有哪些方法,首先是NestedScrollingParent:
Android嵌套滑动简介——手把手教你打造黏性头部控件_第2张图片
  然后是NestedScrollingChild:
Android嵌套滑动简介——手把手教你打造黏性头部控件_第3张图片
  初次看到这么多接口肯定都一脸蒙蔽. 没关系,我们先暂时将接口放一边, 来想一下如果要你来设计一个解决嵌套滑动的框架,要怎么实现. emmm, 首先嵌套滑动肯定是因为一个同方向的滑动, 有两个或者以上的view关心, 然后我们根据消费这个事件的优先级, 来分配这个滑动事件.(如例子中, 我们先把上滑的事件给第一个子view,待其不想继续消费时,即滑到第二部分时,再把事件分发给recyclerview). 按照这个思路, 我们就需要有一个能够将生成出的滑动事件抛出去的类,同时要有一个地方能够接收到这些滑动事件,并且决定怎么分配(消费).
  好了,现在让我们回头看一下上面两个接口,NestedScrollingChild正是上面说的将滑动事件抛出去的类, 所有有可能产生滑动事件的类(如recyclerview、viewpager)都应该继承这个接口,在滑动事件发生时,自己先不处理,抛出去给其他有可能消费这个事件的类(NestedScrollingParent)去处理.然后就很好理解了,触摸事件经由NestedScrollingChild传到我这里, 我来决定消不消费(onStartNestedScroll),消费多少(onNestedPreScroll、onNestedPreFling),怎么消费, 这就是NestedScrollingParent这个接口需要处理的问题. 怎么样,上面这一套机制是不是很熟悉,是不是和触摸事件分发有点像.
  如果你细心看官方文档的话会发现,除了上面两个接口, 谷歌还提供了NestedScrollingChild2、NestedScrollingParent2以及NestedScrollingChild3、NestedScrollingParent3这两对接口,这是怎么回事呢?
  其实这是嵌套滑动处理不断优化的结果,如果你使用过第一代NestedScrollingParent接口就会发现,在处理fling事件的时候,如果第一个view没有把fling的距离和速度消费完,剩下的fling事件也没有机会传回给child了,为了兼容的解决这个问题,谷歌的工程师们想到了一个方法,就是把fling事件分多次作为scroll事件分发,通过type区分Android嵌套滑动简介——手把手教你打造黏性头部控件_第4张图片
  从上图中可以看到, 所有和fling有关的接口都消失了,取而代之的是各个接口的最后多了一个type参数,参数的取值其实就是表示是scroll还是fling. NestedScrollingParent3这个接口, 这里卖个关子暂不介绍, 有兴趣的同学可以作为扩展阅读,查一下官方的文档和源码, 体会一下谷歌的接口优化方案.

嵌套滑动实战

  上面说了一大堆滑动嵌套相关,下面回到开头的demo,如果要实现一个这样的布局要怎么做呢? 相信聪明的你已经有了思路. 没错,其实只要我们实现NestedScrollingParent这个接口,判断一下第一个view的位置,然后相应的去消费或者不消费滑动距离即可.
  那么首先是onStartNestedScroll这个接口,我们只处理垂直方向的滑动,所以需要判断一下滑动的方向.

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes, int type) {
    //只处理垂直方向的滑动
    return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

  我们在垂直滑动的时候返回了true, 表示我们关系垂直方向的滑动,那么当有该方向的滑动发生时, recyclerview(已经实现了NestedScrollingChild接口) 会先询问我们是否要消费, 即回调onNestedPreScroll接口. 这里我们想一下消费的顺序: 初始状态下只能上滑, 头部有两部分, 一部分需要滑出屏幕外, 另外一部分要固定在屏幕顶端. 那么就是在第一个子view还没有滑动到最大可滑动距离(第一部分的高度)之前, 都由parent消费,其他情况则由recyclerview消费. 下滑的时候则正好相反,需要先让recycleview滑动,在recycleview还没有滑倒头时, 需要让recycleview先滑动, 否则则让parent来处理,让头部的控件滑动. 代码如下:

@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
    @ViewCompat.NestedScrollType int type) {
    LinearLayoutManager linearLayoutManager =
        (LinearLayoutManager) recyclerView.getLayoutManager();
    //recycleview 是否滑到顶部
    boolean isFirstItemVisible =
        linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0;

    //向上滑动,且头部没有滑倒最大高度,此时让header滑动
    if (dy > 0 && header.getBottom() > mMaxScrollTop) {
        int maxOffset = header.getBottom() - mMaxScrollTop;
        int offset = Math.min(maxOffset, dy);
        header.offsetTopAndBottom(offset * -1);

        recyclerView.layout(recyclerView.getLeft(), recyclerView.getTop() - offset,
            recyclerView.getRight(), recyclerView.getBottom());
        consumed[1] = offset;
    }
    //向下滑动,且recycleview已经滑倒顶部, header没有滑倒原始位置前,让头部滑动
    else if (dy < 0 && isFirstItemVisible && header.getBottom() < mOriginBottom) {
        int offset = Math.min(mOriginBottom - header.getBottom(), dy * -1);
        header.offsetTopAndBottom(offset);

        recyclerView.layout(recyclerView.getLeft(), recyclerView.getTop() + offset,
            recyclerView.getRight(), recyclerView.getBottom());
        consumed[1] = -1 * offset;
    }
}

  完整的demo可以见: https://github.com/ljcmeng/NestedStickerHeaderView

关于嵌套滑动踩过的坑以及误区

  最后是笔者在研究嵌套滑动时所遇到的一些坑,分享给大家.

  • 目前关于滑动嵌套,网上回答的最多的就是NestedScrollVIew配合RecyclerView的嵌套滑动, 大家的答案基本上千篇一律是禁用Recyclerview的嵌套滑动,即setNestedScrollingEnabled(false), 完全把滑动事件交由scrollview处理. 这种做法我只能说是完全没有理解嵌套滑动的意义. 首先是RecyclerView禁掉NestedScrolling之后失去了复用的功能, 如果你的RecyclerView的item数目非常多(比如支持分页的rv), 那么一直不断的上滑会导致recycleview不断增长, 最终OOM或者ANR.
  • 相信细心的同学应该注意到了,上面这个效果是使用LinearLayout来做的, 头部的滑动也是靠offsetTopAndBottom这种方式来修改布局实现. 为什么不简单一点外层使用ScrollView来做呢? 毕竟ScrollView内部就实现了srcoll方法. 这里直接说结论吧, 笔者最开始使用的是ScrollView, 但是会有一个问题, recyclerview的最后几项没法响应fling事件, 原因猜测是在scroll过程中, recyclerview的位置发生了变化,导致到达最底部时,其内部判断是无法继续滑动的,所以fling事件没有得到消费.

你可能感兴趣的:(android开发)