单选按钮在我的回收器视图中无法正常工作.视图中选择了多个单选按钮,这些单选按钮与聚焦的单选按钮不可见



我使用Recycler View在网格布局管理器中显示来自厨房或设备外部存储的所有图像。我用一个单选按钮来显示图像是否被选中。

问题

每当我从回收器视图中的可见视图中选择或取消选择单选按钮时,可见屏幕外的其他一些视图就会被选中或取消选中。

这就像我在按回收器视图的同一视图,但图像不同。

问题

这是因为循环器视图的概念是重用视图,而不是每次滚动都创建新的视图。

您可以看到,如果您有100个项目要显示在回收器视图中,而其中只有20个项目可以显示给用户,则回收器视图仅创建20个视图保持器来表示这20个项目,无论何时用户滚动回收器视图仍将只有20个视图持握器,但只会切换存储在该视图保持器中的数据,而不是创建新的视图保持器。

现在要处理项目的选择,有两种方法。

天真的方式

  • 在回收视图适配器内的布尔数组中保存选择
  • 每当用户滚动时,适配器都会调用BindViewHolder来用正确的数据更新可见的viewholder
  • 因此,当调用onBindViewHolder时,只需使用方法调用中发送的位置根据布尔数组设置单选按钮选择
  • 在对recyclerator视图的使用结束时,可以在适配器中创建一个getter方法,以获取布尔值的选择数组列表,并基于该列表传递数据
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_ClassType> data;
ArrayList<Boolean> dataSelected ;
public PhotosGalleryAdapter(ArrayList<Your_Data_ClassType> data) {
this.data = data;
dataSelected = new ArrayList<>(data.size()) ;
}
...
@Override
public void onBindViewHolder(@NonNull PhotosGalleryViewHolder holder, int position) {
...
RadioButton radioButton = holder.getRadioButton()
radioButton.setChecked(dataSelected.get(position));
radioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
dataSelected.set(holder.getAbsoluteAdapterPosition() , isChecked) ;
}
});
...
}
}

另一种方法是使用选择跟踪器,它应该是在回收器视图中处理选择的正确方法。

这种方法的问题是,它需要对代码进行大量编辑,并创建新的类作为参数包含在选择跟踪器中,但最终你会发现花在上面的时间是值得的。

为了以这种方式开始,您需要执行以下操作:

  • 首先,决定什么应该是密钥(String Long Parcelable),这样跟踪器就应该用来区分你的数据,最安全的方法是String或Parcelable,因为我曾经尝试过Long,但最终遇到了很多问题(在你的情况下,我认为这是照片的uri,类型为String)

  • 其次,您需要创建两个新类,一个扩展ItemDetailsLookup,另一个扩展了ItemKeyProvider,并且应该使用键作为它们的泛型类型(介于<>之间的类型)
    您的两个类应该是这样的(您可以直接复制它们)

扩展ItemKeyProvider的程序

public class GalleryItemKeyProvider extends ItemKeyProvider<String>{
PhotosGalleryAdapter adapter ;
/**
* Creates a new provider with the given scope.
*
* @param scope Scope can't be changed at runtime.
*/
public GalleryItemKeyProvider(int scope,PhotosGalleryAdapter m_adapter) {
super(scope);
this.adapter = m_adapter;
}
@Nullable
@Override
public String getKey(int position) {
return adapter.getKey(position);
}
@Override
public int getPosition(@NonNull String key) {
return adapter.getPosition(key);
}
}

扩展ItemDetailsLookup的程序:

public class GalleryDetailsLookup extends ItemDetailsLookup<String> {
private final RecyclerView recView ;
public GalleryDetailsLookup(RecyclerView m_recView){
this.recView = m_recView;
}
@Nullable
@Override
public ItemDetails<String> getItemDetails(@NonNull MotionEvent e) {
View view = recView.findChildViewUnder(e.getX(), e.getY());
if (view != null) {
RecyclerView.ViewHolder holder = recView.getChildViewHolder(view);
if (holder instanceof PhotosGalleryViewHolder) {
return ((PhotosGalleryViewHolder) holder).getItemDetails();
}
}
return null;
}
}
  • 第三,您应该在适配器中包含这两个新方法,供上述类使用
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
...
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
...
}
  • forthly(如果有一个英文单词叫forthly),你应该用前面创建的所有类初始化跟踪器,他将处理剩下的,跟踪器将作为参数
  1. 一个唯一的选择跟踪器id(如果这将是您将使用的唯一选择跟踪器,则将其命名为任何名称)
  2. 我们创建的ItemKeyProvider
  3. 我们创建的DetailsLookup
  4. 字符串长Parcelable存储,用于存储在中选择的密钥(在我们的情况下,它将是字符串存储)
  5. 一个选择谓词,它负责处理你想要做的选择方式,你希望它能够(只选择一个项目,无限制地多次选择-基于一种奇怪的算法,如仅偶数或仅奇数),在我的情况下,我将使用默认的多选算法,但如果你想用另一种选择算法来更改它,你应该创建一个扩展SelectionPredicates的新类,并实现你的选择方式,你也可以只检查其他默认的可能是你想要的

