为啥在recyclerview android中滚动后突出显示的项目丢失

Posted

技术标签:

【中文标题】为啥在recyclerview android中滚动后突出显示的项目丢失【英文标题】:Why are highlighted items lost after scrolling in recylerview android为什么在recyclerview android中滚动后突出显示的项目丢失 【发布时间】:2021-12-15 01:13:25 【问题描述】:

我想在适配器类中的操作模式处于活动状态时突出显示该项目。我可以这样做,但滚动后突出显示状态消失了。我尝试了各种解决方案,但我不明白为什么会这样?

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder>  

   
    class MyViewHolder extends RecyclerView.ViewHolder 
 
     
     public void bind(Items viewHolder_item)      

       
            itemView.setOnLongClickListener(new View.OnLongClickListener() 
            @Override
            public boolean onLongClick(View v) 
                isSelectMode = true;
                if (viewHolder_item.getIsSelect())
                    
                    itemView.setBackgroundColor(Color.TRANSPARENT);
                    item.get(position).setSelect(false);
                    selectedList.remove(item.get(position));
                 else 
                    itemView.setBackgroundColor(Color.GRAY);
                    item.get(position).setSelect(true);
                    selectedList.add(item.get(position));
                
                if (selectList.size() == 0)
                    isSelectMode = false;
                
                return true;
            
        );
        itemView.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                if (isSelectMode)
                    if (viewHolder_item.getIsSelect())
                        itemView.setBackgroundColor(Color.TRANSPARENT);
                        item.get(position).setSelect(false);
                        selectedList.remove(item.get(position));
                     else 
                                                  
                       itemView.setBackgroundColor(Color.GRAY);
                       item.get(position).setSelect(true);   
                       selectedList.add(item.get(position));                      
                    
                    if (selectList.size() == 0)
                        isSelectMode = false;
                    
                
            
        );

无论实施哪种解决方案,结果总是相同的。滚动后突出显示的颜色消失了。任何帮助将不胜感激。

【问题讨论】:

在您的代码中,您可以同时使用 backgroundColor 和 backgroundResource。尝试只使用一个。同样在 onBindViewHolder 而不是调用 getadapterposition() 您可以简单地使用方法提供的位置 感谢您的回答,但即使进行了这些更改,结果也是一样的。 我在问题上方也进行了编辑,以向您展示实现。 在 onBindViewHolder 方法的第一个 if 语句中添加 Log.d() 以查看 contains 子句是否正常工作,即找出错误是否发生在绑定或设置红色时 我在 if 块中添加了Log.e("TAG", " contains " + selectedList.contains(items.get(position)));,如上所示。即使您突出显示一个项目并向上滚动,它也总是返回 false。 【参考方案1】:

我建议您采用替代方法来解决您的问题。在您的 Item 对象中添加一个名为 public boolean isSelected=false 的布尔字段,然后在单击项目时使用 items.get(position).isSelected=true 在您的 items 列表中将该字段设置为 true,然后使用此字段来评估每个项目应该处于哪个状态,而不是使用您的selectedList。你实际上不再需要这个列表了。

所以现在你只需要修改你的 bindViewHolder 如下:

@Override
    public void onBindViewHolder(@NonNull MyAdapter.MyViewHolder holder, int position) 
        Items item=items.get(position);
            
        if (!item.getIsSelect())
               holder.itemView.setBackgroundColor(Color.TRANSPARENT);
             else 
            holder.itemView.setBackgroundColor(activity.getResources().getColor(R.color.red));
            

            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() 
                @Override
                public boolean onLongClick(View v) 
                    isSelectMode = true;
                    if (item.getIsSelect())
                        holder.itemView.setBackgroundColor(Color.TRANSPARENT);
                        items.get(position).setSelect(false);
                        selectedList.remove(items.get(position));
                     else 
                        holder.itemView.setBackgroundColor(activity.getResources().getColor(R.color.red));
                        items.get(position).setSelect(true);
                        selectedList.add(items.get(position));
                    
                    if (selectList.size() == 0)
                        isSelectMode = false;
                    
                    return true;
                
            );
            holder.itemView.setOnClickListener(new View.OnClickListener() 
                @Override
                public void onClick(View v) 
                    if (isSelectMode)
                        if (item.getIsSelect())
                            holder.itemView.setBackgroundColor(Color.TRANSPARENT);
                            items.get(position).setSelect(false);
                            selectedList.remove(items.get(position));
                         else 
                                                      holder.itemView.setBackgroundColor(activity.getResources().getColor(R.color.red));
                        items.get(position).setSelect(true);                         
                        
                        if (selectList.size() == 0)
                            isSelectMode = false;
                        
                    
                
            );

        

