摘要

最近项目有个列表页需要实现线性列表和瀑布流展示的切换,首先我想到的就是上

[RecyclerView],他本身已经很好的提供了三种布局方式,只是简单做个切换应该是很简单的事情,如果要用RecyclerView的方式来实现,那就是目前的设计方案(listView)都不能用,更改为RecyclerView,我需要做如下工作:

  • 下拉刷新,
  • 上拉自动加载更多,
  • 同时支持切换布局方式。

自定义RecyclerView实现自动加载

为了实现自动加载更多的功能,我选择自定义实现一个特殊的RecyclerView来实现,网络上也有自定义一个包含RecyclerView和loading_more的布局,现在我想试试adapter处理的方式,所以有了这个自定义实现类

java片段1: RecyclerView实现LoadMoreRecyclerView

/**
* Alipay.com Inc.
* Copyright (c) 2004-2015 All Rights Reserved.
*/
package com.leaf8.alicx.myapplication; import android.content.Context;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; /**
* 支持上拉加载更多的
*
* @author 肖肖
* @version $$Id: LoadMoreRecyclerView.java, v 0.1 11/17/15 10:07 alicx Exp $$
*/
public class LoadMoreRecyclerView extends RecyclerView {
/**
* item 类型
*/
public final static int TYPE_NORMAL = 0;
public final static int TYPE_HEADER = 1;//头部--支持头部增加一个headerView
public final static int TYPE_FOOTER = 2;//底部--往往是loading_more
public final static int TYPE_LIST = 3;//代表item展示的模式是list模式
public final static int TYPE_STAGGER = 4;//代码item展示模式是网格模式 private boolean mIsFooterEnable = false;//是否允许加载更多 /**
* 自定义实现了头部和底部加载更多的adapter
*/
private AutoLoadAdapter mAutoLoadAdapter;
/**
* 标记是否正在加载更多,防止再次调用加载更多接口
*/
private boolean mIsLoadingMore;
/**
* 标记加载更多的position
*/
private int mLoadMorePosition;
/**
* 加载更多的监听-业务需要实现加载数据
*/
private LoadMoreListener mListener; public LoadMoreRecyclerView(Context context) {
super(context);
init();
} public LoadMoreRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
} public LoadMoreRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
} /**
* 初始化-添加滚动监听
* <p/>
* 回调加载更多方法,前提是
* <pre>
* 1、有监听并且支持加载更多:null != mListener && mIsFooterEnable
* 2、目前没有在加载,正在上拉(dy>0),当前最后一条可见的view是否是当前数据列表的最好一条--及加载更多
* </pre>
*/
private void init() {
super.addOnScrollListener(new OnScrollListener() { @Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
} @Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (null != mListener && mIsFooterEnable && !mIsLoadingMore && dy > 0) {
int lastVisiblePosition = getLastVisiblePosition();
if (lastVisiblePosition + 1 == mAutoLoadAdapter.getItemCount()) {
setLoadingMore(true);
mLoadMorePosition = lastVisiblePosition;
mListener.onLoadMore();
}
}
}
});
} /**
* 设置加载更多的监听
*
* @param listener
*/
public void setLoadMoreListener(LoadMoreListener listener) {
mListener = listener;
} /**
* 设置正在加载更多
*
* @param loadingMore
*/
public void setLoadingMore(boolean loadingMore) {
this.mIsLoadingMore = loadingMore;
} /**
* 加载更多监听
*/
public interface LoadMoreListener {
/**
* 加载更多
*/
void onLoadMore();
} /**
*
*/
public class AutoLoadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { /**
* 数据adapter
*/
private RecyclerView.Adapter mInternalAdapter; private boolean mIsHeaderEnable;
private int mHeaderResId; public AutoLoadAdapter(RecyclerView.Adapter adapter) {
mInternalAdapter = adapter;
mIsHeaderEnable = false;
} @Override
public int getItemViewType(int position) {
int headerPosition = 0;
int footerPosition = getItemCount() - 1; if (headerPosition == position && mIsHeaderEnable && mHeaderResId > 0) {
return TYPE_HEADER;
}
if (footerPosition == position && mIsFooterEnable) {
return TYPE_FOOTER;
}
/**
* 这么做保证layoutManager切换之后能及时的刷新上对的布局
*/
if (getLayoutManager() instanceof LinearLayoutManager) {
return TYPE_LIST;
} else if (getLayoutManager() instanceof StaggeredGridLayoutManager) {
return TYPE_STAGGER;
} else {
return TYPE_NORMAL;
}
} @Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_HEADER) {
return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(
mHeaderResId, parent, false));
}
if (viewType == TYPE_FOOTER) {
return new FooterViewHolder(
LayoutInflater.from(parent.getContext()).inflate(
R.layout.list_foot_loading, parent, false));
} else { // type normal
return mInternalAdapter.onCreateViewHolder(parent, viewType);
}
} public class FooterViewHolder extends RecyclerView.ViewHolder { public FooterViewHolder(View itemView) {
super(itemView);
}
} public class HeaderViewHolder extends RecyclerView.ViewHolder {
public HeaderViewHolder(View itemView) {
super(itemView);
}
} @Override
public void onBindViewHolder(ViewHolder holder, int position) {
int type = getItemViewType(position);
if (type != TYPE_FOOTER && type != TYPE_HEADER) {
mInternalAdapter.onBindViewHolder(holder, position);
}
} /**
* 需要计算上加载更多和添加的头部俩个
*
* @return
*/
@Override
public int getItemCount() {
int count = mInternalAdapter.getItemCount();
if (mIsFooterEnable) count++;
if (mIsHeaderEnable) count++; return count;
} public void setHeaderEnable(boolean enable) {
mIsHeaderEnable = enable;
} public void addHeaderView(int resId) {
mHeaderResId = resId;
}
} @Override
public void setAdapter(RecyclerView.Adapter adapter) {
if (adapter != null) {
mAutoLoadAdapter = new AutoLoadAdapter(adapter);
}
super.swapAdapter(mAutoLoadAdapter, true);
} /**
* 切换layoutManager
*
* 为了保证切换之后页面上还是停留在当前展示的位置,记录下切换之前的第一条展示位置,切换完成之后滚动到该位置
* 另外切换之后必须要重新刷新下当前已经缓存的itemView,否则会出现布局错乱(俩种模式下的item布局不同),
* RecyclerView提供了swapAdapter来进行切换adapter并清理老的itemView cache
*
* @param layoutManager
*/
public void switchLayoutManager(LayoutManager layoutManager) {
int firstVisiblePosition = getFirstVisiblePosition();
// getLayoutManager().removeAllViews();
setLayoutManager(layoutManager);
//super.swapAdapter(mAutoLoadAdapter, true);
getLayoutManager().scrollToPosition(firstVisiblePosition);
} /**
* 获取第一条展示的位置
*
* @return
*/
private int getFirstVisiblePosition() {
int position;
if (getLayoutManager() instanceof LinearLayoutManager) {
position = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
} else if (getLayoutManager() instanceof GridLayoutManager) {
position = ((GridLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
} else if (getLayoutManager() instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) getLayoutManager();
int[] lastPositions = layoutManager.findFirstVisibleItemPositions(new int[layoutManager.getSpanCount()]);
position = getMinPositions(lastPositions);
} else {
position = 0;
}
return position;
} /**
* 获得当前展示最小的position
*
* @param positions
* @return
*/
private int getMinPositions(int[] positions) {
int size = positions.length;
int minPosition = Integer.MAX_VALUE;
for (int i = 0; i < size; i++) {
minPosition = Math.min(minPosition, positions[i]);
}
return minPosition;
} /**
* 获取最后一条展示的位置
*
* @return
*/
private int getLastVisiblePosition() {
int position;
if (getLayoutManager() instanceof LinearLayoutManager) {
position = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition();
} else if (getLayoutManager() instanceof GridLayoutManager) {
position = ((GridLayoutManager) getLayoutManager()).findLastVisibleItemPosition();
} else if (getLayoutManager() instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) getLayoutManager();
int[] lastPositions = layoutManager.findLastVisibleItemPositions(new int[layoutManager.getSpanCount()]);
position = getMaxPosition(lastPositions);
} else {
position = getLayoutManager().getItemCount() - 1;
}
return position;
} /**
* 获得最大的位置
*
* @param positions
* @return
*/
private int getMaxPosition(int[] positions) {
int size = positions.length;
int maxPosition = Integer.MIN_VALUE;
for (int i = 0; i < size; i++) {
maxPosition = Math.max(maxPosition, positions[i]);
}
return maxPosition;
} /**
* 添加头部view
*
* @param resId
*/
public void addHeaderView(int resId) {
mAutoLoadAdapter.addHeaderView(resId);
} /**
* 设置头部view是否展示
* @param enable
*/
public void setHeaderEnable(boolean enable) {
mAutoLoadAdapter.setHeaderEnable(enable);
} /**
* 设置是否支持自动加载更多
*
* @param autoLoadMore
*/
public void setAutoLoadMoreEnable(boolean autoLoadMore) {
mIsFooterEnable = autoLoadMore;
} /**
* 通知更多的数据已经加载
*
* 每次加载完成之后添加了Data数据,用notifyItemRemoved来刷新列表展示,
* 而不是用notifyDataSetChanged来刷新列表
*
* @param hasMore
*/
public void notifyMoreFinish(boolean hasMore) {
setAutoLoadMoreEnable(hasMore);
getAdapter().notifyItemRemoved(mLoadMorePosition);
mIsLoadingMore = false;
}
}

