单选按钮在我的回收站视图中无法正常工作.多个单选按钮被选中的视图与焦点不可见 [英] 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

查看:30
本文介绍了单选按钮在我的回收站视图中无法正常工作.多个单选按钮被选中的视图与焦点不可见的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

问题

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

这就像我按下了 Recycler View 的同一个 View,但图像不同.

问题

解决方案

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

如果您想在回收站视图中显示 100 个项目,而其中只有 20 个可以向用户显示,回收站视图仅创建 20 个视图持有者来表示 20 个项目,每当用户滚动回收站视图时只有 20 个视图持有者,但只会切换存储在此视图持有者中的数据,而不是创建新的视图持有者.

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

天真的方法

  • 在回收视图适配器内的布尔数组中保存选择.
  • 每当用户滚动时,适配器都会调用 onBindViewHolder 以使用正确的数据更新可见视图.
  • 因此,当 onBindViewHolder 被调用时,只需使用方法调用中发送的位置根据布尔数组设置单选按钮选择
  • 在对回收器视图的使用结束时,您可以在适配器中创建一个 getter 方法来获取布尔值的选择数组列表并基于它传递数据

public class PhotosGalleryAdapter extends RecyclerView.Adapter{ArrayList数据;ArrayList数据选择;公共照片画廊适配器(ArrayList 数据){this.data = 数据;dataSelected = new ArrayList<>(data.size());}...@覆盖public void onBindViewHolder(@NonNull PhotosGalleryViewHolder holder, int position) {...RadioButton radioButton = holder.getRadioButton()radioButton.setChecked(dataSelected.get(position));radioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {@覆盖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{相册适配器适配器;/*** 创建具有给定范围的新提供程序.** @param scope 作用域不能在运行时改变.*/public GalleryItemKeyProvider(int范围,PhotosGalleryAdapter m_adapter){超级(范围);this.adapter = m_adapter;}@Nullable@覆盖公共字符串 getKey(int position) {返回 adapter.getKey(position);}@覆盖public int getPosition(@NonNull String key) {返回适配器.getPosition(key);}}

扩展 ItemDetailsLookup 的那个:

public class GalleryDetailsLookup extends ItemDetailsLookup{私人最终 RecyclerView recView ;公共画廊详细信息Lookup(RecyclerView m_recView){this.recView = m_recView;}@Nullable@覆盖public ItemDetailsgetItemDetails(@NonNull MotionEvent e) {查看视图 = recView.findChildViewUnder(e.getX(), e.getY());如果(查看!= null){RecyclerView.ViewHolder holder = recView.getChildViewHolder(view);if (holder instanceof PhotosGalleryViewHolder) {返回 ((PhotosGalleryViewHolder) 持有人).getItemDetails();}}返回空;}}

  • 第三,您应该在适配器中包含这两个新方法,供上述类使用

public class PhotosGalleryAdapter extends RecyclerView.Adapter{...公共字符串 getKey(int position) {返回 data.get(position).getUri();}公共 int getPosition(字符串键){for (int i = 0; i < data.size(); i++) {if (data.get(i).getUri() == key) 返回 i;}返回0;}...}

  • 第四点(如果有英文单词forthly),你应该用上面创建的所有类初始化跟踪器,他会处理剩下的,跟踪器作为参数

