当前位置:网站首页>RecyclerView细节研究-RecyclerView点击错位问题的探讨与修复

RecyclerView细节研究-RecyclerView点击错位问题的探讨与修复

2022-04-23 14:06:00 森之千手

先描述一下这个问题。举个例子,我有一个列表,其条目的顺序从上到下应该是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;
}

一共有getPositiongetLayoutPositiongetAdapterPositiongetOldPosition。因此,我们为什么在取position的时候为什么就取了getAdapterPosition呢,网上的文章都没有给出解释,都是一概而论。但是现在问题出现了,我们就要研究一下了。

首先从注释上看,getOldPosition是和动画相关的,我们这里暂不讨论。那么就剩下三个了。
getPosition使用了**@Deprecated**注解,表明它是一个废弃的方法。为什么要废弃呢。
英文翻译为:*此方法已被弃用,因为它的含义由于适配器更新的异步处理而不明确.*让我们改用getLayoutPosition或者getAdapterPosition。再从源码上看,getPosition和getLayoutPosition的代码是一样的,我们可以认为getLayoutPosition就是以前的getPosition。那么就剩下getLayoutPosition和getAdapterPosition的对比了。而我们的问题也出现在了我们使用了getAdapterPosition。

getLayoutPosition的注释意思是返回ViewHolder在最新布局过程中的位置。
getAdapterPosition的注释意思是返回此ViewHolder表示的item的适配器位置。

光看意思是不是比较拗口。我这里找来了StackFlow上某位工程师的总结:
https://stackoverflow.com/questions/29684154/recyclerview-viewholder-getlayoutposition-vs-getadapterposition
我简单地总结了一下:
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的探讨文章,可关注本人其他博客。

版权声明
本文为[森之千手]所创,转载请带上原文链接,感谢
https://blog.csdn.net/cjs1534717040/article/details/113862887