顺便说一句。您不需要将 isSelect 添加到 Items 的构造函数中,因为这应该是一个临时会话特定变量

有很多实例会使 contains 无法正常工作,正如您所期望的那样,我们可能必须深入您的 Item 对象才能找到原因这里只是一个链接Why is .contains returning false when I add a duplicate?

【讨论】:

恐怕它给出的结果和以前一样。我一直在尝试各种方法,但都做不到。 这毫无意义。您能否再次在 if 语句中添加日志并记录 isSelect() 变量。同样在您的片段类中,我看不到您创建项目对象的位置,请您证明一下 我已经在片段类中添加了项目创建。 至于这条语句Log.e("TAG","isSelect " + item.IsSelect());它总是像以前一样返回false。 抱歉,我显然忘了将 setSelect 添加到我的代码中。我的错,现在就做。再试一次,让我知道【参考方案2】:

对现有答案的一些补充:

    将您的适配器数据视为不可变。不要改变适配器内的数据。适配器需要嗯...将数据从模型适应到视图(Holder)中。它会根据定位跟踪每个数据所属的位置,这几乎就是它需要做的所有事情(除了自然地膨胀视图)。 如果您提供Thing 的列表,其中isSelected 不是模型的一部分,而是您需要跟踪用户是否选择它的一部分,然后创建一个简单的数据类,如
data class ThingSelection(val thing: Thing, val isSelected: Boolean)

并在您的适配器中使用它,而不仅仅是 List&lt;Thing&gt;,使用 List&lt;ThingSelection&gt;

    不要做你在onBindViewHolder 中所做的事情。你应该做的就是
val item = getItem(position)
holder.bind(item)

ViewHolder 的工作是根据您的业务规则设置其属性、背景颜色等。

如果你的“项目”是ThingSelection,那么 viewHolder 的绑定方法就可以了

if (item.isSelected) ...

也将您需要的任何其他内容传递给此方法,bind(...) 就是您需要的任何内容。

当用户改变item选择时怎么办?

你有一个点击监听器,但是不要改变数据,让你的回调/监听器将发生了什么传递给调用者。

我想在你的 viewHolder 绑定方法中你会做这样的事情:

view.setOnClickListener 
   externalListenerThatTheAdapterReceivedFromTheOutside.onThingClicked(thing, thing.isSelected)


这个外部听众有责任:

    尽其所能(可能直接将其传递给 ViewModel,以便它处理这个新事件),以确保适配器接收新数据(现在 Thing.isSelected 是正确的,因为用户做了什么)。

    确保将新的不可变列表提供给适配器,以便适​​配器可以计算其新数据并进行更新(提示:使用 ListAdapter&lt;T, K&gt;DiffUtil.ItemCallback&lt;T&gt;,这会让您的生活更轻松。

更新:数据类

data class ItemWithSelection(val item: Item, val isSelected: Boolean)

然后将您的适配器更改为:

class YourAdapter(): ListAdapter<ItemWithSelection, YourViewHolder>(DiffUtilCallback()) 
  // You only need to override these
    
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): YourViewHolder 
        val view = LayoutInflater.from(parent.context).inflate(R.layout.your_item_layout, parent, false) // or use ViewBinding, whatever.
        return YourViewHolder(view) // you can pass more stuff if you need here, like a listener...
    

    override fun onBindViewHolder(holder: YourViewHolder, position: Int) 
        val item = getItem(position)
        holder.bindData(item, item.isSelected) //Item is of type ItemWithSelection...
    


