I have an application in which I'd like one row at a time to have a certain color. This seems to work about 95% of the time, but sometimes instead of having just one row with this color, it will allow multiple rows to have the color. Specifically, a row is set to have the "special" color when it is tapped. In rare instances, the last row tapped will retain the color despite a call to setBackgroundColor attempting to make it otherwise.
private OnItemClickListener mDirectoryListener = new OnItemClickListener(){
public void onItemClick(AdapterView parent, View view, int pos, long id){
if (stdir.getStationCount() == pos) {
stdir.moreStations();
return;
}
if (playingView != null)
playingView.setBackgroundColor(Color.DKGRAY);
view.setBackgroundColor(Color.MAGENTA);
playingView = view;
playStation(pos);
}
};
I have confirmed with print statements that the code setting the row to gray is always called.
Can anyone imagine a reason why this code might intermittently fail? If there is a pattern or condition that causes it, I can't tell.
I thought it might have something to do with the activity lifecycle setting the "playingView" variable back to null, but I can't reliably reproduce the problem by switching activities or locking the phone.
private class DirectoryAdapter extends ArrayAdapter {
private ArrayList<Station> items;
public DirectoryAdapter(Context c, int resLayoutId, ArrayList<Station> stations){
super(c, resLayoutId, stations);
this.items = stations;
}
public int getCount(){
return items.size() + 1;
}
public View getView(int position, View convertView, ViewGroup parent){
View v = convertView;
LayoutInflater vi = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (position == this.items.size()) {
v = vi.inflate(R.layout.morerow, null);
return v;
}
Station station = this.items.get(position);
v = vi.inflate(R.layout.songrow, null);
if (station.playing)
v.setBackgroundColor(Color.MAGENTA);
else if (station.visited)
v.setBackgroundColor(Color.DKGRAY);
else
v.setBackgroundColor(Color.BLACK);
TextView title = (TextView)v.findViewById(R.id.title);
title.setText(station.name);
return v;
}
};
ListViews don't create instances of contained views for every item in the list, but only for ones that are actually visible on the screen. For performance reasons, they try and maintain as few views as possible, and recycle them. That's what the convertView parameter is.
When a view scrolls off the screen, it may be recycled or destroyed. You can't hold a reference to an old view and assume that it will refer to the item you expect it to in the future. You should save the ID of the list item you need and look that up instead.
Moreover, there are a couple of other issues with your implementation (from a best practices standpoint). You seem to be ignoring the convertView parameter and creating a new view from scratch each time. That can cause your application to bog down a bit while scrolling if you have a long list. Secondly, instead of adding the "more" element the way you do, you're better of setting it with setFooterView().
There's an excellent talk on the ListView from Google I/O 2010 that covers these and other issues. It's an hour long, but definitely worth watching in its entirety.
Related
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 have a ListView that is getting populated by my custom BaseAdapter with views that contain a couple of TextViews and ImageButtons. If the user selects an item from a PopupMenu on one of the list items, it changes the text of an associated TextView on that list item. This is working great for all of the items in my ListView except the first one. The first item never updates the TextView for some reason. I suspect it has something to do with my convertView for that item not getting reset, but I'm not sure how to go about fixing it.
Here is my setup. I have a ListView that is getting populated by a custom BaseAdapter. In my BaseAdapter, I have this for my getView():
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
IMOModGroup modGroup = modGroups.get(position);
convertView = modGroup.getConvertView(parent);
}
return convertView;
}
IMOModGroup is a special data structure that holds all the states and data for each list item. So when the convertView comes in null, the BaseAdapter requests a new convertView from the IMOModGroup for that position. Here's the IMOModGroup method that creates the new convertView:
public View getConvertView(ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.modifier_list_item, parent, false);
//...
TextView selectedModText = (TextView) convertView.findViewById(R.id.modName);
selectedModText.setText(R.string.no_modifier_selected);
selectedModText.setTextColor(context.getResources().getColor(R.color.grayed_out_text_color));
//...
return convertView;
}
The user can open a PopupMenu on each list item and make a selection. When they do this, the TextView in the above code is changed to reflect the selection with new text. That code looks like this:
public boolean onMenuItemClick(MenuItem item) {
String selectedMod = item.getTitle().toString();
TextView selectedModText = (TextView) convertView.findViewById(R.id.modName);
selectedModText.setText(selectedMod);
selectedModText.setTextColor(context.getResources().getColor(R.color.imo_blue_light));
//...
return false;
}
All of this is working great on every item in the ListView except for the very first one, which never updates to reflect the TextView change. I've tested to make sure the TextView text is actually getting changed and can be called and viewed at a later time, and it's being set correctly. So it seems like the data is correct, but the view is not getting updated.
I'm pretty sure the problem is that my convertView is not getting reset, so the ListView is just continuing to use the old view with the old text. I tried calling notifyDataSetChanged() on the BaseAdapter after I set the text, and that didn't work.
This question looks promising: First item in list view is not displaying correctly
But I'm not sure if that's my problem or not, and I'm not sure how I would apply that to my scenario.
Any ideas? I've read through a few similar questions on SO and Google, but none of them really seemed to be relevant to my specific scenario. I've been using ListViews and Adapters for quite a while now and this is the first time I've seen something like this. Thanks for your help.
EDITS IN RESPONSE TO COMMENTS
Here is the complete BaseAdapter code...
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import java.util.ArrayList;
public class IMOModListAdapter extends BaseAdapter {
private Context context;
private ArrayList<IMOModGroup> modGroups;
public IMOModListAdapter(Context context, ArrayList<IMOModGroup> modGroups) {
this.context = context;
this.modGroups = modGroups;
}
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
IMOModGroup modGroup = modGroups.get(position);
convertView = modGroup.getConvertView(parent);
}
return convertView;
}
public Object getItem(int position) {
return 0;
}
public long getItemId(int position) {
return 0;
}
public int getCount() {
return modGroups.size();
}
}
FURTHER TESTING
As I digged deeper into my debugging, I realized that when the TextView for element 1 in the list is changed, getView() is not even called at all. But when any of the other elements are changed, getView() is called as normal.
As a sanity check, I tried changing the text of the first list item from the outside, from the parent Activity itself. If I try to change the first item, it does nothing, but if I try to change any of the other list items, it works fine! This is so bizarre! This is what I tried:
//Set the text of the first item. This does NOTHING and the TextView
//in the item remains unchanged
IMOModGroup modGroup = modGroups.get(0);
modGroup.setTestText("This is some test text");
modList.notifyDataSetChanged();
//Set the text of the second item. This works completely fine!!!!!
IMOModGroup modGroup = modGroups.get(1);
modGroup.setTestText("This is some test text");
modList.notifyDataSetChanged();
Well I found a solution to this issue. Just researching random SO questions about BaseAdapters and getView(), I finally came across this post by Romain Guy in which he states that you should never use wrap_content for the layout_height of a ListView, since it causes the ListView to call the adapter's getView() tons of times just to create dummy views which it uses to measure the height of the content. It turns out this was causing my issue, so changing my layout_height to match_parent did the trick.
Unfortunately, now this causes a new problem since I can't use match_parent since it will hide other views in my parent layout. But at least the first list item is now updating correctly.
UPDATE:
I learned how to manually and dynamically set the height of my ListView after its adapter is populated. I used the method below, and hopefully this will help someone out running into similar issues. This made it so I didn't have to use wrap_content but I could still set the ListView height to only be as big as its content.
private void setModListHeight() {
int numberOfItems = modGroups.size(); //the ArrayList providing data for my ListView
int totalItemsHeight = 0;
for(int position = 0; position < numberOfItems; position++) {
View item = modListAdapter.getView(position, null, modList);
item.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
totalItemsHeight += item.getMeasuredHeight();
}
int totalDividersHeight = modList.getDividerHeight() * (numberOfItems - 1);
ViewGroup.LayoutParams params = modList.getLayoutParams();
params.height = totalItemsHeight + totalDividersHeight;
modList.setLayoutParams(params);
modList.requestLayout();
}
I am currently having an issue with the creation of my ExpandableListView. I'm unsure what is happening at the moment but I'll explain what I'm trying to do.
I programmatically allocate meals that fall on a certain day. Within each meal (the title is the group) is a child that contains a vote and order button. Both of which are changed to "Ordered" when a order is made on a meal. If another order is made the previous order button goes back to the 'order' state and the selected one goes to ordered. This is the same for vote. I go about this by setting an on click listener to my buttons that loop through all the other buttons and set their text to 'order'/'vote' and set the current buttons text to ordered.
This works fine in some extremely rare cases - but most of the time when I order/vote for an item it changes the selected one to 'ordered' and the last element in my expandable list view to ordered as well. Then if I order, lets say the 3rd element, close and reopen the second element it also changes to ordered and visa versa. Sometimes they all change back to order or all change to ordered. I'm finding it difficult to work out why this may be happening.
I believe it may have something to do with the getChildView function. As when I traverse through the list opening each element the view is never set for the last element even though it has been allocated the appropriate data.
Am I misunderstanding a major concept of the ExpandableListView or is there something else.
Here is the getChildView function that I believe there may be a fault. I'll also supply links to the other relevant classes below. If you need further information, please don't hesitate to ask.
//Static class view holder
static class ViewHolder {
protected Button oBut;
protected Button vBut;
protected TextView description;
}
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
//Getting the appropriate child for the group and position
ExpandListChild child = (ExpandListChild) getChild(groupPosition, childPosition);
View view = null;
//Set up if doesn't exist
if (convertView == null) {
Log.d("OnClickListener", "groupPosition: " + groupPosition + " childPosition: " + childPosition);
Log.d("OnClickListener", "View being set up for: " + child.getName() + " desc: " + child.getDescription());
LayoutInflater infalInflater = (LayoutInflater) context.getSystemService(context.LAYOUT_INFLATER_SERVICE);
view = infalInflater.inflate(R.layout.expandlist_child_item_todays, null);
//Attempt at viewholder
final ViewHolder holder = new ViewHolder();
holder.description = (TextView) view.findViewById(R.id.mealDescription);
holder.vBut = (Button)view.findViewById(R.id.vote);
holder.oBut = (Button)view.findViewById(R.id.order);
view.setTag(holder);
}
else {
view = convertView;
}
ViewHolder holder = (ViewHolder) view.getTag();
holder.description.setText(child.getDescription());
holder.description.setTag(child.getTag());
holder.oBut.setOnClickListener(new OrderOnClick(groups, (Activity)context, holder.oBut, view, child));
holder.vBut.setOnClickListener(new VoteOnClick(groups, (Activity)context, holder.vBut, view, child));
return view;
}
ExpandListAdapterTodays (extends and extends off my BaseExpandableListAdapter)
ExpandListAdapter (extends the BaseExpandableListAdapter)
VoteOnClick (Class that implements the changing of button text when a successfull vote has been placed)
the way that listviews on android work to be efficient is that they implement a "view recycling" method where if some view goes off screen, that same view is put back somewhere on the screen with all the necessary bits changed. That keeps things using much less resources, actually reusing the same resources, but wreaks havoc if you need states persisting in a specific order like you do. What you should do is to implement some sort of map or arraylist of which the position on the object corresponds to its position of the listview and then make changes through the made object. A little lame but its kinda like an adapter method for your adapter.
Now i apologize because i can't exactly visualize how the onlick method is supposed to work, but looking through it.. something like:
for (int i = 0; i < groups.size(); i++) {
....
but.setText("Vote");
would become
ArrayList<String> group_string_states = new ArrayList<String> ();
private void fillGroupStringStates () {
for(int i = 0; i < groups.length; i++) {
group_string_states.add("Order");
}
}
....
for (int i = 0; i < group_string_states.size(); i++) {
....
group_string_states.set(i, "Vote");
Then you do a conditional, like
if group_string_states.get[position] says "vote", then do this.
Well, that's how i'd attempt to do it. i hope it helped and i'm really sorry if it didn't.
I have a GridView in which I want to always show 7 icons, and sometimes an additional icon depending on a request. In the beginning the additional icon is never shown. This is the structure:
0 1 2
3 4 5
6 [7]
All the icons fit into the screen so I don't need/have scroll. Each icon is composed by an image and a text.
For this, I have a CustomAdapter which extends BaseAdapter. I have overriden the getView method in which I set the text and the image for each icon.
public View getView(int position, View convertView, ViewGroup parent) {
View v = null;
if (convertView == null) {
LayoutInflater li = ((Activity) context).getLayoutInflater();
v = li.inflate(R.layout.icon, null);
} else {
v = convertView;
}
TextView tv = (TextView) v.findViewById(R.id.icon_textView);
tv.setText(position);
ImageView iv = (ImageView) v.findViewById(R.id.icon_ImageView);
iv.setImageResource(imageResourcesArray[position]);
if ((position == ADDITIONAL_ICON)) && !showAdditionalIcon) {
v.setVisibility(View.INVISIBLE);
}
return v;
}
The imageResourcesArray[] is an array of integers with the image resources.
The other functions and variables in the CustomAdapter are:
public static final int ADDITIONAL_ICON = 7;
private boolean showAdditionalIcon = false;
public showAdditionalIcon(){
this.showAdditionalIcon = true;
notifyDataSetChanged();
// notifyDataSetInvalidated();
}
public hideAdditionalIcon(){
this.showAdditionalIcon = false;
notifyDataSetChanged();
// notifyDataSetInvalidated();
}
Later on, I create and set the CustomAdapter to the GridView from a class which extends Activity (say ClassA):
GridView grid = (GridView) findViewById(R.id.main_gridView);
customAdapter = new CustomAdapter(this);
grid.setAdapter(customAdapter);
My problem appears when after some calculations and requests to a server, I have to show the additional icon (number 7). So I call (from ClassA):
customAdapter.showAdditionalIcon();
Now, the additional icon appears, but the first icon disappears... I have tried to use notifyDataSetInvalidated() and notifyDataSetChanged() but both had the same result.
Of course, I could generate a new CustomAdapter with the additional icon allowed, but I would preffer not to do it...
Thanks in advance.
I'm not sure if this counts as an answer for you. Root of the problem seems to be the convertView we are using. I did not dig so deep into Android source, but I think there is no guarantee on how views are reused even it is obvious that all views are already visible and there should be no reuse behind the scenes.
What this means is that the view we linked to position 7 as we visualize this whole scenario is actually reused later at position 0. Since your code does not explicitly reset a view to be visible, the view will be reused with visibility set to INVISIBLE, thus the mystery of the disappearing first item.
Simplest solution should be as #Vinay suggest above, by explicitly setting to View.VISIBLE.
if ((position == ADDITIONAL_ICON))) {
if (!showAdditionalIcon)
v.setVisibility(View.INVISIBLE);
else
v.setVisibility(View.VISIBLE);
}
Hope this helps, but I'm really hoping some Android expert pops by to tell us more about how this whole thing of reusing old views actually works.
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...