Android ListView Layout inflater - android

We are working with list views in college at the moment. My lecturer gave us a simple application that displays mail messages in a list and when the user selects one it displays the content of the message in a new activity. I understand pretty much all of what is going on but there are a few grey areas I want to clear up!
Basically I am wondering what this section of code does?
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.inbox_row, null);
}
This method is located within a class that extends ArrayAdapter. Am I right in thinking that it is some form of recycling? for when views go on and off the screen?....
Any help is much appreciated. thanks.

it's exactly what you said, a form of recycling.
Inflating a layout takes a lot of memory and a lot of time, so for the efficiency sake, the system passes to you that just went off the screen and you can simply update its text and images and give them back to the UI.
So for example, if your list view is showing 6 items on its list (due to the height of it), it will only inflate 6 items and during scroll it just keeps recycling them.
there's some extra optimisations tricks that you should use and I'm sure that the video link that the commenter posted will explain them.
edit
that example is an ArrayAdapter of Store items, but you can make it to whatever you need.
the adapter does the match and separation layer between UI and data.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null)
convertView = newView();
// Store is the type of this ArrayAdapter
Store store = getItem(position);
Holder h = (Holder) convertView.getTag();
// And here I get the data and address them to the UI
// as you can see, if the convertView is not null,
// I'm not creating a new one, I'm just changing text, images, etc
h.storeName.setText(store.getStoreName());
h.address.setText(store.getAddressLine1());
h.postcode.setText(store.getPostCode());
h.distance.setText(store.getDistance());
return convertView;
}
// I like to separate in a different method when the convertView is null
// but that's me being organisation obsessive
// but it also makes easy to see which methods are only being called the 1st time
private View newView() {
LayoutInflater inf = LayoutInflater.from(getContext());
View v = inf.inflate(R.layout.map_result_list, null);
Holder h = new Holder();
// here we store that holder inside the view itself
v.setTag(h);
// and only call those findById on this first start
h.storeName = (TextView) v.findViewById(R.id.txtLine1);
h.address = (TextView) v.findViewById(R.id.txtLine2);
h.postcode = (TextView) v.findViewById(R.id.txtLine3);
h.distance = (TextView) v.findViewById(R.id.txtDistance);
return v;
}
// this class is here just to hold reference to the UI elements
// findViewById is a lengthy operation so this is one of the optimisations
private class Holder {
TextView storeName;
TextView address;
TextView postcode;
TextView distance;
}

Related

convertView in custom adapter with multiple line views

I'm implementing custom adapter which handles multiple type of lines in a listview based on this (very useful) tutorial: http://logc.at/2011/10/10/handling-listviews-with-multiple-row-types/
Now, I thought I understood everything but one thing puzzles me.
In the getView method we receive the convertView which suppose to be the view (group) with specific layout to display in the specific line in the listview.
public View getView(int position, View convertView, ViewGroup parent) {
//first get the animal from our data model
Animal animal = animals.get(position);
//if we have an image so we setup an the view for an image row
if (animal.getImageId() != null) {
ImageRowViewHolder holder;
View view;
//don't have a convert view so we're going to have to create a new one
if (convertView == null) {
ViewGroup viewGroup = (ViewGroup)LayoutInflater.from(AnimalHome.this)
.inflate(R.layout.image_row, null);
//using the ViewHolder pattern to reduce lookups
holder = new ImageRowViewHolder((ImageView)viewGroup.findViewById(R.id.image),
(TextView)viewGroup.findViewById(R.id.title));
viewGroup.setTag(holder);
view = viewGroup;
}
//we have a convertView so we're just going to use it's content
else {
//get the holder so we can set the image
holder = (ImageRowViewHolder)convertView.getTag();
view = convertView;
}
//actually set the contents based on our animal
holder.imageView.setImageResource(animal.getImageId());
holder.titleView.setText(animal.getName());
return view;
}
//basically the same as above but for a layout with title and description
else {
DescriptionRowViewHolder holder;
View view;
if (convertView == null) {
ViewGroup viewGroup = (ViewGroup)LayoutInflater.from(AnimalHome.this)
.inflate(R.layout.text_row, null);
holder = new DescriptionRowViewHolder((TextView)viewGroup.findViewById(R.id.title),
(TextView)viewGroup.findViewById(R.id.description));
viewGroup.setTag(holder);
view = viewGroup;
} else {
view = convertView;
holder = (DescriptionRowViewHolder)convertView.getTag();
}
holder.descriptionView.setText(animal.getDescription());
holder.titleView.setText(animal.getName());
return view;
}
}
However, in the case of multiple types of lines in the listview (for example, list of animals with separators lines with titles like 'mamals','fish','birds') how does the listview know what convertView to send? it can be one of two completely different types. something is very unclear to me. can someone explain please?
From the tutorial you provided :)
The two additional methods android Adapters provide for managing different row types are:
getItemViewType(int position) and getViewTypeCount().
The list view uses these methods create different pools of views to reuse for different types of rows.
Good Luck :)

