在Android开发中,我们经常与ListView、GridView打交道,它们为数据提供了列表和视图的展示方式,方便用户的操作。随着Android的不断发展,单一的listview逐渐满足不了需求多变的项目。谷歌在support v7中加入了新的控件——RecyclerView,该控件整合了ListView、GridView的特点,而且最大的优点是可以很方便实现瀑布流效果。
RecyclerView有几个它常用的内部类,很重要,贯穿整个使用过程:
1)、RecyclerView.Adapter:抽象类,为RecyclerView提供数据,一般根据不同的业务需求来编写具体的实现类。
2)、RecyclerView.LayoutManager:抽象类,主要用于测量RecyclerView的子Item,以及根据不同的布局方式来实现Item的布局效果,v包自带的实现类有:LinearLayoutManager、、GridLayoutManager StaggeredGridLayoutManager。
3)、RecyclerView.ItemDecoration:抽象类,这个主要用于不同的Item之间添加分割线(可选)。我们可以自定义抽象类实现自己的分割线效果。
4)、RecyclerView.ItemAnimator:抽象类,这个主要用于当一个item添加或者删除的时候出现的动画效果,官方提供一个默认的实现类。如果想要使我们的RecyclerView在添加、删除数据的时候有炫酷的动画,可以实现这个抽象类。
1、使用方法
1)、在build.gradle中添加依赖
compile 'com.android.support:recyclerview-v7:25+'
2)、布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
item布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="item1"
android:gravity="center"
android:background="@color/colorAccent"/>
</LinearLayout>
3)、创建Adpater
通过继承RecyclerView.Adapter实现自己的适配器。RecyclerView.Adapter如下:
public static abstract class Adapter<VH extends ViewHolder> {
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
public abstract void onBindViewHolder(VH holder, int position);
public abstract int getItemCount();
}
继承该类的时候,必须重写这三个方法,我们分别解释一下这三个方法是什么作用:
a、onCreateViewHolder:创建ViewHolder,该方法会在RecyclerView需要展示一个item的时候回调。重写该方法时,应该使ViewHolder加载item view的布局。这个可以避免了不必要的findViewById操作,提高了性能。和ListView的ViewHolder操作类似。
b、onBindeViewHolder:该方法在RecyclerView在特定位置展示数据时候回调,把数据绑定填充到相应的item view中。
c、getItemCount:返回数据的数量。
Adapter如下:
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
private List<String> datas;
public RecyclerViewAdapter(List<String> datas) {
this.datas = datas;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 使用 inflate(R.layout.item_recyler_view, null); RecyclerView子View宽度不能全屏,是wrap_content效果
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recyler_view, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.textView.setText(datas.get(position));
}
@Override
public int getItemCount() {
return datas.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public ViewHolder(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.tv_content);
}
}
}
4)、Activity中代码处理,需要完全以下3点才可以正常显示
a、获取RecyclerView实例,通过findViewById()方法。
b、为RecyclerView设置布局管理器
c、为RecyclerView设置适配器Adapter。
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new RecyclerViewAdapter(datas);
recyclerView.setAdapter(adapter);
2、Item之间的分割线
上图中RecyclerView每个item之间没有分割线。RecycleView没有像ListView那样可以直接在xml属性中添加android:divider。 我们可以实现RecyclerView.ItemDecoration抽象类添加分割线。RecyclerView.ItemDecoration如下:
public static abstract class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), parent);
}
}
onDraw和onDrawOver这两个方法是用于绘制的,onDraw是在item view绘制之前调用,而onDrawOver是在item view绘制之后调用。因此我们一般选择重写其中一个方法即可。getItemOffset告诉RecyclerView在绘制完一个item view的时候,应该留下多少空位用于绘制分割线。
下面是实现类,和RecyclerView.ItemDecoration是一样,这里只是为了方便打印日志才重写的。设置RecyclerView的divider时我们可以不用自定义,直接使用系统自带的RecyclerView.ItemDecoration。
public class DividerItemDecoration 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 DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
@Override
public void onDraw(Canvas c, RecyclerView parent) {
Log.v("recyclerview - itemdecoration", "onDraw()");
if (mOrientation == VERTICAL_LIST) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
public 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);
android.support.v7.widget.RecyclerView v = new android.support.v7.widget.RecyclerView(parent.getContext());
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public 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;
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
该实现类可以看到通过读取系统主题中的 android.R.attr.listDivider作为Item间的分割线,并且支持横向和纵向。获取到listDivider以后,该属性的值是个Drawable,在getItemOffsets中,outRect去设置了绘制的范围。onDraw中实现了真正的绘制。
在Activity中添加如下代码:
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new RecyclerViewAdapter(datas);
recyclerView.setAdapter(adapter);
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
如何更改分割线的样式呢?该分割线是系统默认的android.R.attr.listDivider,你可以在theme.xml中找到该属性的使用情况。可以在style.xml中指定我们的divider,如下:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="colorControlNormal">@color/grey</item>
<item name="colorControlActivated">@color/blue_black</item>
<item name="android:listDivider">@drawable/shape_divider</item> // 定义RecyclerView的divider
</style>
在res->drawable中新建shape:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size
android:height="5dp"/>
<solid
android:color="@color/blue_dark"/>
</shape>
效果如下:
3、更改布局管理器
官方提供了三种布局方式,LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager。LinearLayoutManager是线性布局管理器,使得item呈竖直排列或者水平排列。GridLayoutManager是表格形式的布局,类似于GridView。StaggeredGridLayoutManager是瀑布流表格布局。
1)、LinearLayoutManager
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
第2个参数表示是水平滑动或者是竖直方向滑动,第3个参数表示是否从数据的尾部开始显示。
效果就是上面图片。
2)、GridLayoutManager
recyclerView.setLayoutManager(new GridLayoutManager(this, 4, GridLayoutManager.VERTICAL, false));
第2个参数表示表示表格的行数或者列数,第3个参数表示是水平滑动或者是竖直方向滑动,第4个参数表示是否从数据的尾部开始显示。
效果如下:
3)、StaggeredGridLayoutManager
瀑布流效果
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(4, StaggeredGridLayoutManager.VERTICAL));
第一个参数表示列数或者行数,第二个参数表示滑动方向。这些改动是不足够的,达不到瀑布流的效果。因为每个Item view的高度都是一样的。因此要在Adapter中修改每一个Item View的Height。
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.textView.setText(datas.get(position));
ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
layoutParams.height = (int)(Math.random() * 300 + 100);
holder.itemView.setLayoutParams(layoutParams);
}
效果如下:
4、监听Item的点击事件
系统没有提供ClickListener和LongClickListener,需要我们自己去实现点击事件。实现点击事件有两种方式:
方法一:利用View.onClickListener及onLongClickListener,
方法二:利用RecyclerView.OnItemTouchListener。
方法一比较简单,我们暂时实现方法一。
1)定义点击事件监听接口(Adapter中)
public interface OnItemClickListener{
void onClick(View view, int position);
void onLongClick(View view, int position);
}
2)将ItemView的点击事件传递到OnItemClickListener(Adapter中)
public void onBindViewHolder(ViewHolder holder, final int position) {
holder.textView.setText(datas.get(position));
ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
layoutParams.height = (int)(Math.random() * 300 + 100);
holder.itemView.setLayoutParams(layoutParams);
/**
* 为item设置点击监听
*/
if(onItemClickListener != null) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onItemClickListener.onClick(v, position);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
onItemClickListener.onLongClick(v, position);
return true;
}
});
}
}
注意上面回调函数返回的position是不正确的。如果使用onBindViewHolder()参数中的position,删除了数据position还是按照删除前的顺序返回的。应该使用如下方法获取position:
int realPosition = holder.getAdapterPosition();
onItemClickListener.onClick(v, realPosition);
3)回调点击监听(Activity中)
adapter = new RecyclerViewAdapter(datas);
recyclerView.setAdapter(adapter);
adapter.setOnItemClickListener(new RecyclerViewAdapter.OnItemClickListener() {
@Override
public void onClick(View view, int position) {
Toast.makeText(RecyclerViewTestActivity.this, "onClick--view:" + view.toString() + ", position:" + position, Toast.LENGTH_LONG).show();
}
@Override
public void onLongClick(View view, int position) {
Toast.makeText(RecyclerViewTestActivity.this, "onLongClick--view:" + view.toString() + ", position:" + position, Toast.LENGTH_LONG).show();
}
});
5、Adapter中数据的增加、删除、更改
RecyclerView.Adapter中提供的四个操作数据的方法,如下:
final void notifyItemInserted(int position)该方法用于增加一个数据的时候,position表示新增数据显示的位置
final void notifyItemRemoved(int position) 该方法用于删除一个数据的时候,position表示数据删除的位置
final void notifyItemChanged(int position) 该方法表示所在position对应的item位置不会改变,但是该item内容发生变化
final void notifyDataSetChanged() 该方法一般用于:适配器之前装载的数据大部分已经过时了,需要重新更新数据。调用该方法的时候,recyclerView会重新计算子item及所有子item重新布局,出于效率考虑,官方建议用更加精确的方法(比如上面三个方法)来取代这个方法
1)、在Adapter中封装操作数据的方法
public void addData(int position, String data){
datas.add(position, data);
notifyItemInserted(position);
}
public void removeData(int position){
datas.remove(position);
notifyItemRemoved(position);
}
public void updateData(int position, String data){
datas.set(position, data);
notifyItemChanged(position);
}
2)在Activity中调用上面的方法
6、添加、删除、更改数据时动画
RecyclerView默认添加了动画DefaultItemAnimator。我们也可以通过下面代码设置动画:
recyclerView.setItemAnimator(new DefaultItemAnimator());
也可以自定义RecyclerView.ItemAnimator实现动画效果。
7、打造通用Adapter
通用Adapter的3个问题。数据怎么办?布局怎么办?绑定怎么办?数据采用泛型通过构造函数传递,布局使用构造函数传递,绑定数据使用抽象类子类实现。
public abstract class CommonRecyclerViewAdapter<T> extends RecyclerView.Adapter<CommonRecyclerViewAdapter.CommonViewHolder> {
private static final String TAG = CommonRecyclerViewAdapter.class.getSimpleName();
private Context mContext;
private int mLayoutId;
private List<T> mDatas; // ###数据使用泛型
private OnItemClickListener mOnItemClickListener;
public CommonRecyclerViewAdapter(Context context, int layoutId, List<T> datas){
mContext = context;
mLayoutId = layoutId; // ###布局怎么办?布局使用构造函数传递
mDatas = datas; // ###数据怎么办?数据使用构造函数传递
}
/**
* 子类实现bind数据
* @param holder 点击item对应的ViewHolder
* @param position 点击item对应的position
*/
public abstract void bind(CommonViewHolder holder, int position); // ### bind怎么办?使用抽象类并由子类实现
@Override
public CommonViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(mLayoutId, parent,false);
CommonViewHolder commonViewHolder = new CommonViewHolder(view);
Log.i(TAG, "onCreateViewHolder--holder:" + commonViewHolder.toString());
return commonViewHolder;
}
@Override
public void onBindViewHolder(final CommonRecyclerViewAdapter.CommonViewHolder holder, int position) {
Log.i(TAG, "onBindViewHolder--holder:" + holder.toString());
bind(holder, position);
// 设置点击监听
if(mOnItemClickListener != null){
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mOnItemClickListener.onClick(v, holder.getLayoutPosition());
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
mOnItemClickListener.onLongClick(v, holder.getLayoutPosition());
return true;
}
});
}
}
public class CommonViewHolder extends RecyclerView.ViewHolder{
// 用来存放子View减少findViewById的次数
private SparseArray<View> mViews;
public CommonViewHolder(View itemView) {
super(itemView);
mViews = new SparseArray<>();
}
public View getView(int id){
View view = mViews.get(id);
if(view == null) {
Log.i(TAG, "getView2:" + id);
view = itemView.findViewById(id);
mViews.put(id, view);
}else{
Log.i(TAG, "getView1:" + id);
}
return view;
}
}