简介

这个轮子是对RecyclerView的封装,主要完成了下拉刷新上拉加载更多RecyclerView头部。在我的Material Design学习项目中使用到了项目地址,感觉还不错。趁着毕业答辩还有2个星期,先把这个轮子拆了看看,这个项目地址在XRecyclerView,先贴个效果图,更多效果图请进入项目中查看。

使用

使用起来也比较简单,首先向普通RecyclerView那样:

LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdapter);

下拉刷新和加载更多需要实现其接口即可:

 mRecyclerView.setLoadingListener(new XRecyclerView.LoadingListener() {
    @Override
    public void onRefresh() {
       //refresh data here
    }

    @Override
    public void onLoadMore() {
       // load more data here
    }
});

这里要注意的是需要人为的通知刷新和加载都已经完成,通过如下代码

mRecyclerView.refreshComplete(); //下拉刷新完成
mRecyclerView.loadMoreComplete();//加载更多完成

类关系图

首先梳理了一下框架,用UML图画了这个轮子的结构,这样有利于帮助我理解,右击-查看图像 可以查看清晰大图)

可以看出主要的类只有3个 XRecyclerView,LoadingMoreFooter,ArrowRefreshHeader,而AVLoadingIncatorViewSimpleViewSwitcher是用来辅助刷新或者加载时候的动画。

下面分析源码时限于篇幅原因只展现出关键代码,具体可以参考项目源码。

  1. XRecyclerView的实现
  • XRecyclerView 的head和footer的view实现

    XRecyclerView在RecyclerView的基础上做了进一步的工作因而需要继承RecyclerView,由于支持RecyclerView Header而不同的header可以自己实现,因此需要对外暴露,而footerView则是固定的,因此在init初始化时候直接初始化了。此外这里使用了两个ArrayList存储不同的view,并且记录了viewType
    private ArrayList<View> mHeaderViews = new ArrayList<>();
    private ArrayList<View> mFootViews = new ArrayList<>();
    ……
    private void init() {
        if (pullRefreshEnabled) {
            //若支持下拉刷新则加入Headerview列表,设置加载图标
            ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext());
            mHeaderViews.add(0, refreshHeader);//从这里看出headerView可以添加多个
            mRefreshHeader = refreshHeader;
            mRefreshHeader.setProgressStyle(mRefreshProgressStyle);
        }
        //加载更多无需触发
        LoadingMoreFooter footView = new LoadingMoreFooter(getContext());
        footView.setProgressStyle(mLoadingMoreProgressStyle);
        addFootView(footView);//加入footerView
        mFootViews.get(0).setVisibility(GONE);
    }
    ……
    /**
     * @param view 对外提供添加header的方法
     */
    public void addHeaderView(View view) {
        if (pullRefreshEnabled && !(mHeaderViews.get(0) instanceof ArrowRefreshHeader)) {
            ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext());
            mHeaderViews.add(0, refreshHeader);
            mRefreshHeader = refreshHeader;
            mRefreshHeader.setProgressStyle(mRefreshProgressStyle);
        }
        mHeaderViews.add(view);
        sHeaderTypes.add(HEADER_INIT_INDEX + mHeaderViews.size());//记录viewType
    }

但是这样仅仅只是存储了View,那么实现的地方在哪里呢?数据展现很显然是在dapater中,但是在使用RecycleView时需要展示item数据,那么header和footer如何加载?这里就需要对传入的数据adapter再做一层封装。

    @Override
    public void setAdapter(Adapter adapter) {
        mWrapAdapter = new WrapAdapter(adapter);//对传入的adapter做封装
        super.setAdapter(mWrapAdapter);
        adapter.registerAdapterDataObserver(mDataObserver);
        mDataObserver.onChanged();
    }

由于RecycleView支持LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager,而GridLayoutManagerStaggeredGridLayoutManager在添加header时候需要注意横跨整个屏幕宽度即:

GridLayoutManager 是要设置SpanSize每行的占位大小