Recycling views in custom array adapter: how exactly is it handled?

I am having an unclear issue concerning the recycling of views in a getView method of a custom array adapter.
I understand that elements are reused, but how do I know exact what to implement in the first part of the if statement, and what in the second?
Right now I am having following code. I came to this question due to dropping the code in the second part of the statement which results in a list of the first 9 elements, which are repeated numberous times instead of all elements. I didn't really know what is causing this exactly...
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
if (row == null) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
title = getItem(position).getTitle();
size = calculateFileSize(position);
txtTitle = (TextView) row.findViewById(R.id.txtTitle);
tvFileSize = (TextView) row.findViewById(R.id.tvFileSize);
txtTitle.setText(title);
tvFileSize.setText(size);
} else {
title = getItem(position).getTitle();
size = calculateFileSize(position);
txtTitle = (TextView) row.findViewById(R.id.txtTitle);
tvFileSize = (TextView) row.findViewById(R.id.tvFileSize);
txtTitle.setText(title);
tvFileSize.setText(size);
}
return row;
}
It's easy. The first time no row is created, so you have to inflate them. Afterwards, the Android os may decide to recycle the views that you already inflated and that are not visible anymore. Those are already inflated and passed into the convertView parameter, so all you have to do is to arrange it to show the new current item, for example placing the right values into the various text fields.
In short, in the first part you should perform the inflation AND fill the values, in the second if (if convertView != null) you should only overwrite the field because, given the view has been recycled, the textviews contain the values of the old item.
This post and this are good starting points
I understand that elements are reused, but how do I know exact what to implement in the first part of the if statement, and what in the second?
The organization is quite simple once you get the hang of it:
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
/* This is where you initialize new rows, by:
* - Inflating the layout,
* - Instantiating the ViewHolder,
* - And defining any characteristics that are consistent for every row */
} else {
/* Fetch data already in the row layout,
* primarily you only use this to get a copy of the ViewHolder */
}
/* Set the data that changes in each row, like `title` and `size`
* This is where you give rows there unique values. */
return convertView;
}
For detailed explanations of how ListView's RecycleBin works and why ViewHolders are important watch Turbo Charge your UI, a Google I/O presentation by Android's lead ListView programmers.
You want to create a ViewHolder class in your MainActivity. Something like
static class ViewHolder
{
TextView tv1;
TextView tv2;
}
then in your getView, the first time you get your Views from your xml in the if and reuse them after that in the else
View rowView = convertView;
if (rowView == null)
{
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
rowView = inflater.inflate(R.layout.layout_name_to_inflate, parent, false);
holder = new ViewHolder();
holder.tv1= (TextView) rowView.findViewById(R.id.textView1);
holder.tv2 = (RadioGroup) rowView.findViewById(R.id.textView2);
rowView.setTag(holder);
}
else
{
holder = (ViewHolder) rowView.getTag();
}
I would recommend that you use the View holder and convertview pattern to create your listView as it will be more efficient.Here is a good explanation of how it works with a re-use strategy. This will answer your question on how re-cycling works. If you want to refer to a code sample, I have it on GitHub.
Hope this helps.
The last part of the question I really couldn't grasp without a picture of the effect but for the first part "what to implement in the first part of the if statement, and what in the second" I think I've found the this implementation very common.
You would find the view references first and store them to a static class ViewHolder which then you attach to the tag of the new inflated view. As the listview recycles the views and a convertView is passed getView you get the ViewHolder from the convertView's tag so you don't have to find the references again (which greatly improves performance) and update the view data with that of your object at the position given.
Technically you don't care what position the view was since all you care for is the references to the views you need to update which are held within it's ViewHolder.
#Override
public View getView(int position, View convertView, ViewGroup container) {
ViewHolder holder;
Store store = getItem(position);
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.item_store, null);
// create a holder to store references
holder = new ViewHolder();
// find references and store in holder
ViewGroup logoPhoneLayout = (ViewGroup) convertView
.findViewById(R.id.logophonelayout);
ViewGroup addressLayout = (ViewGroup) convertView
.findViewById(R.id.addresslayout);
holder.image = (ImageView) logoPhoneLayout
.findViewById(R.id.image1);
holder.phone = (TextView) logoPhoneLayout
.findViewById(R.id.textview1);
holder.address = (TextView) addressLayout
.findViewById(R.id.textview1);
// store holder in views tag
convertView.setTag(holder);
} else {
// Retrieve holder from view
holder = (ViewHolder) convertView.getTag();
}
// fill in view with our store (at this position)
holder.phone.setText(store.phone);
holder.address.setText(store.getFullAddress());
UrlImageViewHelper.setUrlDrawable(holder.image, store.storeLogoURL,
R.drawable.no_image);
return convertView;
}
private static class ViewHolder {
ImageView image;
TextView phone;
TextView address;
}

