单选按钮在我的 Recycler 视图中无法正常工作。从视图中选择了多个单选按钮,这些按钮在聚焦的视图中不可见

Radio button are not working normally in my Recycler View. Multiple Radio Buttons got selected of the view which are not visible with the focused one

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

问题

每当我 select 或 deselect 回收站视图中可见视图中的单选按钮时,可见屏幕之外的一些其他视图得到 selected 或 deselected.

就像我按的是 Recycler View 的同一个 View,但是图像不一样。

问题

那是因为回收器视图的概念是重复使用视图而不是每次滚动时都创建新视图。

你看看你是否有 100 个项目要在回收站视图中显示,而其中只有 20 个可以显示给用户,回收站视图只创建 20 个视图持有者来表示这 20 个项目,每当用户滚动回收站视图仍然只有 20 个视图持有者,但只会切换存储在这个视图持有者中的数据,而不是创建新的视图持有者。

现在要处理 select您的物品,有两种方法可以做到这一点。

天真的方式

  • 将 selection 保存在回收视图适配器内的布尔数组中。
  • 每当用户滚动时,适配器都会调用 onBindViewHolder 以使用正确的数据更新可见的查看器。
  • 因此,当调用 onBindViewHolder 时,只需使用方法调用中发送的位置
  • 根据布尔数组设置单选按钮 selection
  • 在使用回收器视图结束时,您可以在适配器中创建一个 getter 方法来获取布尔值的 selection 数组列表并基于它传递数据
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) ;

                }
            });
            ...
        }
    }

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

这种方式的问题是它需要对代码进行大量编辑并创建新的 classes 以作为参数包含在 selection tracker 中,但最后你会觉得花时间在上面是值得的。

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

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

  • 其次,您需要创建两个新的classes,一个扩展ItemDetailsLookup,另一个扩展ItemKeyProvider,并且应该使用密钥作为它们的通用类型(放在 <> 之间的类型)
    你的两个 classes 应该看起来像这样(你可以直接复制它们)

扩展 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;
    }
}
  • 第三,您应该在您的适配器中包含这两个新方法,以供上述 classes
  • 使用
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),你应该用之前创建的所有上述classes初始化跟踪器,他会处理剩下的,跟踪器作为参数
  1. 一个唯一的select离子跟踪器 ID(如果那将是您将使用的唯一 select离子跟踪器,则可以任意命名)
  2. 我们创建的 ItemKeyProvider
  3. 我们创建的 DetailsLookup
  4. 一个 String-Long-Parcelable 存储来存储 selected 的键(在我们的例子中它将是一个字符串存储)
  5. 一个Selection predicate,它负责处理你想做的selection的方式,你希望它能够(select only one item-multiple select没有限制的离子-基于一种奇怪的算法,例如仅偶数或仅奇数),在我的情况下,我将使用默认的多个 selection 算法,但是如果您想使用另一种 selection 算法来更改它你应该创建一个新的 class 扩展 SelectionPredicates 并实现你的 selection 方式,你也可以只检查其他默认的可能是你的寻找。

无论如何,这就是初始化的样子(无论是在片段还是 activity 方法中,您都应该将这段代码放在初始化回收器视图的任何地方):

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();
         ...
}
  • 我没有找到让我用数据初始化适配器然后创建跟踪器以使查看者知道他们的 selection 的方法,所以在这种情况下我首先创建了跟踪器,然后使用 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();
         ...
}
  • 最后但同样重要的是,您应该处理视图持有者应该如何知道它们是否被 selected,因此您应该通过创建一个 [=154] 让适配器知道跟踪器及其数据=] 方法,适配器最终应该是这样的:

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

(你可能会注意到,如果你通过构造函数用它的数据初始化适配器,当他询问跟踪器是否有一个项目 selected 或没有时,它会导致 NullPointerException初始化适配器的那一刻你还没有初始化跟踪器)

  • 这样你就可以按照 google 在他们的文档中建议的方式跟踪你的 selection(老实说,我不知道为什么它会变得非常复杂,比如那个)。

  • 如果你想知道你 application/fragment 使用结束时所有的 selected 项目,你应该调用 tracker.getSelection() 它将 return 供您迭代的选择列表

  • 跟踪器有一个很小的 ​​problem/feature 它不会开始 select 第一个项目,直到你长按它,这只发生在第一项 select,如果您确实需要此功能(通过长按启动 selecting 模式),请保持原样
    如果你不想要它,你可以在开始时让跟踪器 select 成为幽灵键(任何对你的数据没有任何意义的唯一字符串键),稍后应该使用一个简单的启用 selection 模式点击任何照片

        tracker.select("");

这也是在开头制作 default/old selection 的方法,如果您确实希望跟踪器以 few 开始,您可以制作一个 for 循环并调用 tracker.select(Key)项目正在 selected

N.B :如果你使用 Ghost Key 方法,你应该注意 selection 数组将 returned 时你调用 tracker.getSelection() 也会包含这个 Ghost Key。

最后,如果您确实有兴趣阅读文档中有关 selection tracker 的内容,请遵循此 link

或者,如果您知道如何阅读 kotlin,请访问这两个链接

implementing-selection-in-recyclerview

a guide to recyclerview selection

我在 selection 问题上被困了好几天,然后才想出如何做所有这些,所以我希望你能找到解决它的方法。

Omar Shawky 已涵盖解决方案。

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

原因:

出现此问题是因为 RecyclerView 回收了视图。因此,RecyclerView 项目的视图一旦膨胀就可以重新用于显示另一个屏幕外(要滚动到)项目。这有助于减少重新inflation 的观看次数,否则可能会造成负担。

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

解法:

此类问题的最简单解决方案是在您的 ViewHolder 中使用 if else 逻辑来为 selectedde-selected[=42= 提供逻辑] 例。我们也不依赖单选按钮本身的信息进行初始设置(我们在设置时不使用 radioButton.isSelected())

例如要在 ViewHolder 中写入的代码 class:

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