SimpleRefreshLayout: 用NestedScroll打造自己的刷新加载控件

关于NestedScroll

NestedScroll(嵌套滑动)其实在Android5.0就已经出了,大名鼎鼎的CoordinatorLayout就是嵌套滑动的产物。

传统的滑动,一旦事件被子view获取,那么也将由子view进行消费,父控件无法参与事件的消费过程。而嵌套滑动则是在子view无法继续消费滑动距离时,将产生的距离传递给父控件(滑动事件依旧由子view获取,只是将滑动的距离传递给了父控件),形成子view和父控件嵌套滑动的效果,从而产生了下拉刷新、上拉加载。

NestedScroll

与嵌套滑动相关的有一下四个类:

  • NestedScrollChild
  • NestedScorllChildHelper
  • NestedScrollParent
  • NestedScrollParentHelper
    关于这四个类的介绍和分析,可以参考以下文章:
    http://www.jianshu.com/p/6547ec3202bd

看完是不是依旧有点懵逼呢?没事!我们上代码走流程!

1.先创建一个SimpleRefreshLayout, 实现NestedScrollParent接口。实现一下接口里面的方法.

public interface NestedScrollingParent {
 
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
    public void onStopNestedScroll(View target);
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
    public int getNestedScrollAxes();
}

2.用SimpleRefreshLayout作为父控件,初始化RecyclerView假数据,开始走流程!

相关方法打上断点,滑动RecyclerView, 观察!

  1. 事件先从recyclerView开始(因为获取到事件的是子view),走recyclerView的
    StartNestedScroll


    001.png
  2. (Rv) startNestedScroll --> (SimpleRefreshLayout) onStartNestedScroll
    recyclerView告诉父控件:我要开始滑动了!你要不要动呢?
    很显然如果onStartNestedScroll返回true,则说明:儿子阿,我也要一起动!


    002.png
  3. (SimpleRefreshLayout) onStartNestedScroll --> onNestedScrollAccepted
    通过parentHelper表明该次滑动是否被接受了


    003.png
  4. (SimpleRefreshLayout) onNestedScrollAccepted --> (Rv) stopNestedScroll
    rv结束嵌套滑动过程


    004.png
  5. (Rv) stopNestedScroll --> (SimpleRefreshLayout) onStopNestedScroll
    rv告诉父控件:我停止事件,你也停了吧!
    父控件会在该方法做一些停止的操作,比如回弹、状态回调等。


    005.png
以上,Down事件结束!开始 Move !
  1. rv开始了又一轮的事件


    006.png
  2. 继续来到(SimpleRefreshLayout) onStartNestedScroll


    007.png
  3. 继续(SimpleRefreshLayout) 接受事件


    008.png

    9)终于到重点了!
    RecyclerView开始dispatch嵌套滑动的距离,通过recyclerView源码可以知道,该距离会通过childHelper最终回调到NestedScrollParent的相关方法。


    009.png
  4. 来了来了! (Rv) dispatchNestedPreScroll --> (simpleLayout) OnNestedPreScroll
    父控件:儿阿,你终于把你要滑动的距离dy给我了!我计算下自己要滑动多少再告诉你!
    最后父控件会把自己消耗的距离通过cousumed[ ]回传给子view
    010.png
  5. Rv 自己消耗之后,把剩余的距离传给父控件


    011.png
  6. 父控件接收到Rv消耗之后剩余的距离,并消耗掉剩余的距离。


    012.png

    13)Rv结束move


    013.png
  7. SimpleRefreshLayout 结束move


    014.png
最后,RecyclerView在up事件 结束整个过程
  1. Rv结束up


    015.png

整个过程一目了然!有木有!

总结:

  1. Down事件为何走onStopNestedScroll?
    这是因为 view需要停止上一次可能存在的滑动。最开始可能很难理解为什么会先走onStop,然后再走嵌套滑动流程,直到最终在view的触摸事件Down找到stopNestedScroll.
  2. NestedScroll的流程
  1. 事件都是从recyclerView的 startNestedScroll开始(除了up事件)。嵌套滑动始于recyclerView(startNestedScroll),也终于recyclerView(stopNestedScroll)。
    通过上面的流程可以看到,down和move事件都是从rv的startNestedScroll开始的,因为rv是事件源,并且在整个过程中rv始终获取着事件的处理权,只是将消耗的距离传递给了父控件,一起处理这部分距离而已。
    2)核心流程
    1>(recyclerView)dispatchNestedPreScroll --> (父) onNestedPreScroll
    在这个过程中,父类拿到了子view传递过来的距离,到底自己要消耗多少呢可以自己考虑,并且移动自己想要的距离,最后将这部分距离通过consumed[1] = yConsumed 回传给子view,子view获取的consumed[1]就是父控件消耗的距离。
    2> (父) onNestedPreScroll --> (recyclerView) dispatchNestedScroll --> (父) onNestedScroll
    前面父控件已经消耗了一部分距离(称为预消耗), 回传给子view后子view也消耗了一部分,最后剩余的子view不想再消耗的距离将通过 dispatchNestedScroll传到父控件 onNestedScroll,这是告诉父控件: 我滑不动了,还是你来吧!
    即先父控件预消耗,然后子view消耗,,最后父控件对剩余进行消耗。
    3> 刷新、加载的思路
    通过上面的分析,我们有了清晰的思路:
    当recyclerView滑动到顶部时,rv将距离传递给父控件,此时我们让父控件去消耗这部分距离,使其显示 刷新的布局就好了! 加载也一样!