StaggerLayoutManager 就是要获取StaggerLayoutManager的LayoutParams 的setFullSpan 方法来设置占位宽度,因此在WrapAdapter中做了针对性处理

        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
            RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
            if (manager instanceof GridLayoutManager) {
                final GridLayoutManager gridManager = ((GridLayoutManager) manager);
                gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                    @Override
                    public int getSpanSize(int position) {
                        return (isHeader(position) || isFooter(position))
                                ? gridManager.getSpanCount() : 1;
                    }
                });
            }
        }

        @Override
        public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
            super.onViewAttachedToWindow(holder);
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            if (lp != null
                    && lp instanceof StaggeredGridLayoutManager.LayoutParams
                    && (isHeader(holder.getLayoutPosition()) || isFooter(holder.getLayoutPosition()))) {
                StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
                p.setFullSpan(true);
            }
        }

到此只是为展示head提供了必要条件,具体展示还是要靠WrapAdapter的 onCreateViewHolder配合getItemViewType方法,根据viewtype从对应的ArrayList中取出view来展示

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == TYPE_REFRESH_HEADER) {
                mCurrentPosition++;
                return new SimpleViewHolder(mHeaderViews.get(0));
            } else if (isContentHeader(mCurrentPosition)) {
                if (viewType == sHeaderTypes.get(mCurrentPosition - 1)) {
                    mCurrentPosition++;
                    return new SimpleViewHolder(mHeaderViews.get(headerPosition++));
                }
            } else if (viewType == TYPE_FOOTER) {
                return new SimpleViewHolder(mFootViews.get(0));
            }
            return adapter.onCreateViewHolder(parent, viewType);
        }
    ……
     @Override
        public int getItemViewType(int position) {
            if (isRefreshHeader(position)) {
                return TYPE_REFRESH_HEADER;
            }
            if (isHeader(position)) {
                position = position - 1;
                return sHeaderTypes.get(position);
            }
            if (isFooter(position)) {
                return TYPE_FOOTER;
            }
            int adjPosition = position - getHeadersCount();
            int adapterCount;
            if (adapter != null) {
                adapterCount = adapter.getItemCount();
                if (adjPosition < adapterCount) {
                    return adapter.getItemViewType(adjPosition);
                }
            }
            return TYPE_NORMAL;
        }

