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

Posted zuguorui

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优雅的嵌套滑动解决方式-NestedScroll相关的知识,希望对你有一定的参考价值。

优雅的嵌套滑动解决方式-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

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

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.

以上是关于优雅的嵌套滑动解决方式-NestedScroll的主要内容,如果未能解决你的问题,请参考以下文章

Android NestedScrolling解决滑动冲突问题 - fling问题与NestedScroll++

Android中不同方向嵌套滑动的解决方式(ListView为样例)

解决ScrollView嵌套RecyclerView的显示及滑动问题

(转)ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

Android高级ui13-nestedscrollview嵌套滚动机制

如果ListView中嵌套HorizontalScrollView怎么解决滑动错乱