Android从零开搞系列:自定义View(2)继承RecyclerView实现下拉刷新和加载更多

转载请注意:http://blog.csdn.net/wjzj000/article/details/53558723


我和一帮应届生同学维护了一个公众号:IT面试填坑小分队。旨在帮助应届生从学生过度到开发者,并且每周树立学习目标,一同进步!
这里写图片描述


写在前面

这是一篇分析继承RecyclerView实现下拉刷新和加载更多的博客。
分析项目:https://github.com/jdsjlzx/LRecyclerView


首先分析一下项目结构:

  • LRecyclerView:核心的RecyclerView类。
    • LScrollListener:自定义的滑动回调内部类。
  • ArrowRefreshHeader:下来刷新的View类。
  • LoadingMoreFooter:加载更多的View类
  • CommonHeader: Items中最上边的那个View,也就是头部View。
  • CommonFooter: Items中最下边的那个View,也就是底部View。
  • LRecyclerViewAdapter:核心Adapter类。

接下来从一个最经典的效果来着手分析

先看一下效果:

这里写图片描述

首先必须得说这个项目以我现在的水品来看,写非常的完善,很多地方基本上都考虑到了。所以导致,阅读源码的时候比较的困难。因此在这里我抽取了其中直接实现最基本效果的代码加以自己的理解和梳理。

先从核心LRecyclerView开始

声明的部分变量
  • ArrowRefreshHeader mRefreshHeader
  • RecyclerView.AdapterDataObserver mDataObserver = new DataObserver()
  • LScrollListener mLScrollListener
  • DataObserver extends RecyclerView.AdapterDataObserver
重写的部分方法
  • setAdapter(Adapter adapter)
  • onTouchEvent(MotionEvent ev)
  • onScrolled(int dx, int dy)
  • onScrollStateChanged(int state)

在开始分析源码的时候,让我们先看一下怎么使用

