优雅的嵌套滑动解决方式-NestedScroll

来源:互联网 发布:电脑编程自学 编辑:程序博客网 时间:2024/06/05 09:09

优雅的嵌套滑动解决方式-NestedScroll

嵌套滑动相信大家一定经常遇到,最烦人的就是我们有两层view,它们都能在同一个方向上滑动,这时候滑动的判断就是个头疼的问题。不过这也还好说,毕竟只要根据业务需要来决定上层layout相对于下层的layout滚动的优先级,然后决定是否拦截滑动事件即可。最最让人绝望的,就是在同一个事件流中要分别让两层view滑动!!!比如滑动时,前半段上层滑动,上层滑不动之后,下层再接着开始滑动。

这怎么办?以前我是这么办的,上层判断自己能滑动时,就拦截这个事件,然后滑动,滑不动之后呢?就把接下来的事件再按照正常流程分发下去。但这步经常会出问题,如果你下层的是自定义的layout,你知道其中的事件消耗规则,那还好办。但对于系统的那些view,它们会更加复杂。比如这个地方,如果你是直接把接下来的ACTION_MOVE事件分发下去,那可能因为没有ACTION_DOWN事件导致View的某些状态没有被激活,然后它就会忽略掉之后的事件。当然你也可以再自己伪造一个ACTION_DOWN事件骗骗它们。

那如果是要求下层先滑动呢?这可就真让人扎心了,因为:首先,上层view没法判断下层view什么时候会滑不动。尽管View类中有OnScrollChangeListener,但并不是所有的view都乖乖地遵循这个规范,比如我们的AbsListView(它是ListView和ScrollView的父类),它就是用的自己的OnScrollListener。还有RecyclerView,它也有自己的OnScrollListener,并且和AbsListView的那个还不一样。你真的愿意为了这么多View都分别实现它们的监听器吗?

其次就是下层View先滑动,那就意味着事件先交给下层View。我们之前的文章讲过,子view可以告诉上层view不要拦截事件,但找来找去都没有找到子view能把事件还给上层view的方法。。。。

当然上面那些问题都可以通过自定义view去继承一个通用的接口啊啥的来解决,但一来代码侵入性太强,二来工作量会大很多。而今天我们要介绍的这个方式,其实就是和这个想法一样,让view继承一套接口来解决这个问题,只是它是官方支持的,意味着那些能滚动的View都支持它。这两个接口就是NestedScrollingParentNestedScrollingChild。其中NestedScrollingChild扮演的角色就是我们这里说的下层view,而NestedScrollingParent就是上层view。这两个接口是Android5.0之后在所有控件中就默认支持了,也就是集成了它们,不必再去显式地继承它们。而如果你的程序要跑在更老一点的系统上,那还是显式地继承并重写比较好。

让我们先看个demo

NestedScroll Demo

首先看看这两个接口具体的方法。

