RecycleView
RecyclerView是谷歌V7包下新增的控件,用来替代ListView的使用,它提供了一种插拔式的体验,高度的解耦,异常的灵活
控制其显示的方式,请通过布局管理器LayoutManager
控制Item间的间隔(可绘制),请通过ItemDecoration
控制Item增删的动画,请通过ItemAnimator
控制点击、长按事件需要自己实现
首先要用这个控件,你需要在gradle文件中添加包的引用(和V7包版本必须一致):
compile 'com.android.support:appcompat-v7:25.3.0'
compile 'com.android.support:recyclerview-v7:25.3.0'
然后是在布局文件用使用它:
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recycler_view"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"/>
目前都使用AndroidX包下的RecyclerView,需要添加如下依赖:
implementation 'androidx.recyclerview:recyclerview:1.1.0'
然后在布局文件中使用它:
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"/>
基本使用
由于我们很熟悉ListView,下面对照ListView来看下RecycleView的使用:
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
// 设置布局管理器
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 设置适配器
mRecyclerView.setAdapter(mAdapter);
// 设置Item动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
// 添加分割线
mRecyclerView.addItemDecoration(new DividerItemDecoration(getApplicationContext(), DividerItemDecoration.HORIZONTAL));
可以看出,RecycleView只负责回收和复用View条目,其他的比如分割线和动画都已经由专门的类去实现,以此达到高度解耦。下面我们来看看RecycleView的基本实现:
1、首先实现页面布局文件和Item条目的布局文件
// activity_main布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.legend.recycle.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_recycleview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
// item布局文件
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="#44ff0000"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/tv_item_content"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="1" />
</FrameLayout>
2、接下来是Activity的代码实现:
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
private List<String> mDatas;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView) findViewById(R.id.rv_recycleview);
initData();
// 设置布局管理器
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 设置适配器
mRecyclerView.setAdapter(new RecycleAdapter());
}
protected void initData() {
mDatas = new ArrayList<String>();
for (int i = 'A'; i < 'z'; i++)
{
mDatas.add("" + (char) i);
}
}
}
3、最后创建适配器,代码如下所示:
// 创建适配器
public class RecycleAdapter extends RecyclerView.Adapter {
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(MainActivity.this).inflate(
R.layout.item_recycleview, parent, false);
MyViewHold hold = new MyViewHold(view);
return hold;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((MyViewHold)holder).mTvContent.setText(mDatas.get(position));
}
@Override
public int getItemCount() {
return mDatas.size();
}
class MyViewHold extends RecyclerView.ViewHolder {
TextView mTvContent;
public MyViewHold(View itemView) {
super(itemView);
mTvContent = (TextView) itemView.findViewById(R.id.tv_item_content);
}
}
}
显示效果如下:
ItemDecoration
我们可以通过addItemDecoration()
给RecycleView添加分割线,该方法的参数为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);
}
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
当我们调用mRecyclerView.addItemDecoration()
方法添加decoration
时,RecyclerView会去绘制decorato
,即调用该类的onDraw
和onDrawOver
方法,
onDraw方法先于drawChildren
onDrawOver在drawChildren之后,一般我们选择复写其中一个即可。
getItemOffsets 可以通过outRect.set()为每个Item设置一定的偏移量,主要用于绘制Decorator。
接下来我们看下如何给RecycleView添加Item间隔:
1、 首先创建类继承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 int mOrientation;
private final Drawable mDrawable;
public DividerItemDecoration(Context context, int oritation) {
TypedArray array = context.obtainStyledAttributes(ATTRS);
mDrawable = array.getDrawable(0);
array.recycle();
setOrientation(oritation);
}
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, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST)
drwaVertical(c, parent);
else
drwaHorizontal(c, parent);
}
private void drwaVertical(Canvas c, RecyclerView parent) {
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int top = 0;
int bottom = 0;
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
top = child.getBottom() + params.bottomMargin;
bottom = top + mDrawable.getIntrinsicHeight();
mDrawable.setBounds(left, top, right, bottom);
mDrawable.draw(c);
}
}
private void drwaHorizontal(Canvas c, RecyclerView parent) {
int left = 0;
int right = 0;
int top = parent.getPaddingTop();
int bottom = parent.getHeight() - parent.getPaddingBottom();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
left = child.getRight() + params.rightMargin;
right = left + mDrawable.getIntrinsicHeight();
mDrawable.setBounds(left, top, right, bottom);
mDrawable.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
if (mOrientation == VERTICAL_LIST)
outRect.set(0, 0, 0, mDrawable.getIntrinsicHeight());
else
outRect.set(0, 0, mDrawable.getIntrinsicWidth(), 0);
}
}
2、接下来只要修改Activity中的添加分割线的代码即可:
mRecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
运行效果如下所示:
该分割线是系统默认的,你可以在theme.xml中找到该属性的使用情况。使用系统的listDivider
的好处就是方便我们去随意的改变,该属性我们可以直接声明在:
<!-- Application theme. -->
<style name="AppTheme" parent="AppBaseTheme">
<item name="android:listDivider">@drawable/divider_bg</item>
</style>
然后自己编写一个Drawable即可,这样可以通过Drawable给分割线设置各种不同的效果:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<gradient
android:centerColor="#ff00ff00"
android:endColor="#ff0000ff"
android:startColor="#ffff0000"
android:type="linear" />
<size android:height="4dp"/>
</shape>
运行显示效果如下:
LayoutManager
RecyclerView.LayoutManager一个抽象类,其中系统提供了3个实现类:
LinearLayoutManager 现行管理器,支持横向、纵向。
GridLayoutManager 网格布局管理器
StaggeredGridLayoutManager 瀑布就式布局管理器
LinerLayoutManager上面的教程已经进行叙述过,接下来介绍网格布局管理器和瀑布式布局管理器。
GridLayoutManager
修改上方代码的LayoutManager为GridLayoutManager
即可
// 设置布局管理器
mRecyclerView.setLayoutManager(new GridLayoutManager(this, 4));
修改为GridLayoutManager以后,分割线也需要修改为适合网格的分割线:
public class DividerGridItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[] { android.R.attr.listDivider };
private Drawable mDivider;
public DividerGridItemDecoration(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) {
drawHorizontal(c, parent);
drawVertical(c, parent);
}
private int getSpanCount(RecyclerView parent) {
// 列数
int spanCount = -1;
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
spanCount = ((StaggeredGridLayoutManager) layoutManager)
.getSpanCount();
}
return spanCount;
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
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.getLeft() - params.leftMargin;
final int right = child.getRight() + params.rightMargin
+ mDivider.getIntrinsicWidth();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public void drawVertical(Canvas c, RecyclerView parent) {
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.getTop() - params.topMargin;
final int bottom = child.getBottom() + params.bottomMargin;
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
private boolean isLastColum(RecyclerView parent, int pos, int spanCount, int childCount) {
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
// 如果是最后一列,则不需要绘制右边
if ((pos + 1) % spanCount == 0){
return true;
}
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int orientation = ((StaggeredGridLayoutManager) layoutManager)
.getOrientation();
if (orientation == StaggeredGridLayoutManager.VERTICAL) {
// 如果是最后一列,则不需要绘制右边
if ((pos + 1) % spanCount == 0) {
return true;
}
} else {
childCount = childCount - childCount % spanCount;
if (pos >= childCount)// 如果是最后一列,则不需要绘制右边
return true;
}
}
return false;
}
private boolean isLastRaw(RecyclerView parent, int pos, int spanCount, int childCount) {
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
childCount = childCount - childCount % spanCount;
if (pos >= childCount)// 如果是最后一行,则不需要绘制底部
return true;
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int orientation = ((StaggeredGridLayoutManager) layoutManager)
.getOrientation();
// StaggeredGridLayoutManager 且纵向滚动
if (orientation == StaggeredGridLayoutManager.VERTICAL) {
childCount = childCount - childCount % spanCount;
// 如果是最后一行,则不需要绘制底部
if (pos >= childCount)
return true;
} else { // StaggeredGridLayoutManager 且横向滚动
// 如果是最后一行,则不需要绘制底部
if ((pos + 1) % spanCount == 0) {
return true;
}
}
}
return false;
}
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
int spanCount = getSpanCount(parent);
int childCount = parent.getAdapter().getItemCount();
// 如果是最后一行,则不需要绘制底部
if (isLastRaw(parent, itemPosition, spanCount, childCount)) {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
// 如果是最后一列,则不需要绘制右边
} else if (isLastColum(parent, itemPosition, spanCount, childCount)) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(),
mDivider.getIntrinsicHeight());
}
}
}
主要在getItemOffsets
方法中,去判断如果是最后一行,则不需要绘制底部;如果是最后一列,则不需要绘制右边,整个判断也考虑到了StaggeredGridLayoutManager
的横向和纵向,如果仅仅希望有空隙,设置item的margin最方便。
改变指定位置列尺寸
如果想改变指定位置的列尺寸,可以使用如下方式:
假设希望第1个item,单独占据一行的全部空间。我们可以使用setSpanSizeLookup
方法实现这一需求:
GridLayoutManager layoutManager = new GridLayoutManager(this, 4);
mRecyclerView.setLayoutManager(layoutManager);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position == 0){
return 4;
}
return 1;
}
});
这样第一行单独占据整行空间,而其他列还是以四个网格铺满的方式呈现
StaggereGridLayoutManage
瀑布流式的布局,其实他可以实现GridLayoutManager
一样的功能,仅仅按照下列代码:
// 设置布局管理器
mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(4, StaggeredGridLayoutManager.VERTICAL));
StaggeredGridLayoutManager构造的第二个参数传一个orientation,如果传入的是StaggeredGridLayoutManager.VERTICAL
代表有多少列;
那么传入的如果是StaggeredGridLayoutManager.HORIZONTAL
就代表有多少行。
如果想实现瀑布流效果,只需要在适配器中onBindViewHolder
方法设置item随机高度即可:
@Override
public void onBindViewHolder(MyViewHold holder, int position) {
// 设置随机高度
int randomInt = (int)(300+Math.random()*400);
ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
layoutParams.height = randomInt;
holder.itemView.setLayoutParams(layoutParams);
// 设置显示
holder.mContent.setText(mListData.get(position));
}
ItemAnimator
ItemAnimator也是一个抽象类,系统提供了默认的实现类:
// 设置item动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
使用动画后,更新数据集使用 notifyItemInserted(position)
与notifyItemRemoved(position)
,否则没有动画效果。
点击事件处理
RecycleView中系统没有提供ClickListener和LongClickListener,需要我们自己在适配器中去实现。
public class LinearRecycleAdapter extends RecyclerView.Adapter<LinearRecycleAdapter.MyViewHold> {
private Context mContext;
private List<String> mListData;
private OnItemClickLitener mOnItemClickLitener;
public LinearRecycleAdapter(Context context, List<String> listData) {
this.mContext = context;
this.mListData = listData;
}
public interface OnItemClickLitener {
void onItemClick(View view, int position);
void onItemLongClick(View view , int position);
}
@Override
public MyViewHold onCreateViewHolder(ViewGroup parent, int viewType) {
MyViewHold viewHold = new MyViewHold(LayoutInflater.from(mContext)
.inflate(R.layout.item_recycle_single, parent, false));
return viewHold;
}
@Override
public void onBindViewHolder(final MyViewHold holder, int position) {
holder.mContent.setText(mListData.get(position));
// 如果设置了回调,则设置点击事件
if (mOnItemClickLitener != null) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int pos = holder.getLayoutPosition();
mOnItemClickLitener.onItemClick(holder.itemView, pos);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
int pos = holder.getLayoutPosition();
mOnItemClickLitener.onItemLongClick(holder.itemView, pos);
return false;
}
});
}
}
@Override
public int getItemCount() {
return mListData.size();
}
static class MyViewHold extends RecyclerView.ViewHolder {
private TextView mContent;
public MyViewHold(View itemView) {
super(itemView);
mContent = itemView.findViewById(R.id.tv_item_content);
}
}
// 条目点击事件
public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener) {
this.mOnItemClickLitener = mOnItemClickLitener;
}
}
多样式布局
RecyclerView多样式布局具体流程如下:
1、编写基本的Bean
public class StyleItem {
private int type;
private String content;
private int resId;
public StyleItem(int type, String content, int resId) {
this.type = type;
this.content = content;
this.resId = resId;
}
......
}
2、编写适配器
public class MultiStyleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context mContext;
public static final int TYPE_ONE = 0;
public static final int TYPE_TWO = 1;
private List<StyleItem> mStyleItems;
public MultiStyleAdapter(Context context, List<StyleItem> styleItems) {
this.mContext = context;
this.mStyleItems = styleItems;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_ONE) {
View view = LayoutInflater.from(mContext).inflate(R.layout.rectycle_item_style1, parent, false);
ViewHolderStyle1 viewHolderHead = new ViewHolderStyle1(view);
return viewHolderHead;
} else {
View view = LayoutInflater.from(mContext).inflate(R.layout.rectycle_item_style2, parent,false);
ViewHolderStyle2 viewHolderHead = new ViewHolderStyle2(view);
return viewHolderHead;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (mStyleItems == null && mStyleItems.size() < 0)
return;
StyleItem styleItem = mStyleItems.get(position);
int type = styleItem.getType();
if (type == TYPE_ONE && holder instanceof ViewHolderStyle1){
((ViewHolderStyle1) holder).tvContent.setText(styleItem.getContent());
}else if (type == TYPE_TWO && holder instanceof ViewHolderStyle2){
((ViewHolderStyle2) holder).ivBg.setImageResource(styleItem.getResId());
}
}
@Override
public int getItemCount() {
if (mStyleItems != null && mStyleItems.size() > 0)
return mStyleItems.size();
return 0;
}
@Override
public int getItemViewType(int position) {
StyleItem styleItem = mStyleItems.get(position);
return styleItem.getType();
}
class ViewHolderStyle1 extends RecyclerView.ViewHolder {
TextView tvContent;
public ViewHolderStyle1(View itemView) {
super(itemView);
tvContent = itemView.findViewById(R.id.tv_content);
}
}
class ViewHolderStyle2 extends RecyclerView.ViewHolder {
ImageView ivBg;
private ViewHolderStyle2(View itemView) {
super(itemView);
ivBg = itemView.findViewById(R.id.iv_icon);
}
}
}
其中使用到的布局文件如下:
rectycle_item_style1.xml
<?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="wrap_content"
android:background="@color/purple_200"
android:gravity="center"
android:padding="10dp"
android:text="Style1" />
</LinearLayout>
rectycle_item_style2.xml
<?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">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center"
android:scaleType="fitXY"
android:src="@drawable/bg" />
</LinearLayout>
3、在Activity中的使用方式如下:
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = findViewById(R.id.recyclerview);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(RecyclerView.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
List<StyleItem> styleItems = new ArrayList<>();
for (int i = 0; i < 10; i++){
int type = i % 2 == 0 ? MultiStyleAdapter.TYPE_ONE : MultiStyleAdapter.TYPE_TWO;
StyleItem styleItem = new StyleItem(type, "Style" + i, R.drawable.bg);
styleItems.add(styleItem);
}
MultiStyleAdapter adapter = new MultiStyleAdapter(this, styleItems);
mRecyclerView.setAdapter(adapter);
}
}
复用错位
RecyclerView在展示大量的控件和内容的时候,会采取复用的核心机制,所以会导致数据错乱相关的Bug。
RecyclerView在加载过程中会出现数据来源数同步或异步的情况,这里会分两种情况介绍:
数据同步情况
当数据来源数同步的情况下,这种处理方式会比较简单,我们只需要保证在OnBindViewHolder
方法调用的时候,ItemView中每个View的状态都有默认值即可。
假设我们实现RecyclerView中放入CheckBox的需求,就可以使用如下方式来保证CheckBox的选中状态不被复用而导致错乱。
1、在Activity的布局文件中使用RecyclerView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"/>
</LinearLayout>
2、编写CheckItem.java对象,需要注意的是需要加入select值来保存当前 Item的状态:
public class CheckItem {
private String content;
private boolean select;
public CheckItem(String content, boolean select) {
this.content = content;
this.select = select;
}
......
}
3、编写RecyclerView的适配器代码如下所示:
public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.MyViewHold> {
private Context mContext;
private List<CheckItem> mCheckItems;
public CustomAdapter(Context context, List<CheckItem> checkItems) {
this.mContext = context;
this.mCheckItems = checkItems;
}
@Override
public MyViewHold onCreateViewHolder(ViewGroup parent, int viewType) {
View layout = View.inflate(mContext, R.layout.item_recycler_content, null);
MyViewHold viewHold = new MyViewHold(layout);
return viewHold;
}
@Override
public void onBindViewHolder(final MyViewHold holder, int position) {
final CheckItem checkItem = mCheckItems.get(position);
holder.mTvContent.setText(checkItem.getContent());
Log.e("CustomAdapter", checkItem.isSelect() + "|" + position);
if (checkItem.isSelect()){
holder.mCheckBox.setChecked(true);
}else {
holder.mCheckBox.setChecked(false);
}
// Event
holder.mCheckBox.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean checked = holder.mCheckBox.isChecked();
checkItem.setSelect(checked);
}
});
}
@Override
public int getItemCount() {
return mCheckItems.size();
}
class MyViewHold extends RecyclerView.ViewHolder {
TextView mTvContent;
CheckBox mCheckBox;
public MyViewHold(View itemView) {
super(itemView);
mTvContent = itemView.findViewById(R.id.tv_recycler_item_content);
mCheckBox = itemView.findViewById(R.id.cb_recycler_item_check);
}
}
}
每次CheckBox被选中的时候都会在Item中保存CheckBox的选中状态,所以即使ItemView被复用的情况下都不会丢失CheckBox的状态,直接会在'onBindViewHolder'中还原选中状态。
其中使用到的Item布局文件item_recycler_content.xml
如下:
<?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="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="30dp"
android:padding="5dp">
<TextView
android:id="@+id/tv_recycler_item_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:gravity="center"
android:textSize="17sp"
android:text="0000"/>
<CheckBox
android:id="@+id/cb_recycler_item_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"/>
</RelativeLayout>
</LinearLayout>
4、最后在Activity中的代码如下:
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = findViewById(R.id.recyclerView);
List<CheckItem> checkItems = new ArrayList<>();
for (int i = 0; i < 100; i++){
CheckItem checkItem = new CheckItem("这是选项" + i, false);
checkItems.add(checkItem);
}
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
CustomAdapter adapter = new CustomAdapter(this, checkItems);
mRecyclerView.setAdapter(adapter);
}
}
数据异步情况
在RecyclerView的使用过程中,异步加载图片的场景是非常常见的,由于复用机制会导致图片的加载出现错乱的情况,出现该问题的原因如下:
当我为第一个ImageView请求图片的时候,手指向下滑动来屏幕到某个位置,此时第一个ItemView已不可见,这个时候某个位置复用第一个位置的ImageView,刚好此时图片加载完成返回,所以就导致加载的图片展示在某个位置上,就引起来图片错乱的问题。
我们要解决这个问题,主要有三种常用的解决方案:
第一种方式:使用Glide异步加载图片的时候,使用占位图的方式解决。(Glide其实也是使用绑定Tag的方式)
第二种方式:为每个ImageView绑定TAG的方式,将ImageView和图片的URL地址进行绑定,在设置图片的时候判断URL是否一致来处理错乱问题。
第三种方式:在ItemView不可见被复用的时候,如果图片还没有加载完成,则取消该图片的请求。
设置占位图
使用Glide设置占位图有两种方式,直接链式调用'placeholder'方法,或者为Glide添加监听器,在回调方法中设置占位图。
1、第一种设置占位图的方法:
Glide.with(mContext)
.load(imageItem.getUrl())
.placeholder(R.mipmap.ic_launcher)
.into(holder.imageView);
2、第二种设置占位图的方法:
Glide.with(mContext)
.load(imageItem.getUrl())
.error(R.mipmap.ic_launcher)
.into(new SimpleTarget<Drawable>() {
@Override
public void onResourceReady(Drawable resource, Transition<? super Drawable> transition) {
holder.imageView.setImageDrawable(resource);
}
@Override
public void onStart() {
super.onStart();
holder.imageView.setImageResource(R.mipmap.ic_launcher);
}
});
由于'SimpleTarget'回调已经过时,在新版的Glide中可以通过listener
或addListener
的方式添加监听器。
Glide.with(mContext)
.load(imageItem.getUrl())
.addListener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
holder.imageView.setImageResource(R.mipmap.ic_launcher);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
holder.imageView.setImageDrawable(resource);
return false;
}
}).into(holder.imageView);
绑定TAG
使用绑定TAG的方式来处理复用错乱问题的时候,如果图片加载使用Glid的话需要使用setTag(key,value)
方式进行设置,因为Glide内部就是通过setTag()
的方式设置的。
@Override
public void onBindViewHolder(final MyViewHold holder, final int position) {
final ImageItem imageItem = mImageUrls.get(position);
holder.imageView.setTag(R.id.glide_custom_view_target_tag, position);
Glide.with(mContext)
.load(imageItem.getUrl())
.error(R.mipmap.ic_launcher)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
holder.imageView.setImageResource(R.mipmap.ic_launcher);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
if (position == holder.imageView.getTag(R.id.glide_custom_view_target_tag)){
holder.imageView.setImageDrawable(resource);
}
return false;
}
}).into(holder.imageView);
}
取消请求
在onBindViewHolder中发起加载请求,然后在view被回收时取消网络请求,所以我们需要实现RecyclerView的Adapter中的onViewRecycled()
方法,然后在该方法中取消请求即可。
@Override
public void onBindViewHolder(final MyViewHold holder, final int position) {
final ImageItem imageItem = mImageUrls.get(position);
holder.imageView.setTag(imageItem.getKey(), position);
Glide.with(mContext)
.load(imageItem.getUrl())
.placeholder(R.mipmap.ic_launcher)
.into(holder.imageView);
}
@Override
public void onViewRecycled(MyViewHold holder) {
if (holder != null){
Glide.with(mContext).clear(holder.imageView);
holder.imageView.setImageResource(R.mipmap.ic_launcher);
}
super.onViewRecycled(holder);
}
使用取消图片请求的方式来解决复用导致图片加载错乱的问题是比较方便也比较常用的一种方式,推荐使用。