实现原理是利用RecyclerView.Adapter的getItemType来区别不一样的item布局,这里是实现了一个支持头部和底部的额外itemVIew的apdater壳子,将数据的adapter放到这个壳子中去代理,保证业务方实现的adapter的纯净。

另外LoadMoreRecyclerView管理了5个itemType,除了TYPE_HEADER和TYPE_FOOTER之外,其余三个会传递给业务方的onCreateViewHolder方法,业务方根据这个参数决定使用布局,当然业务方页可以用自己的方式实现判断条件:

java片段2: 业务方可能的onCreateViewHolder实现

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == LoadMoreRecyclerView.TYPE_STAGGER) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_item_staggel, parent, false);
return new StaggerViewHolder(view);
} else {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_item, parent, false);
return new ViewHolder(view);
}
}

实现下拉刷新

下拉刷新的实现,我也是直接用了google提供的support-v4包种的SwipeRefreshLayout,只要实现下拉刷新的获取数据动作即可,

layout1:

<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.leaf8.alicx.myapplication.LoadMoreRecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/fragment_item" />
</android.support.v4.widget.SwipeRefreshLayout>

java片段3: refreshlistener

swipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
swipeRefreshLayout.setRefreshing(false);
page = 0;
myItemRecyclerViewAdapter.setData(DummyContent.generyData(page));
recyclerView.setAutoLoadMoreEnable(DummyContent.hasMore(page));
myItemRecyclerViewAdapter.notifyDataSetChanged();
}
});