// Somewhere in your ViewHolder...
fun bindData(item: ItemWithSelection, isSelected: Boolean) 
   // do what it needs, e.g.:
   if (isSelected)  ...  



// You also need a DiffUtilCallback...
internal class DiffUtil.ItemCallback<ItemWithSelection>() 

    internal class DiffUtilCallback : DiffUtil.ItemCallback<ItemWithSelection>() 
        override fun areItemsTheSame(oldItem: ItemWithSelection, newItem: ItemWithSelection) = oldItem.item.xxx == newItem.item.xxx

        override fun areContentsTheSame(oldItem: ItemWithSelection, newItem: ItemWithSelection) = oldItem == newItem
    

所有这些都连接起来......

在您的ViewModel 或“获取数据的层”(很可能不是片段/活动)中提供List&lt;ItemWithSelection&gt;...

如何构造这个真的取决于你如何存储选择的内容。

但为了简单起见,假设您有两个列表:

val all: List<Item>
val selected: List<Item>

当需要为 UI 生成列表时,您可以(注意:我假设您使用协程......您可以选择自己的异步/反应式编程风格:

class YourViewModel: ViewModel() 

fun getYourListWithSelections() 
        viewModelScope.launch(Dispatchers.Computation) //for e.g.
            val tempSelection = mutableListOf<ItemWithSelection>()
            all.mapTo(tempSelection) 
                    ItemWithSelection(item = it, isSelected = selection.contains(it))
                

           // Assuming you have a MutableLiveData somewhere in this viewmodel...
           _uiState.postValue(SomeSealedClass.ListAvailable(tempSelection))
        

这是从Fragment中自然观察到的……

class YourFragment: Fragment() 

  fun observe() 
       viewModel.uiState.observe(viewLifecycleOwner,  viewState ->
            when (viewState) 
                is SomeSealedClass.ListAvailable -> adapter.submitList(viewState.items.toMutableList())//need to pass a new list or ListAdapter won't refresh the list.
                else ... //exercise for the reader ;)
            
        )
  


这里真正缺少的只是样板文件。

    ViewHolder(在 View 上设置 click listener)如何将信息传递回 ViewModel? 为您的适配器提供您选择的侦听器:
interface ItemClickListener 
  fun onItemClicked(val item: Item, val isSelected: Boolean)

实现它并在您创建它时将其传递给您的适配器:

class YourAdapter(private val listener: ItemClickListener): ListAdapter<ItemWithSelection, YourViewHolder>(DiffUtilCallback()) 

并将其传递到您的片段中:

class YourFragment: Fragment() 

private val listener = object : ItemClickListener 
   fun onItemClicked(val item: Item, val isSelected: Boolean) 
      // pass it directly, it's the VM job to deal with this and produce a new list via LiveData.
      viewModel.onItemClicked(item, isSelected)
   

val adapter = YourAdapter(listener)

希望对您有所帮助。

【讨论】:

感谢您的回答。我按照您的建议从适配器中移动了数据,并在上面的代码中也进行了更改,但我对关于简单数据类的第二点感到困惑。我不知道如何创建和使用一个。如果你能用一点代码解释一下,那将是非常有帮助的。

以上是关于为啥在recyclerview android中滚动后突出显示的项目丢失的主要内容,如果未能解决你的问题,请参考以下文章

为啥在recyclerview android中滚动后突出显示的项目丢失

为啥 CardView 和 RecyclerView 需要 minSdkVersion L?

为啥 RecyclerView 项目在 GridLayoutManager 中随机移动位置?

为啥 RecyclerView 项目在点击两次后才改变背景颜色?

为啥即使在自定义 OnTouchListener 实现后 RecyclerView 仍然滚动?

为啥 recyclerview$adapter 在片段中为空