【问题标题】: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单选按钮在我的回收站视图中无法正常工作。从视图中选择了多个单选按钮,这些单选按钮在焦点上不可见
【发布时间】:2021-11-08 03:59:18
【问题描述】:

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

问题

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

这就像我在 Recycler View 的同一个 View 上按下一样,但图像不同。

问题

【问题讨论】:

  • 我终于完成了我的答案,如果你想检查它,如果你有任何问题,请写评论;)
  • 请提供足够的代码,以便其他人更好地理解或重现问题。

标签: java android android-recyclerview radio-button


【解决方案1】:

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

如果您想在回收站视图中显示 100 个项目,并且其中只有 20 个可以显示给用户,则回收站视图仅创建 20 个视图持有者来代表这 20 个项目,只要用户滚动回收站视图仍将只有 20 个视图持有者,但只会切换存储在此视图持有者中的数据,而不是创建新的视图持有者。

现在要处理您的项目选择,有两种方法可以做到这一点。

天真的方式

  • 将选择保存在回收视图适配器内的布尔数组中。
  • 每当用户滚动时,适配器都会调用 onBindViewHolder 以使用适当的数据更新可见视图。
  • 所以当 onBindViewHolder 被调用时,只需使用方法调用中发送的位置根据布尔数组设置单选按钮选择
  • 在您对回收站视图的使用结束时,您可以在适配器中创建一个 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(如果有一个英文单词称为throwly),您应该使用之前创建的所有上述类初始化跟踪器,其余的将由他处理,跟踪器作为参数
  1. 唯一的选择跟踪器 ID(如果这将是您将使用的唯一选择跟踪器,则将其命名)
  2. 我们创建的 ItemKeyProvider
  3. 我们创建的 DetailsLookup
  4. String-Long-Parcelable Storage 用于存储在其中选择的键(在我们的例子中,它将是 String Storage)
  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)

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

最后,如果您有兴趣阅读文档中有关选择跟踪器的内容,请关注link

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

implementing-selection-in-recyclerview

a guide to recyclerview selection

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

【讨论】:

  • 哦,现在不应该发布,点击发布错误回答,给我一些时间来包括其他方式
  • 结束,这是迄今为止我在任何地方写的最长的答案,但我很满意,即使它不能满足你的问题作为答案:D
  • 注:代码中的任何三个点 (...) 表示我不关心此处编写的代码,您应该包含您的代码
【解决方案2】:

Omar Shawky 已经介绍了解决方案。

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

原因:

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

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

解决方案:

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

例如要在 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();
   }

【讨论】:

    猜你喜欢
    • 2016-05-15
    • 2016-06-17
    • 1970-01-01
    • 2014-06-19
    • 2018-09-19
    • 2017-07-24
    • 2019-01-16
    • 2011-07-12
    • 1970-01-01
    相关资源
    最近更新 更多