public interface NestedScrollingParent {    /**     * 开始NestedScroll时调用,返回true就意味着后面可以接受到NestedScroll事件,否则就无法接收。     * @param child 该view的直接子view     * @param target 发出NestedScroll事件的子view,和child不一定是同一个     * @param nestedScrollAxes 滑动的方向,为ViewCompat#SCROLL_AXIS_HORIZONTAL或者ViewCompat#SCROLL_AXIS_VERTICAL,亦或两者都有。     * @return 返回true代表要消耗这个NestedScroll事件,否则就是false。     * */    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);    /**     *在onStartNestedScroll之后调用,参数意义同上,可什么都不做     * */    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);    /**     * 结束NestedScroll事件时调用,可什么都不做     * */    public void onStopNestedScroll(View target);    /**     *在target滑不动的时候会调用这个方法,这时就通知本view可以进行滑动。如果目标view可以一直滑动,那么这个方法就不会被调用     * @param target 发出NestedScroll事件的子view     * @param dxConsumed target在x方向上已经消耗的滑动距离     * @param dxUnconsumed 这次滑动事件在x方向除去target已经消耗的还剩下的距离,通常如果我们需要滑动的话就使用这个值。     * @param dyConsumed 同上     * @param dyUnconsumed 同上     *                                          * */    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,                               int dxUnconsumed, int dyUnconsumed);    /**     *在target每次滑动之前会调用这个方法,。     * @param target 发出NestedScroll事件的子view     * @param dx 这次滑动事件在x方向上滑动的距离     * @param dy 这次滑动事件在y方向上滑动的距离     * @param consumed 一个长度为2的数组。第0位时我们在x方向消耗的滑动距离,第1位是我们在y方向上消耗的滑动距离。子view会根据这个和dx/dy来计算余下的滑动量,来决定自己是否还要进行剩下的滑动。     *                 比如我们使consumed[1] = dy,那么子view在y方向上就不会滑动。     *      * */    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);    /**     * 在target进行fling后调用。注意这个方法并不是像onNestedScroll在子view滑不动之后调用,而是紧跟着onNestedPreFling后会被调用。因此对于它的使用场景一般比较少。     *      * @param target 目标view     * @param velocityX 在x方向的速度,注意这是fling的起始速度,并不是目标在滑不动时停止时刻的速度,它和onNestedPreFling中的velocityX是一样的。     * @param velocityY 在y方向的速度,注意这是fling的起始速度,并不是目标在滑不动时停止时刻的速度,它和onNestedPreFling中的velocityY是一样的。     * @param consumed 目标view是否消耗了此次fling     * @return 本view是否消耗了这次fling     * */    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);    /**     * 在target判断为fling并且执行fling之前调用,我们可以通过返回true来拦截目标的fling,这样它就不会执行滑动。     * @param target 目标view     * @param velocityX 在x方向的起始速度     * @param velocityY 在y方向的起始速度     * @return 我们是否消耗此次fling,返回true代表拦截,返回false,目标view就进行正常的fling     * */    public boolean onNestedPreFling(View target, float velocityX, float velocityY);    public int getNestedScrollAxes();}

然后是NestedScrollingChild

public interface NestedScrollingChild {    /**     * 设置使该view可以进行NestedScroll,一般都是设为true     * */    public void setNestedScrollingEnabled(boolean enabled);    public boolean isNestedScrollingEnabled();    public boolean startNestedScroll(int axes);    public void stopNestedScroll();    public boolean hasNestedScrollingParent();    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);    public boolean dispatchNestedPreFling(float velocityX, float velocityY);}

我修改了注释,使其更加易懂。同时也加入了一些原来注释中未曾提到的注意事项。我相信看了它之后,基本心中也就有点数了。而NestedScrollingChild基本就没写注释,是因为它基本和NestedScrollingParent中的方法是一一对应的。看了前面的,后面相信不难理解。另外一个原因则是根据官方推荐,使用者两个接口都需要两个Helper类:NestedScrollingChildHelperNestedScrollingParentHelper,而NestedScrollingChild中的方法在NestedScrollingChildHelper都有对应的实现,只要在NestedScrollingChild的方法中调用NestedScrollingChildHelper的方法即可。

可能看了不是很明白,接下来我们就按流程写一个实际的用法。

首先,我们的自定义View需要实现NestedScrollingParentNestedScrollingChild接口。

public class DemoLayout extends ViewGroup implements NestedScrollingParent, NestedScrollingChild

然后,根据官方推荐,我们需要两个final的NestedScrollingChildHelperNestedScrollingParentHelper对象来进行一些辅助性的工作。

private final NestedScrollingChildHelper mNestedChildHelper;private final NestedScrollingParentHelper mNestedParentHelper;