SwipeRefreshLayout没什么可以多说的,大家都知道的东西。

实现上拉加载更多

我自己实现了一个LoadMoreRecyclerView,来完成自动加载的功能。代码见#java片段1#,这个LoadMoreRecyclerView需要业务方设置LoadMoreListener,当分页结束还需要设置不再继续自动加载,LoadMoreRecyclerView开放了一个接口notifyMoreFinish来支持该设置。

java片段4

recyclerView.setLoadMoreListener(new LoadMoreRecyclerView.LoadMoreListener() {
@Override
public void onLoadMore() {
recyclerView.postDelayed(new Runnable() {
@Override
public void run() {
swipeRefreshLayout.setRefreshing(false);
myItemRecyclerViewAdapter.addDatas(DummyContent.generyData(++page));
recyclerView.notifyMoreFinish(DummyContent.hasMore(page));
}
}, 1000);
}
});

这样完了之后,就能实现上拉自动加载,并且加载中还有一个加载中...显示。

在notifyMoreFinish方法中为了实现新数据被刷新到页面上,用了adapter.notifyItemRemoved(lastvisiblePosition),发现用notifyDataSetChanged在此时不能起作用,原因应该是加载中...这个,在列表中是一个正常的view,而新数据加上之后,其实是在这个view的位置插入数据的操作,所以此处adapter.notifyItemInsert/adapter.notifyItemRemoved都能起作用,但是notifyDataSetChanged不会重新执行该position的CreateView(原因应该是他认为该position的view已经被缓存了,所以直接展示了缓存中的view)。这点在日常运用中实现对数据的增删的时候也有遇到。

