RecyclerView 是伴随着 android5.x 出来的控件,第一次提出应该是在14年的 Google I/O 大会(猜测,懒得查,反正我不 care 它是什么时候出来的),到现在17年 Google I/O 大会结束正好三年,相信大家都早已经把 RecyclerView 使用到项目当中了。
我们都知道,RecyclerView 的出现,是为了取代 ListView、GridView 而出现的。记得有次面试的时候,面试官问我为什么要使用 RecyclerView,你 RecyclerView 能实现的列表,我 ListView 同样可以实现,我当时是这样回答的:整体上看RecyclerView架构,提供了一种插拔式的体验,高度的解耦,异常的灵活
,这段话我随便在网上复制的,大意差不多。好了,扯远了。
为什么说 RecyclerView 让我欢喜让我忧?
欢喜因为用了 RecyclerView 之后,感觉就再也不想去写 ListView 了;忧则是因为尽管用了几年 RecyclerView,但直到现在感觉 RecyclerView 还是玩不溜,同感玩不溜的同学请握个抓。-
为什么说 RecyclerView 高度解耦
我们来看看 RecyclerView 的几个大家熟悉的方法:mRecyclerView.setLayoutManager(layout);
//设置条目布局规则
mRecyclerView.setItemAnimator();
//设置条目动画
mRecyclerView.addItemDecoration();
//自定义条目装饰
mItemTouchHelper.attachToRecyclerView(mRecyclerView);
//定制条目触摸
现在我们来回顾一下,对应的这几个规则,我们的ListView 是怎么实现的, layoutmanger:ListView 并不支持这个功能
ItemAnimation:在 Adapter的 getView()方法里面给创造出来的 View 加动画
ItemDecoration:在 Adapter 的 getView()方法里面自行处理
ItemTouchHelp: 在 Adapyer 的 getView()方法里面自行给 View 设置 Touch 事件处理。
对比一下,瞬间感觉 ListView 弱爆了。。。
说了这么多优点,说说缺点,既然 RecyclerView 给我们提供了这么多扩展,可以高度定制,但是上手难度同样增加了不止一个等级。还有条目点击事件,简直丧心病狂,竟然接口都没提供。
好了,我们要开始写代码了。
一、RecyclerView 的基本使用
先实现一个简单的列表:
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycle_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(new RecyclerView.Adapter() {
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//创建一个 ViewHolder 并且返回
return null;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
//在这里给 ViewHolder 绑定数据
}
@Override
public int getItemCount() {
//控制RecyclerView的条目数
return 0;
}
});
这样我们就使用RecyclerView实现了一个简单的列表,由于代码比较简单,我直接一笔带过了。这里对比我们的 Listview,就多了一行代码recyclerView.setLayoutManager(new LinearLayoutManager(this));
,至于这行代码具体有什么作用呢,我们后面再说。
二、封装RecyclerView 的 Adapter
Adapter是 RecycleView 中最重要的一个类,虽然没有特别复杂难理解的代码,但是 RecycleView 的刷新条目、多条目、点击事件都需要在里面处理。而且每一个 RecycleView 的 Adapter 几乎都需要带读写,所以这是一个高频率、代码量稍多的一个类,因此我们在使用的过程中一般会对 Adapter 进行一下封装。
- 数据处理用泛型规范输入数据
我们在实际项目开发当中,会有很多地方都用到 RecycleView(太长了,下面我用 rv 简称吧),而且数据的结构各有不同,因此,在 BaseAdapter 里面,我们需要用一个泛型 T 去规范数据结构的类型,避免误操作。
- 默认实现条目的增删方法和getItemCount();
这个比较简单,等下直接看代码
- 配合 ButtonKnife,简化 ViewHolder 里面的 findViewbyid 操作
这个也简单,抽取了一个 BaseViewHolder,在构造方法里面绑定的。
- 好像也没什么好写的,我直接贴我项目中封装的 BaseAdapter 的代码吧
public abstract class BaseAbstractAdapter extends RecyclerView.Adapter {
protected final String TAG = getClass().getSimpleName();
protected final Context mContext;
protected final LayoutInflater mLayoutInflater;
protected List mDataList = new ArrayList<>();
public BaseAbstractAdapter(Context context) {
this.mContext = context;
this.mLayoutInflater = LayoutInflater.from(mContext);
}
public Context getContext() {
return mContext;
}
public List getDataList() {
return mDataList;
}
public T getItemData(int position) {
return (position >= 0 && position < mDataList.size()) ? mDataList.get(position) : null;
}
@Override
public int getItemCount() {
return mDataList == null ? 0 : mDataList.size();
}
/**
* 移除某一条记录
*
* @param position 移除数据的position
*/
public void removeItem(int position) {
if (position >= 0 && position < mDataList.size()) {
mDataList.remove(position);
notifyItemRemoved(position);
}
}
/**
* 添加一条记录
*
* @param data 需要加入的数据结构
* @param position 插入位置
*/
public void addItem(T data, int position) {
if (position >= 0 && position <= mDataList.size()) {
mDataList.add(position, data);
notifyItemInserted(position);
}
}
/**
* 添加一条记录
*
* @param data 需要加入的数据结构
*/
public void addItem(T data) {
addItem(data, mDataList.size());
}
/**
* 移除所有记录
*/
public void clearItems() {
int size = mDataList.size();
if (size > 0) {
mDataList.clear();
notifyItemRangeRemoved(0, size);
}
}
/**
* 批量添加记录
*
* @param data 需要加入的数据结构
* @param position 插入位置
*/
public void addItems(List data, int position) {
if (position >= 0 && position <= mDataList.size() && data != null && data.size() > 0) {
mDataList.addAll(position, data);
notifyItemRangeChanged(position, data.size());
}
}
/**
* 批量添加记录
*
* @param data 需要加入的数据结构
*/
public void addItems(List data) {
addItems(data, mDataList.size());
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof BaseViewHolder) {
((BaseViewHolder) holder).bindViewData(getItemData(position));
}
}
}
//不是内部类哦
public abstract class BaseViewHolder extends RecyclerView.ViewHolder {
public BaseViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
public abstract void bindViewData(T data);
}
好了,代码贴完了,应该没有什么难懂的代码吧,这一套封装基本上可以满足98%以上的单Item 列表了,至于点击事件,并不是所有的列别都需要,根据实际需要自己定义接口回调就行了。
到这里可能有的同学会问,那多**** Item ****的**** rv ****怎么办,在实际开发中,多**** Item ****的**** rv ****情景也不算少,特别是聊天列表,动辄八九种**** Item****。别急,我们慢慢来****~****
rv 的方法中有个抽象方法onCreateViewHolder(ViewGroup parent, int viewType)
,这个方法是用来创建 ViewHolder 的,ViewHolder 我们可以把它理解成RecycleView 一个 ItemView 的包装类,也就是说一个 ViewHolder 就是一个条目,如果我们需要多条目,那么直接在这里返回不同的条目就行了,方法参数里面正好有个 viewType可以用来控制条目类型。
那么问题来了,这个 viewType值是从哪里来的呢,想知道这个,那就只能去看源码了,我们通过产看 RecyclerView.Adapter 的源码发现,onCreateViewHolder方法是在createViewHolder里面调用
public final VH createViewHolder(ViewGroup parent, int viewType) {
TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
final VH holder = onCreateViewHolder(parent, viewType);
holder.mItemViewType = viewType;
TraceCompat.endSection();
return holder;
}
看到这里,我们只能继续追createViewHolder的调用。然后通过全局搜索,在getViewForPosition方法里面找到了viewType这个参数的来源,里面有一行代码 final int type = mAdapter.getItemViewType(offsetPosition);
于是再继续追getItemViewType。
public int getItemViewType(int position) {
return 0;
}
好了,追到这里我也不再赘述了,本来大家都知道重写getItemViewType方法就行了。
回到正题,怎么封装多 ItemAdapter。多 Item 用到的场景一般都是需要给 RV 添加一个头或者添加一个尾,因此,考虑到通用性,我就只做了三种类型条目的扩展,下面直接贴代码:
public abstract class BaseAbstractMultipleItemAdapter extends BaseAbstractAdapter {
private static final int ITEM_TYPE_HEADER = 1;
private static final int ITEM_TYPE_BOTTOM = 2;
private static final int ITEM_TYPE_CONTENT = 3;
@IntDef({ITEM_TYPE_HEADER, ITEM_TYPE_BOTTOM})
@interface ItemType {
}
protected int mHeaderCount;//头部View个数
protected int mBottomCount;//底部View个数
public BaseAbstractMultipleItemAdapter(Context context) {
super(context);
}
public void setHeaderCount(int headerCount) {
this.mHeaderCount = headerCount;
}
public void setBottomCount(int bottomCount) {
this.mBottomCount = bottomCount;
}
public int getHeaderCount() {
return mHeaderCount;
}
public int getBottomCount() {
return mBottomCount;
}
public boolean isHeaderView(int position) {
return mHeaderCount != 0 && position < mHeaderCount;
}
public boolean isBottomView(int position) {
return mBottomCount != 0 && position >= (mHeaderCount + super.getItemCount());
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ITEM_TYPE_HEADER) {
return onCreateHeaderView(parent);
} else if (viewType == ITEM_TYPE_BOTTOM) {
return onCreateBottomView(parent);
} else {
return onCreateContentView(parent, viewType);
}
}
@Override
public int getItemViewType(int position) {
if (isHeaderView(position)) {//头部View
return ITEM_TYPE_HEADER;
} else if (isBottomView(position)) {//底部View
return ITEM_TYPE_BOTTOM;
} else {
return getContentViewType(position);
}
}
@Override
public int getItemCount() {
return mHeaderCount + super.getItemCount() + mBottomCount;
}
@Override
public T getItemData(int position) {
int index = position - mHeaderCount;
if (index >= super.getItemCount()) {
return null;
}
return super.getItemData(index);
}
/**
* 移除某一条记录
*
* @param position 移除数据的position 如果有Header需要减去Header数量
*/
public void removeItem(int position) {
if (position < mDataList.size()) {
mDataList.remove(position);
notifyItemRemoved(mHeaderCount + position);
}
}
/**
* 添加一条记录
*
* @param data 需要加入的数据结构
* @param position 插入数据的位置 如果有Header需要减去Header数量
*/
public void addItem(T data, int position) {
if (position <= mDataList.size()) {
mDataList.add(position, data);
notifyItemInserted(mHeaderCount + position);
}
}
/**
* 移除所有记录
*/
public void clearItems() {
int size = mDataList.size();
if (size > 0) {
mDataList.clear();
notifyItemRangeRemoved(mHeaderCount, size);
}
}
/**
* 批量添加记录
* @param data 需要加入的数据结构
* @param position 插入数据的位置 如果有Header需要减去Header数量
*/
public void addItems(List data, int position) {
if (position <= mDataList.size() && data != null && data.size() > 0) {
mDataList.addAll(position, data);
notifyItemRangeChanged(mHeaderCount + position, data.size());
}
}
public int getContentViewType(int position) {
return ITEM_TYPE_CONTENT;
}
public RecyclerView.ViewHolder onCreateHeaderView(ViewGroup parent) {//创建头部View
return null;
}
public abstract RecyclerView.ViewHolder onCreateContentView(ViewGroup parent, int viewType);//创建中间内容View
public abstract RecyclerView.ViewHolder onCreateBottomView(ViewGroup parent);//创建底部View
}
代码都有注释,应该没有什么看不懂多看几遍不能理解的通过mHeaderCount和mBottomCount分别控制头尾条目数,然后需要什么条目就重写对应的 CreateViewHolder 方法即可,如果 head 或者 bottom 需要绑定数据就在onBindViewHolder里面根据holder 和 position自行绑定。
三、ItemDecoration
Decoration:n.装饰品;装饰,装潢;装饰图案,装饰风格;奖章
顾名思义,条目装饰。
我们在写开发中经常会遇到这个的需求,列表的条目和条目之间需要添加一个间隔线,不知道你们是怎么解决的,反正我之前是直接在条目布局的底部直接写了一个分割线进去。这样写当然也可以,虽然最后一个条目不需要分割线可以通过代码手动隐藏掉,但是真的很 low有木有,做为一个自命不凡的 Coder,怎么写出如此高耦合重复的代码。
ItemDecoration 就可以完美的帮我们解决分割线的问题,当然ItemDecoration的功能可不仅仅如此,一口吃不成胖子,我们一步一步来
使用:mRecycleView.addItemDecoration(new RecyclerView.ItemDecoration() {});
然而这只是一个抽象类。。。
首先我们点进源码,看这个类的注释
/**
* An ItemDecoration allows the application to add a special drawing and layout offset
* to specific item views from the adapter's data set. This can be useful for drawing dividers
* between items, highlights, visual grouping boundaries and more.
*
* All ItemDecorations are drawn in the order they were added, before the item
* views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
* and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
* RecyclerView.State)}.
*/
我英语不怎么好,就不一句一句的翻译了,大意就是:
一个ItemDecoration允许添加一个特殊的图形和布局偏移。比如说涌入绘制项目之间的分割、突出显示、视觉分组边界等等。
所有的 ItemDecoration 的绘制顺序和条目添加顺序一致,后面这句话我翻译不通顺♀️。
查看了一下ItemDecoration类的继承关系,发现Google 给我们默认实现的子类就只有一个ItemTouchHelper,这是一个比较特殊的子类,我们在后面会讲。那么既然这样,我们就只能手撸。
手撸之前我们先看一下ItemDecoration类的结构,在这里提取出三个比较关键得方法
public void onDraw(Canvas c, RecyclerView parent, State state)
public void onDrawOver(Canvas c, RecyclerView parent, State state)
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
源码上面三个方法都有方法说明,但是英语渣的我就不误导大家的英语了,我直接讲方法吧。
-
getItemOffsets()
顾名思义,获取条目偏移,可以实现类似padding的效果,我这里偷了一张图便于大家理解:
图片来源于网络、不知道哪位好汉的原创 onDraw()
绘制背景,就是绘制的东西会在条目的下层onDrawOver()
绘制覆盖物,就是绘制的东西会覆盖在条目上
可能还有点懵逼,但是知道了这三个方法,我们就可以动手给 RecycleView 设置分割线了,下面是一个设置1px 分割线的代码
public class DividerDecoration extends RecyclerView.ItemDecoration {
private int dividerHeight;
private Paint dividerPaint;
public SimpleDividerDecoration(Context context) {
dividerPaint = new Paint();
dividerPaint.setColor(Color.RED);
dividerHeight = 1
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.bottom = dividerHeight;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
int childCount = parent.getChildCount();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
for (int i = 0; i < childCount - 1; i++) {
View view = parent.getChildAt(i);
float top = view.getBottom();
float bottom = view.getBottom() + dividerHeight;
c.drawRect(left, top, right, bottom, dividerPaint);
}
}
}
注意:这里有个坑getItemOffsets()里面最好不要调用 super 方法,因为里面有一个默认实现outRect.set(0, 0, 0, 0);super 方法最先调用还好,如果在最后调用,我们的outRect参数就被重置了。
四、ItemAnimator
顾名思义,条目动画。本来不想写的,RecyclerView 有默认的实现动画,而且列表中根本用不到酷炫的条目动画,无奈被我早起规划的时候,加入了 RecyclerView 的知识点里面,在这里简单讲一下吧。
使用方法如下:
public void setItemAnimator(ItemAnimator animator) {
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
mItemAnimator.setListener(null);
}
mItemAnimator = animator;
if (mItemAnimator != null) {
mItemAnimator.setListener(mItemAnimatorListener);
}
}
从这个方法里面,我们可以 get 到两个重要信息:
- 自定义条目动画必须继承ItemAnimator。
- RecyclerView 有默认的实现动画 DefaultAnimation,并且在定义变量的时候就赋值给mItemAnimator。
好了,那么接下来我们就通过学习DefaultAnimation的实现来自定义 ItemAnimation。
DefaultAnimation 继承自SimpleItemAnimator,通过阅读 SimpleItemAnimator 的注释信息,我们知道它是RecyclerView.ItemAnimator的直接子类,并且添加了ItemHolderInfo(一个简单的数据结构,保存了 Item 的边界信息,用于计算项目动画)来辅助条目动画的执行。因此我们如果要自定义 ItemAnimation 最好继承自SimpleItemAnimator。
好了,到这里,我们算是知道了如何自定义 ItemAnimation,接下来我们只需要顺藤摸瓜就可以了。
要顺藤摸瓜,得先找到藤,我们去看看ItemAnimation的抽象方法,自定义 ItemAnimation 一共有八个相关的方法需要我们去手动实现
//Item移除回调
@Override
public boolean animateRemove(RecyclerView.ViewHolder holder) {
return false;
}
//Item添加回调
@Override
public boolean animateAdd(RecyclerView.ViewHolder holder) {
return false;
}
//用于控制添加,移动更新时,其它Item的动画执行
@Override
public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
return false;
}
//Item更新回调
@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
return false;
}
//真正控制执行动画的地方
@Override
public void runPendingAnimations() {
}
//停止某个Item的动画
@Override
public void endAnimation(RecyclerView.ViewHolder item) {
}
//停止所有动画
@Override
public void endAnimations() {
}
@Override
public boolean isRunning() {
return false;
}
没个方法的作用我都在上面写了注释,相信看懂应该不难,接下来我们再根据这根藤回到 DefaultItemAnimation。
- animateAdd开始吧,方法很简单,就三行代码。一是清楚和删除 Item 里面所有的动画相关代码,二是把条目初始化为透明状态(可对比默认执行动画),三是把条目添加到等待运行动画列表里面。
@Override
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
mPendingAdditions.add(holder);
return true;
}
- animateRemove,和animateAdd一样,就少了一行初始化条目的代码。
@Override
public boolean animateRemove(final ViewHolder holder) {
resetAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
- 再看animateMove()方法,忘记这个方法作用的同学请再回头看看。这个方法里面有几个参数,分别是要移动的条目,起始 xy 轴的位置。里面的操作也很简单,移动Item,然后保存 Item 的移动信息。
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += ViewCompat.getTranslationX(holder.itemView);
fromY += ViewCompat.getTranslationY(holder.itemView);
resetAnimation(holder);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
if (deltaX != 0) {
ViewCompat.setTranslationX(view, -deltaX);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, -deltaY);
}
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
- 接下来看animateChange(),我们看到,如果是同一个 ViewHolder 则直接调用 animateMove()方法,否则在内部多记录了一个 alpha 的值
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
if (oldHolder == newHolder) {
// Don't know how to run change animations when the same view holder is re-used.
// run a move animation to handle position changes.
return animateMove(oldHolder, fromX, fromY, toX, toY);
}
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
resetAnimation(oldHolder);
int deltaX = (int) (toX - fromX - prevTranslationX);
int deltaY = (int) (toY - fromY - prevTranslationY);
// recover prev translation state after ending animation
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null) {
// carry over translation values
resetAnimation(newHolder);
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
ViewCompat.setAlpha(newHolder.itemView, 0);
}
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
endAnimation()方法和endAnimations()方法一样,就是循环把待处理的动画信息全部删掉,然后调用 cancelAll()停止正在运行的动画。
isRunning,通过判断动画队列,看是否有动画待执行动画或者正在执行的动画
runPendingAnimations(),真正执行动画的地方,判断待执行的动画队列里面是否有需要执行的动画,如果有,就顺序执行,如果没有就退出。
animateAddImpl()、animateChangeImpl()、animateMoveImpl、animateRemoveImpl()这几个方法分别是几种类型动画的具体实现。
好累啊,终于把它分析完了,接下来我们一鼓作气,动手撸一个。但是我项目中好像没有现成的,然后写动画这种需要创意的事情我心累,而且关键是实际开发中几乎也不怎么用得到。但是有一部分同学可能会对动画比较感兴趣,于是机智的我去 github 上找了一个RecyclerView 的 Item 动画库,大家可以对着我的分析去看看别人的实现,贴上传送门:https://github.com/wasabeef/recyclerview-animators
五、ItemTouchHelper
ItemTouchHelper 条目触摸助手,顾名思义,这个类就是用来帮助我们处理 RV 条目触摸事件的,如常见的滑动删除,长按拖拽。效果图如下:
没有啥特别的,我直接贴代码吧:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
if (from < to) {
for (int i = from; i < to; i++)
Collections.swap(mNewTopsAdapter.getDataList(), i, i + 1);
} else {
for (int i = from; i > to; i--)
Collections.swap(mNewTopsAdapter.getDataList(), i, i - 1);
}
mNewTopsAdapter.notifyItemMoved(from, to);
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mNewTopsAdapter.removeItem(viewHolder.getAdapterPosition());
}
});
itemTouchHelper.attachToRecyclerView(mRecycleView);
实现起来很简单,new一个 ItemTouchHelper, 然后调用ItemTouchHelper的 attachToRecyclerView() 依附给 RV 就行了。关键点在 ItemTouchHelper 的构造方法里面必传的参数ItemTouchHelper.Callback()。由于这里需求比较简单,我直接用了已经做过一次封装的SimpleCallback。
SimpleCallback:继承自ItemTouchHelper.Callback,对父类进行了包装,只暴露出两个简单的方法供开发者去实现。
构造方法:
- SimpleCallback(int dragDirs, int swipeDirs)
- dragDirs:条目的拖动方向,可配参数有ItemTouchHelper.UP、ItemTouchHelper.DOWN、ItemTouchHelper.LEFT、ItemTouchHelper.RIGHT,如果要同时配置多个方向,用运算符号|连接
- swipeDirs:条目滑动方向,参数类型同上
我们在 new SimpleCallback()的时候把拖拽的参数配置好,然后再实现如下两个抽象方法
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)
其中 onMove方法会在 Item 拖动的时候不断调用,此时我们需要在条目拖动的时候调用 adapter 的notifyItemMoved()方法刷新条目位置,具体代码如下:
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
if (from < to) {
for (int i = from; i < to; i++)
Collections.swap(mNewTopsAdapter.getDataList(), i, i + 1);
} else {
for (int i = from; i > to; i--)
Collections.swap(mNewTopsAdapter.getDataList(), i, i - 1);
}
mNewTopsAdapter.notifyItemMoved(from, to);
return true;
首先获取当前条目和目标条目position,这里需要注意getAdapterPosition()和getLayoutPosition(),前者是在adapter 调用界面刷新的时候就给 position 赋值了,而后者是在界面刷新结束之后才能获取到正确的赋值。我们都知道,RV 的界面刷新是异步的,大概会有一个16毫秒左右的延时,因此使用getLayoutPosition()获取 position 可能会出错哦。
onSwiped方法则是用来控制条目滑动删除之后的逻辑处理。其中direction参数是滑动的方向。比如说滑动删除:我们直接在方法体里面调用 adapter 的 notifyItemRemoved()方法即可。
好了,ItemTouchHelper 的基本用法就这些,基本也能满足开发过程中的大部分需求了,如果还有更高的需求,那么继续跟我去肛一波ItemTouchHelper.Callback的源码。
****ItemTouchHelper.Callback****
抽象方法有三个:
- public int getMovementFlags(RecyclerView, RecyclerView.ViewHolder)
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)
其中第二个第三个方法,我们在SimpleCallback已经解释过一次了,这里不在赘述。我们来讲一下第一个方法
- public abstract int getMovementFlags(RecyclerView recyclerView,ViewHolder viewHolder);
方法说明上,我用我三级英语水平加上翻译软件,大概可以读出这个方法是要返回一个控制 item 移动方向的混合标志,混合标志怎么生成,可以使用方法makeMovementFlags();好,那么实现getMovementFlags的方法体大概就是 return getMovementFlags(dragFlags,swipeFlags);
而 getMovementFlags 要求我们传两个参数,这两个参数怎么传呢,我们继续去追getMovementFlags();
public static int makeMovementFlags(int dragFlags, int swipeFlags) {
return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) |
makeFlag(ACTION_STATE_SWIPE, swipeFlags) | makeFlag(ACTION_STATE_DRAG,
dragFlags);
}
方法说明上,我们可以知道这个方法是用来创建移动 flag 的,说白了就是用来控制 Item 的移动/滑动方向,方法中两个参数分别是拖动 flag 和滑动 flag。看到这里,我们来回想一下SimpleCallback的构造方法,是不是也要传这两个参数,而SimpleCallback不需要实现 getMovementFlags()方法,是不是因为已经帮我们实现了,通过查看源码验证了我们的猜想。
然后就是一些公共方法,可重写定制的:
//是否可以把拖动的ViewHolder拖动到目标ViewHolder之上
@Override
public boolean canDropOver(RecyclerView recyclerView,RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
return true;
}
//获取拖动
@Override
public RecyclerView.ViewHolder chooseDropTarget(RecyclerView.ViewHolder selected, List dropTargets, int curX, int curY) {
return dropTargets.get(0);
}
//调用时与元素的用户交互已经结束,也就是它也完成了它的动画时候
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
}
@Override
public int convertToAbsoluteDirection(int flags, int layoutDirection) {
return super.convertToAbsoluteDirection(flags, layoutDirection);
}
//设置手指离开后ViewHolder的动画时间
@Override
public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
return super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy);
}
@Override
public int getBoundingBoxMargin() {
return super.getBoundingBoxMargin();
}
//返回值作为用户视为拖动的距离
@Override
public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) {
return super.getMoveThreshold(viewHolder);
}
//返回值滑动消失的距离,滑动小于这个值不消失,大于消失
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
return super.getSwipeEscapeVelocity(defaultValue);
}
//返回值滑动消失的距离, 这里是相对于RecycleView的宽度,0.5f表示为RecycleView的宽度的一半,取值为0~1f之间
@Override
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
return super.getSwipeThreshold(viewHolder);
}
//返回值作为滑动的流程程度,越小越难滑动,越大越好滑动
@Override
public float getSwipeVelocityThreshold(float defaultValue) {
return 1f;
}
//当用户拖动一个视图出界的ItemTouchHelper调用
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll) {
return super.interpolateOutOfBoundsScroll(recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
}
//返回值决定是否有滑动操作
@Override
public boolean isItemViewSwipeEnabled() {
return super.isItemViewSwipeEnabled();
}
//返回值决定是否有拖动操作
@Override
public boolean isLongPressDragEnabled() {
return super.isLongPressDragEnabled();
}
//自定义拖动与滑动交互
@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);
}
//自定义拖动与滑动交互
@Override
public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
//当onMove return ture的时候调用
@Override
public void onMoved(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int fromPos, RecyclerView.ViewHolder target, int toPos, int x, int y) {
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y);
}
//当拖动或者滑动的ViewHolder改变时调用
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
}
六、LayoutManger
LayoutManger:布局管理
LayoutManger是 RecyclerView 用来管理子 view 布局的一个组件(另一个组件是 Recycler,负责回收视图),它主要负责三个事情:
1.布局子视图
2.在滚动的过程中根据子视图在布局中所处的位置,决定何时添加子视图和回收视图
3.滚动子视图
其中只有滚动子视图才需要对子视图回收或添加,而添加子视图则必然伴随着所添加对象的布局处理,在滚动过程中,添加一次子视图只会影响到被添加对象,原有子视图的相对位置不会变化。
LayoutManger 是 RecyclerView 的一个抽象内部类,一般我们使用它都是使用它的子类:
- LinearLayoutManager
- GridLayoutManager
- StaggeredGridLayoutManager
这三个类的用法我就不过多的赘述了,相信大家都用过,一般情况下,这三个 LayoutManger 也能够满足大家99%的需求了。自定义LayoutManger 是一件比较有难度的工程,而且使用场景很少(反正我是没碰到过这样的需求)。但是网上有很多炫酷的自定义 LayoutManger 效果,最经典的当属防探探的卡片式布局,在网上也看过很多自定义 LayoutManger 的文章,但现在还是半吊子。
感兴趣的同学可以看看这个库,里面有 bolg 链接。当然,需要的时候再去看也行。传送门:https://github.com/mcxtzhang/ZLayoutManager
好了,RecyclerView 到这里就讲完了,可能有些地方深度不够,但是基本能满足大部分的需求了,如果有问题可以留言提问,后者直接私信我。