I have a recycler view item that has few Views. I have a setting that will set the visibility of one Image view after the Recycler view is displayed. What is the best way to achieve this behavior?
What I tried, in the adapter ViewHolder class, I set a boolean that would hide the element and then call notifysetDataChanged()
public static class ViewHolder extends RecyclerView.ViewHolder {
public ImageView imageView;
public ViewHolder(View itemView) {
super(itemView);
.....
}
public void bind(item: Model){
//check and set visibility
if(showImage) imageview.setVisibility
else imageview.setVisibility
}
}
Calling this bind function from onBindViewHolder.
Is there any better way to achieve this as all the data remain the same only one visibility is changed?
your bind method is fine, but calling notifyDataSetChanged() will make your whole list redraw, all items. it's way better for perfomance to redraw only these items which has changed, use then notifyItemChanged(int position). calling it will make only single onBindViewHolder call with desired position, thus only one desired item will be redrawn (your bind call)
note there are more methods for preventing whole list redraw:
notifyItemChanged(int)
notifyItemInserted(int)
notifyItemRemoved(int)
notifyItemRangeChanged(int, int)
notifyItemRangeInserted(int, int)
notifyItemRangeRemoved(int, int)
Are you try this?
android:visibility="gone"
Related
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
a unique selection tracker id (if that will be the only selection tracker you will use then name it anything)
the ItemKeyProvider that we created
the DetailsLookup that we created
a String-Long-Parcelable Storage to store the keys that were selected in (in our case it will be a String Storage)
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.
Omar Shawky has covered the solutions.
With my answer I will stress on the reason why someone may face this sort of an issues with recycler views and how to avoid this common issue in the future (avoiding pitfalls).
Reason:
This issue happens because RecyclerView recycles views. So a RecyclerView item's view once inflated can get reused to show another off screen (to be scrolled to) item. This helps reduces re-inflation of views which otherwise can be taxing.
So if the radio button of an item's view is selected, and the same view gets reused to show some other item, then that new item can also have a selected radio button.
Solution:
The simplest solution for such issues is to have an if else logic in your ViewHolder to provide logic for both selected and de-selected cases. We also do not rely on information from radio button itself for initial setup (we do not use radioButton.isSelected() at the time of setup)
e.g code to write inside your 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);
Do not do any of the following while setting up:
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);
OR
// During initial setup do not use radio button itself to get information.
if (radioButton.isChecked()) { // --> Pitfall
radioButton.setChecked();
}
class file easily access to every page in Android
Initially set it zero
lastFirstVisiblePosition = 0;
You can store a scroll position on the scroll and set a scroll position like this.
((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(lastFirstVisiblePosition);
and if that not work try below
((LinearLayoutManager) rv.getLayoutManager()).scrollToPositionWithOffset(lastFirstVisiblePosition,0)
you don't need to save instance manually, just make sure that all views has an unique id in xml layout and android will do it for you.
You could use this method
mAdapter.notifyItemRangeChanged(position, list.size());
At the time you add new elements in adapter's list use this method to notify you adapter. it will retain the current position and add all new elements below the last item.
mAdapter.notifyDataSetChanged();
will notify your entire list ,which will make your list to scroll to top position so use
notifyItemChanged(int position) which will update only that position
and if you want to notify some range then use notifyItemRangeChanged(position,size)
for details refer this link for more detail. RecyclerView.Adapter to notify
Override onAttachedToRecyclerView to get the RecyclerView reference in your adaptor class
#Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
mRecyclerView = recyclerView;
}
Override onViewRecycled to get the lastVisiblePosition which you can use when you will again reload your recycler view
#Override
public void onViewRecycled(SiteHolder viewHolder) {
lastVisiblePosition = ((GridLayoutManager) mRecyclerView.getLayoutManager()).findLastCompletelyVisibleItemPosition();
}
Where ever you are doing your reloading of data(as a setter of Arraylist or whatever way)
first call
notifyDataSetChanged();
and then
((GridLayoutManager) mRecyclerView.getLayoutManager()).scrollToPosition(lastVisiblePosition);
This will make sure data reload to happen first then scrolling
I've been working on an online shop type of application, and I've hit a bump: I've been tasked to add a favorites system, but I can't figure out how to enable pressing a button that's part of the RecyclerView item to add it to favorites.
(In this case, the heart, which is supposed to turn to a full heart when clicked)
Add a boolean value for favourite in your list . Initially , keep it false .
You need to have two drawables , one for selected state and another for unselected state .
In your onBindViewHolder , set the drawable on runtime on the basis of above condition .
if(list.isfav)
{
holder.ivHeart.setImageDrawable(ContextCompat.getDrawable(context,(R.drawable.selected));
}else{
holder.ivHeart.setImageDrawable(ContextCompat.getDrawable(context,(R.drawable.unselected));
}
Put onClick on this ivHeart eg:
holder.ivHeart.setOnClickListener(v -> {
if(list.isfav) {
list[adapterPosition].isfav = false;
}else{
list[adapterPosition].isfav = true;
}
notifyItemChanged(adapterPosition);
});
Dont forget to notify the item while changing item .
In your RecyclerView adapter's onBindViewHolder() method, add click listener to your view and change the drawable programmatically.
The code will be something like this
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
holder.your_like_imageview.setOnClickListener{
holder.your_like_imageview.setImageDrawable(ContextCompat.getDrawable(context,R.drawable.something_else));
};
}
I'm assuming you're using an ImageView for the heart. What you can do is set a click listener on that ImageView and process that click.
An ideal way to do this would be to use an interface to handle click events that you pass to the Adapter.
However you could do something like this in the onBindViewHolder method:
imageView.setOnClickListener {
// depending your logic change the tint for the icon or the drawable
onClick(data[position], addToWishlist)
notifyDataSetChanged()
}
The onClick method will receive the particular item and a flag to add or remove it from
the wishlist:
fun onClick(data: Data, addToWishlist: Boolean) {
// you can perform the addition/ deletion from the wishlist here
}
I have got a simple viewpager with a few views.
I have a button in each view that once clicked it should change the background color of the view to a pre-determined color.
In my instantiateItem I have the following:
public CustomPagerAdapter(Context context) {
mContext = context;
}
#Override
public Object instantiateItem(ViewGroup collection, int position) {
LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.child_item, collection, false);
PopulateView(position);
collection.addView(layout);
return layout;
}
I have another function (populateView) that takes care of drawing my view and creating the button.
I was thinking of holding on to an instance of "ViewGroup layout" as class variable and use it but clicking the button in view 1 would cause a random view (say view 2)'s background color to change.
So I kept a copy of "ViewGroup collection" and used
collection.getChildAt(position).setBackgroundColor(mContext.getResources().getColor(sky));
but the app would crash on the last view color change with the error
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.setBackgroundColor(int)' on a null object reference
collection = {ViewPage#4834} "android.support.v4.view.ViewPager{e1f06a0 VFED..... ......ID 0,0-1080,1536 #7f0d0091 app:id/viewpager}"
mContext = {MainActivity#4835}
position = 2
The strangest thing is when I look inside "collection", and expand the "Children" there are only 2 children listed. There should be 3. I can't understand why this is.
Can someone please help me?
Well you need to give the position at which to add the child. See here
#Override
public Object instantiateItem(ViewGroup collection, int position) {
.....
collection.addView(layout, 0);
return layout;
}
I have a listview with a checkbox, an image and a text field. Now I do want to display more than one items in the text field. The problem is that I want the arguments in neat colums. Can I somehow set tabstops or a format or something like that in the text field or do I have to actually make more text fields and set each value in its own field ?
My list should look somewhat like:
[Image] [checkbox] "arg11 arg12 arg13"
[Image] [checkbox] "arg21 arg22 arg23"
[Image] [checkbox] "arg31 arg32 arg33"
Thanks for any advice.
Edit: Just to clarify things. I have a working listview/adapter with the image/checkbox and one textfield already. I am kind of new to Android so all I want to know if there is an elegant way to handle displaying several text items in the text field. In Windows I would simply use tabstopps and they would look ordered and in a column but I don't know my way well enough around android to know if there is something similar or if individual text fields are required to get the text fields in columns.
You create new xml say row.xml, which we can inflate inside listview for each row.
Design the row however you want, for ex:
row.xml
<LinearLayout android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:orientation="horizontal">
<ImageView ..../>
<CheckBox ...../>
<TextView ...../>
<TextView ...../>
</LinearLayout>
Then use CustomAdapter for your listview
like,
listView.setAdapter(new CustomAdapter());
CustomAdapter,
class CustomAdapter extends BaseAdapter{
publicCustomAdapter() {
mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public int getCount() {
return xyz.size();
}
#Override
public String getItem(int position) {
return xyz.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.row, null);
holder = new ViewHolder();
holder.imageView = (ImageViewView)convertView.findViewById(R.id.image);
holder.checkbox = (CheckBox)convertView.findViewById(R.id.checkbox);
holder.textView1 = (TextView)convertView.findViewById(R.id.text1);
holder.textView2 = (TextView)convertView.findViewById(R.id.text2);
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
//TODO set the values for views and return view
return convertView;
}
}
You should Re-work on the row xml. I think you may know about the row xml where you can chage the design of each and every row. Take a Relative or Linear Layout and add the views as shown below.
[Image] [checkbox] "text1" "text2 "text3"
[Image] [checkbox] "text1" "text2 "text3"
[Image] [checkbox] "text1" "text2 "text3"
Instead of taking tab spaces, take three different TextView's. keep '1' as weight for each and every TextView and add these to a LinearLayout which is having total weight sum as 3.So all the three TextView will be equally placed in the parent view.
Try this and lemme know whether you fixed it.....
Basically you need to implement your own adapter for your ListView.
First you need to create your own adapter class which either baseAdapter or their derivatives.And then you need to implement your ListView item display logic in it.
Like setting your textViews,imageViews etc.
Check this tutorial for more info.
http://www.vogella.de/articles/AndroidListView/article.html
From one of my earlier posts:
Whenever you want to do processing with the views in a ListView you
need to create a custom adapter that will handle your logic
implementation and pass that information to the views as necessary.
If you are sure about the sizes of text then you can use tabstops in your text. If that doesnt work then your are stuck with extending your row layout. as mentioned by Pavandroid