简单说明
为什么有了ListView还需要RecyclerView?
主要有这几个原因:
- 只支持竖直方向上的列表形状排列,不支持横向、网格(GridView)、瀑布流等其它排列方式,不灵活,适用性不广。
- 在缓存机制不是很好,还有一些优化的空间。
RecyclerView相比于ListView的优缺点:
- 更灵活,适用性更广。
- 更方便添加Item的动画,分割线等
- 支持局部刷新和定向刷新
- 使用起来没有ListView简单
- 不支持Item的点击事件,需要自己处理。
一般使用
RecyclerView的一般使用和ListView在总体上差不多。区别主要有以下几点:
- 必须添加一个布局管理器来声明列表中的Item的排列方式
- 如果需要分割线,可以单独添加,且名字叫Item装饰:ItemDecoration。
- Adapter的创建,相对于ListView更规范化,且稍微复杂一些。
RecyclerView rv = findViewById(R.id.rv_second);
// 设置布局管理器,这里是最简单的竖直线性排列的布局
rv.setLayoutManager(new LinearLayoutManager(this));
// 设置Adapter
rv.setAdapter(adapter);
// 设置分割线
rv.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
Adapter的使用
1. 整体结构
Adatper需要继承RecyclerView.Adapter类,且需要额外增加一个ViewHolder类继承RecyclerView.ViewHolder。然后将自定义的Holder作为Adapter的泛型类型。关于Holder的处理,放到后面再说
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.Holder> {
static class Holder extends RecyclerView.ViewHolder {
}
}
2. 复写方法
Adapter需要复写父类的三个抽象方法,分别如下。
// 直接返回数量,固定式写法
@Override
public int getItemCount() {
return data == null ? 0 : data.size();
}
// 创建ItemView,将ItemView和Holder绑定,当然也要绑定itemView中的控件
onCreateViewHolder()
// 在这里处理数据,将position对应的JavaBean对象中的数据设置进holder.xx控件中
onBindViewHolder()
其实写法和ListView 的Adapter在优化之后的写法是一样的。只是将ListView.Adapter中的getView()方法中的代码分开放到onCreateViewHolder(),Holder类,和onBindViewHolder()方法三部分中去。
ListView.Adapter的getView方法和RecyclerView.Adapter的onCreateViewHolder、onBindViewHolder方法的比较:
ListView的Adapter
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 对convertView和view中的控件的复用
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
holder = new ViewHolder();
holder.iv = convertView.findViewById(R.id.iv_fruit);
holder.tv = convertView.findViewById(R.id.tv_fruit);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// 下面是将数据设置进具体的convertView的控件中
Fruit fruit = data.get(position);
holder.iv.setImageResource(fruit.getDrawableResId());
holder.tv.setText(fruit.getName());
return convertView;
}
RecyclerView的Adapter
/**
* 创建ItemView,将ItemView和Holder绑定,当然也要绑定itemView中的控件
*
* @param parent 就是RecyclerView对象本身
* @param viewType 如果有多种布局,根据这个viewType的值不同,要加载不同的布局
* @return 在onBindViewHolder方法中使用的Holder对象
*/
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
// 这里将view和holder的绑定方式与ListViewAdapter不同,不再是使用Tag的方式了,而是将view作为holder的成员变量
Holder holder = new Holder(view);
holder.iv = view.findViewById(R.id.iv_fruit);
holder.tv = view.findViewById(R.id.tv_fruit);
return holder;
}
/**
* 在这里处理数据,将position对应的JavaBean对象中的数据设置进holder.xx控件中
*
* @param holder 就是或新建,或复用的Holder对象
* @param position item对应的索引
*/
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
Fruit fruit = data.get(position);
holder.iv.setImageResource(fruit.getDrawableResId());
holder.tv.setText(fruit.getName());
}
3. 不要忘记将数据设置进Adapter中
private ArrayList<Fruit> data;
public FruitAdapter(ArrayList<Fruit> data) {
this.data = data;
}
4. ViewHolder
与ListView中的Holder不同,RecyclerViewAdapter中的Holder需要继承RecyclerView.ViewHolder,且因为RecyclerView.ViewHolder只有一个要itemView作为参数的构造方法,所以我们自定义的ViewHolder也要必须添加构造方法。
/**
* 因为父类只有一个需要view参数的构造方法,所以Holder类必须添加一个构造方法,能够调用父类的这个构造方法
*/
static class Holder extends RecyclerView.ViewHolder {
ImageView iv;
TextView tv;
/**
* 构造方法,将itemView与holder对象绑定,并调用父类的有itemView作为参数的构造方法
*
* @param itemView 就是Adapter的onCreateViewHolder方法创建的View
*/
public Holder(@NonNull View itemView) {
super(itemView);
}
}
5. 完整代码如下
/**
* 1. 一般情况下我们同时需要自定义Holder类继承Rv中的ViewHolder,然后将Holder类设置为Adapter中的泛型
* 2. 继承Rv.Adapter类之后,需要复写3个抽象方法
*/
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.Holder> {
private ArrayList<Fruit> data;
public FruitAdapter(ArrayList<Fruit> data) {
this.data = data;
}
/**
* 和ListView一样,也是获取列表需要渲染加载的数据的数量
*/
@Override
public int getItemCount() {
return data == null ? 0 : data.size();
}
/**
* 创建ItemView,将ItemView和Holder绑定,当然也要绑定itemView中的控件
*
* @param parent 就是RecyclerView对象本身
* @param viewType 如果有多种布局,根据这个viewType的值不同,要加载不同的布局
* @return 在onBindViewHolder方法中使用的Holder对象
*/
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
Holder holder = new Holder(view);
holder.iv = view.findViewById(R.id.iv_fruit_first);
holder.tv = view.findViewById(R.id.tv_fruit_first);
return holder;
}
/**
* 在这里处理数据,将position对应的JavaBean对象中的数据设置进holder.xx控件中
*
* @param holder 就是或新建,或复用的Holder对象
* @param position item对应的索引
*/
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
Fruit fruit = data.get(position);
holder.iv.setImageResource(fruit.getDrawableResId());
holder.tv.setText(fruit.getName());
}
/**
* 因为父类只有一个需要view参数的构造方法,所以Holder类必须添加一个构造方法,能够调用父类的这个构造方法
*/
static class Holder extends RecyclerView.ViewHolder {
ImageView iv;
TextView tv;
/**
* 构造方法,将itemView与holder对象绑定,并调用父类的有itemView作为参数的构造方法
*
* @param itemView 就是Adapter的onCreateViewHolder方法创建的View
*/
public Holder(@NonNull View itemView) {
super(itemView);
}
}
}
其它布局:LayoutManager
常用的布局管理器有2种,分别是线性和网格。可以达到普通的列表、横向列表、网格状、瀑布流布局的效果。
LinearLayoutManager
最常用的就是LinearLayoutManager。
下面就是创建一个最普通的类似ListView的布局管理器。
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
rv.setLayoutManager(linearLayoutManager);
LinearLayoutManager还有一个常用的构造方法。
- 参数orientation表示排列方向
- 参数reverseLayout表示是否倒序展示数据
/**
* @param context Current context, will be used to access resources.
* @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
* #VERTICAL}.
* @param reverseLayout When set to true, layouts from end to start.
*/
public LinearLayoutManager(Context context, @RecyclerView.Orientation int orientation,
boolean reverseLayout) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
}
因此我们如果想要显示水平方向列表,直接使用这个构造方法即可。下面的代码,再将itemView的宽度不设置为match_parent,就可以实现水平方向的排列。当然就算itemView的宽度是match_parent,也是水平排列的列表,但是每个item的宽度就会都占用屏幕的宽度了。
LinearLayoutManager linearLayoutManager =
new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
rv.setLayoutManager(linearLayoutManager);
StaggeredGridLayoutManager
如果想要网格状的布局,就可以使用StaggeredGridLayoutManager来完成。
和LinearLayoutManager不同,我们常用的StaggeredGridLayoutManager构造方法是不传Context,而必须指定方向和行列数的构造方法。
/**
* Creates a StaggeredGridLayoutManager with given parameters.
*
* @param spanCount If orientation is vertical, spanCount is number of columns. If
* orientation is horizontal, spanCount is number of rows.
* @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
*/
public StaggeredGridLayoutManager(int spanCount, int orientation) {
mOrientation = orientation;
setSpanCount(spanCount);
mLayoutState = new LayoutState();
createOrientationHelpers();
}
StaggeredGridLayoutManager的使用如下,不过要注意itemView的宽高的设置。
// 创建一个竖直方向排列,一共只有2列的网格状布局管理器
StaggeredGridLayoutManager staggeredGridLayoutManager
= new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
rv.setLayoutManager(staggeredGridLayoutManager);
// 创建一个水平方向排列,一共只有2行的网格状布局管理器
StaggeredGridLayoutManager staggeredGridLayoutManager
= new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.HORIZONTAL);
rv.setLayoutManager(staggeredGridLayoutManager);
瀑布流
瀑布流就是控件的宽度或者高度不等的网格型布局。
瀑布流的实现很简单,就是在StaggeredGridLayoutManager的基础之上,更改ItemView的高度或者宽度即可。只是这里注意,要使用控件的LayoutParams来修改宽高等尺寸属性。
/**
* 将数据设置进itemView中的控件,也就是ViewHolder中的成员变量。
*/
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
// 不能通过view直接设置它的宽高,需要通过一个LayoutParams的成员变量来修改宽高。
ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
// 想办法让itemView的高度在某个范围内变化
layoutParams.height = DensityUtil.dip2px(holder.itemView.getContext(), 60) +
DensityUtil.dip2px(holder.itemView.getContext(), new Random().nextInt(60));
// 不要忘记设置数据
Fruit fruit = data.get(position);
holder.iv.setImageResource(fruit.getDrawableRes());
holder.tv.setText(fruit.getName());
}
dp转px的工具方法也很简单。获取系统的屏幕像素密度,乘以要dp数值,就是像素值。
public class DensityUtil {
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
}
分割线/装饰:ItemDecoration
分割线
一般情况下,我们在RecyclerView中添加分割线的方式是将线画在itemView中,然后根据条件决定线是否显示。
如果想要itemView之间有间距,我们一般也是用在itemView中添加margin的方式完成。
但是如果有比较复杂的的对于itemView分割线、背景样式等的处理的时候,我们就需要使用ItemDecoration来完成了。
ItemDecoration
案例完整代码:
public class FruitDecoration extends RecyclerView.ItemDecoration {
private final Paint mPaint;
public FruitDecoration(Context context) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(context.getResources().getColor(R.color.purple_200));
}
/**
* 可以实现类似于Padding的效果。就是控制各个Item之间的间距等。
*
* @param outRect 就是ItemView的四周的边距。就是系统会根据outRect的值来扩展item的区域。
* @param view 当前ItemView
* @param parent 就是RecyclerView
* @param state 存储一些RecyclerView的状态等,用的不多
*/
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
/**
* 这里有两个获取position的方法。他们一般情况下没有区别。
* 仅在RecyclerView刷新布局后,layoutPosition会晚adapterPosition大约16ms。
* 因为绘制完成之后,layoutPosition才有正确的值,所以我们一般用adapterPosition就好。
*
* 只有在使用findViewHolderForLayoutPosition获取当前item的ViewHolder时,用layoutPosition才更好,因为此时layoutPosition 和用户在屏幕上看到的一定是一样的
*/
int position = parent.getChildAdapterPosition(view);
int layoutPosition = parent.getChildLayoutPosition(view);
/**
* 这里注意,下面两个count的值的不同。
* viewCount是当前RecyclerView中的itemView的数量。并不是数据的数量,Adapter中的getItemCount才是
* 因为RecyclerView不会一次性创建所以的itemView,而是会进行view的复用。
*/
int viewCount = parent.getChildCount();
int childCount = parent.getAdapter().getItemCount();
if (position == 0) {
outRect.bottom = 20;
} else if (position == childCount - 1) {
outRect.top = 20;
} else {
outRect.top = 20;
outRect.bottom = 20;
}
}
/**
* 绘制ItemView的背景。意思就是这里画出来的图像,会显示在itemView的下面。这个方法会在绘制itemView之前调用.
* 这里的绘制区域是根据上面的getItemOffsets决定的。
* 注意这里的canvas指的是RecyclerView的布局部分,而不是itemView的界面。如果想在每一个ItemView的相同位置画图案,需要计算对应的坐标。
*
* @param c 绘画的布
* @param parent 就是RecyclerView对象本身
* @param state state本身
*/
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
}
/**
* 绘制ItemView的前景。意思就是这里画出来的图像,会显示在itemView的上面
*
* @param c 绘画的布
* @param parent 就是RecyclerView对象本身
* @param state state本身
*/
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int radius = child.getHeight() / 2;
int centerX = child.getRight() - 20 - radius;
int centerY = child.getTop() + child.getHeight() / 2;
c.drawCircle(centerX, centerY, radius, mPaint);
}
}
}
动画:ItemAnimator
点击事件
主要的实现方式还是在Adapter的onBindViewHolder中设置点击事件。
多种布局
对应的Layout布局文件
getItemViewType
onCreateViewHolder
Holder类
onBindViewHolder
添加头尾
下拉刷新上拉更多
好用的第三方框架
BRVAH(BaseRecyclerViewAdapterHelper)(RecyclerView使用框架):http://www.recyclerview.org/
SmartRefrshLayout(下拉刷新框架):https://gitee.com/scwang90/SmartRefreshLayout
参考资料
Android RecyclerView 使用完全解析 体验艺术般的控件
Android 优雅的为RecyclerView添加HeaderView和FooterView