简介

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

使用

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

  1. LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
  2. layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
  3. mRecyclerView.setLayoutManager(layoutManager);
  4. mRecyclerView.setAdapter(mAdapter);

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

  1. mRecyclerView.setLoadingListener(new XRecyclerView.LoadingListener() {
  2. @Override
  3. public void onRefresh() {
  4. //refresh data here
  5. }
  6. @Override
  7. public void onLoadMore() {
  8. // load more data here
  9. }
  10. });

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

  1. mRecyclerView.refreshComplete(); //下拉刷新完成
  2. 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
  1. private ArrayList<View> mHeaderViews = new ArrayList<>();
  2. private ArrayList<View> mFootViews = new ArrayList<>();
  3. ……
  4. private void init() {
  5. if (pullRefreshEnabled) {
  6. //若支持下拉刷新则加入Headerview列表,设置加载图标
  7. ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext());
  8. mHeaderViews.add(0, refreshHeader);//从这里看出headerView可以添加多个
  9. mRefreshHeader = refreshHeader;
  10. mRefreshHeader.setProgressStyle(mRefreshProgressStyle);
  11. }
  12. //加载更多无需触发
  13. LoadingMoreFooter footView = new LoadingMoreFooter(getContext());
  14. footView.setProgressStyle(mLoadingMoreProgressStyle);
  15. addFootView(footView);//加入footerView
  16. mFootViews.get(0).setVisibility(GONE);
  17. }
  18. ……
  19. /**
  20. * @param view 对外提供添加header的方法
  21. */
  22. public void addHeaderView(View view) {
  23. if (pullRefreshEnabled && !(mHeaderViews.get(0) instanceof ArrowRefreshHeader)) {
  24. ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext());
  25. mHeaderViews.add(0, refreshHeader);
  26. mRefreshHeader = refreshHeader;
  27. mRefreshHeader.setProgressStyle(mRefreshProgressStyle);
  28. }
  29. mHeaderViews.add(view);
  30. sHeaderTypes.add(HEADER_INIT_INDEX + mHeaderViews.size());//记录viewType
  31. }

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

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

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

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

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

  1. @Override
  2. public void onAttachedToRecyclerView(RecyclerView recyclerView) {
  3. super.onAttachedToRecyclerView(recyclerView);
  4. RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
  5. if (manager instanceof GridLayoutManager) {
  6. final GridLayoutManager gridManager = ((GridLayoutManager) manager);
  7. gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
  8. @Override
  9. public int getSpanSize(int position) {
  10. return (isHeader(position) || isFooter(position))
  11. ? gridManager.getSpanCount() : 1;
  12. }
  13. });
  14. }
  15. }
  16. @Override
  17. public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
  18. super.onViewAttachedToWindow(holder);
  19. ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
  20. if (lp != null
  21. && lp instanceof StaggeredGridLayoutManager.LayoutParams
  22. && (isHeader(holder.getLayoutPosition()) || isFooter(holder.getLayoutPosition()))) {
  23. StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
  24. p.setFullSpan(true);
  25. }
  26. }

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

  1. @Override
  2. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  3. if (viewType == TYPE_REFRESH_HEADER) {
  4. mCurrentPosition++;
  5. return new SimpleViewHolder(mHeaderViews.get(0));
  6. } else if (isContentHeader(mCurrentPosition)) {
  7. if (viewType == sHeaderTypes.get(mCurrentPosition - 1)) {
  8. mCurrentPosition++;
  9. return new SimpleViewHolder(mHeaderViews.get(headerPosition++));
  10. }
  11. } else if (viewType == TYPE_FOOTER) {
  12. return new SimpleViewHolder(mFootViews.get(0));
  13. }
  14. return adapter.onCreateViewHolder(parent, viewType);
  15. }
  16. ……
  17. @Override
  18. public int getItemViewType(int position) {
  19. if (isRefreshHeader(position)) {
  20. return TYPE_REFRESH_HEADER;
  21. }
  22. if (isHeader(position)) {
  23. position = position - 1;
  24. return sHeaderTypes.get(position);
  25. }
  26. if (isFooter(position)) {
  27. return TYPE_FOOTER;
  28. }
  29. int adjPosition = position - getHeadersCount();
  30. int adapterCount;
  31. if (adapter != null) {
  32. adapterCount = adapter.getItemCount();
  33. if (adjPosition < adapterCount) {
  34. return adapter.getItemViewType(adjPosition);
  35. }
  36. }
  37. return TYPE_NORMAL;
  38. }

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

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

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

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

    涉及到滑动需要重写onTouchEvent,特别是针对MotionEvent.ACTION_MOVE处理
  1. @Override
  2. public boolean onTouchEvent(MotionEvent ev) {
  3. //通过处理onTouchEvent处理下拉刷新
  4. if (mLastY == -1) {
  5. mLastY = ev.getRawY();
  6. }
  7. switch (ev.getAction()) {
  8. case MotionEvent.ACTION_DOWN:
  9. mLastY = ev.getRawY();
  10. break;
  11. case MotionEvent.ACTION_MOVE:
  12. final float deltaY = ev.getRawY() - mLastY;
  13. mLastY = ev.getRawY();
  14. if (isOnTop() && pullRefreshEnabled) {
  15. mRefreshHeader.onMove(deltaY / DRAG_RATE);//显示刷新的关键代码
  16. if (mRefreshHeader.getVisibleHeight() > 0 && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {
  17. // Log.i("getVisibleHeight", "getVisibleHeight = " + mRefreshHeader.getVisibleHeight());
  18. // Log.i("getVisibleHeight", " mRefreshHeader.getState() = " + mRefreshHeader.getState());
  19. return false;
  20. }
  21. }
  22. break;
  23. default:
  24. mLastY = -1; // reset
  25. if (isOnTop() && pullRefreshEnabled) {
  26. if (mRefreshHeader.releaseAction()) {
  27. if (mLoadingListener != null) {
  28. mLoadingListener.onRefresh();
  29. }
  30. }
  31. }
  32. break;
  33. }
  34. return super.onTouchEvent(ev);
  35. }

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

  1. @Override
  2. public void onMove(float delta) {
  3. //由于下拉时候区域是动态变化因此需要动态设置
  4. if (getVisibleHeight() > 0 || delta > 0) {
  5. setVisibleHeight((int) delta + getVisibleHeight());
  6. if (mState <= STATE_RELEASE_TO_REFRESH) { // 未处于刷新状态,更新箭头
  7. if (getVisibleHeight() > mMeasuredHeight) {
  8. setState(STATE_RELEASE_TO_REFRESH);
  9. } else {
  10. setState(STATE_NORMAL);
  11. }
  12. }
  13. }
  14. }

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

  1. addView(mContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 0));//初始化时候高度设置为0,通过后面setVisibleHeight设置可见高度
  2. ……
  3. /**
  4. * 设置可见高度
  5. *
  6. * @param height
  7. */
  8. public void setVisibleHeight(int height) {
  9. if (height < 0) height = 0;
  10. LayoutParams lp = (LayoutParams) mContainer.getLayoutParams();
  11. lp.height = height;
  12. mContainer.setLayoutParams(lp);
  13. }

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

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

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