  1. 唯一的选择跟踪器 ID(如果这将是您将使用的唯一选择跟踪器,则可以任意命名)
  2. 我们创建的 ItemKeyProvider
  3. 我们创建的 DetailsLookup
  4. 一个 String-Long-Parcelable Storage 用于存储在其中选择的键(在我们的例子中它将是一个 String Storage)
  5. 一个选择谓词,它负责处理您想要做的选择方式,您希望它能够(仅选择一个项目 - 没有限制的多项选择 - 基于奇怪的算法,例如仅偶数或仅奇数),在我的例子中,我将使用默认的多选,但如果你想用另一种选择算法改变它,你应该创建一个扩展 SelectionPredicates 的新类并实现你的选择方式,你也可以只需检查其他默认设置可能就是您要查找的内容.

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

private void initRecycleView() {...SelectionTrackertracker = new SelectionTracker.Builder<>(PhotosGallerySelection",Your_Recycler_View,new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),新的 GalleryDetailsLookup(Your_Recycler_View),存储策略.createStringStorage()).withSelectionPredicate(SelectionPredicates.createSelectAnything()).建造();...}

  • 我没有找到一种方法让我用数据初始化适配器,然后创建跟踪器,以便让查看者知道他们的选择与否,所以在这种情况下,我首先创建了跟踪器,然后让适配器知道使用 setter 和 notifyDataSetChanged 关于它的数据我的意思是在创建跟踪器后立即将跟踪器和数据设置到适配器,因此 initRecycleView 应该如下所示

private void initRecycleView() {...SelectionTrackertracker = new SelectionTracker.Builder<>(PhotosGallerySelection",Your_Recycler_View,new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),新的 GalleryDetailsLookup(Your_Recycler_View),存储策略.createStringStorage()).withSelectionPredicate(SelectionPredicates.createSelectAnything()).建造();照片适配器.setTracker(跟踪器);照片适配器.setData(数据);照片适配器.notifyDataSetChanged();...}

  • 最后但同样重要的是,您应该处理视图持有者如何知道它们是否被选中,因此您应该通过在其中创建一个 setter 方法让适配器了解跟踪器及其数据,这就是适配器应该如何最后看起来像:
<预><代码>公共类 PhotosGalleryAdapter 扩展 RecyclerView.Adapter{ArrayList数据;私有 SelectionTracker追踪器;公共照片画廊适配器(){数据 = 新的 ArrayList();}公共 ArrayList获取数据(){返回数据;}public void setData(ArrayList m_data) {this.data = m_data;}@覆盖public Sc​​heduleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {...}@覆盖public void onBindViewHolder(@NonNull PhotosGalleryViewHolder holder, int position) {...boolean isSelected = tracker.isSelected(data.get(i).getUri());RadioButton radioButton = holder.getRadioButton;radioButton.setChecked(isSelected);}@覆盖公共 int getItemCount() {返回数据大小();}公共字符串 getKey(int position) {返回 data.get(position).getUri();}公共 int getPosition(字符串键){for (int i = 0; i < data.size(); i++) {if (data.get(i).getUri() == key) 返回 i;}返回0;}public void setTracker(SelectionTracker m_tracker) {this.tracker = m_tracker;}}

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

  • 这样你就可以按照谷歌在他们的文档中建议的方式跟踪你的选择(老实说,我不知道为什么它会变得如此复杂).

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

  • 跟踪器有一个小问题/功能,它不会开始选择第一个项目,直到您长按它,这只会发生在您选择的第一个项目中,如果您确实需要此功能(长按开始选择模式)然后保持原样
    如果您不想要它,您可以让跟踪器在开始时选择一个幽灵键(任何对您的数据没有任何意义的唯一字符串键),稍后只需单击任何照片即可启用选择模式

 tracker.select("");

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

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

最后,如果您确实有兴趣阅读文档中有关选择跟踪器的内容,请遵循此 链接

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

implementing-selection-in-recyclerview

recyclerview 选择指南

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

I'm using a Recycler View to show all the images from the galley or the external storage of a device in a Grid Layout Manager. And I'm using a Radio Button to show if the image is selected or not.

PROBLEM

Whenever I select or deselect a Radio Button from the visible Views in the Recycler View some other Views which are outside the Visible Screen got selected or deselected.

It is like I'm pressing on the same View of the Recycler View, but the images are different.

PROBLEM

解决方案

well that's because of the recycler view concept of reusing the views instead of creating new views every time you scroll.

you see if you have 100 items you want to show in a recycler view and only 20 of them could appear to the user, recycler view creates only 20 view holder to represent the 20 items, whenever the user scroll recycler view will still have 20 view holder only but will just switch the data stored in this view holders rather than create new view holders.

now to handle selection of your items there's two ways to do this.

the naive way

  • hold selection in a boolean array inside the recycle view adapter.
  • whenever the user scrolls, the adapter calls onBindViewHolder to update the visible viewholder with the proper data.
  • so when onBindViewHolder gets called just set the radio button selection according the boolean array using the position sent in the method call
  • at the end of your usage to the recycler view you can create a getter method in the adapter to get the selection array list of boolean and pass the data based on it

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

                }
            });
            ...
        }
    }


the other way is to use a selection tracker and it should be the correct way to handle selections in a recycler view.

the problem with this way is it needs a lot of editing to the code and creating new classes to include as parameters in the selection tracker, but in the end you'll find it worth the time you spent on it.

in order to start with this way you need to do the following :

