I have an ArrayAdapter class that creates comment boxes. There is a button within the comment box that will be either blue or black. The color of the button is dependent on an array which is received through JSON. If the array looks like this "NO","NO","YES","NO","NO","NO" the third button will have blue text. My JSON and ArrayAdapter class create 7 comment boxes at a time. The problem is once the code changes a button to blue it continuously changes the button blue. By this I mean if an array is received that looks like this "NO","NO","YES","NO","NO","NO" the third button will be blue, then I receive another set of comments so this time the array looks like this "NO","NO","NO","NO","NO","NO" according to this code no button should be blue, but for some reason the third button is still blue. I could load multiple more sets of comments and the third button will always be blue even though the code clearly says it should be black. Strangely the button will be blue but will act as if it were a black button. Here is my ArrayAdapter,
class ListAdapter extends ArrayAdapter<Item> {
public ListAdapter(Context context, int textViewResourceId) {
super(context, textViewResourceId);
}
private List<Item> items;
public ListAdapter(Context context, int resource, List<Item> items) {
super(context, resource, items);
this.items = items;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi;
vi = LayoutInflater.from(getContext());
v = vi.inflate(R.layout.list_item_layout, null);
}
final Item p = items.get(position);
if (p != null) {
//set xml objects
//must be done inside of class
ButtonListViewItem = (TextView) v.findViewById(R.id.button_listview_item);
if(p.getJSONArray().equals("NO")){
ButtonListViewItem.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
ButtonListViewItem.setTextColor(0xff000000);
new AsyncTask().execute();
}//end on click
});
}//end if equals NO
if(p.getJSONArray().equals("YES")){
ButtonListViewItem.setClickable(false);
ButtonListViewItem.setTextColor(0xff3399FF);
}//end if equals yes
}//end if null
return v;
}//end getView
}//end ListAdapter class
The text color is wrong because you're not correctly handling recycled views.
The shortest and simplest solution is to remove this check:
if (v == null)
and inflate a new view every time. This is less efficient, but will make your code easier to work with.
The solution if you opt to continue using recycled views is to explicitly set the text color and clickability of the button every time:
if (p.getJSONArray().equals("YES")) {
ButtonListViewItem.setClickable(false);
ButtonListViewItem.setTextColor(0xff3399FF);
} else {
ButtonListViewItem.setClickable(true);
ButtonListViewItem.setTextColor(0xff000000);
}
The reason you need to do that is because recycled views are handed over just as you left them, changed attributes and all. They will no longer match your XML layout. So when a view that was previously tied to a "YES" is recycled, the changes you made to the text color will still be in place: the text will be blue and the button won't be clickable.
Inflating a new view allows you start in a known state every time---you'll always have something that starts off matching your XML. The tradeoff is efficiency, inflating views is relatively expensive. If your apps needs to be more efficient you should look into the view holder pattern as well, as finding views is also an expense that can be avoided.
Related
Could someone please help before this drives me completely insane!
Imagine you have a list view. It has in it 9 items, but there is only space to display 6 without scrolling. If an item is selected the background colour will change to indicate this.
If you select any item from 2 to 8 inclusive all is well in the world.
If you select item 1 it also selects item 9 and vica versa. Also with this selection if you scroll up and down a random number of times, the selection will change. If you continue to scroll up and down, the selection changes back to 1 and 9. The value of the selected item is always the actual item you selected.
This is my code from my adapter :
public class AvailableJobAdapter extends ArrayAdapter<JobDto> {
private Context context;
private ArrayList<JobDto> items;
private LayoutInflater vi;
public AvailableJobAdapter(Context context, ArrayList<JobDto> items) {
super(context, 0, items);
this.context = context;
this.items = items;
vi = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
JobDto jh = getItem(position);
if (convertView == null) {
convertView = vi.inflate(R.layout.inflator_job_list, null);
holder = new ViewHolder();
holder.numberText = (TextView) convertView.findViewById(R.id.txtNumber);
holder.descriptionText = (TextView) convertView.findViewById(R.id.txtDescription);
holder.statusText = (TextView) convertView.findViewById(R.id.txtStatus);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.numberText.setText(jh.getJobNumber());
holder.descriptionText.setText(jh.getDescription());
holder.statusText.setText(jh.getStatus());
return convertView;
}
public static class ViewHolder {
public TextView numberText;
public TextView descriptionText;
public TextView statusText;
}
}
and this is the code from my click listener :
jobs.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Button btnOk = (Button) findViewById(R.id.btnOk);
view.setSelected(true);
int selected = position;
int pos = val.get(selected);
int firstItem = jobs.getFirstVisiblePosition();
int viewIndex = selected - firstItem;
if (pos == 0) {
jobs.getChildAt(viewIndex).setBackgroundColor(getResources().getColor(R.color.selected));
val.set(selected, 1);
selectedCount ++;
} else {
jobs.getChildAt(viewIndex).setBackgroundColor(getResources().getColor(R.color.unselected));
val.set(selected, 0);
selectedCount --;
}
if (selectedCount > 0 ){
btnOk.setEnabled(true);
} else {
btnOk.setEnabled(false);
}
}
});
I have spent hours researching this and trying various suggestions.
Any help will be greatly appreciated.
****EDIT****
After playing with some suggestion I tested it with a HUGE list. This is exactly what the behaviour is :-
If your screen has space for 10 items, if you select item 1 it also highlights 11, 21, 31, 41 etc.
Anything in between these values behaves correctly.
From this guide:
When your ListView is connected to an adapter, the adapter will instantiate rows until the ListView has been fully populated with enough items to fill the full height of the list. At that point, no additional row items are created in memory.
Instead, as the user scroll through the list, items that leave the screen are kept in memory for later use and then every new row that enters the screen reuses an older row kept around in memory. In this way, even for a list of 1000 items, only ~7 item view rows are ever instantiated or held in memory.
That's the root of the problem you are facing. You are changing the background color of the views in your click listener. But once a selected item is scrolled out of the screen, its view will be reused for the new item that is swiping in. As the reused view had its background color changed, the new item will consequently have that same color.
You need to take in account that views are recycled, so they might be "dirty" when you get them in getView(). I recommend you to take a look at the guide from where I got the quotes above, it's a nice and important read.
One possible way to fix that is to add a boolean field to your JobDto class and use it to track if an item is selected or not. Then in getView(), you could update the background color accordingly. You'll also probably need to add the item root view(convertView) to your ViewHolder in order to change its background color.
In setOnItemClickListener() just update setSelected() true of false for clicked position and notifydataset. In getView put a condition
if(jh.isSelelcted())
{
// background is selected color
}else{
// background is non selected color
}
note : handle else condition.
Having a weird issue here where the ListView doesn't render what the code reflects when debugging or in LogCat output.
It's pretty simple. I'm populating a ListView using a custom ArrayAdapter, with some POJO entities. The data is correct and works perfectly, no issues there. When a user clicks one of the items I'm setting a flag in the table to mark the record as 'read'. In the ArrayAdapter, I'm checking for that flag and setting the item bold if it's unread.
Problem: The first item is never un-bolded until all other items are clicked, first. I can click item 1, go back, and it's still bold. I can then click item 2, go back, and then both items 1 and 2 are no longer bold. It's the same no matter how many items I click. Also, some other testers here have seen other inconsistencies where all items will become bold, then upon returning to the Activity from another, they're no longer bold.
I've logged and debugged, the entities passing through all reflect correct data, exactly as expected. It's the rendering that doesn't correctly reflect the data.
It's my first one, I'm sure I'm missing something obvious.
My ArrayAdapter:
public class MessageAdapter extends ArrayAdapter<Message>
{
private List<Message> messages;
public MessageAdapter(Context context, int resource, List<Message> entities)
{
super(context, resource, entities);
this.messages = entities;
}
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
if (convertView == null)
convertView = LayoutInflater.from(getContext()).inflate(R.layout.list_item_message, parent, false);
Message message = this.messages.get(position);
if (message != null)
{
TextView messageText = (TextView)convertView.findViewById(R.id.messageItemText);
messageText.setText(String.format("Type: %s Received: %s", message.getType(), StringUtil.getDateTime(message.getReceived())));
if (message.getIsRead() == 0)
messageText.setTypeface(null, Typeface.BOLD);
}
return convertView;
}
}
My Activity:
#Override
protected void onCreate(Bundle savedInstanceState)
{
List<Message> msgData = data.getAll();
MessageAdapter adapter = new MessageAdapter(this, R.layout.list_item_message, msgData);
}
Thanks for the help!
you have to add else statement to your if condition
if (message.getIsRead() == 0)
messageText.setTypeface(null, Typeface.BOLD);
else
messageText.setTypeface(null, Typeface.NORMAL);
I set the layout which has a listview to a popupwindow,and then I set the popupwindow.setFocusale(false);,and add "android:focusable="true"" attribute to listview,
after that I click the listview in the popupwindow ,the item in listview cannot be selected,
can anyone tell the solution?
Thanks in advance!
I had the same problem, and in my case PopupWindow.setFocusble(false) was required (and using ListPopupWindow was not a solution in my case as a lot of stuff in the project already used base PopupWindow's functionality including extending).
If someone in the same situation there is a kind of workaround based on bug discusson here (post #9)
The main idea is that ListView's hierarchy is still receives touch events so we can manually trigger onItemClick().
However this approach is not 100% identical to real ListView's touch handling (like there is no glow of selection while tapping a row) this done pretty well for me for the moment.
If someone has more precise solution of this problem, please share.
So, here is complete Adapter's code which can be used with ListView inside PopupWindow which is setFocusable(false):
private class CustomAdapter extends ArrayAdapter {
private LayoutInflater mInflater;
private ListView mOwningListView;
public CustomAdapter(Context context, List<String> objects, ListView listView) {
super(context, android.R.layout.simple_list_item_1, objects);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mOwningListView = listView;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.font_pick_row, null);
}
// this is the key point of workaround
convertView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
/*
* as every row is still receiving their touches
* we can use this to manually trigger onItemClick
* since it doesn't firing in popupWindow.setFocusable(false)
*/
mOwningListView.getOnItemClickListener().onItemClick(mOwningListView, v, position, getItemId(position));
}
});
//... other stuff
return convertView;
}
}
why are u making popupwindow focus false..does it affect your UI if it left focussable.i guess thats the reason ListView is not getting focus.
I have a ListView that's populated via an ArrayAdapter. Within the adapter, i set the view background color dependent on a conditional. It works, but while scrolling the remaining rows adopt this color. Here's some code:
class DateAdapter extends ArrayAdapter<DateVO> {
private ArrayList<DateVO> items;
public ViewGroup listViewItem;
//constructor
public DateAdapter(Context context, int textViewResourceId, ArrayList<DateVO> items) {
super(context, textViewResourceId, items);
this.items = items;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
try {
if (view == null) {
LayoutInflater vi = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = vi.inflate(R.layout.row, null);
}
final DateVO dateItem = items.get(position);
if (dateItem != null) {
//is this my issue here? does position change while scrolling?
if(items.get(position).getField().equals("home")){
view.setBackgroundResource(R.drawable.list_bg_home);
}
...
}
}catch (Exception e) {
Log.i(ArrayAdapter.class.toString(), e.getMessage());
}
return view;
}
}
That is the default behavior of a ListView. It can be overridden by setting the cacheColorHint to transparent.
Just add,
android:cacheColorHint="#00000000"
in your xml file.
For more details you can go through the ListView Backgrounds article.
Here is an excerpt:
To fix this issue, all you have to do is either disable the cache color hint optimization, if you use a non-solid color background, or set the hint to the appropriate solid color value. You can do this from code (see setCacheColorHint(int)) or preferably from XML, by using the android:cacheColorHint attribute. To disable the optimization, simply use the transparent color #00000000. The following screenshot shows a list with android:cacheColorHint="#00000000" set in the XML layout file
EDIT : The view passed as convertView is essentially a view which is a part of the list view, but isn't visible anymore (due to scrolling). So it is actually a view you had created, and might be a view for which you had set the custom background. To solve this just make sure that you reset the background if the condition is not satisfied. Something like this :
if(condition_satisfied) {
//set custom background for view
}
else {
//set default background for view
convertView.setBackgroundResource(android.R.drawable.list_selector_background);
}
Essentially if your condition is not satisfied you will have to undo whatever customisations you are doing when your condition is satisfied, because you might have received an old customised view as convertView.
That should solve your problem.
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.