概述
官方介绍,RecyclerView用于在有限的窗口展现大量的数据,其实早已经有了类似的控件,如ListView、GridView,那么相比它们,RecyclerView有什么样优势呢?
RecyclerView标准化了ViewHolder,而且异常的灵活,可以轻松实现ListView实现不了的样式和功能,通过布局管理器LayoutManager可控制Item的布局方式,通过设置Item操作动画自定义Item添加和删除的动画,通过设置Item之间的间隔样式,自定义间隔。
- 设置布局管理器以控制Item的布局方式,横向、竖向以及瀑布流方式。
- 可设置Item操作的动画(删除或者添加等)
- 可设置Item的间隔样式(可绘制)
但是关于Item的点击和长按事件,需要用户自己去实现。
在使用RecyclerView时候,必须指定一个适配器Adapter和一个布局管理器LayoutManager。适配器继承RecyclerView.Adapter
类,具体实现类似ListView的适配器,取决于数据信息以及展示的UI。布局管理器用于确定RecyclerView中Item的展示方式以及决定何时复用已经不可见的Item,避免重复创建以及执行高成本的findViewById()
方法。
来看一下用法。
可以看见RecyclerView相比ListView会多出许多操作,这也是RecyclerView灵活的地方,它将许多动能暴露出来,用户可以选择性的自定义属性以满足需求。
RecyclerView提供了三种布局管理器:
- LinerLayoutManager 以垂直或者水平列表方式展示Item
- GridLayoutManager 以网格方式展示Item
- StaggeredGridLayoutManager 以瀑布流方式展示Item
基本使用
在build.gradle文件中引入该类。
compile 'com.android.support:recyclerview-v7:23.4.0'
Activity代码
public class MDRvActivity extends MDBaseActivity {
private RecyclerView mRecyclerView;
private RecyclerView.Adapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rv);
initData();
initView();
}
private void initData() {
mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mAdapter = new MyAdapter(getData());
}
private void initView() {
mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
// 设置布局管理器
mRecyclerView.setLayoutManager(mLayoutManager);
// 设置adapter
mRecyclerView.setAdapter(mAdapter);
}
private ArrayList<String> getData() {
ArrayList<String> data = new ArrayList<>();
String temp = " item";
for(int i = 0; i < 20; i++) {
data.add(i + temp);
}
return data;
}
}
Activity布局文件activity_rv.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/my_recycler_view"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
RecyclerView适配器Adapter代码
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
private ArrayList<String> mData;
public MyAdapter(ArrayList<String> data) {
this.mData = data;
}
public void updateData(ArrayList<String> data) {
this.mData = data;
notifyDataSetChanged();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 实例化展示的view
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_item, parent, false);
// 实例化viewholder
ViewHolder viewHolder = new ViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// 绑定数据
holder.mTv.setText(mData.get(position));
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView mTv;
public ViewHolder(View itemView) {
super(itemView);
mTv = (TextView) itemView.findViewById(R.id.item_tv);
}
}
}
Item的布局文件view_rv_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="@dimen/md_common_view_height">
<TextView
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:text="item"/>
</LinearLayout>
运行结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/a7d115a55fa2a49c50abcba71f11a80f.webp?x-image-process=image/format,png)
可以看见展示效果和ListView基本上无差别,但是Item之间并没有分割线,在xml去找divider属性的时候,发现RecyclerView没有divider属性,当然也可以在Item布局中加上分割线,但是这样做并不是很优雅。前文说过,RecyclerView可是支持自定义间隔样式的。通过mRecyclerView.addItemDecoration()
来设置我们定义好的间隔样式。
间隔样式
自定义间隔样式需要继承RecyclerView.ItemDecoration
类,该类是个抽象类,主要有三个方法。
- onDraw(Canvas c, RecyclerView parent, State state),在Item绘制之前被调用,该方法主要用于绘制间隔样式
- onDrawOver(Canvas c, RecyclerView parent, State state),在Item绘制之前被调用,该方法主要用于绘制间隔样式
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state),设置item的偏移量,偏移的部分用于填充间隔样式,在RecyclerView的
onMesure()
中会调用该方法
onDraw()
和onDrawOver()
这两个方法都是用于绘制间隔样式,我们只需要复写其中一个方法即可。直接来看一下自定义的间隔样式的实现吧,参考官方实例。
public class MyDividerItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider
};
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
/**
* 用于绘制间隔样式
*/
private Drawable mDivider;
/**
* 列表的方向,水平/竖直
*/
private int mOrientation;
public MyDividerItemDecoration(Context context, int orientation) {
// 获取默认主题的属性
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// 绘制间隔
if (mOrientation == VERTICAL_LIST) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
private void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
/**
* 绘制间隔
*/
private void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin +
Math.round(ViewCompat.getTranslationY(child));
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
/**
* 绘制间隔
*/
private void drawHorizontal(Canvas c, RecyclerView parent) {
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getRight() + params.rightMargin +
Math.round(ViewCompat.getTranslationX(child));
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}
然后在代码中设置RecyclerView的间隔样式。
mRecyclerView.addItemDecoration(new MyDividerItemDecoration(this, LinearLayoutManager.VERTICAL));
来看一下展示效果。
![](https://i-blog.csdnimg.cn/blog_migrate/d5748aff3a7a11aeb7939ebde958bfc6.webp?x-image-process=image/format,png)
既然RecyclerView还支持水平列表,简单改一下属性,看看水平列表的显示效果。
修改Item的布局文件view_rv_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="@dimen/md_common_view_width"
android:layout_height="match_parent">
<TextView
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:text="item"/>
</LinearLayout>
修改LayoutManager的初始化和间隔样式初始化。
mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
// 设置Item之间间隔样式
mRecyclerView.addItemDecoration(new MyDividerItemDecoration(this, LinearLayoutManager.HORIZONTAL));
看一下水平列表效果。
![](https://i-blog.csdnimg.cn/blog_migrate/e9c8cfc3a81110d0226a022262c2a9be.webp?x-image-process=image/format,png)
动画
前面说过,RecyclerView可以设置列表中Item删除和添加的动画,在v7包中给我们提供了一种默认的Item删除和添加的动画,如果没有特殊的需求,默认使用这个动画即可。
// 设置Item添加和移除的动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
下面就添加一下删除和添加Item的动作。在Adapter里面添加方法。
public void addNewItem() {
if(mData == null) {
mData = new ArrayList<>();
}
mData.add(0, "new Item");
notifyItemInserted(0);
}
public void deleteItem() {
if(mData == null || mData.isEmpty()) {
return;
}
mData.remove(0);
notifyItemRemoved(0);
}
添加事件的处理。
public void onClick(View v) {
int id = v.getId();
if(id == R.id.rv_add_item_btn) {
mAdapter.addNewItem();
// 由于Adapter内部是直接在首个Item位置做增加操作,增加完毕后列表移动到首个Item位置
mLayoutManager.scrollToPosition(0);
} else if(id == R.id.rv_del_item_btn){
mAdapter.deleteItem();
// 由于Adapter内部是直接在首个Item位置做删除操作,删除完毕后列表移动到首个Item位置
mLayoutManager.scrollToPosition(0);
}
}
准备工作完毕后,来看一下运行的效果。
![](https://i-blog.csdnimg.cn/blog_migrate/e0d23c899e2cffc464f3fa9f40d989a3.webp?x-image-process=image/format,png)
点击事件
RecyclerView并没有像ListView一样暴露出Item点击事件或者长按事件处理的api,也就是说使用RecyclerView时候,需要我们自己来实现Item的点击和长按等事件的处理。实现方法有很多,可以监听RecyclerView的Touch事件然后判断手势做相应的处理,也可以通过在绑定ViewHolder的时候设置监听,然后通过Apater回调出去,我们选择第二种方法,更加直观和简单。
看一下Adapter的完整代码。
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
/**
* 展示数据
*/
private ArrayList<String> mData;
/**
* 事件回调监听
*/
private MyAdapter.OnItemClickListener onItemClickListener;
public MyAdapter(ArrayList<String> data) {
this.mData = data;
}
public void updateData(ArrayList<String> data) {
this.mData = data;
notifyDataSetChanged();
}
/**
* 添加新的Item
*/
public void addNewItem() {
if(mData == null) {
mData = new ArrayList<>();
}
mData.add(0, "new Item");
notifyItemInserted(0);
}
/**
* 删除Item
*/
public void deleteItem() {
if(mData == null || mData.isEmpty()) {
return;
}
mData.remove(0);
notifyItemRemoved(0);
}
/**
* 设置回调监听
*
* @param listener
*/
public void setOnItemClickListener(MyAdapter.OnItemClickListener listener) {
this.onItemClickListener = listener;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 实例化展示的view
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_item, parent, false);
// 实例化viewholder
ViewHolder viewHolder = new ViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
// 绑定数据
holder.mTv.setText(mData.get(position));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if(onItemClickListener != null) {
int pos = holder.getLayoutPosition();
onItemClickListener.onItemClick(holder.itemView, pos);
}
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if(onItemClickListener != null) {
int pos = holder.getLayoutPosition();
onItemClickListener.onItemLongClick(holder.itemView, pos);
}
//表示此事件已经消费,不会触发单击事件
return true;
}
});
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView mTv;
public ViewHolder(View itemView) {
super(itemView);
mTv = (TextView) itemView.findViewById(R.id.item_tv);
}
}
public interface OnItemClickListener {
void onItemClick(View view, int position);
void onItemLongClick(View view, int position);
}
}
设置Adapter的事件监听。
mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
Toast.makeText(MDRvActivity.this,"click " + position + " item", Toast.LENGTH_SHORT).show();
}
@Override
public void onItemLongClick(View view, int position) {
Toast.makeText(MDRvActivity.this,"long click " + position + " item", Toast.LENGTH_SHORT).show();
}
});
最后的实现效果。
![](https://i-blog.csdnimg.cn/blog_migrate/01d5aa371532f692947b7059ee1dfd1b.webp?x-image-process=image/format,png)
总结
可以看见相比于ListView,RecyclerView非常灵活,但其实这篇文章只是介绍了RecyclerView的基本使用,并没有深入,比如像网格展示和瀑布流展示都没有介绍,而且这篇文章为了详细的介绍使用方法,贴了大量的源代码,导致篇幅过长,不得以要将RecyclerView的使用分好几篇来介绍。就目前而言,我们已经知道RecyclerView的一些功能如下。
- 水平列表展示,设置LayoutManager的方向性
- 竖直列表展示,设置LayoutManager的方向性
- 自定义间隔,RecyclerView.addItemDecoration()
- Item添加和删除动画,RecyclerView.setItemAnimator()
所以在项目中如果再遇见列表类的布局,就可以优先考虑使用RecyclerView,更灵活更快捷的使用方式会给编码带来不一样的体验。如果你以为这些就是RecyclerView相比ListView/GridView优势的话,那就大错特错了,关于RecyclerView还有更多灵活的功能,在后面文章会慢慢介绍。
网格样式
上篇文章中已经了解到,RecyclerView展示的样式是有布局管理器LayoutManager来控制。网格样式的管理器是GridLayoutManager
,看一下它最常用的两个构造函数以及参数含义。
- GridLayoutManager(Context context, int spanCount)
- spanCount,每列或者每行的item个数,设置为1,就是列表样式
- 该构造函数默认是竖直方向的网格样式
- GridLayoutManager(Context context, int spanCount, int orientation,boolean reverseLayout)
- spanCount,每列或者每行的item个数,设置为1,就是列表样式
- 网格样式的方向,水平(OrientationHelper.HORIZONTAL)或者竖直(OrientationHelper.VERTICAL)
- reverseLayout,是否逆向,true:布局逆向展示,false:布局正向显示
看一下使用。
// 竖直方向的网格样式,每行四个Item
mLayoutManager = new GridLayoutManager(this, 4, OrientationHelper.VERTICAL, false);
mRecyclerView.setLayoutManager(mLayoutManager);
看一下运行效果。
![](https://i-blog.csdnimg.cn/blog_migrate/76b75f5e8323ec6673500bfe603160fd.webp?x-image-process=image/format,png)
网格样式已经显示出来了,和之前遇见的问题一样,没有间隔线,非常丑,间隔线必须加,而且要使用自定义,不使用系统自带的。
新建文件md_divider.xml,是一个灰色的矩形。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<solid android:color="@android:color/darker_gray"/>
<size android:height="4dp" android:width="4dp"/>
</shape>
在styles.xml中的自定义的应用主题里替换掉listdivider属性。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:listDivider">@drawable/md_divider</item>
</style>
然后继承RecyclerView.ItemDecoration
类,在构造函数里获取自定义的间隔线,复写绘制间隔线的方法。
public class MDGridRvDividerDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider
};
/**
* 用于绘制间隔样式
*/
private Drawable mDivider;
public MDGridRvDividerDecoration(Context context) {
// 获取默认主题的属性
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// 绘制间隔,每一个item,绘制右边和下方间隔样式
int childCount = parent.getChildCount();
int spanCount = ((GridLayoutManager)parent.getLayoutManager()).getSpanCount();
int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
boolean isDrawHorizontalDivider = true;
boolean isDrawVerticalDivider = true;
int extra = childCount % spanCount;
extra = extra == 0 ? spanCount : extra;
for(int i = 0; i < childCount; i++) {
isDrawVerticalDivider = true;
isDrawHorizontalDivider = true;
// 如果是竖直方向,最右边一列不绘制竖直方向的间隔
if(orientation == OrientationHelper.VERTICAL && (i + 1) % spanCount == 0) {
isDrawVerticalDivider = false;
}
// 如果是竖直方向,最后一行不绘制水平方向间隔
if(orientation == OrientationHelper.VERTICAL && i >= childCount - extra) {
isDrawHorizontalDivider = false;
}
// 如果是水平方向,最下面一行不绘制水平方向的间隔
if(orientation == OrientationHelper.HORIZONTAL && (i + 1) % spanCount == 0) {
isDrawHorizontalDivider = false;
}
// 如果是水平方向,最后一列不绘制竖直方向间隔
if(orientation == OrientationHelper.HORIZONTAL && i >= childCount - extra) {
isDrawVerticalDivider = false;
}
if(isDrawHorizontalDivider) {
drawHorizontalDivider(c, parent, i);
}
if(isDrawVerticalDivider) {
drawVerticalDivider(c, parent, i);
}
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
int position = parent.getChildLayoutPosition(view);
if(orientation == OrientationHelper.VERTICAL && (position + 1) % spanCount == 0) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
return;
}
if(orientation == OrientationHelper.HORIZONTAL && (position + 1) % spanCount == 0) {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
return;
}
outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
}
/**
* 绘制竖直间隔线
*
* @param canvas
* @param parent
* 父布局,RecyclerView
* @param position
* irem在父布局中所在的位置
*/
private void drawVerticalDivider(Canvas canvas, RecyclerView parent, int position) {
final View child = parent.getChildAt(position);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getTop() - params.topMargin;
final int bottom = child.getBottom() + params.bottomMargin + mDivider.getIntrinsicHeight();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
/**
* 绘制水平间隔线
*
* @param canvas
* @param parent
* 父布局,RecyclerView
* @param position
* item在父布局中所在的位置
*/
private void drawHorizontalDivider(Canvas canvas, RecyclerView parent, int position) {
final View child = parent.getChildAt(position);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
final int left = child.getLeft() - params.leftMargin;
final int right = child.getRight() + params.rightMargin + mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
}
设置RecyclerView的间隔线。
mRecyclerView.addItemDecoration(new MDGridRvDividerDecoration(this));
运行效果如下图。
![](https://i-blog.csdnimg.cn/blog_migrate/e9688b3ecfbd4066dd70d8104b4ec3e1.webp?x-image-process=image/format,png)
关于网格样式的RecyclerView使用大体和列表样式相同,主要在于间隔线的实现上有些不同,来看一下如果真正的使用自定义的间隔线需要做些什么。
- 实现间隔线样式,可以是xml文件也可以是图片
- 覆盖应用主题的listdivider属性,使用自定义的间隔线样式
- 继承
RecyclerView.ItemDecoration
类,并实现其中的绘制间隔线方法 - 设置RecyclerView间隔线样式
关于第三步,实现绘制线的方法,上面的代码提供了一种大体的思路,可以供大家借鉴,下面就让我们看看期待已久的瀑布流样式的列表。
瀑布流样式
RecyclerView的瀑布流布局管理器是taggeredGridLayoutManager,它最常用的构造函数就一个,StaggeredGridLayoutManager(int spanCount, int orientation),spanCount代表每行或每列的Item个数,orientation代表列表的方向,竖直或者水平。
看在代码中的使用。
// 初始化布局管理器
mLayoutManager = new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL);
// 设置布局管理器
mRecyclerView.setLayoutManager(mLayoutManager);
// 设置adapter
mRecyclerView.setAdapter(mAdapter);
// 设置间隔样式
mRecyclerView.addItemDecoration(new MDStaggeredRvDividerDecotation(this));
要实现瀑布流效果(仅讨论竖直方向的瀑布流样式),每一个Item的高度要有所差别,如果所有的item的高度相同,就和网格样式是一样的展示效果。示例中就实现两中不同高度的Item,一个高度为80dp,一个高度为100dp。
view_rv_staggered_item.xml布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="80dp">
<TextView
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:text="item"/>
</LinearLayout>
view_rv_staggered_item_two.xml布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="100dp">
<TextView
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:text="item"/>
</LinearLayout>
Item不同的布局是在Adapter里面绑定的,看一下Adapter的实现。
public class MDStaggeredRvAdapter extends RecyclerView.Adapter<MDStaggeredRvAdapter.ViewHolder> {
/**
* 展示数据
*/
private ArrayList<String> mData;
public MDStaggeredRvAdapter(ArrayList<String> data) {
this.mData = data;
}
public void updateData(ArrayList<String> data) {
this.mData = data;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
// 瀑布流样式外部设置spanCount为2,在这列设置两个不同的item type,以区分不同的布局
return position % 2;
}
@Override
public MDStaggeredRvAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 实例化展示的view
View v;
if(viewType == 1) {
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_staggered_item, parent, false);
} else {
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_staggered_item_two, parent, false);
}
// 实例化viewholder
ViewHolder viewHolder = new ViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(MDStaggeredRvAdapter.ViewHolder holder, int position) {
// 绑定数据
holder.mTv.setText(mData.get(position));
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView mTv;
public ViewHolder(View itemView) {
super(itemView);
mTv = (TextView) itemView.findViewById(R.id.item_tv);
}
}
}
接下来是设置瀑布流样式的间隔线样式的,上面代码中使用的是MDStaggeredRvDividerDecotation
类,其实是直接拷贝的网格样式的间隔线绘制类。看一下运行效果。
![](https://i-blog.csdnimg.cn/blog_migrate/d16f85f89de069a413d52037898a9b03.webp?x-image-process=image/format,png)
很奇怪,间隔线并没有按照我们想象中的方式绘制,仔细看瀑布流中Item的分布,发现瀑布流样式的Item分布和网格样式的Item分布有些不同。对比一下两者Item的分布,如下图。
![](https://i-blog.csdnimg.cn/blog_migrate/35ef1cd7aac199edfa0abbb35004b95d.webp?x-image-process=image/format,png)
网格样式的Item分布规律很明显,竖直方向的网格,Item是从左向右从上到下依次按顺序排列分布。
瀑布流样式的Item分布也是从上到下,从左到右的顺序排列,但是有一个高度的优先级,如果某一列中有一个高度最低的位置为空,最优先在此处添加Item。看第三张图的3 item
,因为该位置最低,优先在此处添加Item。
分析出了瀑布流样式的Item的分布规律,就会发现,按照以往列表样式或者网格样式去设置间隔线是有问题的,因为不知道Item具体的位置,上下左右间隔线是否需要绘制不确定,参考第二张图,其实第三张图的间隔线也有问题,向上滑动就会展示出来。
目前能考虑到的瀑布流添加间隔线的思路:
- Item布局中设置四周间隔padding/margin
- 代码中动态修改ItemView的间隔padding/margin
设置间隔有两个方法:
- 上下左右都设置间隔
- 相邻两边设置间隔(左上/左下/右上/右下)
第一种设置间隔的方法会导致相邻的Item间距是间隔的两倍,第二种设置间隔的方法会导致Item某一个方向上的与父布局边缘无间隔,但是另一个方向与父布局边缘有间隔,例如左上相邻两边设置了间隔,最左边一列的Item左边与父布局边缘有间隔,但是最右边一列Item右边与父布局无间隔,第一行和最后一行的Item也会出现这种情况。
要解决上面的问题,父布局RecyclerView也需要根据相应的情况设置padding让整个布局的间隔都一致。下面的例子是选择在Item布局中设置间隔,因为可以自己在布局文件中控制颜色比较方便,选择右下两边设置间隔。
首先修改两个Item的布局文件。
view_rv_staggered_item.xml修改背景色和外层间距背景色。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="@dimen/md_common_view_height"
android:background="@color/md_divider"
android:paddingBottom="5dp"
android:paddingRight="5dp">
<TextView
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@color/md_white"
tools:text="item"/>
</LinearLayout>
同样修改view_rv_staggered_item_two.xml。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="100dp"
android:paddingBottom="5dp"
android:paddingRight="5dp"
android:background="@color/md_divider">
<TextView
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@color/md_white"
tools:text="item"/>
</LinearLayout>
最后修改RecyclerView的一些属性。
<android.support.v7.widget.RecyclerView
android:id="@+id/my_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/md_divider"
android:paddingLeft="5dp"
android:paddingTop="5dp"
android:fadeScrollbars="true"/>
运行一下,看看最后的效果。
![](https://i-blog.csdnimg.cn/blog_migrate/087bf9f2ae06cc28b9326e569c9c7463.webp?x-image-process=image/format,png)
差不多完美的解决了间隔线的问题,有细心的同学可能发现,在RecyclerView滑动的时候上面一直有一条灰色的间隔线,这个可以通过取消xml布局文件中RecyclerView的paddingTop属性去掉顶部灰色的间隔线。
总结
- 水平列表展示,设置LayoutManager的方向性
- 竖直列表展示,设置LayoutManager的方向性
- 自定义间隔,RecyclerView.addItemDecoration()
- Item添加和删除动画,RecyclerView.setItemAnimator()
所以在项目中如果再遇见列表类的布局,就可以优先考虑使用RecyclerView,更灵活更快捷的使用方式会给编码带来不一样的体验。如果你以为这些就是RecyclerView相比ListView/GridView优势的话,那就大错特错了,关于RecyclerView还有更多灵活的功能,在后面文章会慢慢介绍。
本篇文章主要介绍网格样式和瀑布流样式的RecyclerView,列表样式、网格样式和瀑布流样式在某种程度上是可以转换的。
- 网格样式的布局管理器的spanCount设置为1,就是列表样式
- 瀑布流样式如果Item的布局文件是等高,竖直方向,就是竖直方向的网格样式;如果Item是等宽,水平方向,那就是水平方向的网络样式
- 如果瀑布流样式的布局管理器spanCount设置为1,竖直方向,是竖直方向的列表;水平方向,就是水平方向的列表
目前为止关于RecyclerView的基本使用的介绍可以告一段落了,但其实关于RecyclerView深入使用可不止着一些,比如说单个Item横滑,拖动Item之间转换位置等等,官方都有提供,当然这些使用会在后面依次介绍。