How to keep list items in memory?

I have custom listview. When I scroll my listview, android keeps in memory (as far as I understand) items which is displaying on screen and doesn't keep items which is hidden (not scrolled to).
In my case (I think) keeping all list items would be better than generating hidden items.
So, how to "tell" android to keep all items in memory? (15-20 items). PS: if it's wasting of resources, I'd like just to try.
My adapter (some funcs):
private View newView(Context context, ViewGroup parent) {
LayoutInflater layoutInflater = LayoutInflater.from(context);
return layoutInflater.inflate(R.layout.myl,parent,false);
}
public View getView(int position,View convertView,ViewGroup parent) {
View view=null;
if(convertView!=null) view=convertView; else view=newView(context,parent);
HashMap<String,String> d=new HashMap<String,String>();
d=data.get(position);
String qweqwe=d.get("qweqwe"); //9 more lines like this.
TextView txt=(TextView)view.findViewById(R.id.mfmf); //
txt.setText(qweqwe); //
txt.setTypeface(mlf); //5 more blocks of 3 lines like this.
if (smth.equals("0")){
view.setBackgroundDrawable(context.getResources().getDrawable(R.drawable.mvmv));
} else {
view.setBackgroundDrawable(context.getResources().getDrawable(R.drawable.mvmv2));
}
return view;
}
Ok.. there are some things that can be optimized here, instead of trying to fix lag with workarounds :)
You should implement a static class, where you can store references to the Views in your myl.xml. For each View you want to manipulate in myl.xml, you create a View in this static class. So if you have 10 TextViews, you fill this class with 10 TextViews.
static class AdapterViewsHolder {
TextView txt1;
TextView txt2;
TextView txt3;
...
ImageView img1;
... etc etc.
}
In the adapter, you now only do the findViewById() calls if the convertView is null. findViewById() is not cheap, so limiting the amount of calls increases performance.
private HashMap<String, String> mData;
private LayoutInflater mInflater;
private TypeFace mCustomTypeFace;
// Some contructor for passing data into the Adapter.
public BaseAdapter(HashMap<String, String> data, Context ctx) {
mData = data;
mInflater = LayoutInflater.from(ctx);
mCustomTypeFace = Typeface.createFromAsset(ctx.getAssets(), "yourTypeFace.ttf");
}
public View getView(int position, View convertView, ViewGroup parent) {
// This AdapterViewsHolder will hold references to your views, so you don't have to
// call findViewById() all the time :)
AdapterViewsHolder holder;
// Check if convertView is null
if(convertView == null) {
// If it is, we have to inflate a new view. You can probably use the newView() call here if you want.
convertView = mInflater.inflate(R.layout.myl, null);
// Initialize the holder
holder = new AdapterViewsHolder();
// Now we do the smart thing: We store references to the views we need, in the holder. Just find all the views you need, by id.
holder.txt1 = (TextView) convertView.findViewById(R.id.textview1);
holder.txt2 = (TextView) convertView.findViewById(R.id.textview2);
...
holder.img1 = (ImageView) convertView.findViewById(R.id.imageview1);
// Store the holder in the convertViews tag
convertView.setTag(holder);
}
else {
// If convertView is not null, we can get get the holder we stored in the tag.
// This holder now contains references to all the views we need :)
holder = (AdapterViewsHolder) convertView.getTag();
}
// Now we can start assigning values to the textviews and the imageviews etc etc
holder.txt1.setText(mData.get(position));
...
holder.txt1.setTypeface(mCustomTypeFace);
...
holder.img1.setImageResource("IMAGE RESOURCE HERE");
if(someThing.equals("sometext") {
convertView.setBackgroundDrawable(somedrawable);
}
else {
convertView.setBackgroundDrawable(someotherdrawable);
}
// Finally, we return the convertView
return convertView;
}
I do not know how your data is organized, so you have to change this code a bit.
One more thing that can cause lag is the android:cacheColorHint xml attribute. Usually you set this to either the same color as you application background, or transparent. Setting it transparent have been known to cause rapid Garbage collections on some occasions.
You could override the Adapter and have it inflate all of the views in the constructor, then just return the proper one with getView(). Might make it easy if you store the Views in some data object (array, list etc..)
But really you should let the system use the convertView like it was designed to. Overall you'd get better performance doing it that way I think.

ListView adapter gets confused when convertView is recycled

I have a straight forward BaseAdapter for my ListView. It downloads a JSON feed and displays the data in the rows. There is a ViewHolder which contains the views and a data object called "Story". Everything works just fine.
However, after some scrolling of longer lists, I notice two things.
1) My log shows that the adapter is reloading the feed when scrolling further down. This is strange, as I put the whole JSON array into a variable, so why does it have to reload?
2) More importantly, after some scrolling back and forth, the rows contain the wrong "Story" objects. Here are the relevant parts of the getView routine:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
Story story = stories.get(position);
if (convertView == null) {
//create holder
holder = new ViewHolder();
convertView = inflator.inflate(R.layout.story_list_item, parent, false);
holder.titleView = (TextView) convertView.findViewById(R.id.story_list_title);
holder.dateView = (TextView) convertView.findViewById(R.id.story_list_date);
holder.story = story;
holder.imageView = (ImageView) convertView.findViewById(R.id.story_list_image);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// configure the view
holder.titleView.setText(story.title);
return convertView;
}
Simple enough. Now the strange thing is that I can fix the problem by eliminating the if statement if (convertView == null) (and, I presume, eliminating the row recycling as well).
But will I not run into memory problems this way? Why does the plain vanilla version not work?
Thanks for your help.
Regards,
S
You are aware that you're only assigning
holder.story = story
when convertView == null ? Consider moving holder.story = story to just after your convertView if-case and it should work a lot better. Btw, do you even need to store the "story" inside your view holder? Typically that pattern should only be used to store Views and view state information, not the data of the actual position.

