Android RecyclerView —— 适配器封装探索
Android RecyclerView —— 自定义分割线
RecyclerView 我相信大家都不陌生,是Google在Android 5.0 的时候推出的一个可以在有限的窗口中展示大量数据集的控件(类似的控件有ListView、GridView),放在了 com.android.support:recyclerview-v7:xx.x.x 包下(xx表示版本),那么既然已经有了ListView、GridView,为什么还要使用RecyclerView呢?主要是因为RecyclerView高度解耦,非常灵活,使用简单的代码就能达到一些绚丽的效果。不过 RecyclerView 有一个地方比较坑,那就是 item 的点击和长按事件系统都没有实现,需要自己实现。
recyclerView = findView(R.id.id_recyclerview);
// 设置布局管理器
recyclerView.setLayoutManager(layout);
// 设置Adapter
recyclerView.setAdapter(adapter)
// 设置Item增加、移除动画(根据需求确认是否需要)
recyclerView.setItemAnimator(new DefaultItemAnimator());
// 添加分割线(根据需求确认是否需要)
recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.HORIZONTAL_LIST));
看到上面的代码,我们发现至少需要设置一个 LayoutManager
和 Adapter
,而以前的 ListView
和 GridView
最基本的使用,只需要设置 Adapter
就行了,那么感觉使用 RecyclerView
比 ListView
更加麻烦,代码量更多,这是因为 RecyclerView
这个控件的主要功能就是复用与回收,其他的效果和功能都可以开发者根据需求自定义,这也就是我们为什么可以只要改变 RecyclerView.LayoutManager
就可以实现 ListView、GridView
以及瀑布流的效果了。
ListView
效果(LinearLayoutManager
)想要实现 ListView
效果,只需设置 RecyclerView.LayoutManager
为 LinearLayoutManager
就行了,然后设置 Adapter
。
LinearLayoutManager
常用的 构造方法有2个:
// 只需要一个上下文参数,默认表示一个垂直方向的列表
public LinearLayoutManager(Context context)
// 参数1:上下文;
// 参数2:列表方向,值为 水平:HORIZONTAL(0) 垂直:VERTICAL(1);
// 参数3:false表示RecycleView中item从上到下依次添加;true表示RecycleView中item从下到上依次添加,一般为 false
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout)
GridView
效果(GridLayoutManager
)想要实现 GridView
效果,只需设置 RecyclerView.LayoutManager
为 GridLayoutManager
就行了,然后设置 Adapter
。
GridLayoutManager
常用的 构造方法有2个:
// 参数1:上下文参数
// 参数2:列数,默认表示一个垂直方向的列表
public GridLayoutManager(Context context, int spanCount) {
}
// 参数1:上下文;
// 参数2:列数
// 参数3:列表方向,值为 水平:HORIZONTAL(0) 垂直:VERTICAL(1);
// 参数4:false表示RecycleView中item从上到下依次添加;true表示RecycleView中item从下到上依次添加,一般为 false
public GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
}
对于 GridLayoutManager
类,有一个方法是比较使用的,那就是 GridLayoutManager
的 setSpanSizeLookup(GridLayoutManager.SpanSizeLookup spanSizeLookup)
,该方法可以对每一个 item 所占列数进行设置。
比如,设置如下:
// 设置当前位置占用的列数
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
// 该方法返回 position 位置的 item 占用 GridLayout 的多少列
@Override
public int getSpanSize(int position) {
if (position <= 3) {
return 6;
} else if (position <= 9) {
return 3;
} else if (position <= 15) {
return 2;
} else if (position <= 27) {
return 1;
} else if (position <= 35) {
return 3;
} else if (position <= 39) {
return 6;
} else {
return 1;
}
}
});
StaggeredGridLayoutManager
)想要实现瀑布流效果,只需设置 RecyclerView.LayoutManager
为 StaggeredGridLayoutManager
就行了,然后设置 Adapter
。
StaggeredGridLayoutManager
常用的 构造方法为:
// 参数1:列数
// 参数2:列表方向,值为 水平:HORIZONTAL(0) 垂直:VERTICAL(1);
public StaggeredGridLayoutManager(int spanCount, int orientation) {
}
RecyclerView.Adapter
中 notifyXxx
系列方法使用说明:该系列方法都表示类表数据发生变化时,调用用来更新列表数据。但是可以刷新全部或者刷新、插入、移除指定位置的数据。也就是全部刷新或者局部刷新。
刷新改变:
notifyDataSetChanged()
:刷新全部列表
notifyItemChanged(int position)
和 notifyItemChanged(int position, @Nullable Object payload)
:刷新指定位置数据
notifyItemRangeChanged(int positionStart, int itemCount)
和 notifyItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload)
:刷新指定范围的数据
插入数据(调用下面方法有动画效果,动画效果可以自定义 recyclerView.setItemAnimator() 方法可以设置动画效果):
notifyItemInserted(int position)
:插入了单条数据到指定位置
notifyItemRangeInserted(int positionStart, int itemCount)
:从指定位置开始,插入了指定数量的item数据
** 移除数(调用下面方法有动画效果,动画效果可以自定义 recyclerView.setItemAnimator() 方法可以设置动画效果):**
notifyItemRemoved(int position)
:移除了单条数据到指定位置
notifyItemRangeRemoved(int positionStart, int itemCount)
:从指定位置开始,移除了指定数量的item数据
数据位置移动(交换 item 位置时调用)
notifyItemMoved(int fromPosition, int toPosition)
:用来交换2个item的位置
使用 Adapter 的 notifyItemRemoved(position)
和 notifyItemInserted(position)
方法产生的问题:
问题:使用上面两个方法之后不会使 position 及其之后位置的 itemView 重新 onBindViewHolder,会导致下标错乱,如果一直调用notifyItemRemoved(position)
来移除的话,那么就会发现真正移除的并不是想要移出的,而且还非常有可能出现 IndexOutOfBoundsException
异常。
解决办法:在调用上面两个方法(其中一个)之后继续调用 Adapter 的 notifyItenRangeChanged(int positionStart, int itemCount)
方法,使下面的 itemView 重新 onBind,就可以了。
使用瀑布流显示图片可能出现的问题以及解决办法:
问题1:item 的位置不断发生变化。
解决办法:调用 StaggeredGridLayoutManager
的 setGapStrategy(int gapStrategy)
方法:
staggeredGridLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
问题2:当解决 “问题1” 时,会产生另外一个问题,顶部会留下空白
解决办法:给 RecyclerView 控件增加监听,并且在监听中调用调用 StaggeredGridLayoutManager
的 invalidateSpanAssignments()
方法:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
layoutManager.invalidateSpanAssignments();
}
});
问题3:因为 item 布局的复用,但是每张图片的高度又不同,所以导致 item 图片闪烁问题
解决办法:使用一个集合保存每一个 item 的高度,然后在显示的时候对每一个 item 的高度重新设置
// 使用 ItemTouchHelper 类实现 RecyclerView 的拖拽和侧滑删除功能
private ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
/**
* 用于设置拖拽和滑动的方向
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = 0, swipeFlags = 0;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof StaggeredGridLayoutManager || layoutManager instanceof GridLayoutManager) {
// 网格式布局有4个方向拖拽,但是不能测试
dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
} else if (layoutManager instanceof LinearLayoutManager) {
// 线性式布局有2个方向
dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; // 设置拖拽方向为 上下
swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; // 设置侧滑方向 左右
}
// 调用 makeMovementFlags() 方法 或者 makeFlag() 方法使值生效
return makeMovementFlags(dragFlags, swipeFlags);// swipeFlags 为0的话item不能滑动
}
/**
* 长按 item 拖拽时会回调这个方法
*/
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
// 获取拖拽位置
int srcPosition = viewHolder.getAdapterPosition();
int targetPosition = target.getAdapterPosition();
// 交换两个位置的数据
List datas = adapter.getDatas();
changePosition(srcPosition, targetPosition, datas);
// 更新Adapter
adapter.notifyItemMoved(srcPosition, targetPosition);
return true;
}
/**
* List集合交换两个位置的数据
*/
private void changePosition(int srcPosition, int targetPosition, List datas) {
String srcData = datas.get(srcPosition);
String targetData = datas.get(targetPosition);
datas.add(srcPosition, targetData);
datas.add(targetPosition + 1, srcData);
datas.remove(srcPosition + 1);
datas.remove(targetPosition + 1);
}
/**
* 处理滑动删除
* @param viewHolder
* @param direction
*/
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 获取位置
int adapterPosition = viewHolder.getAdapterPosition();
List datas = adapter.getDatas();
// 移除数据
datas.remove(adapterPosition);
// 更新Adapter数据,一定要调用 notifyItemRangeChanged() 方法,否则角标会错乱
adapter.notifyItemRemoved(adapterPosition);
adapter.notifyItemRangeChanged(adapterPosition, datas.size() - adapterPosition);
RLog.i("删除 item .......");
}
/**
* 开始拖拽时回调
* @param viewHolder
* @param actionState
*/
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
// 判断状态
if (ItemTouchHelper.ACTION_STATE_DRAG == actionState || ItemTouchHelper.ACTION_STATE_SWIPE == actionState) {
viewHolder.itemView.setBackgroundColor(getResources().getColor(R.color.start_drag_color));
RLog.i("开始拖拽或者开始侧滑.......");
}
}
/**
* 拖拽完成时回调,可以恢复颜色
* @param recyclerView
* @param viewHolder
*/
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setBackgroundColor(getResources().getColor(R.color.item_bg));
RLog.i("拖拽完成.......");
}
/**
* 拖拽视图位置发生变化时回调,动态改变颜色
*/
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
// 改变颜色(这里只有侧滑时改变颜色)
viewHolder.itemView.setAlpha(1 - (Math.abs(dX) / screenWidth));
RLog.i("位置发生变化.......");
}
@Override
public boolean isLongPressDragEnabled() {
// 返回true则为所有item都设置可以拖拽
return true;
}
});
// 记住:需要将 RecyclerView 和 ItemTouchHelper 绑定到一起
itemTouchHelper.attachToRecyclerView(recyclerView);
相关代码可在 github 上查看下载