default分支:

  1. default:
  2. mLastY = -1; // reset
  3. if (isOnTop() && pullRefreshEnabled) {
  4. if (mRefreshHeader.releaseAction()) {//上弹关键代码
  5. if (mLoadingListener != null) {
  6. mLoadingListener.onRefresh();
  7. }
  8. }
  9. }
  10. break;

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

ArrowRefreshHeader中实现

  1. @Override
  2. public boolean releaseAction() {
  3. //释放动作,此时需要处理缓慢回到顶部
  4. boolean isOnRefresh = false;
  5. int height = getVisibleHeight();
  6. if (height == 0) // not visible.
  7. isOnRefresh = false;
  8. if (getVisibleHeight() > mMeasuredHeight && mState < STATE_REFRESHING) {
  9. setState(STATE_REFRESHING);
  10. isOnRefresh = true;
  11. }
  12. // refreshing and header isn't shown fully. do nothing.
  13. if (mState == STATE_REFRESHING && height <= mMeasuredHeight) {
  14. //return;
  15. }
  16. int destHeight = 0; // default: scroll back to dismiss header.
  17. // is refreshing, just scroll back to show all the header.
  18. if (mState == STATE_REFRESHING) {
  19. destHeight = mMeasuredHeight;
  20. }
  21. smoothScrollTo(destHeight);
  22. return isOnRefresh;
  23. }
  24. ……
  25. private void smoothScrollTo(int destHeight) {
  26. ValueAnimator animator = ValueAnimator.ofInt(getVisibleHeight(), destHeight);
  27. animator.setDuration(300).start();
  28. animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  29. @Override
  30. public void onAnimationUpdate(ValueAnimator animation) {
  31. setVisibleHeight((int) animation.getAnimatedValue());
  32. }
  33. });
  34. animator.start();
  35. }

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

  • 加载更多实现

    通常实现该功能是在手指滑动停止后进行加载,在XRecyclerView中重写了onScrollStateChange方法,加载更多主要是需要获得最后可见的位置即lastVisibleItem,如下所示
  1. @Override
  2. public void onScrollStateChanged(int state) {
  3. super.onScrollStateChanged(state);
  4. //重写该方法主要是在IDLE态即手指滑动停止后处理加载更多
  5. if (state == RecyclerView.SCROLL_STATE_IDLE && mLoadingListener != null && !isLoadingData && loadingMoreEnabled) {
  6. LayoutManager layoutManager = getLayoutManager();
  7. int lastVisibleItemPosition;
  8. if (layoutManager instanceof GridLayoutManager) {
  9. lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
  10. } else if (layoutManager instanceof StaggeredGridLayoutManager) {
  11. //瀑布流布局发现最后可见的item位置
  12. int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
  13. ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into);
  14. lastVisibleItemPosition = findMax(into);
  15. } else {
  16. lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
  17. }
  18. if (layoutManager.getChildCount() > 0
  19. && lastVisibleItemPosition >= layoutManager.getItemCount() - 1 && layoutManager.getItemCount() > layoutManager.getChildCount() && !isNoMore && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {
  20. View footView = mFootViews.get(0);
  21. isLoadingData = true;
  22. if (footView instanceof LoadingMoreFooter) {
  23. ((LoadingMoreFooter) footView).setState(LoadingMoreFooter.STATE_LOADING);
  24. } else {
  25. footView.setVisibility(View.VISIBLE);
  26. }
  27. mLoadingListener.onLoadMore();
  28. }
  29. }
  30. }
  • 空白数据处理

    数据变化时处理布局,这里主要通过AapterDataObserver监听数据变化以此来更换布局
  1. @Override
  2. public void onChanged() {
  3. //重写该方法是在数据发生变化时更换布局
  4. Adapter<?> adapter = getAdapter();
  5. if (adapter != null && mEmptyView != null) {
  6. int emptyCount = 0;
  7. if (pullRefreshEnabled) {
  8. emptyCount++;
  9. }
  10. if (loadingMoreEnabled) {
  11. emptyCount++;
  12. }
  13. if (adapter.getItemCount() == emptyCount) {
  14. mEmptyView.setVisibility(View.VISIBLE);
  15. XRecyclerView.this.setVisibility(View.GONE);
  16. } else {
  17. mEmptyView.setVisibility(View.GONE);
  18. XRecyclerView.this.setVisibility(View.VISIBLE);
  19. }
  20. }
  21. if (mWrapAdapter != null) {
  22. mWrapAdapter.notifyDataSetChanged();
  23. }
  24. }
  1. ArrowRefreshHeader与LoadingMoreFooter

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

    主要功能就是初始化好加载更多的动画view和家在文字,然后通过state统统暴露在setState函数中供外界调用
  1. public void setState(int state) {
  2. switch(state) {
  3. case STATE_LOADING:
  4. progressCon.setVisibility(View.VISIBLE);
  5. mText.setText(getContext().getText(R.string.listview_loading));
  6. this.setVisibility(View.VISIBLE);
  7. break;
  8. case STATE_COMPLETE:
  9. mText.setText(getContext().getText(R.string.listview_loading));
  10. this.setVisibility(View.GONE);
  11. break;
  12. case STATE_NOMORE:
  13. mText.setText(getContext().getText(R.string.nomore_loading));
  14. progressCon.setVisibility(View.GONE);
  15. this.setVisibility(View.VISIBLE);
  16. break;
  17. }
  18. }

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