ok,到这里就完成了head和footer的view显示,

  • 上拉滑动中的下拉刷新和释放后刷新界面的缓慢消失的实现

    上拉刷新分为两部分,首先是手指滑动,刷新条慢慢显示出来(而且显示的大小跟滑动距离有关);释放后刷新界面慢慢隐藏,这里刷新的动画部分后面分析。

    先看刷新条随着手指滑动慢慢显示

    涉及到滑动需要重写onTouchEvent,特别是针对MotionEvent.ACTION_MOVE处理
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //通过处理onTouchEvent处理下拉刷新
        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();
                if (isOnTop() && pullRefreshEnabled) {
                    mRefreshHeader.onMove(deltaY / DRAG_RATE);//显示刷新的关键代码
                    if (mRefreshHeader.getVisibleHeight() > 0 && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {
//                        Log.i("getVisibleHeight", "getVisibleHeight = " + mRefreshHeader.getVisibleHeight());
//                        Log.i("getVisibleHeight", " mRefreshHeader.getState() = " + mRefreshHeader.getState());
                        return false;
                    }
                }
                break;
            default:
                mLastY = -1; // reset
                if (isOnTop() && pullRefreshEnabled) {
                    if (mRefreshHeader.releaseAction()) {
                        if (mLoadingListener != null) {
                            mLoadingListener.onRefresh();
                        }
                    }
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

onMove在ArrowRefreshHeader中实现,这里多插一句getRawY():获取点击事件相对整个屏幕顶边的y轴坐标,即点击事件距离整个屏幕顶边的距离注意与getY()区别。

    @Override
    public void onMove(float delta) {
        //由于下拉时候区域是动态变化因此需要动态设置
        if (getVisibleHeight() > 0 || delta > 0) {
            setVisibleHeight((int) delta + getVisibleHeight());
            if (mState <= STATE_RELEASE_TO_REFRESH) { // 未处于刷新状态,更新箭头
                if (getVisibleHeight() > mMeasuredHeight) {
                    setState(STATE_RELEASE_TO_REFRESH);
                } else {
                    setState(STATE_NORMAL);
                }
            }
        }
    }

在onMove方法输入参数中可以看出手指滑动距离的1/3作为刷新显示的高度,由于init方法初始化时将刷新显示高度设置为0,同样在ArrowRefreshHeader中

addView(mContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 0));//初始化时候高度设置为0,通过后面setVisibleHeight设置可见高度
 ……
     /**
     * 设置可见高度
     *
     * @param height
     */
    public void setVisibleHeight(int height) {
        if (height < 0) height = 0;
        LayoutParams lp = (LayoutParams) mContainer.getLayoutParams();
        lp.height = height;
        mContainer.setLayoutParams(lp);
    }

这样就不难理解onMove方法为何可以在下拉时慢慢出现下拉刷新.

在看释放是刷新界面慢慢变为0

同样在在XrecycleView中的onTouch方法中:

default分支:

 default:
                mLastY = -1; // reset
                if (isOnTop() && pullRefreshEnabled) {
                    if (mRefreshHeader.releaseAction()) {//上弹关键代码
                        if (mLoadingListener != null) {
                            mLoadingListener.onRefresh();
                        }
                    }
                }
                break;

mRefreshHeader.releaseAction()中处理了手指释放后即刷新慢慢向上隐藏的动作,该接口在

ArrowRefreshHeader中实现

    @Override
    public boolean releaseAction() {
        //释放动作,此时需要处理缓慢回到顶部
        boolean isOnRefresh = false;
        int height = getVisibleHeight();
        if (height == 0) // not visible.
            isOnRefresh = false;

        if (getVisibleHeight() > mMeasuredHeight && mState < STATE_REFRESHING) {
            setState(STATE_REFRESHING);
            isOnRefresh = true;
        }
        // refreshing and header isn't shown fully. do nothing.
        if (mState == STATE_REFRESHING && height <= mMeasuredHeight) {
            //return;
        }
        int destHeight = 0; // default: scroll back to dismiss header.
        // is refreshing, just scroll back to show all the header.
        if (mState == STATE_REFRESHING) {
            destHeight = mMeasuredHeight;
        }
        smoothScrollTo(destHeight);

        return isOnRefresh;
    }
    ……
        private void smoothScrollTo(int destHeight) {
        ValueAnimator animator = ValueAnimator.ofInt(getVisibleHeight(), destHeight);
        animator.setDuration(300).start();
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                setVisibleHeight((int) animation.getAnimatedValue());
            }
        });
        animator.start();
    }

其中主要是通过smoothScrollTo的属性动画+setVisibleHeight函数来实现刷新部分慢慢隐藏

  • 加载更多实现

    通常实现该功能是在手指滑动停止后进行加载,在XRecyclerView中重写了onScrollStateChange方法,加载更多主要是需要获得最后可见的位置即lastVisibleItem,如下所示
 @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);
        //重写该方法主要是在IDLE态即手指滑动停止后处理加载更多

        if (state == RecyclerView.SCROLL_STATE_IDLE && mLoadingListener != null && !isLoadingData && loadingMoreEnabled) {
            LayoutManager layoutManager = getLayoutManager();
            int lastVisibleItemPosition;
            if (layoutManager instanceof GridLayoutManager) {
                lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                //瀑布流布局发现最后可见的item位置
                int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
                ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into);
                lastVisibleItemPosition = findMax(into);
            } else {
                lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
            }

            if (layoutManager.getChildCount() > 0
                    && lastVisibleItemPosition >= layoutManager.getItemCount() - 1 && layoutManager.getItemCount() > layoutManager.getChildCount() && !isNoMore && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {

                View footView = mFootViews.get(0);
                isLoadingData = true;
                if (footView instanceof LoadingMoreFooter) {
                    ((LoadingMoreFooter) footView).setState(LoadingMoreFooter.STATE_LOADING);
                } else {
                    footView.setVisibility(View.VISIBLE);
                }
                mLoadingListener.onLoadMore();
            }
        }
    }
  • 空白数据处理

    数据变化时处理布局,这里主要通过AapterDataObserver监听数据变化以此来更换布局
 @Override
        public void onChanged() {
            //重写该方法是在数据发生变化时更换布局
            Adapter<?> adapter = getAdapter();
            if (adapter != null && mEmptyView != null) {
                int emptyCount = 0;
                if (pullRefreshEnabled) {
                    emptyCount++;
                }
                if (loadingMoreEnabled) {
                    emptyCount++;
                }
                if (adapter.getItemCount() == emptyCount) {
                    mEmptyView.setVisibility(View.VISIBLE);
                    XRecyclerView.this.setVisibility(View.GONE);
                } else {
                    mEmptyView.setVisibility(View.GONE);
                    XRecyclerView.this.setVisibility(View.VISIBLE);
                }
            }
            if (mWrapAdapter != null) {
                mWrapAdapter.notifyDataSetChanged();
            }
        }
  1. ArrowRefreshHeader与LoadingMoreFooter

    这俩个都是继承viewgroup的自定义控件,前者要比后者稍微复杂一些,先拣软柿子捏,看看LoadingMoreFooter:

    主要功能就是初始化好加载更多的动画view和家在文字,然后通过state统统暴露在setState函数中供外界调用
    public void  setState(int state) {
        switch(state) {
            case STATE_LOADING:
                progressCon.setVisibility(View.VISIBLE);
                mText.setText(getContext().getText(R.string.listview_loading));
                this.setVisibility(View.VISIBLE);
                    break;
            case STATE_COMPLETE:
                mText.setText(getContext().getText(R.string.listview_loading));
                this.setVisibility(View.GONE);
                break;
            case STATE_NOMORE:
                mText.setText(getContext().getText(R.string.nomore_loading));
                progressCon.setVisibility(View.GONE);
                this.setVisibility(View.VISIBLE);
                break;
        }

    }