实现线性及瀑布流切换

新版列表还要做的一个事情是要实现瀑布流和线性流的切换,这个开始做之前我认为应该很简单,但是当我真的去实施的时候,发现其实还有很多细节问题。

demo中我只是对这个切换简单的做了一个文案按钮,点击实现切换:

java片段5

if (1 == mColumnCount) {
mColumnCount = 2;
((TextView) v).setText(R.string.list_mode_stagger);
myItemRecyclerViewAdapter.switchMode(true);
recyclerView.switchLayoutManager(new StaggeredGridLayoutManager(mColumnCount, StaggeredGridLayoutManager.VERTICAL));
} else {
mColumnCount = 1;
((TextView) v).setText(R.string.list_mode_list);
myItemRecyclerViewAdapter.switchMode(false);
recyclerView.switchLayoutManager(new LinearLayoutManager(getActivity()));
}

看看switchLayoutManager方法实现:

java片段6

int firstVisiblePosition = getFirstVisiblePosition();
// getLayoutManager().removeAllViews();
setLayoutManager(layoutManager);
//super.swapAdapter(mAutoLoadAdapter, true);
getLayoutManager().scrollToPosition(firstVisiblePosition);

实现原则就是切换布局之后,页面还是停留在当前浏览的position位置,所以切换之前,我得先记录下当前的第一个可见的item的位置,代码不贴了,就看LoadMoreRecyclerView.getFirstVisiblePosition()方法。然后我setLayoutManager设置布局,然后我通过scrollToPosition滚动到先前停留的位置,结果发现切换之后页面上会出现俩种布局错乱。恩应该是LoadMoreRecyclerView中还保留了先前布局的item view cache导致,必须要让他知道我的模板已经过时了需要重新生成了,通过什么方法告知我需要刷新布局呢,这个我尝试了几种方法,如上注释掉的,但是还是不顶用,最后我想到adapter.getItemType(),这个应该可行,于是我针对俩种布局添加了俩个类型TYPE_LIST和TYPE_STAGGER,传递到业务的onCreateViewHolder,让业务知道当前的不同布局标识,结果证实该方案可行。

待解问题 解决了切换布局混乱的问题,又遇到了一个新问题,从list切换到StaggerGride,如果当前的position是在第二屏,切换过来之后,滑动到第一屏发现瀑布流的第一行俩个列是不对齐的,右边一列会滑动上去的一个过程。这个时候再上拉翻到第三屏就会出现一个IndexOutOfBoundsException的crash异常,但是当我切换过来之后,不去下滑,而是继续上拉,则不会出现这个问题,但是下滑到第一屏还是会有一个第二列往上移动的过程。感觉是这个移动的过程造成了后续数据的一个position错乱,目前还在排查这个问题

无图无真相

  • 列表模式

  • 瀑布流模式

  • 问题:切换之后滑动到第一屏出现不对齐现象

总结

RecyclerView某些情况下确实很好用的,很方便的就实现了一些特性。但是因为是个新东西,很多方面还是欠缺了一些成熟性,不可控性,在实际应用中还是需要多多演练才行。

Demo地址

打上我的demo地址:供大家参考-->

:因为业务需要,该业务涉及的list需要放到Fragment中实现,所以本demo也是在这种架构中实现

