I am new to Android development and reading through some example code. I have copied one method from the sample code in an Adapter class (derived from ArrayAdapter), the derived class has a checkbox in addition to the text view:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View listItem = super.getView(position, convertView, parent);
CheckedTextView checkMark = null;
ViewHolder holder = (ViewHolder) listItem.getTag();
if (holder != null) {
checkMark = holder.checkMark;
} else {
checkMark = (CheckedTextView) listItem.findViewById(android.R.id.text1);
holder = new ViewHolder(checkMark);
listItem.setTag(holder);
}
checkMark.setChecked(isInCollection(position));
return listItem;
}
private class ViewHolder {
protected final CheckedTextView checkMark;
public ViewHolder(CheckedTextView checkMark) {
this.checkMark = checkMark;
}
}
The sample code is to optimize the getView by caching the View within a ViewHolder object.
Where I am confused is I thought the convertView, if not null, would be re-purposed and then the View data is populated into it and returned.
If this is the case, then how could the setTag / getTag methods called in the code be relied upon? It would seem that the same object would have to be retrieved in order for it to work?
perhaps view returned from getTag on a subsequent call is for a different list item, and returns the wrong view
Adapters use a RecycleBin. This class allows the ListView to only create as many row layouts as will fit on the screen, plus one or two for scrolling and pre-loading. So if you have a ListView with 1000 rows and a screen that only displays 7 rows, odds are the ListViiew will only have 8 unique Views.
Now to your question using my example above: only eight row layouts and 8 subsequent ViewHolders are ever created. When the users scrolls no new row layouts are ever created; only the content of the row layout changes. So getTag() will always have a valid ViewHolder that references the appropriate View(s).
(Does that help?)
You're on the right track, here's some information that may help make more sense of how ListViews work:
A simple implementation of the getView() method has two goals. The first is inflating the View to be shown on the list. The second is populating the View with the data that needs to be shown.
As you stated, ListViews re-purpose the Views that compose the list. This is sometimes referred to as view recycling. The reason for this is scalability. Consider a ListView that contains the data of 1000 items. Views can take up a lot of space, and it would not be feasible to inflate 1000 Views and keep them all in memory as this could lead to performance hits or the dreaded OutOfMemoryException. In order to keep ListViews lightweight, Android uses the getView() method to marry Views with the underlying data. When the user scrolls up and down the list, any Views that move off the screen are placed in a pool of views to be reused. The convertView parameter of getView() comes from this list. Initially, this pool is empty, so null Views are passed to getView(). Thus, the first part of getView should be checking to see if convertView has been previously inflated. Additionally, you'll want to configure the attributes of convertView that will be common to all list items. That code will look something like this:
if(convertView == null)
{
convertView = new TextView(context);
convertView.setTextSize(28);
convertView.setTextColor(R.color.black);
}
The second part of an implementation of getView() looks at your underlying data source for the list and configures this specific instance of the View. For example, in our test list, we may have an Array of Strings to set the text of the view, and want to set the tag as the current position in the Data of this View. We know which item in the list we're working with based on the position parmeter. This configuration comes next.
String listText = myListStringsArray[position];
((TextView)convertView).setText(listText);
convertView.setTag(position);
This allows us to minimize the amount of time we spend inflating/creating new views, a costly operation, while still being able to quickly configuring each view for display. Putting it all together, your method will look like this:
#Override
public View getView(int position, View convertView, ViewGroup)
{
if(convertView == null)
{
convertView = new TextView(context);
//For more complex views, you may want to inflate this view from a layout file using a LayoutInflator, but I'm going to keep this example simple.
//And now, configure your View, for example...
convertView.setTextSize(28);
convertView.setTextColor(R.color.black);
}
//Configure the View for the item at 'position'
String listText = myListStringsArray[position];
((TextView)convertView).setText(listText);
convertView.setTag(position);
//Finally, we'll return the view to be added to the list.
return convertView;
}
As you can see, a ViewHolder isn't needed because the OS handles it for you! The Views themselves should be considered temporary objects and any information they need to hold onto should be managed with your underlying data.
One further caveat, the OS does nothing to the Views that get placed in the pool, they're as-is, including any data they've been populated with or changes made to them. A well-implemented getView() method will ensure that the underlying data keeps track of any changes in the state of views. For example, if you change text color of your TextView to red onClick, when that view is recycled the text color will remain red. Text color, in this case, should be linked to some underlying data and set outside of the if(convertView == null) conditional each time getView() is called. (Basically, static setup common for all convertViews happens inside the conditional, dynamic setup based on the current list item and user input happens after) Hope this helps!
Edited - Made the example simpler and cleaned up the code, thanks Sam!
Related
As far as I know, the position returns specific chosen item from the whole list. So how does the adapter use the position and transfers all of the items without some kind of loop? I guess there is a basic mistake in my sight regarding lists and positions.
This is the code:
(THANK YOU IN ADVANCE):
public View getView(int position, View convertView, ViewGroup parent) {
viewHolder holder;
if (convertView==null){
convertView= LayoutInflater.from(mContext).inflate(R.layout.customupdatestatus, null);
holder=new viewHolder();
holder.statusHomePage=(TextView)convertView.findViewById(R.id.statusUploaded);
holder.userNameHomePage=(TextView)convertView.findViewById(R.id.userNameUpdate);
convertView.setTag(holder);
}else{
holder=(viewHolder) convertView.getTag();
}
ParseObject statusObject= mStatus.get(position);
String username= statusObject.getString("userName");
holder.userNameHomePage.setText(username);
String status=statusObject.getString("newStatus");
holder.statusHomePage.setText(status);
return convertView;
}
Ok, this is something that was bugging me for some time. So, when the view is created, you can see certain amount of rows. From row one to the last row in the view, adapter is counting. When you scroll down, the counter resets, or, I would say, it starts again. So, lets say you have 10 rows on screen when view is created. When you scroll down, if you select to check row number 5, it will select on the first view, when you scroll, it will show the other element is checked too. I tried to find solution to this, but couldn't.
So how does the adapter use the position and transfers all of the items without some kind of loop?
In short, it doesn't. This is exactly what happens.
The Android framework has separated the process of creating a list into a few different pieces. The ones we care about here are the ListView and the Adapter.
The ListView's job is to ask the adapter how to map a position to a View, then use that information to correctly layout, measure, and draw those Views.
The Adapter's job is to tell the ListView how to map a position in the list to a View instance. Because the Android framework takes care of looping through the items for you in ListView, you don't need to worry about doing that yourself- you just need to provide the mapping.
Here is the general idea of what ListView does:
// Figure out how many children we have
int numChildren = adapter.getCount();
for (int i = 0; i < numChildren; i++) {
// Get the view for this position
View childView = adapter.getView(i, null, this);
// Add it to ourselves
addView(childView);
}
This is of course highly simplified, but is a good high level idea of what is happening with your adapter behind the scenes.
I wonder what is convert view ? I understand that it is
The old view to reuse, if possible. Note: You should check that this
view is non-null and of an appropriate type before using. If it is not
possible to convert this view to display the correct data, this method
can create a new view. Heterogeneous lists can specify their number of
view types, so that this View is always of the right type (see
getViewTypeCount() and getItemViewType(int)).
What is this under the hood ?
Is it just simple view inflated earlier ? Or something other ?
Why we cannot use it like this ?
if (convertView!=null) {
return convertView;
}
else {
return new ......
}
Why we have to use setTag method to set reference to the our inflated view ?
convertView.setTag(holder);
We are setting one view another view reference as the TAG , why not just to pass it directly as convertView ?
Please help with this question, I can't write any code because cannot understand this.
Under the hood speaking you would see that it inflates the views just a couple of times (enough to fill up the screen) as this is really slow. It is as you said reusing the views it had inflated earlier. When you scroll, it just tells you that you need to fill the earlier inflated views with the data appropriate for the current position.
You actually can always return a new view you inflate every time, but this will have a negative impact on your performance and there by scrolling smoothness.
With the setTag you are telling it to remember the object which holds information about the view itself (called ViewHolder).
What is this under the hood ? Is it just simple view inflated earlier
? Or something other ?
it is both. In particular is an array of views of the type you inflated. The size of this array is equal to the number of views enough to fill completely your screen. This way you allocate only a constant number of views.
Why we cannot use it like this ?
if (convertView!=null) {
return convertView;
}
else {
return new ......
}
you still need to fill up your view with the data of your dataset. In the snippet you posted above you aren't doing it. You could do something like
if (convertView!=null) {
// convertView.findVIewById(...)
// set data to view
return convertView;
}
else {
convertView = new ......
// convertView.findVIewById(...)
// set data to view
return converView;
}
Why we have to use setTag method to set reference to the our inflated
view ?
it's called ViewHolder pattern, and you are not setting the inflated view, but a small object that contains the inflated's view component. This way you can avoid calling findViewById every time getView is called and you want to fill up the view with content of your dataset (at position)
I have a custom list view which is being popluated via an array adaptor.
Each item/row contains three buttons and some related textViews.
All elements in a row describe the details for a device on the cloud. So data is fetched from the cloud and then the list is populated. No. of rows is equal of the number of devices.
Everything was fine till I added the feature for a periodic update for the items.
The problem is that after each periodic update it over writes the data for a device in the wrong row.
I tried two ways to refresh each row.
I kept a map for (DeviceID and view) and then based on the deviceId
i would get the view and update it. Now,this didn't work as the views are reused and so as i scroll
down, basically the same view is reused as shows the new data. And
so the map entry of the previous device is over written with the new
one.
I tried to directly call getView() and pass the position but that
also didn't work.
I understand that the views are reused so there is no way to know exactly which view is associated with a deviceID.
But could some please help me figure out how to update the correct view with the correct data?
Thanks.
If you are using Holder pattern, then there is a way to do this.
Step 1: Add one attribute i.e. position to Holder.
private class ViewHolder {
....
....
int position;
}
Step 2: Initialise the holder position into getView()
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
...
holder.position = position;
}
....
}
Step 3: Check holder position and view's position values. If both are same then do your task.
if (mHolder.position == mPosition) {
// This is you required row. Do your task.
}
Read Async loading for more details.
I'm creating kind of music player which has a listview with songs (having progress bar near currently playing song)
What is important is that I have an Item with views which can be changed from outside (in some handler)
public View getView(int i, View convertView, ViewGroup viewGroup) {
RelativeLayout result;
if (convertView == null) {
result = (RelativeLayout) inflater.inflate(R.layout.list_item, viewGroup, false);
} else {
result = (RelativeLayout) convertView;
}
...
ProgressBar progressBar = result.findViewById(R.id.progressBar)
...
if (i == currentSong) {
// saving to instance variable
this.currentlyPlayingProgressBar = progressBar;
} else {
progressBar.setVisibility(View.GONE);
}
...
return result;
}
(Code was changed to focus on my problem)
Btw currentSong can be changed from outside, adapter.notifyDataSetChanged() is being called in this case.
It seems like I'm using listView incorrectly, but I don't know the better way.
The main problem is that I need to have links to views to change them.
And the only way where I can get them is in getView method which reuses those view in a way only google developers can explain=(
First problem
This is all happening in Fragment which is just a part of a viewPager. when user scrolls of this fragment and then scrolls back then getView method is being called with some strange objects inside.. And I override currentlyPlayingProgressBar with this invalid value. Which causes the freeze of my statusbar. (it starts updating wrong view)
And I have no idea which view is it..
Second problem
I am reusing list items and this means that when user scrolls list view down - then sometimes he gets actually the same list item with the same progressBar.
This progressBar must be invisible but it's not (I think it's all because of my usage of currentlyPlayingProgressBar from outside)
Thanks in advance.
You can do this in two ways:
1) notifyDataSetChanged, which just resets entire ListView and assigns all visible list items again to views. notifyDataSetChanged is very expensive, since it makes entire list and view hierarchy to be recreated, layouts are measured, etc, which is slow. For frequent update of single list item, such as progress bar change, this is overkill.
2) Find view of particular listview item, and update only that one. We'll focus on this approach.
First you need to somehow identify which list view contains which list item. Common approach is to use View.setTag in your Adapter.getView, where setTag parameter is Object of your choice, may be same item as you return for Adapter.getItem.
So later you know which ListView child view has which item assigned.
Assuming you want to update particular item displayed in ListView, you have to iterate through ListView child views to find which view displays your item:
Object myItem = ...;
for(int i = list.getChildCount(); --i>=0; ){
View v = list.getChildAt(i);
Object tag = v.getTag();
if(tag==myItem) {
// found, rebind this item
bindItemToView(myItem, v);
break;
}
}
You must expect that ListView currently may not display your list item (is scrolled away).
From code above you see that it calls bindItemToView, which is your function to bind item to list view. You'd probably call same function to setup the item view in Adapter.getView.
You may also optimize it further, assuming you want to update only ProgressBar, then don't call bindItemToView, but update only ProgressBar in your listitem view (findViewById, setup values).
Hint: you can make it even more optimal by using ViewHolder approach. Then setTag would not contain your item object, but your ViewHolder object.
#Pointer null has also given very usefull aproach, but in your case I think u are updating the list which is not visible, in this case you have to set the tag from the adapter just like the list index and curresponding check if the list item exist between the last visible item or first visible item then update it else donot update..
This question already has answers here:
Android ListView Refresh Single Row
(7 answers)
Closed 8 years ago.
I'm wondering if it is possible to rerender just one element in a listview? I assume by calling notifyDatasetChanged() is gonna rerender the whole list?
Thanks,
you can't render (refresh) a single row, but instead you can get the requested view and make chages on it directly by calling yourListView.getChildAt(int VisiblePosition); where the visiblePostion is the position in the ListView minus yourListView.getFirstVisiblePosition()
Like this :
View v = listViewItems.getChildAt(position -
listViewItems.getFirstVisiblePosition());
v.setBackgroundColor(Color.GREEN);
I hope this helps...
You can, but it's a bit convoluted. You would have to get the index of the first visible item in the list and then use that do decide how how far down in the list of visual items the item is that needs updated, then grab its view and update it there.
It's much easier to just call notifyDatasetChanged().
Also you can use this:
myListView.invalidateViews();
dataAdapter.remove(dataAdapter.getItem(clickedpos));
dataAdapter.insert(t.getText().toString(), clickedpos);
This is how I did it:
Your items (rows) must have unique ids so you can update them later. Set the tag of every view when the list is getting the view from adapter. (You can also use key tag if the default tag is used somewhere else)
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
View view = super.getView(position, convertView, parent);
view.setTag(getItemId(position));
return view;
}
For the update check every element of list, if a view with given id is there it's visible and update must be performed on it.
private void update(long id)
{
int c = list.getChildCount();
for (int i = 0; i < c; i++)
{
View view = list.getChildAt(i);
if ((Long)view.getTag() == id)
{
// update view
}
}
}
It's actually easier than other methods and better when you dealing with ids not positions! Also you must consider scenario when your view get invisible and visible again.
You need to keep track of your adapter (or custom adapter if you are set on fancy features). When you change the data for an item, simply change the fields you are interested in , in your adapter.
Then call notifyDatasetChanged , and the changes will be reflected in your listview.
Note that this approach works exactly the same for Gallery Views as well.