可以看出,通过不同的状态来处理文字和加载动画。

在看ArrowRefreshHeader,稍微复杂点,主要是要处理随着手指滑动刷新界面慢慢显示和释放释放手指刷新界面慢慢返回,刷新完成后的状态重置,这些都实现接口BaseRefreshHeader处理。其中该自定义控件在初始化时候将高度设置为0,通过setVisibleHeight来设置高度,这样就可以处理刷新高度的动态变化,在介绍XRecyclerView中已经对这几个接口方法做了详细介绍了,这里就不赘述了。这里处理方式与LoadingMoreFooter相同,根据不同刷新状态来处理控件的显示状态。

抽象来看,这两个控件的核心就是使用 SimpleViewSwitcher做中转将AVLoadingIndicatorView不同的加载动画呈现的过称。

其中 SimpleViewSwitcher比较简单就是一个设置view的很普通的自定义viewgroup,而AVLoadingIndicatorView则是另一个加载动画库了github项目地址这次就不分析了。

到此基本上这个轮子就大致分析完了。

尾声

作为一个android彩笔,还是应该多读读源码,包括android源码和github上的一些多星的优秀项目的源码,通过拆这个轮子,可以收获到:

  • 熟悉uml拆分框架
  • recycleView针对不同布局(如StaggeredGridLayoutManager)获取findLastVisibleItemPositions和header的处理方式
  • 下拉刷新手指滑动距离与刷新高度变化、释放后刷新头部自动消失(onTouch)
  • 改变不同不通布局的方式,根据状态设置empytyview可见还是 recycleView可见与否
  • recycleView增加头部底部后使用对传入的数据adapter来进行二次封装
  • 自定义viewgroup的使用
  • 属性动画的简单使用
  • view坐标系
  • 熟悉了设计模式的里氏替换、接口隔离、依赖倒置原则

