I am building a ListView with sections according to the technique described at http://bartinger.at/listview-with-sectionsseparators/ . But I would like to extend it by reusing convertView for the non-section items. However, I am finding that the convertView variable is null each time getView() method is entered. Could someone explain why this is the case?
ViewHolder holder;
final ListViewItem item = items.get(position);
if (item.isSection()) {
Section section = (Section)item;
convertView = inflater.inflate(R.layout.section, null);
TextView title = (TextView) convertView.findViewById(R.id.section_title);
title.setText(section.title);
} else {
if (convertView == null) {
Log.d("Adapter", "convertView was null");
}
Server server = (Server)item;
convertView = inflater.inflate(R.layout.server_row, null);
holder = new ViewHolder();
holder.serverName = (TextView) convertView.findViewById(R.id.server_name);
holder.serverStatusIcon = (ImageView)convertView.findViewById(R.id.server_status_icon);
convertView.setTag(holder);
holder.serverName.setText(server.name);
}
return convertView;
The list is being created and displayed without errors and contains both sections and non-section items just fine.
Are you implementing correctly
getItemViewType (int position) ?
See from Android's documentation:
Returns
An integer representing the type of View. Two views should share the same type if one can be converted to the other in getView(int, View, ViewGroup). Note: Integers must be in the range 0 to getViewTypeCount() - 1. IGNORE_ITEM_VIEW_TYPE can also be returned.
So maybe the convertView is always null because the adapter doesn't know which items belong together, so it doesn't know which ones pass to be recycled...
Try this:
#Override
public int getItemViewType(int position) {
if (((MyItem)getItem(position)).isHeader()) {
return 1;
} else {
return 0;
}
}
#Override
public int getViewTypeCount() {
return 2;
}
The index which you return in getItemViewType is just an identifier to group headers and not-headers together.
In this case you have to implement a method "isHeader" (or analogous) in your model items.
Thank you to Ixx for jogging my mind: what I hadn't noticed was that my list was so short it never actually filled the screen so no recycling was taking place.
For completeness sake, I will add that if you create multiple view types, getView() does return convertView - even if you do not override getItemViewType() and getViewTypeCount() - according to their default implementation (below). Of course, it is likely not the behaviour you want.
public int getItemViewType(int position) {
return 0;
}
public int getViewTypeCount() {
return 1;
}
I just went through this with a GridView that I created. I had problems when I tried to assign a newly inflated view to convertView. The generic structure I adopted was
public View getView(int position, #Nullable View convertView, ViewGroup parent) {
View newView = null;
TextView someText;
// Test to see if there is already a view, if not create one, else use what is
// already existant in convertView
if (null == convertView) {
// inflate your view type into newView here
newView = myInflater.inflate(R.layout._________);
// Get all of your subviews you wish to edit here from newView
someText = (TextView)newView.findViewById(R.id._______);
}else{
// Get all of your subviews you wish to edit here from convertView
someText = (TextView)convertView.findViewById(R.id._______);
}
// Perform all re-alignments, view layouts etc... here
// Perform all updating of subviews data here
// Return structure
if (null == convertView) {
return newView;
} else {
return convertView;
}
}
Hope this helps!
Related
I am using this code to layout my ListView, using a different layout based on some data:
#Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
ViewHolder viewHolder;
MyInfo myInfo = getItem(i);
String label = myInfo.getLabel();
if (convertView == null) {
if (!"".equals(label)) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.info_grey, null);
Log.d(SapphireApplication.TAG, "GREY, label=" + label);
} else {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.info_plain, null);
Log.d(SapphireApplication.TAG, "PLAIN, label=" + label);
}
viewHolder = new ViewHolder();
viewHolder.tvLabel = convertView.findViewById(R.id.tvLabel);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder)convertView.getTag();
}
viewHolder.tvLabel.setText(label);
return convertView;
}
However, the Log.d is never done for some items in the list. Does that mean Android re-uses an existing convertView, causing it (in this case) to use the wrong layout?
Yes. They are being re-used. And that is the reason you are seeing that message log for few items only.
#Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
if (convertView == null) {
// convertView is null. It means the ListView does not have a view to give to you
// This way, you need to create a new one.
// You will enter here until the ListView has enough Views to fill
// the screen.
// So, just inflate the view and set the View holder here.
// Don't customize your view here (set text, contenet etc)
// So, any log message here will be printed only when the ListView becomes visible (and when you scroll to next item)
// After that, views will be re-used so convertView will no longer be null
} else {
// ListView gave a convertView to you. It means that you are receiving a View
// that was created in the past and it is be re-used now.
// At this moment, convertView still has the content of the old item it was
// representing.
// This view was created in the statement above and after user scrolled the ListView
// it becomes hidden and ready to be re-used.
// Don't customize the view here.. just get the ViewHolder from the View
}
// Here you customize the View. Set content, text, color, background etc
// The ViewHolder is just a helpful class to help you to access
// all View inside the convertView without needing to perform the
// findViewById again.
return convertView;
}
In this basic example however, all convertViews are similar. They were inflated from the same layout.
This works fine for when you have a single view type per line. all items are similar but with different content.
This still works if you have small differences. For example, you can inflate same layout and control the visibility of some of its Views according to the position (position 1 has an image and position 2 don't).
However, there are some cases where you really need to inflate different layouts per row.
For example, the "grey" layout is very different from the "plain" layout. On this case, you need to update you
code as follows:
private static final int GREY = 1;
private static final int PLAIN = 2;
private static final int TOTAL_VIEW_TYPES = 2; // Grey and Plain
#Override
public int getViewTypeCount() {
// Tell the list view that you have two types of Views (Grey and plain)
return TOTAL_VIEW_TYPES;
}
#Override
public int getItemViewType(int position) {
// You must inform view type for given position
String label = myInfo.getLabel();
if (!"".equals(label)) {
return GREY;
} else {
return PLAIN;
}
}
#Override
public View getView(int position, View convertView, ViewGroup viewGroup) {
if (convertView == null) {
// If view is null, you must create the view. But you need to create the
// correct view for given position
if(getItemViewType(position) == GREY) {
// Inflate grey;
} else {
// inflate plain
}
} else {
// convertView is not null. It is being reused.
// Android will give you the proper view here. If you are expecting
// a plain type, that's what you will get. Android won't re-use
// plain layout where you are expecting to have the grey layout.
// It will re-use the proper view for each position (following to the getItemViewType()).
// ListView is very robust.
}
// Update the view here.. Just remember that here you may have two different
// types of view.. grey or plain.
return convertView;
}
The most cool about theses concepts is that they are valid for any view that uses this View<->Adapter relation..
RecyclerView, ListView, Spinner, PagerView etc.
I have got a situation.I am making chat application, on that I have one sender side and one receiver side. All the chats is shown in a List View individual rows.On get view lets suppose I have 5 sender messges and 5 receiver messages (including images) how will I handle the case.For sending side I have a layout (I am inflating using holder view pattern) it contains progress bar to show status image uploading on the other hand I am inflating another layout but on that I don't have any progress bar.In this case How recyles works?I mean last item(10th item visible to screen) is image that belongs to reciver side and it has no progress bar but as soon as scroll for 11th item recyle works and 11 th item is from sender side and it contains progress bar.Further more for system messages there is a third layout (to show status today,yesterday) and this third alot has just single text view what would happen when it gets reccyle for that item that contains image(when this layout does not have any image view)
Should I use single row for all this and show/hide accordin to data Or is there any better approach available?
You need to use
getViewTypeCount()- Returns the number of types of Views that will be created by getView(int, View, ViewGroup).
getItemViewType(int position) - Get the type of View that will be created by getView(int, View, ViewGroup) for the specified item.
#Override
public int getItemViewType(int pos) {
return pos % 2;
}
#Override
public int getViewTypeCount() {
return 2;
}
Then update getView to handle 2 types of layouts
public View getView(int pos, View convertView, ViewGroup parent) {
..............................................................
..............................................................
if (convertView == null) {
inflater = (LayoutInflater) context.getSystemService(LAYOUT_INFLATER_SERVICE);
if (getItemViewType(pos) == 0) {
convertView = inflater.inflate(R.layout.layout_type_one, null);
} else {
convertView = inflater.inflate(R.layout.layout_type_two, null);
}
..............................................................
..............................................................
}
...................................................................
...................................................................
}
You can create a type field in your model class and inflate your View according to this type only.
your getView method will look like this:
if (convertView == null) {
if (model.getType() == 0) {
convertView = mInflater.inflate(R.layout.row_sent_message,
null);
} else if (model.getView() == 1) {
convertView = mInflater.inflate(R.layout.row_sent_message,
null);
} else if (model.getView() == 2) {
convertView = mInflater.inflate(R.layout.row_other, null);
}
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
And your ViewHolder Class should be like this:
private class ViewHolder {
public ViewHolder(View rowView, int rowType) {
// TODO Auto-generated constructor stub
// According to your rowType find id of your view here
}
}
I'm using Adapter with 3 types of Views
public class MyAdapter extends ArrayAdapter<MyClass> {
#Override
public int getViewTypeCount() {
return 3;
}
//...
}
And it appears that sometimes wrong types of Views are passed to getView. Indeed docs warn about it:
Note: You should check that this view is non-null and of an appropriate type before using.
How should I check if a view is of an appropriate type before using?
Should I just check by findViewById if it contains some id from appropriate xml? But does it really check "if a view is of an appropriate type"?
EDIT: Answers so far seems to not miss my question so to clarify:
Yeah I'm using getItemViewType but as I have 3 types of Views, then sometimes convertView in getView has wrong type (not the View which was inflated for the getItemViewType) and cannot be converted to the right one - the question is not about how to check which View SHOULD be returned (and this is covered by "bigdestroyer" answer), but if the View passed to getView can be reused (and in my case it cannot).
"Appropriate type" means that you have to check in getView method the type of the convertView in order to return a custom View or another one.
You can override getItemViewType method of BaseAdapter. For instance:
public int getItemViewType(int position) {
MyItem item = getItem(position);
return item.getViewType();
}
and:
public View getView(int position, View convertView, ViewGroup parent) {
int viewType = getItemViewType(position);
if (convertView != null) {
if (viewType == 1) return recycleViewOfType1(position, convertView);
if (viewType == 2) return recycleViewOfType2(position, convertView);
}
else {
if (viewType == 1) return newViewOfType1(position);
if (viewType == 2) return newViewOfType2(position);
}
return null;
}
You can use getItemViewType() method to determine view type before creating/ reusing views in getView() method.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
int type = getItemViewType(position);
if(type == 1){
}else {
}
}
check this tutorial for more info
In arrayAdptor we use following code:
final LayoutInflater inflater = activity.getLayoutInflater();
row = (LinearLayoutCustom) inflater.inflate(R.layout.row, null);
final TextView label = (TextView) row.findViewById(R.id.title);
label.setText(position + "" + items[position]);
return row;
Now suppose some value are null (for example at position 2 , items[2] = null ) so i dont want to show it in row. i want to hide it. if i use
row.setVisibility(View.GONE)
it leaves a blank space at this row which i dont want. so what should i do?
AFAIK you can't return a null view from getView, but you could just make the view invisible and height 1. Although manipulating using the getCount is probably the preferred way.
view.setVisibility(View.INVISIBLE);
view.getLayoutParams().height = 1;
You'll need to have the adapter return the total number of non-null items with getCount and then keep a mapping of position to your internal data structure.
For example.
You have a list
1 - John
2 - null
3 - Bill
4 - Susan
5 - null
When getCount is called it returns 3.
Then when getView is called on position 1 you return the item at list[1].
getView on position 2 returns list[3] (as it's the 2nd non-null),
and so forth.
This is the only way I've found to do this.
You can use a View that has no height for the "hidden" items so that you don't have to do all the model housekeeping and mapping. For example, suppose you had a "filter" EditText field that when data is entered it only keeps matching items:
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) MyActivity.this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
RelativeLayout view = (RelativeLayout) inflater.inflate(R.id.myListLayout, null, false);
...
// if we didn't match filter be GONE and leave
if (filterText.length() > 0 && myModelValueAtPosition.toLowerCase().indexOf(filterText) < 0){
view = (RelativeLayout) inflater.inflate(R.layout.myListLayoutWithZeroHeight, null, false);
view.setVisibility(View.GONE); // this doesn't really do anything useful; I'd hoped it would work by itself, but turns out the zero height layout is the key
return view;
}
view.setVisibility(View.VISIBLE);
...
}
Here you need to write the logic in your getCount(),getItemId() and getItem(),
It will create the no of rows what the getCount return
//How many items are in the data set represented by this Adapter
public int getCount() {
return //Should return the count of rows you need to display (here the count excluding null values)
}
And
//This need to return data item associated with the specified position in the data set.
public Object getItem(int position) {
return //Return the object need to display at position, need the logic to skip null value
}
Edit:So in your getview
public View getView(int position, View convertView, ViewGroup parent) {
----
getItem(position);//Object corresponding to position ,In your case it will not be null since you need to write the logic to skip null object at getItem
----
}
This is the solution I implemented, here is a code example for everyone that is looking it:
class Shipment_Adapter extends ArrayAdapter<Shipment>{
....
ArrayList<Integer> emptyPositions = new ArrayList<>();
public Shipment_Adapter(Context context, int shipment_row, Shipment[] myShipments){
super(context, R.layout.shipment_row,myShipments);
//populate emptyPositions list
for(int i = 0; i< myShipments.length; i++){
if(myShipments[i]==null){
emptyPositions.add(i);
}
}
this.mShipment = myShipments;
this.mContext = context;
}
//corrects size of List
#Override
public int getCount() {
return (mShipment.length - emptyPositions.size());
}
//recursive function that checks if position is not empty until it isn't
public int isEmpty(int positiontocheck){
int newposition;
if(emptyPositions.contains(positiontocheck)){
//true? check that next one is free
return isEmpty(positiontocheck+1);
}else{
newposition = positiontocheck;
}
return newposition;
}
}
public View getView(int position, View convertView, ViewGroup parent) {
//now just need to use isEmpty to get the next not empty position in
//case our real position is empty
position= isEmpty(position);
Shipment shipment = mShipment[position];
...//and so on
}
hope this helps!
I am wondering how to manage the views inside a ListView.
I have a custom Adapter that is set on the ListView, this Adapter overrides the getView method
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
v = mInflater_.inflate(R.layout.news_newsentry, null);
}
final NewsItem newsItem = getItem(position);
if (newsItem != null) {
// Do stuff
}
return v;
}
But the thing is that when the user clicks on an item, I slightly change the view to make it bigger. It works well, but when the item view is recycled, it keeps the "big" height to display another item.
To prevent that, I changed the code to create a new View each time
Change:
View v = convertView;
if (v == null) {
v = mInflater_.inflate(R.layout.news_newsentry, null);
}
By
View v = mInflater_.inflate(R.layout.news_newsentry, null);
The problem now is that when the item disappears from the list and reappears (the list is scrolled), the view is completely new and the height is set to "small".
My question then: how to manage the items views to keeps their properties, without messing with the other views and the view recycling?
I think you can get the result you want by using the ListView built in support for more than one view type in a list.
In your adapter you would implement additional methods similar to
#Override
public int getItemViewType(int position) {
int type = 0;
if (position == mySelectedPosition) {
type = 1;
}
return type;
}
#Override
public int getViewTypeCount() {
return 2;
}
Then your getView method will be handed a view of the correct type for the position of the item. Ie, the selected item will always be given a "big" view to re-use.
Creating a new View every time is not recommended for performance and memory reasons.