在看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. JAVA面向对象-----final关键字

    JAVA面向对象-–final关键字 1:定义静态方法求圆的面积 2:定义静态方法求圆的周长 3:发现方法中有重复的代码,就是PI,圆周率. 1:如果需要提高计算精度,就需要修改每个方法中圆周率. 4 ...

  2. JBOSS EAP6 系列二 客户端访问位于EAR中的EJB时,jndi name要遵守的规则

    EJB 的 jndi语法(在整个调用远程ejb的过程中语法的遵循是相当重要的) 参见jboss-as-quickstarts-7.1.1.CR2\ejb-remote\client\src\main\ ...

  3. Linux--NFS和DHCP服务器

     (1) 在网络中,时常需要进行文件的共享,如果都是在Linux系统下,可以使用NFS 来搭建文件服务器,达到文件共享的目的. (2) 在网络管理中,为了防止IP 冲突和盗用,有效的控制IP 资源 ...

  4. 15 ActionBar 总结

    ActionBar 一, 说明 是一个动作栏 是窗口特性 提供给用户动作 导航模式 可以适配不同的屏幕 二, ActionBar 提供的功能 1. 显示菜单项 always:总是展示到ActionBa ...

  5. java中遍历map的几种方法介绍

          喜欢用Java写程序的朋友都知道,我们常用的一种数据结构map中存储的是键值对,我们一般存储的方式是: map.put(key, value); 而提取相应键的值用的方法是: map.ge ...

  6. 参数估计:最大似然估计MLE

    http://blog.csdn.net/pipisorry/article/details/51461997 最大似然估计MLE 顾名思义,当然是要找到一个参数,使得L最大,为什么要使得它最大呢,因 ...

  7. DBCP连接池TestOnBorrow的坑

    生产环境连接池TestOnBorrow设置为false,导致有时获取的连接不可用.分析如下: TestOnBorrow=false时,由于不检测池里连接的可用性,于是假如连接池中的连接被数据库关闭了, ...

  8. Error处理:Unable to execute dex: java.nio.BufferOverflowException. Check the Eclipse log for stack tra

    [2014-04-20 20:59:23 - MyDetectActivity] Dx  trouble writing output: already prepared [2014-04-20 20 ...

  9. React Native入门教程 1 -- 开发环境搭建

    有人问我为啥很久不更新博客..我只能说在学校宿舍真的没有学习的环境..基本上在宿舍里面很颓废..不过要毕业找工作了,我要渐渐把这个心态调整过来,就从react-native第一篇博客开始.话说RN也出 ...

  10. 《java入门第一季》模拟用户登陆注册案例集合版

    需求:校验用户名和密码,登陆成功后玩猜数字小游戏. 在这里先写集合版.后面还有IO版.数据库版. 一.猜数字小游戏类: 猜数字小游戏的代码见博客:http://blog.csdn.net/qq_320 ...