ListView in ArrayAdapter order get's mixed up when scrolling

I have a ListView in a custom ArrayAdapter that displays an icon ImageView and a TextView in each row. When I make the list long enough to let you scroll through it, the order starts out right, but when I start to scroll down, some of the earlier entries start re-appearing. If I scroll back up, the old order changes. Doing this repeatedly eventually causes the entire list order to be seemingly random. So scrolling the list is either causing the child order to change, or the drawing is not refreshing correctly.
What could cause something like this to happen? I need the order the items are displayed to the user to be the same order they are added to the ArrayList, or at LEAST to remain in one static order. If I need to provide more detailed information, please let me know. Any help is appreciated. Thanks.
I was having similar issues, but when clicking an item in the custom list, the items on the screen would reverse in sequence. If I clicked again, they'd reverse back to where they were originally.
After reading this, I checked my code where I overload the getView method. I was getting the view from the convertedView, and if it was null, that's when I'd build my stuff. However, after placing a breakpoint, I found that it was calling this method on every click and on subsequent clicks, the convertedView was not null therefore the items weren't being set.
Here is an example of what it was:
public View getView(int position, View convertView, ViewGroup parent)
{
View view = convertView;
if (view == null)
{
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.listitemrow, null);
RssItem rssItem = (RssItem) super.getItem(position);
if (rssItem != null)
{
TextView title = (TextView) view.findViewById(R.id.rowtitle);
if (title != null)
{
title.setText(rssItem.getTitle());
}
}
}
return view;
}
The subtle change is moving the close brace for the null check on the view to just after inflating:
public View getView(int position, View convertView, ViewGroup parent)
{
View view = convertView;
if (view == null)
{
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.listitemrow, null);
}
RssItem rssItem = (RssItem) super.getItem(position);
if (rssItem != null)
{
TextView title = (TextView) view.findViewById(R.id.rowtitle);
if (title != null)
{
title.setText(rssItem.getTitle());
}
}
return view;
}
I hope this helps others who experience this same problem.
To further clarify the answer of farcats below in more general way, here is my explanation:
The vi.inflate operation (needed here for parsing of the layout of a row from XML and creating the appropriate View object) is wrapped by an if (view == null) statement for efficiency, so the inflation of the same object will not happen again and again every time it pops into view.
HOWEVER, the other parts of the getView method are used to set other parameters and therefore should NOT be included within the if (view == null) statement.
Similarily, in other common implementation of this method, some textView, ImageView or ImageButton elements need to be populated by values from the list[position], using findViewById and after that .setText or .setImageBitmap operations.
These operations must come after both creating a view from scratch by inflation and getting an existing view if not null.
Another good example where this solution is applied for BaseAdapter appears in BaseAdapter causing ListView to go out of order when scrolled
The ListView reuses view objects when you scroll. Are you overriding the getView method? You need to make sure you set each property for every view, don't assume that it will remember what you had before. If you post that method, someone can probably point you at the part that is incorrect.
I have a ListView, AdapterView and a View (search_options) that contains EditText and 3 Spinners. ListView items are multiple copies of (search_options) layout, where user can add more options in ListView then click search to send sql query built according to users options.
I found that convertView mixing indecies so I added a global list (myViews) in activity and passed it to ArrayAdapter. Then in ArrayAdapter (getView) I add every newly added view to it (myViews).
Also on getView instead of checking if convertView is null, I check if the global list (myViews) has a view on the selected (position).. It totally solved problems after losing 3 days reading the internet!!
1- on Activity add this:
Map<Integer, View> myViews = new HashMap<>();
and then pass it to ArrayAdapter using adapter constructor.
mSOAdapter = new SearchOptionsAdapter(getActivity(), resultStrs, myViews);
2- on getView:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
if (!myViews.containsKey(position)) {
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(R.layout.search_options, parent, false);
/// ...... YOUR CODE
myViews.put(position, view);
FontUtils.setCustomFontsIn(view, getContext().getAssets());
}else {
view = myViews.get(position);
}
return view;
}
Finally no more mixing items...

Categories

Resources