无论如何,初始化应该是这样的(无论是片段还是活动方法,您都应该将此代码放在初始化回收器视图的任何位置):

private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
...
}
  • 我没有找到一种方法让我用数据初始化适配器,然后创建跟踪器,以使视图持有者知道他们的选择与否,所以在这种情况下,我首先创建了跟踪器,然后使用setter和notifyDataSetChanged让适配器知道它的数据我的意思是,在创建跟踪器之后,立即将跟踪器和数据设置到适配器中,所以initRecycleView应该是这样的
private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
photosAdapter.setTracker(tracker);
photosAdapter.setData(data);
photosAdapter.notifyDataSetChanged();
...
}
  • 最后但同样重要的是,你应该处理视图持有者应该如何知道他们是否被选中,所以你应该通过在其中创建一个setter方法来让适配器知道跟踪器及其数据,这就是适配器最终的样子:

public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_Class> data;
private SelectionTracker<String> tracker;
public PhotosGalleryAdapter() {
data = new ArrayList<>();
}
public ArrayList<Your_Data_Class> getData() {
return data;
}
public void setData(ArrayList<Your_Data_Class> m_data) {
this.data = m_data;
}
@Override
public ScheduleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
...
}
@Override
public void onBindViewHolder(@NonNull PhotosGalleryViewHolder holder, int position) {
...
boolean isSelected = tracker.isSelected(data.get(i).getUri());
RadioButton radioButton = holder.getRadioButton;
radioButton.setChecked(isSelected);
}
@Override
public int getItemCount() {
return data.size();
}
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
public void setTracker(SelectionTracker<String> m_tracker) {
this.tracker = m_tracker;
}
}

(正如您可能注意到的那样,如果您通过构造函数用其数据初始化适配器,当他询问跟踪器是否选择了项时,它将导致NullPointerException,因为在初始化适配器时,您仍然没有初始化跟踪器)

  • 这样你就可以像谷歌在文档中建议的那样跟踪你的选择(老实说,我不知道为什么会让它变得如此复杂)。

  • 如果您想在应用程序/片段使用结束时知道所有选定的项目,您应该调用tracker.getSelection(),它将返回一个选择列表,供您在上迭代

  • 跟踪器有一个小问题/功能,在你长按它之前,它不会开始选择第一个项目,这只发生在你选择的第一个项目中,如果你确实想要这个功能(长按开始选择模式),那么保持原样
    如果你不想要,你可以让跟踪器在开始时选择一个重影键(任何对你的数据毫无意义的唯一字符串键),稍后只需点击任何照片即可启用选择模式

tracker.select("");

这也是在开始时进行默认/旧选择的方法,如果您确实希望跟踪器从选择的少数项目开始,则可以进行for循环并调用tracker.select(Key)

N。B:如果使用Ghost Key方法,则应注意在调用tracker.getSelection()时返回的选择数组也将包含此Ghost Key。

最后,如果你有兴趣阅读文档中的选择跟踪器,请点击链接

或者如果你知道如何阅读kotlin跟随这两个链接

在再循环视图中实现选择

回收视图选择指南

在我想好如何做到这一切之前,我在选择问题上被困了好几天,所以我希望你能找到解决问题的方法

Omar Shawky介绍了解决方案。

在我的回答中,我将强调为什么有人可能会面临这种与回收商观点有关的问题,以及如何在未来避免这种常见问题(避免陷阱)

原因:

出现此问题是因为RecyclerView回收视图。因此,RecyclerView项目的视图一旦膨胀,就可以重复使用,以显示屏幕外的另一个项目(滚动到)。这有助于减少观点的再次膨胀,否则可能会带来税收。

因此,如果一个项目视图的单选按钮被选中,并且同一个视图被重用以显示其他项目,那么这个新项目也可以有一个选中的单选按钮。

解决方案:

对于此类问题,最简单的解决方案是在ViewHolder中使用if-else逻辑,为选中

取消选中例如,在ViewHolder类中编写的代码:

private boolean isRadioButtonChecked = false; // ViewHolder class level variable. Default value is unchecked
// Now while binding in your ViewHolder class:
// Setup Radio button (assuming there is just one radio button for a recyclerView item). 
// Handle both selected and de-selected cases like below (code can be simplified but elaborating for understanding):
if (isRadioButtonChecked) {
radioButton.setChecked(true);
} else {
radioButton.setChecked(false);
}
radioButton.setOnCheckedChangeListener(
(radioButton, isChecked) -> isRadioButtonChecked = isChecked);

设置时不要执行以下任何操作:

private boolean isRadioButtonChecked = false; // class variable
//while binding do not only handle select case. We should handle both cases.
if (isRadioButtonChecked) { // --> Pitfall 
radioButton.setChecked(true);
}
radioButton.setOnCheckedChangeListener((radioButton, isChecked) -> isRadioButtonChecked = isChecked);

// During initial setup do not use radio button itself to get information.
if (radioButton.isChecked()) { // --> Pitfall
radioButton.setChecked();
}

最新更新