拆解轮子之XRecyclerView的更多相关文章

  1. 一步一步拆解一个简单的iOS轮播图(三图)

    导言(可以不看): 不吹不黑,也许是东半球最简单的iOS轮播图拆分注释(讲解不敢当)了(tree new bee).(一句话包含两个人,你能猜到有谁吗?提示:一个在卖手机,一个最近在卖书)哈哈... ...

  2. 拆解大数据总线平台DBus的系统架构

    Dbus所支持两类数据源的实现原理与架构拆解. 大体来说,Dbus支持两类数据源: RDBMS数据源 日志类数据源 一.RMDBMS类数据源的实现 以mysql为例子. 分为三个部分: 日志抽取模块 ...

  3. 避免重复造轮子的UI自动化测试框架开发

    一懒起来就好久没更新文章了,其实懒也还是因为忙,今年上半年的加班赶上了去年一年的加班,加班不息啊,好了吐槽完就写写一直打算继续的自动化开发 目前各种UI测试框架层出不穷,但是万变不离其宗,驱动PC浏览 ...

  4. 【疯狂造轮子-iOS】JSON转Model系列之二

    [疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...

  5. 【疯狂造轮子-iOS】JSON转Model系列之一

    [疯狂造轮子-iOS]JSON转Model系列之一 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 之前一直看别人的源码,虽然对自己提升比较大,但毕竟不是自己写的,很容易遗 ...

  6. h5engine造轮子

    基于学习的造轮子,这是一个最简单,最基础的一个canvas渲染引擎,通过这个引擎架构,可以很快的学习canvas渲染模式! 地址:https://github.com/RichLiu1023/h5en ...

  7. 存在即合理,重复轮子orm java版本

    1,业务描述前序? 需求来源于,公司的运营部门.本人所在公司(私营,游戏行业公司),从初创业,我就进入公司,一直致力于服务器核心研发. 公司成立块3年了,前后出产了4款游戏,一直在重复的制造公司游戏对 ...

  8. Appium 三种wait方法(appium 学习之改造轮子)

    前些日子,配置好了appium测试环境,至于环境怎么搭建,参考:http://www.cnblogs.com/tobecrazy/p/4562199.html   知乎Android客户端登陆:htt ...

  9. JS实现常用排序算法—经典的轮子值得再造

    关于排序算法的博客何止千千万了,也不多一个轮子,那我就斗胆粗制滥造个轮子吧!下面的排序算法未作说明默认是从小到大排序. 1.快速排序2.归并排序3.冒泡排序4.选择排序(简单选择排序)5.插入排序(直 ...

随机推荐

  1. T-SQL动态查询(3)——静态SQL

    接上文:T-SQL动态查询(2)--关键字查询   本文讲述关于静态SQL的一些知识和基础技巧. 简介: 什么是静态SQL?静态SQL是和动态SQL相对而言的,其实我们没必要过于纠结精确定义,只要大概 ...

  2. 第一行代码阅读笔记---AndroidMainfest.xml分析

    按照这本书的指引,我随作者一样创建了一个安卓应用,开始了安卓开发的启程. 找到AndroidMainfest.xml这个文件,打开后看到了我创建的Activity在这个文件里被成功注册,文件内容如下: ...

  3. 1.0、Android Studio管理你的项目

    项目概览 Android Studio中的项目包含了开发一个app的工作环境所需要的一切.从代码,到资源,到测试到构建配置.当你创建一个新的项目的时候,Android Studio为所有的文件创建了必 ...

  4. Linux系统编程----僵尸进程

    什么是僵尸进程? 僵尸进程, 指子进程退出后, 父进程还没有回收子进程的资源,这个子进程就处在于僵尸状态. 来看看如何产生? #include <stdio.h> #include < ...

  5. Android初级教程三个Dialog对话框小案例

    这里把三个对话框形式写在一个项目程序里面,用三个按钮控制显示什么样式的对话框. 先看布局文件代码: <LinearLayout xmlns:android="http://schema ...

  6. 15个易遗忘的java知识点

    1.java中的基本数据类型以及所占内存大小 (1)整形  byte 1字节  short 2字节  int 4字节  long 8字节  (2)浮点型  float 4字节  double 8字节  ...

  7. Linux IPC实践(13) --System V IPC综合实践

    实践:实现一个先进先出的共享内存shmfifo 使用消息队列即可实现消息的先进先出(FIFO), 但是使用共享内存实现消息的先进先出则更加快速; 我们首先完成C语言版本的shmfifo(基于过程调用) ...

  8. Lambda的使用与实战

    简介 (下面的简介也可以自己百度,一般进来的都是想知道怎么去用,所以这里主要也是重点在用法与实战上) Lambda表达式是Java SE 8中一个重要的新特性.lambda表达式允许你通过表达式来代替 ...

  9. Python基础 语法特别注意笔记(和Java相比)

    Python变量和数据类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ...

  10. Java模式之模板方法模式

    当我们遇到的业务逻辑具有大致相同的方式的时候,我们也许就该将这个业务逻辑抽象出来,采用模板方法,来进行封装我们的代码,提高代码的重用性,以及可维护性.下面是我的一个复习用的案例: 第一步:我们需要一个 ...