  • firstly, decide what should be a key (String-Long-Parcelable) so the tracker should use to differentiate between your data , the safest way is either String or Parcelable as I once tried Long and ended up with lots and lots of problems (in your case I will assume it's the photo's uri which will be of type string)

  • secondly, you need to create two new classes, one that extends ItemDetailsLookup, and the other extends ItemKeyProvider, and should use the key as their generic type (the type that is put between <> )
    your two classes should look like this (that you might copy them straight forward)

the one that extends 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);
    }
}

the one that extends 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;
    }
}

  • thirdly, you should include this new two methods in your adapter to be used by the above 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 (if there's an english word called forthly), you should initialize the tracker with all the above classes that were created before and he will handle the rest, the tracker takes as parameters

  1. a unique selection tracker id (if that will be the only selection tracker you will use then name it anything)
  2. the ItemKeyProvider that we created
  3. the DetailsLookup that we created
  4. a String-Long-Parcelable Storage to store the keys that were selected in (in our case it will be a String Storage)
  5. a Selection predicate, it's responsible to handle the way of selection you want to do, you want it to be able to (select only one item-multiple selection with no limits- based on a weird algorithm like even only or odd only), in my case I will use a default multiple selection one but if you want to alter it with another selection algorithm you should create a new class that extends SelectionPredicates and implement your way of selection, you could also just check the other default ones might be what you're looking for.

anyway, that's how the initialization should look (you should put this code wherever you initialize your recycler view at whether it's in fragment or activity method):

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

  • I didn't find a way to let me initialize the adapter with data and then create the tracker inorder to make the viewholders know about their selection or not, so in this case I firstly created the tracker and then made the adapter know about it's data using a setter and notifyDataSetChanged what I mean by that is after creating the tracker instantly set the tracker and data to the adapter, so the initRecycleView should look like this

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

  • Last but no least, you should handle how the view holders should know if they were selected or not, so you should let the adapter know about the tracker and its data by creating a setter method in it, that's how the adapter should look like in the end :


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

(as you may notice if you initialized the adapter with its data through the constructor, when he asks the tracker if there were an item selected or not, it will result in a NullPointerException as at the moment of initializing the adapter you still didn't initialize the tracker)

  • that way you could keep track of your selection the way google suggests in their documentation (which I honestly don't know why the made it very complicate like that).

  • if you want to know all the selected item in the end of your application/fragment use, you should call tracker.getSelection() which will return a Selection List for you to iterate on

  • There's a tiny problem/feature with the tracker that it won't start selecting the first item until you use a long press on it, that happens only in the first item you select, if you do want this feature (start selecting mode by long press) then leave it as it is
    incase you don't want it you can make the tracker select a ghost key (any unique string key that means nothing to your data) at the beginning which should later enable the selection mode with a simple click on any photo

        tracker.select("");

this also the way to make a default/old selection at the beginning, you could make a for loop and call tracker.select(Key) if you do want the tracker to start with few items being selected

N.B : incase you use the Ghost Key method you should watchout that the selection array that will get returned when you call tracker.getSelection() will also contain this Ghost Key.

at the end if you do have the curiosity of reading about selection tracker in the documentation follow this link

or maybe if you know how to read kotlin follow this two links

implementing-selection-in-recyclerview

a guide to recyclerview selection

I was stuck in the selection problem for days before I figure how to do all that so I hope you find your way through it.

这篇关于单选按钮在我的回收站视图中无法正常工作.多个单选按钮被选中的视图与焦点不可见的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