然后在View的构造函数中初始化它们。

    public HideHeadLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {        super(context, attrs, defStyleAttr, defStyleRes);        mNestedChildHelper = new NestedScrollingChildHelper(this);        mNestedParentHelper = new NestedScrollingParentHelper(this);        setNestedScrollingEnabled(true);        //其它工作        //....    }

接着,主要是实现这两个接口的方法,需要注意的是,如果你的targetSdk大于等于Android5.0,那么你不实现这些接口也不会报错,但如果为了兼容性,那还是全都实现了比较好。首先是NestedScrollingChild,因为它的实现很简单。

    /*NestedScrollingChild APIs*/    @Override    public void setNestedScrollingEnabled(boolean enabled) {        mNestedChildHelper.setNestedScrollingEnabled(enabled);    }    @Override    public boolean isNestedScrollingEnabled() {        return mNestedChildHelper.isNestedScrollingEnabled();    }    @Override    public boolean startNestedScroll(int axes) {        return mNestedChildHelper.startNestedScroll(axes);    }    @Override    public void stopNestedScroll() {        mNestedChildHelper.stopNestedScroll();    }    @Override    public boolean hasNestedScrollingParent() {        return mNestedChildHelper.hasNestedScrollingParent();    }    @Override    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {        return mNestedChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);    }    @Override    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {        return mNestedChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);    }    @Override    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {        return mNestedChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);    }    @Override    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {        return mNestedChildHelper.dispatchNestedPreFling(velocityX, velocityY);    }

对,你没看错,这些接口方法的实现就是调用NestedScrollingChildHelper对应的方法。

接着,就是实现NestedScrollingParent的方法。这里NestedScrollingParentHelper不像NestedScrollingChildHelper会给我们全部方法的对应实现,而是关键方法需要我们自己去实现,毕竟这部分逻辑只有开发者自己清楚。

    /*NestedScrollingParent APIs*/    @Override    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {        //检查滑动方向是不是我们需要的,如果是就返回true,不是就返回false。        if((ViewCompat.SCROLL_AXIS_VERTICAL & nestedScrollAxes) == ViewCompat.SCROLL_AXIS_VERTICAL)        {            return true;        }else {            return false;        }    }    @Override    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {        //这个方法只是状态通知,没有多大的作用,因此NestedScrollingParentHelper为我们提供了对应方法,直接调用即可。当然也可以不处理。        mNestedParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);    }    //同上,不过你可以在这里做一些收尾的工作    @Override    public void onStopNestedScroll(View target) {        mNestedParentHelper.onStopNestedScroll(target);    }    //这个方法是真正重要的方法,执行滑动。    @Override    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {        //根据自己的方向,使用dxUnconsumed或者dyUnconsumed进行滑动。        offsetChild(dyUnconsumed);    }    //在target滑动之前调用。    @Override    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {        //根据自己的方向,使用dx或者dy进行滑动,并且要将自己消耗的距离放入consumde数组里,这里以y方向举例。        int realOffset = computeOffsetDis(dy);        offsetChild(realOffset);        consumed[1] = realOffset;        consumed[0] = 0;    }    //这个方法基本就没什么用了,因为它并不能反映在target停止滑动时的状态。也就只能监测一下状态而已。直接返回false即可。    //当然,如果consumed = false时你需要滑动,也可以进行滑动然后返回true。    @Override    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {        return false;    }    //这个方法就是target在fling之前问你要不要fling,你fling了它就不fling了,要不然它就上了。    @Override    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {        //可以判断一下自己的状态是不是需要fling,然后进行操作。        if((velocityY > 0 && canScrollY(1)) || (velocityY < 0 && calScrollY(-1)))        {            //进行你自己的fling            return true;        }else        {            return false;        }    }    //获取当前滚动方向。可以推测NestedScrollingParentHelper内部肯定维护了一些状态,因此尽管某些方法看起来无足轻重,但我    //们还是应该按照规定实现它,以确保它内部的状态是正确的。对于NestedScrollingChildHelper也是如此。    @Override    public int getNestedScrollAxes() {        return mNestedParentHelper.getNestedScrollAxes();    }

好了,以上就是基本的用法了,都是些伪代码。当然,这并不完全标准,因为我们只是作为一个Parent来消耗事件,但是并没有作为一个合格的Child去分发事件。如果我们的上层View也需要我们的NestedScroll事件,那可就惨了。

下一篇文章我会实现本篇开头的那个demo,这是目前已经非常常用的一种布局。掌握它还是很有用的。

本人目前在找Android开发的工作,地点在深圳南山科技园附近,有伯乐可以留言或者发邮件给我:zu_guorui@126.com.

阅读全文
0 0