拆解轮子之XRecyclerView

简介

这个轮子是对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坐标系
  • 熟悉了设计模式的里氏替换、接口隔离、依赖倒置原则
时间: 2024-10-13 04:35:28

拆解轮子之XRecyclerView的相关文章

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

导言(可以不看): 不吹不黑,也许是东半球最简单的iOS轮播图拆分注释(讲解不敢当)了(tree new bee).(一句话包含两个人,你能猜到有谁吗?提示:一个在卖手机,一个最近在卖书)哈哈... 我第一次项目中需要使用轮播图的时候我是用的别人写好的一个轮子,那个轮播封装很多东西,包括比如可以设置pageControl的位置,可以传图片url或本地图片,缓存网络图片等等.但是我觉得没必要搞那么复杂,我喜欢简单并足够做事的东西.现在有时间便想自己把它拆解一下.看了一些简书上一些作者写的关于轮播图

Python基础教程:Python学习视频Python让你敲的代码不再是造轮子

你敲的代码是在造轮子?那就学Python呗!_Python基础教程 Bruce大神说" 人生苦短,我用Python ". 从公司角度而言: 国内基于Python创业成功的案例不在少数,豆瓣.知乎.果壳,全栈都是 Python,大家对Python自然有信心.并且从这几家公司出来的程序员与 CTO,创业的话一般都会选择Python. 从开发者个人角度而言: 计算机语言只是用来达成目的工具,?各种强大的第三方库,拿来就能用才是王道,让程序替代我们执行一些枯燥繁琐的工作.?至于句式是否优美.能

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

Dbus所支持两类数据源的实现原理与架构拆解. 大体来说,Dbus支持两类数据源: RDBMS数据源 日志类数据源 一.RMDBMS类数据源的实现 以mysql为例子. 分为三个部分: 日志抽取模块 增量转换模块 全量拉取模块 1.1 日志抽取模块(Extractor) mysql 日志抽取模块由两部分构成: canal server:负责从mysql中抽取增量日志. mysql-extractor storm程序:负责将增量日志输出到kafka中,过滤不需要的表数据,保证at least on

C后端设计开发 - 第4章-武技-常见轮子下三路

正文 第4章-武技-常见轮子下三路 后记 如果有错误, 欢迎指正. 有好的补充, 和疑问欢迎交流, 一块提高. 在此谢谢大家了. Moonlight Shadow   纪念那个我爱的, 被我感动的女孩 ~ 愿你在天国安好 ?    -    云飘宁

vue之better-scroll的封装,包含下拉刷新,上拉加载功能及UI(核心为借鉴,我仅仅是给轮子套上了外胎...)

先发原文作者.地址等信息.我把内容全部搬过来了,也可以去看原文.内容绝对是满满的干货,给原作者点赞!(我添加的内容在转载过来的后面,内容不多) 作者: ustbhuangyi 链接:http://www.imooc.com/article/18232 来源:慕课网 在我们日常的移动端项目开发中,处理滚动列表是再常见不过的需求了,以滴滴为例,可以是这样竖向滚动的列表,如图所示: 也可以是横向滚动的导航栏,如图所示: 可以打开"微信 -> 钱包->滴滴出行"体验效果. 我们在实

python 造轮子(一)——序列与字典

虽然说造轮子很少用了,什么底层东西很少写,但是还是很想学扎实,还是好多东西还是的会,没有底层的支持,比较高级的库学起来还是很困难的. 序列的普遍用法: 1 #-*-coding:utf8-*- 2 3 #索引 4 l = [1,2,3,4] 5 t = (1,2,3,4) 6 d = {1:1,2:2,3:3,4:4} 7 8 9 print l[0] 10 print t[0] 11 print d[1] #键索引 12 13 #切片 14 15 print l[0:5] 16 print t

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

关于排序算法的博客何止千千万了,也不多一个轮子,那我就斗胆粗制滥造个轮子吧!下面的排序算法未作说明默认是从小到大排序. 1.快速排序2.归并排序3.冒泡排序4.选择排序(简单选择排序)5.插入排序(直接插入排序)6.希尔排序二分查找 1.快速排序 为什么把快排放在最前面呢,因为传说Chrome中数组的sort方法默认采用的就是快排. 算法思想: (1)在数据集之中,选择一个元素作为"基准"(pivot). (2)所有小于"基准"的元素,都移到"基准&quo

实现一个类似jquery选择器的小轮子(二)

大致的思路已经整理出来, 上一次遍历到的子级是下一次遍历到的父级; 首先开始是对$(str)里面的str字符串进行切片; var str = ' div .abc .edf ' $(str); //切片思路如下 //首先在使用选择器时可能手误,前面空了空格,或者后面空了空格:为了增加容错性,在此先对字符串使用trim方法. str = str.replace(/^\s+|\s+$/g,''); console.log(str);//得到'div .abc .edf'; //开始对字符串进行切割

GitHub Android 最火开源项目Top20 GitHub 上的开源项目不胜枚举,越来越多的开源项目正在迁移到GitHub平台上。基于不要重复造轮子的原则,了解当下比较流行的Android与iOS开源项目很是必要。利用这些项目,有时能够让你达到事半功倍的效果。

1. ActionBarSherlock(推荐) ActionBarSherlock应该算得上是GitHub上最火的Android开源项目了,它是一个独立的库,通过一个API和主题,开发者就可以很方便地使用所有版本的Android动作栏的设计模式. 对于Android 4.0及更高版本,ActionBarSherlock可以自动使用本地ActionBar实现,而对于之前没有ActionBar功能的版本,基于Ice Cream Sandwich的自定义动作栏实现将自动围绕布局.能够让开发者轻松开发