SimpleRefreshLayout

github上一直都有很多很优秀的刷新框架,像TwinklingRefreshLayoutCanRefreshLayout等等。而将刷新和加载封装到adapter里面,个人感觉实用性扩展性都没那么强。况且google早就将SwipeRefreshLayout作为例子,我们应该也可以模仿着打造一个自己的刷新加载框架。

说说主要流程。
  1. header、footer、bottom以及滑动view的获取以及布局处理
    swipeRefreshLayout给出了很好的示范,我们只关心滑动view;因此对滑动view获取:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTarget == null) {
            ensureTarget();
        }
        //省略部分代码...
}
private void ensureTarget() {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child != mHeaderView && child != mFooterView && child != mBottomView) {
                //获取到我们的滑动view mTarget
                mTarget = child;
                break;
            }
        }
    }

很显然,获取到四个布局之后,header放在viewgroup顶部,footer和bottomView放在底部,mTarget在中间。布局的代码在onLayout()中。

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child == mHeaderView) {
                child.layout(0, -child.getMeasuredHeight(), child.getMeasuredWidth(), 0);
            } else if (child == mFooterView) {
                child.layout(0, getMeasuredHeight(), child.getMeasuredWidth(), getMeasuredHeight() + child.getMeasuredHeight());
            } else if (child == mBottomView) {
                child.layout(0, getMeasuredHeight(), child.getMeasuredWidth(), getMeasuredHeight() + child.getMeasuredHeight());
            } else {
                child.layout(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + child.getMeasuredWidth(), getMeasuredHeight() - getPaddingBottom());
            }
        }
    }
  1. 嵌套滑动过程
    通过上面的分析,嵌套滑动的消耗开始于simpleRefreshLayout的onNestedPreScroll, 然后到recyclerView消耗,最后到simpleRefreshLayout的onNestedScroll

这里的做法是先在onNestedScroll让simpleRefreshLayout滑动, 下一次距离到来时就会在onNestedPreScroll进行预消耗。

@Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        if (enable) {
            if (!isLastScrollComplete) return;
            if (direction == SCROLL_DOWN && !pullUpEnable) return;                  //用户不开启加载
            if (direction == SCROLL_UP && !pullDownEnable) return;                  //用户不开启下拉
            doScroll(dyUnconsumed);
        }
    }
@Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (getScrollY() != 0) { //只有在自己滑动的情形下才进行预消耗
            //省略部分代码...
            int yConsumed = Math.abs(dy) >= Math.abs(getScrollY()) ? getScrollY() : dy;
            doScroll(yConsumed);
            consumed[1] = yConsumed;
        }
    }

为什么可以这么做呢?
因为recyclerView滑动到顶部,继续scroll up的距离它肯定是无法滑动的(也就是它无法消耗这部分距离),因此会先走到onNestedPreScroll,而此时simpleRefreshLayout的getScrollY() = 0,无法进行预消耗;距离再次传递给recyclerView,它也无法消耗;最终就会来到onNestedScroll,doScroll()方法会使simpleRefreshLayout滑动。
之后getScrollY()不再为0了, 也就进入了recyclerView不滑动,父控件滑动的环节。之后距离就会走完整的 onNestedPreScroll --> recyclerView --> onNestedScroll 过程。 从而显示出隐藏在顶部的刷新布局。

当然,我们也可以在onNestedPreScroll 直接去判断recyclerView是否到顶部,是则开始滑动我们的父控件。

  1. 其他的一些细节
    包括刷新、加载状态的回调,以及控制滑动回弹结束再开始下一次滑动等。
    比较有意思的是,在上拉加载的过程中,我们希望下一页如果有数据,那么recyclerView能够向上滚动一小段距离,以便让用户们可以感知得到下一页是还有数据的。
    这里我加了一个Handler,当adapter刷新完数据后,让recyclerView向上滚动一点位移。
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MSG_PULL_UP:
                    pullCount++;

                    if (canChildScrollDown()) {
                        //如果上拉滚动结束,此时去判断recyclerView是否可滚动(是则说明有下一页), 位移一段距离
                        pullCount = 0;
                        mHandler.removeMessages(MSG_PULL_UP);
                        mTarget.scrollBy(0, (int) (getResources().getDisplayMetrics().density * 6));
                    } else {
                       //省略部分代码...
                    }
                    break;
}

Handler也是无奈之举。主要是不清楚怎么去获取adapter已经刷新完毕。
如果大家知道,请评论留言,非常感谢!

结尾!我已经尽力说清楚了,感谢你的耐心!
综上~

SimpleRefreshLayout封装了常用的刷新和加载,并增加了没有更多的功能,想以后我们也能做个美美的列表效果,带有刷新和加载,到达底部还有底部布局,美美哒!

实现效果:

simplerefresh.gif
github地址:

https://github.com/dengzq/SimpleRefreshLayout

转载请注明出处哦~

推荐阅读更多精彩内容