I'm facing some trouble with the navigation drawer adapter.
It's supposed to display items as follows: Favorito, Categorias, and small sub categories underneath categorias.
I programmed the navigation drawer adapter to use a big_layout.xml file by default, but if its position is greater than a certain value, then it uses a small_layout.xml file.
It works fine for the first few items, but the problem is when I scroll down to see the rest of the items, they use the big_layout.xml, and then when I scroll back up, the original big items change their view and use the small layout!
below is the code, and this is a screen shot of the bad results: http://i.stack.imgur.com/QWwts.jpg
#Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
LayoutInflater laoutInflater = (LayoutInflater)
context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
if (getItemId(position)>3)
view = laoutInflater.inflate(R.layout.drawer_list_item_small, null);
if (getItemId(position)<=3)
view = laoutInflater.inflate(R.layout.drawer_list_item, null);
}
ImageView icon = (ImageView) view.findViewById(R.id.icon);
icon.setImageResource(drawerItems.get(position).getIcon());
TextView title = (TextView) view.findViewById(R.id.title);
title.setText(drawerItems.get(position).getTitle());
return view;}
Is there anything wrong I'm doing ? , Is there something missing that might be responsible of making the view stable?
How can i fix this ?
Your issue is with recycling. When you scroll down and back up, the views using the small layout are no longer needed, and so are eligible for recycling - now, the view is not null, so the layout will not be reinitialised based on its position, but merely updated with the new content.
You can fix this by using ViewTypes in your list adapter class, overriding the following methods.
#Override
public int getItemViewType(int position) {
return (position > 3) ? 0 : 1;
}
#Override
public int getViewTypeCount() {
return 2;
}
Then, in your getView() you will not be given a view (for recycling) if it is of the wrong view type.
#Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
int layout = getLayoutForViewType(position);
view = LayoutInflater.from(parent.getContext()).inflate(layout, null);
}
...
return view;
}
private int getLayoutForViewType(int position) {
if (getItemViewType(position) == 0) {
return R.layout.blahblahblah;
}
return R.layout.bloobloobloo;
}
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.
As you should know, ListView recycles the view. But i want to work with elements that can be clicked and expanded. Like i already did:
But it was completely messed up, even using:
View checklayout = convertView;
if(checklayout == null){
checklayout = inflater.inflate(R.layout.home_cell, null);
}
When some opened expandable views goes out of the screen, the recycled one, which shouldn't be expandable, receives the vanished's layout. Only view that has "1 AVALIAÇÃO LANÇADA" should open, and show it's content. I add this content by using if(qtdAvaliacoes > 0) that is a property of my Object that comes from ArrayList<>.
I "solved" this disabling the recycler, with:
#Override
public int getViewTypeCount() {
return getCount();
}
#Override
public int getItemViewType(int position) {
return position;
}
Once my listView will only receives 5~10 rows. But i know that isn't a good practice. While i'm writting this question, i found a solution, calling my object before inflate any view, then checking the property:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View checklayout = convertView;
final LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final Disciplina disciplina = lista.get(position);
if(checklayout == null || disciplina.getQtdAvaliacoes() == 0){
checklayout = inflater.inflate(R.layout.home_cell, null);
}
final View layout = checklayout;
But I don't think this is the best way to do this. I read something about Tags, but was little confused. I think if i could bind these onClick methods to the row position it would be better.
Any ideas ? Or is my solution good at you, developer's, point of view.
Thanks.
The easiest way is to not do subinflates within a list item. Do it via view visibilities instead, making the inflated part GONE if you don't want it to display yet. You'll just have to explicitly set the visibility of that view in every call to getView
I'm making an app that uses a listview that has section headers and content. For each header I want to use an image but the view is setting it's height to the image's and not the value that I set the height at. This is in my adapter:
#Override public View getView(int position, View convertView, ViewGroup parent) {
TextView view = (TextView) super.getView(position, convertView, parent);
view.setTextColor(Color.DKGRAY);
Item item = getItem(position);
if (item.type == Item.SECTION) {
if (position == 0) {
view.setBackgroundResource(R.drawable.myImage);
view.setHeight(400);
view.setMaxHeight(400);
}
else {
//deal with other views
}
}
return view;
}
I'm testing this by just setting the first item in the list as a photo, but as I said, the view isn't using 400px as the height. If I set the height the same way in the else block and just set the background as a color it works fine. I should note that I want to avoid just scaling the picture manually to fit because eventually I want the view to just show a frame of the full image and add a scrolling effect.
I'm not quite sure what you are asking. Do you want it only to only show 400px if there is an image and smaller if there is not? With the code you have does the image show?
I immediately want to rewrite your code like this:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
convertView = new TextView(mContext); // where mContext = context; appears in the adapter constructor
convertView.setTextColor(Color.DKGRAY);
Item item = getItem(position);
if (item.type == Item.SECTION) {
if (position == 0) {
convertView.setBackgroundResource(R.drawable.myImage);
convertView.setHeight(400);
convertView.setMaxHeight(400);
//need something like convertView.setText((String) item); to get text to show
}
else {
//deal with other views
}
}
return convertView;
}
I'd recommend using xml to define the layout for the row. Makes it far easier to customise the look of the row.
I intend to create a listview with different layout for each visible row. I also set setStackFromBottom(true).
I know I can use int getItemViewType(int position) in View getView(int position, View convertView, ViewGroup parent) but it uses the fix position and not based on visible positions.
I'd like to use the listview as the following concept shows:
If I scroll down, the next item should replace the previous one and use its large layout.
I tried it with the following code, but it uses fix position so always the last item in the list is the largest, not the last visible item.
#Override
public int getItemViewType(int position)
{
if(position==values.length-1)
return 1;
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View rowView = null;
type=getItemViewType(position);
if (type == 0)
rowView = inflater.inflate(R.layout.list_item, parent, false);
else
rowView = inflater.inflate(R.layout.list_item2, parent, false);
return rowView;
}
I reckon, I should get the position of the first and last visible item inside the Adapter, but I don't know how to do it.
Can I somehow create a listview that follows the aforementioned concept? Can I get the correct positions?
You can get a lot of info by setting a scroll listener on the ListView:
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// firstVisibleItem + visibleItemCount = last item
}
}
However, I can't think of how you could use this info effectively. Even if you change getView() to return a larger View when its position is the same as the last item, you can't easily resize the views as they move up, because getView() won't be called again for items already displayed, unless you call notifyDataSetChanged() on the adapter repeatedly, which would kill performance.
My though is to add another View below the ListView that will display the larger View for the item below the last one in the ListView. Make it look like the last row in the ListView. You could change that view in the onScroll() method.
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.