RecyclerView实例-实现可下拉刷新上拉加载更多并可切换线性流和瀑布流模式(1)的更多相关文章

  1. SwipeRefreshLayout实现下拉刷新上滑加载

    1. 效果图 2.RefreshLayout.java package myapplication.com.myapplication; import android.content.Context; ...

  2. Android 下拉刷新上啦加载SmartRefreshLayout + RecyclerView

    在弄android刷新的时候,可算是耗费了一番功夫,最后发觉有现成的控件,并且非常好用,这里记录一下. 原文是 https://blog.csdn.net/huangxin112/article/de ...

  3. juery下拉刷新,div加载更多元素并添加点击事件(二)

    buffer.append("<div class='col-xs-3 "+companyId+"' style='padding-left: 10px; padd ...

  4. 移动端下拉刷新上拉加载-mescroll.js插件

    最近无意间看到有这么一个上拉刷新下拉加载的插件 -- mescroll.js,个人感觉挺好用的,官网地址是:http://www.mescroll.com 然后我就看了一下文档,简单的写了一个小dem ...

  5. Android如何定制一个下拉刷新,上滑加载更多的容器

    前言 下拉刷新和上滑加载更多,是一种比较常用的列表数据交互方式. android提供了原生的下拉刷新容器 SwipeRefreshLayout,可惜样式不能定制. 于是打算自己实现一个专用的.但是下拉 ...

  6. Android 自定义 ListView 上下拉动“刷新最新”和“加载更多”歌曲列表

    本文内容 环境 测试数据 项目结构 演示 参考资料 本文演示,上拉刷新最新的歌曲列表,和下拉加载更多的歌曲列表.所谓"刷新最新"和"加载更多"是指日期.演示代码 ...

  7. 带你实现开发者头条APP(五)--RecyclerView下拉刷新上拉加载

    title: 带你实现开发者头条APP(五)--RecyclerView下拉刷新上拉加载 tags: -RecyclerView,下拉刷新,上拉加载更多 grammar_cjkRuby: true - ...

  8. RecyclerView下拉刷新上拉加载(三)—对Adapter的封装

    RecyclerView下拉刷新上拉加载(一) http://blog.csdn.net/baiyuliang2013/article/details/51506036 RecyclerView下拉刷 ...

  9. RecyclerView下拉刷新上拉加载(二)

    listview下拉刷新上拉加载扩展(一) http://blog.csdn.net/baiyuliang2013/article/details/50252561 listview下拉刷新上拉加载扩 ...

随机推荐

  1. 深入了解一下PYTHON中关于SOCKETSERVER的模块-B

    请求多个文件的原型. 这个是最草的情况,就是硬编码到内存中的字符串, 真实的应用还是会转到其它端口处理,或是读到硬盘上的文件吧. #!/usr/bin/env python from BaseHTTP ...

  2. c++ new带括号和不带括号

    在new对象的时候有加上(),有不加(),不知道这个到底是什么区别?比如:CBase *base = new CDerived();CBase *base = new CDeviced; 很多人都说, ...

  3. 【转】Xcode7真机调试iOS应用程序

    原文网址:http://i.cnblogs.com/EditPosts.aspx?opt=1 近日苹果发布的新的Xcode7带来了许多特性,比如:swift语言比以前运行更快.功能更强.代码具有更高的 ...

  4. 在.net MVC中异步上传图片或者文件

      @using (Ajax.BeginForm("AddMessages", "MenuInfo", new AjaxOptions { HttpMethod ...

  5. ANDROID Porting系列一、ANDROID编译系统

    译自:http://source.android.com/porting/build_system.html Android使用一个自定义生成系统生成工具,二进制文件和文档.本文档提供了一个建立And ...

  6. [svn] 数据库操作残留,无法进行操作的解决方法

    WINDOWS环境下的解决方法: 1: 下载sqlite3数据库工具,放置于SVN的同级目录 2: CMD路径转移到Sqlite3目录 3: 残留操作选择: sqlite3 .svn/wc.db &q ...

  7. Django中的CSRF

    CSRF(Cross Site Request Forgery, 跨站域请求伪造) CSRF 背景与介绍 CSRF(Cross Site Request Forgery, 跨站域请求伪造)是一种网络的 ...

  8. lib_mysqludf_sys的安装过程

    看了太多资料,累坏了,先杂乱的放在这里吧,回头有时间再排版 一.window系统 1.从sqlmap中找到32为的 lib_mysqludf_sys.dll (64位的我没有测试成功) 选择数据库 U ...

  9. 正则表达式中Pattern类、Matcher类和matches()方法简析

    1.简介:  java.util.regex是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包.  它包括两个类:Pattern和Matcher . Pattern: 一个Pattern是一 ...

  10. oracle从客户端到sql语句追踪

    这两天看小布老师的视频学习了一下从客户端到oracle数据库发送执行的SQL语句的跟踪,整理一下笔记. 需要用到的命令:netstat oracle端要用到的四个视图为: V$session:当前有多 ...