先描述一下这个问题。举个例子,我有一个列表,其条目的顺序从上到下应该是0,1,2…;然后当我删除第二个条目后,原来的第三个条目就应该要变成现在的第二个条目,那我点击现在的第二个条目,就应该得到的位置为1,但是实际得到的位置确是2:
出现这个问题还是比较蛋疼的,因为别的功能都是好好的,只有这个位置错位了。要想解决这个问题,我们还得弄清另外一个问题:ViewHolder的getPosition、getAdapterPosition和getLayoutPosition你搞清楚了没?
为什么我会提出这个问题,让我们回顾一下我们的写法(参考本人另一篇文章RecyclerView高级使用(一)-侧滑删除的简单实现):
我们在自定义的CallBack里面的onSwiped里面是这样写的:
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
recyclerItemList.remove(viewHolder.getAdapterPosition());
recyclerView.getAdapter().notifyItemRemoved(viewHolder.getAdapterPosition());
}
我们在取位置的时候,用的是getAdapterPosition。其实获取position一共有四个方法:
/**
* @deprecated This method is deprecated because its meaning is ambiguous due to the async
* handling of adapter updates. You should use {@link #getLayoutPosition()} or
* {@link #getAdapterPosition()} depending on your use case.
*
* @see #getLayoutPosition()
* @see #getAdapterPosition()
*/
@Deprecated
public final int getPosition() {
return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}
/**
* Returns the position of the ViewHolder in terms of the latest layout pass.
* <p>
* This position is mostly used by RecyclerView components to be consistent while
* RecyclerView lazily processes adapter updates.
* <p>
* For performance and animation reasons, RecyclerView batches all adapter updates until the
* next layout pass. This may cause mismatches between the Adapter position of the item and
* the position it had in the latest layout calculations.
* <p>
* LayoutManagers should always call this method while doing calculations based on item
* positions. All methods in {@link RecyclerView.LayoutManager}, {@link RecyclerView.State},
* {@link RecyclerView.Recycler} that receive a position expect it to be the layout position
* of the item.
* <p>
* If LayoutManager needs to call an external method that requires the adapter position of
* the item, it can use {@link #getAdapterPosition()} or
* {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}.
*
* @return Returns the adapter position of the ViewHolder in the latest layout pass.
* @see #getAdapterPosition()
*/
public final int getLayoutPosition() {
return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}
/**
* Returns the Adapter position of the item represented by this ViewHolder.
* <p>
* Note that this might be different than the {@link #getLayoutPosition()} if there are
* pending adapter updates but a new layout pass has not happened yet.
* <p>
* RecyclerView does not handle any adapter updates until the next layout traversal. This
* may create temporary inconsistencies between what user sees on the screen and what
* adapter contents have. This inconsistency is not important since it will be less than
* 16ms but it might be a problem if you want to use ViewHolder position to access the
* adapter. Sometimes, you may need to get the exact adapter position to do
* some actions in response to user events. In that case, you should use this method which
* will calculate the Adapter position of the ViewHolder.
* <p>
* Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the
* next layout pass, the return value of this method will be {@link #NO_POSITION}.
*
* @return The adapter position of the item if it still exists in the adapter.
* {@link RecyclerView#NO_POSITION} if item has been removed from the adapter,
* {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last
* layout pass or the ViewHolder has already been recycled.
*/
public final int getAdapterPosition() {
if (mOwnerRecyclerView == null) {
return NO_POSITION;
}
return mOwnerRecyclerView.getAdapterPositionFor(this);
}
/**
* When LayoutManager supports animations, RecyclerView tracks 3 positions for ViewHolders
* to perform animations.
* <p>
* If a ViewHolder was laid out in the previous onLayout call, old position will keep its
* adapter index in the previous layout.
*
* @return The previous adapter index of the Item represented by this ViewHolder or
* {@link #NO_POSITION} if old position does not exists or cleared (pre-layout is
* complete).
*/
public final int getOldPosition() {
return mOldPosition;
}
一共有getPosition、getLayoutPosition、getAdapterPosition和getOldPosition。因此,我们为什么在取position的时候为什么就取了getAdapterPosition呢,网上的文章都没有给出解释,都是一概而论。但是现在问题出现了,我们就要研究一下了。
首先从注释上看,getOldPosition是和动画相关的,我们这里暂不讨论。那么就剩下三个了。
getPosition使用了**@Deprecated**注解,表明它是一个废弃的方法。为什么要废弃呢。
英文翻译为:*此方法已被弃用,因为它的含义由于适配器更新的异步处理而不明确.*让我们改用getLayoutPosition或者getAdapterPosition。再从源码上看,getPosition和getLayoutPosition的代码是一样的,我们可以认为getLayoutPosition就是以前的getPosition。那么就剩下getLayoutPosition和getAdapterPosition的对比了。而我们的问题也出现在了我们使用了getAdapterPosition。
getLayoutPosition的注释意思是返回ViewHolder在最新布局过程中的位置。
getAdapterPosition的注释意思是返回此ViewHolder表示的item的适配器位置。
光看意思是不是比较拗口。我这里找来了StackFlow上某位工程师的总结:
我简单地总结了一下:
1、一般情况下,二者相同。
2、getAdapterPosition如名,获取的是适配器上的位置。因为RecyclerView比ListView进化了局部刷新功能,所以在ListView时代的统一刷新和getPosition方式要变一下了。RecylerView在本质上是有两个列表的,一个存储数据源,一个存储视图ViewHolder。通过观察者模式,以数据源驱动视图更新。因此,可以简单认为getAdapterPosition是获取的数据源上的位置。
3、getLayoutPostion是获取上一次绘制后的列表的视图位置。尤其在使用局部刷新(只要不是notifyDataSetChanged)这些操作的时候,推荐使用这个方法来获取位置。相比getAdapterPosition,能立即获取到改变后的位置。
有了上面的总结,那么我们解决这个问题就有思路了,将代码改为:
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
recyclerItemList.remove(viewHolder.getLayoutPosition());
recyclerView.getAdapter().notifyItemRemoved(viewHolder.getLayoutPosition());
}
然后再试,结果发现还是不行。
问题又在哪呢?
我们再看看设置条目点击事件的地方:
public class SimpleRecyclerListAdapter extends RecyclerView.Adapter<SimpleRecyclerViewHolder> {
private List<RecyclerItem> recyclerItemList;
private OnItemCLickListener onItemCLickListener;
public void setOnItemCLickListener(OnItemCLickListener onItemCLickListener) {
this.onItemCLickListener = onItemCLickListener;
}
public SimpleRecyclerListAdapter(List<RecyclerItem> recyclerItemList) {
this.recyclerItemList = recyclerItemList;
}
@NonNull
@Override
public SimpleRecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(getItemLayoutRes(), parent, false);
return new SimpleRecyclerViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull SimpleRecyclerViewHolder holder, int position) {
RecyclerItem item = recyclerItemList.get(position);
holder.iconView.setImageResource(item.getIcon());
holder.textView.setText(item.getText());
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onItemCLickListener != null) {
onItemCLickListener.onItemClick(item, holder, position);
}
}
});
}
@Override
public int getItemCount() {
return recyclerItemList == null ? 0 : recyclerItemList.size();
}
public interface OnItemCLickListener {
void onItemClick(RecyclerItem recyclerItem, SimpleRecyclerViewHolder holder, int position);
}
protected @LayoutRes int getItemLayoutRes(){
return R.layout.item_simple_list;
}
}
其余代码可以不用看,就看onBindViewHolder里面设置onClickListener的地方。这里面传的position直接从外部的position获取了。但是实际上外部的position依旧还是adapterPosition,为什么要这样说呢:
/**
* Called by RecyclerView to display the data at the specified position. This method
* should update the contents of the {@link ViewHolder#itemView} to reflect the item at
* the given position.
* <p>
* Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
* again if the position of the item changes in the data set unless the item itself is
* invalidated or the new position cannot be determined. For this reason, you should only
* use the <code>position</code> parameter while acquiring the related data item inside
* this method and should not keep a copy of it. If you need the position of an item later
* on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
* have the updated adapter position.
* <p>
* Partial bind vs full bind:
* <p>
* The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or
* {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty,
* the ViewHolder is currently bound to old data and Adapter may run an efficient partial
* update using the payload info. If the payload is empty, Adapter must run a full bind.
* Adapter should not assume that the payload passed in notify methods will be received by
* onBindViewHolder(). For example when the view is not attached to the screen, the
* payload in notifyItemChange() will be simply dropped.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
* @param payloads A non-null list of merged payloads. Can be empty list if requires full
* update.
*/
public void onBindViewHolder(@NonNull VH holder, int position,
@NonNull List<Object> payloads) {
onBindViewHolder(holder, position);
}
看到注释,说这个postion within the adapter’s data set。因此,我们点击事件就不能透传这个position.要改成下面的写法:
@Override
public void onBindViewHolder(@NonNull SimpleRecyclerViewHolder holder, int position) {
RecyclerItem item = recyclerItemList.get(holder.getLayoutPosition());
holder.iconView.setImageResource(item.getIcon());
holder.textView.setText(item.getText());
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onItemCLickListener != null) {
onItemCLickListener.onItemClick(item, holder, holder.getLayoutPosition());
}
}
});
}
再测试:
完美解决。
那么除了上面这种比较源码流的解决办法,还有别的解决办法吗?
答案是有的,而且还有两种:
1、不采用getLayoutPosition,依旧使用getAdapterPosition.那么我们的onSwiped方法里面需要修改为:
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
//1、删除源数据
int pos = viewHolder.getAdapterPosition();
RecyclerItem item = recyclerItemList.get(pos);
recyclerItemList.remove(item);
//2、通知视图刷新
recyclerView.getAdapter().notifyItemRemoved(viewHolder.getAdapterPosition());
//3、更正position,防止点击时,position错乱
recyclerView.getAdapter().notifyItemRangeChanged(0, recyclerItemList.size());
}
新增了第三步,调用了notifyItemRangeChanged,让我们看看这个方法:
/**
* Notify any registered observers that the <code>itemCount</code> items starting at
* position <code>positionStart</code> have changed.
* Equivalent to calling <code>notifyItemRangeChanged(position, itemCount, null);</code>.
*
* <p>This is an item change event, not a structural change event. It indicates that
* any reflection of the data in the given position range is out of date and should
* be updated. The items in the given range retain the same identity.</p>
*
* @param positionStart Position of the first item that has changed
* @param itemCount Number of items that have changed
*
* @see #notifyItemChanged(int)
*/
public final void notifyItemRangeChanged(int positionStart, int itemCount) {
mObservable.notifyItemRangeChanged(positionStart, itemCount);
}
通知所有注册的被观察条目更新位置。第一个参数传我们需要更新的起始位置,在这里我们从第一个开始更新(其实可以算一下,从点击的那个位置开始).第二个,需要更新的条目个数,既然一开始我们从第一个开始更新,那么个数就是所有条目,变相为全量更新。
既然想到了全量更新,那么自然就会想到第三种解决办法。
2、采用notifyDataSetChanged全量更新,将代码改为:
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
recyclerItemList.remove(viewHolder.getAdapterPosition());
recyclerView.getAdapter().notifyDataSetChanged();
}
其实notifyDataSetChanged可以看作notifyItemRemoved(notifyItemInserted)和notifyItemRangeChanged的集合。代码量最少,但是属于全量更新,且没有了notifyItemRemoved(notifyItemInserted)的动画。
源码地址:GitHub
更多RecyclerView的探讨文章,可关注本人其他博客。