My ListActivity is bound to an ArrayAdapter where I have overriden the getView() method. If I change the visibility of a widget in that method or modify the view's background color, those changes somehow get lost once that modified list item returns after being scrolled off the screen. In fact, some other view in the list is picking up the changes.
How do I get the modified view to look the same when it's redisplayed after scrolling?
Are you inflating a new view each time or making use of the convertView that is passed in?
Normally the Adapter tries to recycle views, only creating enough to provide smooth scrolling. The existing recycled views are passed in as convertView. You can either inflate and return a new view every time (expensive) or just re-setup the convertView (if it exists) based on position. If recycling you need to re-set all the view attributes, as there is no guarantee that the recycled view you get is the same one used for this position in the past.
It sounds like your bug is that you are not correctly re-setting all the attributes of the recycled view (convertView) to match the data for the current position.
Related
For a recycler view the holder pattern is being used for efficiency.
As we scroll a recycler list, as items as shown views are created or reused.
My question is: if an item is scrolled out of visibility and the corresponding view is not visible is there any method called that indicates this? E.g. if we are downloading a resource in a view and we need to cancel the download as the user scrolls the view, is that done automatically or is there some method call we need to implement on the ViewHolder or something else?
You could override this RecyclerView method to check when item is being recycled.
#Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
super.onViewRecycled(holder);
// check if download is in progress and stop it.
}
Called when a view created by this adapter has been recycled.
A view is recycled when a RecyclerView.LayoutManager decides that it
no longer needs to be attached to its parent RecyclerView. This can be
because it has fallen out of visibility or a set of cached views
represented by views still attached to the parent RecyclerView. If an
item view has large or expensive data bound to it such as large
bitmaps, this may be a good place to release those resources.
RecyclerView calls this method right before clearing ViewHolder's
internal data and sending it to RecycledViewPool. This way, if
ViewHolder was holding valid information before being recycled, you
can call getAdapterPosition() to get its adapter position.
I thought I understood the recycler mechanism of Listview... but it seems I didn't.
I have a ListView of 20 items, only 10 items is visible initially. Why Android calls getView() of positions like 15 or 17 (non visible rows)?
Why convertView parameter of visible rows passed to getView() are the same? I thought if convertView is not null, it refers to a row that isn't visible anymore.
After adding some logging in getView(), I couldn't explain what really happens.
convertView is a View instance that was previously returned by the getView method and is not visible anymore, its purpose is TO BE converted to a NEW ITEM assigned by the new position in the method. The adapter do it to reuse views and avoid inflating new ones. If it is calling the same visible row twice it can only be at listview inflation time (it happens).
To the first question, it inflates non-visible views so it will be available to be scrolled when you reach it.
YOU MUST: re-set all values in a convertView to a new item.
I have been using using listview since long but suddenly I experienced some of its random behaviour.
When I call invalidateViews() on listviews, in some cases it re-inflates the views for each row and in some cases it doesn't. This is completely random. Can you tell me exactly how does it works?
Ideally it should just refresh the data inside views(rows) and not inflate them again.
Thanks
Calling invalidateViews() on a ListView should not re-inflates the ListView item views. The only way to force the ListView to re-inflates its item views, meaning that the recycled views are cleared, is by resetting the ListView adapter ( I had to dig in the source code to discover that ).
If you take a look at the invalidateViews() method implementation (source code in AbsListView.java), you will see this (Android API 16) :
/**
* Causes all the views to be rebuilt and redrawn.
*/
public void invalidateViews() {
mDataChanged = true;
rememberSyncState();
requestLayout();
invalidate();
}
The method rememberSyncState() implemented in AdapterView.java stores the ListView's position of the selected item (if any), you can check it out.
The requestLayout() method takes care of the measuring, laying out, and drawing (as far as I know) so nothing here too.
And finally invalidate() is used to force the list view to draw (with the new measurements, ...)
So calling invalidateViews() should not force the list view to re-inflate its views.
Could you be re-setting the adapter somewhere so it re-inflates all the views ?
Actually, listview.invalidateViews() causes all the views to be rebuilt and redrawn. You can come to know this when you look on the description gets displayed in eclipse when you try to select invalidateViews().
adapter's getView() method has parameter convertView which is null first time when you populate a listview, later when you scroll/invalidate your listview convertView is not null so you can use it and dont need to inflate a new row
In a custom adapter, how to know weither I need to reconfig the convertView or not?
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView;
if (convertView == null) {
imageView = (ImageView) mInflater.inflate(R.layout.avatar, parent, false);
// Should this 2 lines of code be here ?
User user = mUserList.getUserAt(position);
user.setAvatar(imageView);
} else {
imageView = (ImageView) convertView;
}
// or here ?
User user = mUserList.getUserAt(position);
user.setAvatar(imageView);
return imageView;
}
I would think that if it is recycled, I would not need to reset the user's avatar but I often see the configuration happening outside of the if block. Why is that?
I always configure the view outside of if block. This convertView that you get in the getView method might (and most probably will) be set up for another user by some previous call to getView because of ListView's policy to reuse item views when they go offscreen. If you do not set up a proper avatar you will have wrong one for this item. Of course you won't need to reset properties that are independent of the concrete position like background.
For example CursorAdapter separates getView into two parts: newView, that performs inflate and (mostly) position-independent setup and bindView that assigns actual position-dependent data.
It may also happen that you will get exactly the same view that you used for this position earlier. Of course you can avoid resetting a view in this case, but you need to check if data in this view are valid. Setting and then checking View's tag comes to my mind as a most obvious solution.
It is not truly recycled, it just means that you need to fill a "recycled" view with new data according to the its new position. If you don't do it this row will be filled with old data that shouln't be visible on the screen anymore since you scrolled away its position.
So in short you have to reconfigure view with fresh data each time getView() called (outside of if block in your code).
Your issue is only with the understanding of listview.
So here I'll make you clear how it works??
Let's say listview has to contain 20 items but your current screen can accommodate(show on screen) only 8 items(list items, in your case imageview).
When the listview tried to get items for 1----8th it will return you convertView as null because no recycling of objects happened yet.
but, when you try to scroll, in our case(scroll up!).
the 1st element of the list will be recycled when go out of screen, and will be supplied as convertView for 9th item.
In this way listview has to manage only 8th(in our case) to show any number of items.
The opposite will happen if we will scroll down wards.
So, on the basis of convert view (null or not) you have to design your logic either to create and fill or to fill.
Hope this will help you.
What is the correct way to modify all child elements (not only the visible ones) of a listview.
I have an image which is set, by default, to visibilty gone. I wish to make it visible after the user clicks a button (for all items).
Thanks!
What is the correct way to modify all child elements (not only the visible ones) of a listview.
One thing to understand about a ListView is that not all of the list items are generated (inflated/populated) at any given time.
Suppose, for example, your list Adapter has 1000 items in it but the ListView can only display 10 at once. It would be a very bad waste of resources (e.g., memory) to create all 1000 list items.
Instead, only the 10 visible items are created and each time you scroll one off the top or bottom of the screen, the one which has disappeared is re-cycled by being passed as convertView into the Adapter's getView method.
getView (int position, View convertView, ViewGroup parent)
To do what you are asking you should extend whatever Adapter type you wish to use and override the getView method. In that method check if convertView is null or not. If it is, inflate your own instance of your list item layout. If it is not null then re-use the UI elements (TextView, ImageView etc).
To have all ImageView elements visible, use a global Boolean such as showImageView which will be toggled by the button press. Then use that in getView to decide whether or not to set the visibility of the ImageView.
See Adapter.getView(...)
Probably you should set the image visibility in your ListAdapter's getView() depending on some field value. Upon button clicking you change this field value and then you invoke ListAdapter.notifyDataSetChanged so the List View updates - getView then gets called and image changes because your field value has changed.
Inside the getView() of your adapter, you grab the ImageView and set its visibility to gone:
ImageView iv = (ImageView)convertView.findViewById(R.id.image_view);
iv.setVisibility(buttonClicked ? View.GONE : View.VISIBLE);
Then when users click on the button, set buttonClicked = true, and call notifyDataSetChanged() to refresh the ListView.