注意!这个项目的多数类都是自定义的,所以要有心理准备。
在开始之前,咱们先梳理一下正常的写RecyclerView时的节奏。
先定义一个继承自RecyclerView.Adapter的设配器类,然后通过setAdapter与RecyclerView绑定。完事…但是它缺不是这样!!!不是这样!!!不是这样!!!

        mDataAdapter = new DataAdapter(this);
        mDataAdapter.addAll(dataList);
        //是不是看到俩个叫做Adapter的家伙有点懵逼?代码的下边有梳理->
        mLRecyclerViewAdapter = new LRecyclerViewAdapter(this, mDataAdapter);
        mRecyclerView.setAdapter(mLRecyclerViewAdapter);

        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        //省略部分不痛不痒的代码...
        //设置滑动监听
        mRecyclerView.setLScrollListener(
        new LRecyclerView.LScrollListener() {
            @Override
            public void onRefresh() {
               RecyclerViewStateUtils.setFooterViewState(
               mRecyclerView,LoadingFooter.State.Normal);
        //省略部分代码...

梳理:首先不知道大家对观察者模式是否了解。为什么这么问,因为Adapter就是用的观察者模式。
观察者模式:观察者注册在被观察者上,然后被观察者可以通知注册在自身上的观察者。我们常写的notifyDataSetChanged()就是被观察者通知观察者。
通常RecyclerView的用法:我们正常写法是继承RecyclerView.Adapter,如果我们去看它的源码它内部持有一个AdapterDataObservable,没错也就是被观察者。但是观察者在哪?我们查看源码可以发现:RecyclerView内部new了一个继承 AdapterDataObserver的RecyclerViewDataObserver类。而这个方法里边调用了 requestLayout(),所以起到了更新界面的效果。
如何更新界面:正常情况我们有了新的数据,会调用notifyDataSetChanged(),而这个方法。会遍历被观察者(也就是Adapter)持有的一个观察者List,调用每一个观察者的onChange()方法,最终执行了requestLayout(),通知View重绘界面。其实这里有个误区:困惑了我很久就是遍历观察者。其实我们正常的RecyclerView中只会有一个观察者被注册(也就是为什么RecyclerView默认new了一个观察者,因为正常只需要这么一个),而引起界面刷新的是,最终被调用的requestLayout(),新的items如何被刷新都在requestLayout()之中。
关于本例中俩个Adapter:其实本例中的俩个Adapter就是一个是观察者一个是被观察者,作者本身并没有使用默认的观察者。至于本例为什么要自己写一个观察者,而不用默认的,让我们随着源码进去来看!

这四个方法,让我们从上开始,一个个来

源码分析:setAdapter(Adapter adapter)

这个工程基本上所有的东西都进行了自定义,所以很多地方需要重写。
比如接下来要说的观察者和被观察者。
我们正常使用RecyclerView的时候一般不会重写setAdapter(),进到源码里我们会看到,这个方法是进行观察者注册到被观察者的过程。而正常情况我们使用默认的观察者,所以我们并不需要重写setAdapter(),但是远项目作者并没有使用默认的观察者,所以让我们来比较一个这来个观察者有何不同。

    @Override
    public void setAdapter(Adapter adapter) {
        //先对现有的Adapter进行一次解除注册。
        /**
         * 这里的Adapter类里边含有一个mObservable = new AdapterDataObservable()
         * 它是一个被观察者。我们常用于更新adpter数据的notifyDataSetChanged()就在此。
         */
        Adapter oldAdapter = getAdapter();
        if (oldAdapter != null && mDataObserver != null) {
            oldAdapter.unregisterAdapterDataObserver(mDataObserver);
        }
        super.setAdapter(adapter);
        /**
         * 然后将自定义的mDataObserver注册进来。
         * mDataObserver继承RecyclerView.AdapterDataObserver也就是一个观察者。
         * mDataObserver会被注册到adapter中的一个ArrayList中,而adapter的
         * notifyDataSetChanged()最终都会遍历ArrayList,调用观察者其中的
         * onChanged()方法。
         * !!注意:onChanged()方法是一个抽象方法,所以需要我们自己去实现。当然我们的
         * mDataObserver实现了这个方法。
         */
        adapter.registerAdapterDataObserver(mDataObserver);
        mDataObserver.onChanged();
        //一下代码省略...
    }
源码分析:DataObserver官方默认的观察者对比
private class DataObserver extends RecyclerView.AdapterDataObserver {
        @Override
        public void onChanged() {
            Adapter<?> adapter = getAdapter();
            //首先判断获取的被观察者Adapter是不是LRecyclerViewAdapter类型
            if (adapter instanceof LRecyclerViewAdapter) {
                LRecyclerViewAdapter headerAndFooterAdapter = (LRecyclerViewAdapter) adapter;
                /**
                 * 这里的getInnerAdapter()是什么鬼?让我们进去一探究竟!
                 *      //RecyclerView使用的,真正的Adapter
                 *      private RecyclerView.Adapter<RecyclerView.ViewHolder> mInnerAdapter;
                 * 这是原作者写的注释。
                 * 浏览过后我们会发现,LRecyclerViewAdapter并没有做过多的工作,而最终都是通过
                 * mInnerAdapter来调用的各种核心方法。
                 * PS:关于LRecyclerViewAdapter我们一会分析,不要着急。
                 */
                if (headerAndFooterAdapter.getInnerAdapter() != null && mEmptyView != null) {
                    //获取Items的数量,并判空。
                    int count = headerAndFooterAdapter.getInnerAdapter().getItemCount();
                    if (count == 0) {
                        //如果为0设置空白的View可以。当然我们这里内置了item所以等于0的条件并不成立。
                        mEmptyView.setVisibility(View.VISIBLE);
                        LRecyclerView.this.setVisibility(View.GONE);
                    } else {
                        mEmptyView.setVisibility(View.GONE);
                        //不为空则设置可见
                        LRecyclerView.this.setVisibility(View.VISIBLE);
                    }
                }
            } 
            //省略部分代码...
        }
    }
    //官方默认观察者
    private class RecyclerViewDataObserver extends AdapterDataObserver {
        RecyclerViewDataObserver() {
        }

        @Override
        public void onChanged() {
            assertNotInLayoutOrScroll(null);
            if (mAdapter.hasStableIds()) {
                // TODO Determine what actually changed.
                // This is more important to implement now since this callback will disable all
                // animations because we cannot rely on positions.
                mState.mStructureChanged = true;
                setDataSetChangedAfterLayout();
            } else {
                mState.mStructureChanged = true;
                setDataSetChangedAfterLayout();
            }
            if (!mAdapterHelper.hasPendingUpdates()) {
                requestLayout();
            }
        }
        //省略部分代码....
     }

我们可以看到在onChanged()方法中,远项目多了一个对没有数据的判断,也就是在我们的RecyclerView中没有item数据是的显示。正常RecyclerView是白茫茫的一片,而这里我们可以显示特定的空白View,用户体验方面更友好一些。
关于setAdapter就分析到此,接下来进入下一个方法!

源码分析:onTouchEvent(MotionEvent ev)

滑动:我们都知道,看见onTouchEvent()就说明到了,滑动环节了。而作为RecyclerView滑动更是尤为的重要。
思路:在这里通过y坐标然后设置头部刷新View(ArrowRefreshHeader)的高度,让其随y的变化而变化。当头部刷新View的课件高度大于设置的某个值的时候,即判断为刷新状态。

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        /**
         * getY()和getRawY()
         * 通过源码注释我们可以看出区别:
         * 一个是获得相对于控件的左边,一个是相对于屏幕。
         */
        if (mLastY == -1) {
            mLastY = ev.getRawY();
        }
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                final float deltaY = ev.getRawY() - mLastY;
                mLastY = ev.getRawY();
                //这里通过一系列的true和false来判断状态,避免出现问题。
                if (isOnTop() && pullRefreshEnabled
                        &&
                        (appbarState == AppBarStateChangeListener.State.EXPANDED)) {
                    //拉出头部刷新View,onMove()进行相应判断。
                    mRefreshHeader.onMove(deltaY / DRAG_RATE);
                    //此判断如果成立说明,头部刷新View已经出现了。
                    if (mRefreshHeader.getVisibleHeight() > 0
                            &&
                            mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {
                        return false;
                    }
                }
                if (mWrapAdapter.getInnerAdapter() instanceof SwipeMenuAdapter) {
                    if (mOldSwipedLayout != null && mOldSwipedLayout.isMenuOpen()) {
                        mOldSwipedLayout.smoothCloseMenu();
                    }
                }

                break;
                //省略部分代码...
             }
    //onMove()方法
    @Override
    public void onMove(float delta) {
        if (getVisibleHeight() > 0 || delta > 0) {
            //此方法通过LayoutParams,更新头部刷新View的高度。
            setVisibleHeight((int) delta + getVisibleHeight());
            //先判断是否处于未刷新状态
            if (mState <= STATE_RELEASE_TO_REFRESH) {
                // 如果头部刷新View可见高度大于特定值mMeasuredHeight,进行刷新
                if (getVisibleHeight() > mMeasuredHeight) {
                    /**
                     * setState()通过设置不同状态,更换头部刷新View的不同状态。
                     * 例如:下来刷新,松手刷新,正在刷新....
                     */
                    setState(STATE_RELEASE_TO_REFRESH);
                } else {
                    setState(STATE_NORMAL);
                }
            }
        }
    }
    //setState()方法
    public void setState(int state) {
        if (state == mState) return;
        // 正在刷新
        if (state == STATE_REFRESHING) {
            mArrowImageView.clearAnimation();
            mArrowImageView.setVisibility(View.INVISIBLE);
            mProgressBar.setVisibility(View.VISIBLE);
        } //刷新完成
        else if (state == STATE_DONE) {
            mArrowImageView.setVisibility(View.INVISIBLE);
            mProgressBar.setVisibility(View.INVISIBLE);
        } 
        //省略部分代码...
    }  

关于下拉刷新的思路到这基本就梳理清楚了。当然疑问还是存在的,比如:ArrowRefreshHeader怎么和LRecyclerView就勾搭到一块去了?其实很简单,也是我们常用的方式。

//LRecyclerView的setAdapter()方法中有这么一行
mRefreshHeader = mWrapAdapter.getRefreshHeader();
//mWrapAdapter是我们的核心Adapter(LRecyclerViewAdapter)。所以过程很清楚,就是我们常用的方式A,在Adapter中,通过不同的viewType而new出的不同ViewHolder。
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_REFRESH_HEADER) {
            //temporary solution
            if(pullRefreshEnabled) {
                return new ViewHolder(mRefreshHeader);
            }
            //省略部分代码...
        }
    }

到这,这个强大的工程如何实现下拉就很清楚了。那么下面就是如果搞定加载更多。
而这个效果需要LRecyclerView中那四个重写方法的后两个来实现。

源码分析:onScrolled(int dx, int dy)onScrollStateChanged(int state)

思路:想要判断是否需要加载更多,很明显我们需要知道我们是否滑动到了当前显示的Items中最后一个。所以这里重写的这俩个方法就是完成这份工作的。

    //onScrolled()方法
    //虽然很长,但是整体是都特定写法,很好理解
    @Override
    public void onScrolled(int dx, int dy) {
        super.onScrolled(dx, dy);
        if (null != mLScrollListener) {
            int firstVisibleItemPosition = 0;
            RecyclerView.LayoutManager layoutManager = getLayoutManager();

            if (layoutManagerType == null) {
                if (layoutManager instanceof LinearLayoutManager) {
                    layoutManagerType = LayoutManagerType.LinearLayout;
                } else if (layoutManager instanceof GridLayoutManager) {
                    layoutManagerType = LayoutManagerType.GridLayout;
                } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                    layoutManagerType = LayoutManagerType.StaggeredGridLayout;
                } else {
                    throw new RuntimeException(
                            "Unsupported LayoutManager used. Valid ones are LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager");
                }
            }

            switch (layoutManagerType) {
                case LinearLayout:
                    firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
                    lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
                    break;
                case GridLayout:
                    firstVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
                    lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
                    break;
                case StaggeredGridLayout:
                    StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                    if (lastPositions == null) {
                        lastPositions = new int[staggeredGridLayoutManager.getSpanCount()];
                    }
                    staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions);
                    lastVisibleItemPosition = findMax(lastPositions);
                    staggeredGridLayoutManager.findFirstCompletelyVisibleItemPositions(lastPositions);
                    firstVisibleItemPosition = findMax(lastPositions);
                    break;
            }
            // 根据类型来计算出第一个可见的item的位置,由此判断是否触发到底部的监听器
            // 计算并判断当前是向上滑动还是向下滑动
            calculateScrollUpOrDown(firstVisibleItemPosition, dy);
            // 移动距离超过一定的范围,我们监听就没有啥实际的意义了
            mScrolledXDistance += dx;
            mScrolledYDistance += dy;
            mScrolledXDistance = (mScrolledXDistance < 0) ? 0 : mScrolledXDistance;
            mScrolledYDistance = (mScrolledYDistance < 0) ? 0 : mScrolledYDistance;
            if (mIsScrollDown && (dy == 0)) {
                mScrolledYDistance = 0;
            }
            //Be careful in here
            mLScrollListener.onScrolled(mScrolledXDistance, mScrolledYDistance);
        }

    }
    //onScrollStateChanged()方法
    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);
        currentScrollState = state;
        /**
         * 摘自源码翻译
         * RecyclerView当前未滚动(滚动停止后调用)
         * RecyclerView.SCROLL_STATE_IDLE
         *
         * RecyclerView当前正在被外部输入(如用户触摸输入)拖动(正在滑动,且手指不离开屏幕)
         * RecyclerView.SCROLL_STATE_DRAGGING
         *
         * RecyclerView目前正在动画到最终位置,而不在外部控制之下(惯性滑动)
         * * RecyclerView.SCROLL_STATE_SETTLING
         */
        if (currentScrollState == RecyclerView.SCROLL_STATE_IDLE && mLScrollListener != null) {
            RecyclerView.LayoutManager layoutManager = getLayoutManager();
            int visibleItemCount = layoutManager.getChildCount();
            int totalItemCount = layoutManager.getItemCount();
            if (visibleItemCount > 0
                    && lastVisibleItemPosition >= totalItemCount - 1
                    && totalItemCount > visibleItemCount
                    && !isNoMore
                    && !mIsScrollDown
                    && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {
                //如果是底部,调用监听的onBottom()方法,那么我们就可以在setLLScrollListener中,实现onBottom()时进行底部加载更多了。 
                mLScrollListener.onBottom();
            }

        }

    }

PS:相关源码基本都存放于我的这个开源项目之中:
https://github.com/zhiaixinyang/PersonalCollect


尾声

OK,到此。借助这个开源的框架。梳理了下拉刷新和下拉加载的实现思路。
以及简单分析了一发观察者模式的应用。
本次博客就先到此结束了!

最后希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值