当前位置:网站首页>Research on recyclerview details - Discussion and repair of recyclerview click dislocation

Research on recyclerview details - Discussion and repair of recyclerview click dislocation

2022-04-23 14:08:00 Senzhiqianshou

First describe the problem . for instance , I have a list , The order of entries should be... From top to bottom 0,1,2…; Then when I delete the second entry , The original third item should become the current second item , Then I click on the second item now , The position you should get is 1, But the actual position is 2:
 Click dislocation repair
It's still painful to have this problem , Because other functions are good , Only this position is misplaced . To solve this problem , We have to figure out another problem :ViewHolder Of getPosition、getAdapterPosition and getLayoutPosition Have you figured out ?

Why did I ask this question , Let's review our writing ( Refer to my other article RecyclerView Advanced use ( One )- Simple implementation of sideslip deletion ):

We are customizing CallBack Inside onSwiped It's written like this :

@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
    
    recyclerItemList.remove(viewHolder.getAdapterPosition());
    recyclerView.getAdapter().notifyItemRemoved(viewHolder.getAdapterPosition());
}

When we were taking positions , It's using getAdapterPosition. Actually get position There are four ways :

/** * @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;
}

Altogether getPositiongetLayoutPositiongetAdapterPosition and getOldPosition. therefore , Why are we taking position Why did you take it when you were getAdapterPosition Well , There is no explanation in the online articles , It's all a generalization . But now the problem is , We're going to study .

First of all, from the notes ,getOldPosition It's related to animation , We won't discuss it here . Then there are only three left .
getPosition Used **@Deprecated** annotation , It shows that it is an abandoned method . Why abandon it .
Translated into English :* This method is deprecated , Because its meaning is unclear due to the asynchronous processing of adapter updates .* Let's switch to getLayoutPosition perhaps getAdapterPosition. From the source code ,getPosition and getLayoutPosition The code for is the same , We can argue that getLayoutPosition It's the old getPosition. Then there is nothing left getLayoutPosition and getAdapterPosition A comparison of . And our problem is that we use getAdapterPosition.

getLayoutPosition Comment means to return ViewHolder Position in the latest layout process .
getAdapterPosition The comment means to return this ViewHolder It means item The location of the adapter .

Just to see if it means more awkward . I found it here StackFlow A summary of an engineer on the :
https://stackoverflow.com/questions/29684154/recyclerview-viewholder-getlayoutposition-vs-getadapterposition
I briefly summarized :
1、 In general , The two are the same .
2、getAdapterPosition Name , Get the location on the adapter . because RecyclerView Than ListView Evolved local refresh function , So in ListView The unification and renewal of the times getPosition The way is going to change .RecylerView There are essentially two lists , A storage data source , A storage view ViewHolder. Through the observer model , Data source driven view updates . therefore , You can simply say getAdapterPosition Is the location on the acquired data source .
3、getLayoutPostion Is to get the view position of the list drawn last time . Especially when using local refresh ( As long as it's not notifyDataSetChanged) During these operations , It is recommended to use this method to get the location . comparison getAdapterPosition, Can get the changed position immediately .

With the above summary , Then we have a way to solve this problem , Change the code to :

@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
    
    recyclerItemList.remove(viewHolder.getLayoutPosition());
    recyclerView.getAdapter().notifyItemRemoved(viewHolder.getLayoutPosition());
}

Then try again , It turns out it's not going to work .
What's the problem ?
Let's take a look at the place where you set the item click event :

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;
    }

}

The rest of the code can be ignored , Just look onBindViewHolder It set up onClickListener The place of . It's from here position Directly from the outside position Got . But in fact, external position still adapterPosition, Why do you say that :

/** * 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);
 }

See the comments , Say this postion within the adapter’s data set. therefore , If we click on the event, we can't transmit this position. Change it to the following way :

@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());
            }
        }
    });
}

Retest :
 Click to repair the dislocation successfully
Perfect solution .

In addition to the above solution of comparing source flow , Is there any other solution ?
The answer is yes , And there are two :
1、 Do not use getLayoutPosition, Still use getAdapterPosition. So our onSwiped The method needs to be modified to :

@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
    
    //1、 Delete source data 
    int pos = viewHolder.getAdapterPosition();
    RecyclerItem item = recyclerItemList.get(pos);
    recyclerItemList.remove(item);
    //2、 Notify view refresh 
    recyclerView.getAdapter().notifyItemRemoved(viewHolder.getAdapterPosition());
    //3、 correct position, Prevent when clicking ,position Disorder 
    recyclerView.getAdapter().notifyItemRangeChanged(0, recyclerItemList.size());
}

Added a third step , Called notifyItemRangeChanged, Let's look at this method :

/** * 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);
 }

Notify all registered observed entries to update the location . The first parameter is the starting position we need to update , Here we start with the first update ( In fact, you can calculate , Start from where you click ). the second , Number of entries to be updated , Since we started with the first update , Then the number is all entries , Disguised as full update .
Now that I think of all the updates , Then it's natural to think of a third solution .

2、 use notifyDataSetChanged Full update , Change the code to :

@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
    
   recyclerItemList.remove(viewHolder.getAdapterPosition());
   recyclerView.getAdapter().notifyDataSetChanged();
}

Actually notifyDataSetChanged Can be seen as notifyItemRemoved(notifyItemInserted) and notifyItemRangeChanged Set . The least amount of code , But it's a full update , And period notifyItemRemoved(notifyItemInserted) Animation .

Source code address :GitHub
more RecyclerView Discussion articles , You can follow my other blogs .

版权声明
本文为[Senzhiqianshou]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/04/202204231405037310.html