Situation:
My application contains a ToDo-list styled listview (each row has a checkbox). The listview rows are structured with 2 textviews laid out vertically. The topmost text is the title and bottommost is the description, however the description is hidden (View.GONE). Using the ListActivities onListItemClick method I can set both the height and visibility of the pressed row.
#Override
protected void onListItemClick(ListView list, View view, int position, long id) {
super.onListItemClick(list, view, position, id);
view.getLayoutParams().height = 200;
view.requestLayout();
TextView desc = (TextView) view.findViewById(R.id.description);
desc.setVisibility(View.VISIBLE);
}
Note: Code is stripped to the most basic
The above code works fine, except that it expands both the pressed row as well as the 10th row above or below (next unloaded view?). The row expansion will also change place when list is flinged.
Background:
The listview data is retrieved from a SQLite Database through a managed cursor and set by a custom CursorAdapter. The managed cursor is sorted by checkbox value.
private void updateList() {
todoCursor = managedQuery(TaskContentProvider.CONTENT_URI, projection, null, null, TaskSQLDatabase.COL_DONE + " ASC");
startManagingCursor(todoCursor);
adapter = new TaskListAdapter(this, todoCursor);
taskList.setAdapter(adapter);
}
The CursorAdapter consists of the basic newView() and bindView().
Question:
I require some system which keeps track of which rows are expanded. I have tried storing the cursor id's in arrays and then checking in the adapter if row should be expanded, but I can't seem to get it working. Any suggestions would be much appreciated!
The ListView will recycle views as you scroll it up and down, when it needs a new child View to show, it will first see if it doesn't already have one(if it finds one it will use it). If you modify the children of a ListView like you did(in the onListItemClick() method) and then scroll the list, the ListView will eventually end up reusing that child View that you modified and you'll end up with certain views in position that you don't want(if you continue to scroll the ListView you'll see random position changing because of the View recycling).
One way to prevent this is to remember those positions that the user clicked and in the adapter change the layout of that particular row but only for the position that you want. You can store those ids in a HashMap(a field in your class):
private HashMap<Long, Boolean> status = new HashMap<Long, Boolean>();
I used a HashMap but you can use other containers(in the code bellow will see why I choose a HashMap). Next in the onListItemClick() method you'll change the clicked row, but also store that row id so the adapter will know(and take measures so you don't end up with wrong recycled views):
#Override
protected void onListItemClick(ListView l, View v, int position, long id) {
// check and see if this row id isn't already in the status container,
// if it is then the row is already set, if it isn't we setup the row and put the id
// in the status container so the adapter will know what to do
// with this particular row
if (status.get(id) == null) {
status.put(id, true);
v.getLayoutParams().height = 200;
v.requestLayout();
TextView d = (TextView) v.findViewById(R.id.description);
d.setVisibility(View.VISIBLE);
}
}
Then in the adapter use the status container with all the ids to setup the rows correctly and prevent the recycling to mess with our rows:
private class CustomAdapter extends CursorAdapter {
private LayoutInflater mInflater;
public CustomAdapter(Context context, Cursor c) {
super(context, c);
mInflater = LayoutInflater.from(context);
}
#Override
public void bindView(View view, Context context, Cursor cursor) {
TextView title = (TextView) view.findViewById(R.id.title);
title.setText(cursor.getString(cursor.getColumnIndex("name")));
TextView description = (TextView) view
.findViewById(R.id.description);
description.setText(cursor.getString(cursor
.getColumnIndex("description")));
// get the id for this row from the cursor
long rowId = cursor.getLong(cursor.getColumnIndex("_id"));
// if we don't have this rowId in the status container then we explicitly
// hide the TextView and setup the row to the default
if (status.get(rowId) == null) {
description.setVisibility(View.GONE);
// this is required because you could have a recycled row that has its
// height set to 200 and the description TextView visible
view.setLayoutParams(new AbsListView.LayoutParams(
AbsListView.LayoutParams.MATCH_PARENT,
AbsListView.LayoutParams.MATCH_PARENT));
} else {
// if we have the id in the status container then the row was clicked and
// we setup the row with the TextView visible and the height we want
description.setVisibility(View.VISIBLE);
view.getLayoutParams().height = 200;
view.requestLayout();
// this part is required because you did show the row in the onListItemClick
// but it will be lost if you scroll the list and then come back
}
}
#Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View v = mInflater.inflate(R.layout.adapters_showingviews, null);
return v;
}
}
If you want to toggle the row clicked(and something tells me that you want this), show the TextView on a click/hide the TextView on another row clicked then simple add a else clause to the onListItemClick() method and remove the clicked row id from the status container and revert the row:
//...
else {
status.remove(id);
v.setLayoutParams(new AbsListView.LayoutParams(
AbsListView.LayoutParams.MATCH_PARENT,
AbsListView.LayoutParams.MATCH_PARENT));
TextView d = (TextView) v.findViewById(R.id.description);
d.setVisibility(View.GONE);
}
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.
Overview:
I have a ListView with a custom adapter/layout, every time a user adds a new row (which contains a number), I check if that number is the smallest in the list. If so, an image within that row must be set as visible while setting all other row's images as invisible.
Problem:
My ListView does not set any row's image as visible, even though I have the index of the smallest element.
How I'm doing it:
//In MainActivity
private void addProduct(float price) { //User adds product
priceList.add(price); //Add to Float list
adapter.notifyDataSetChanged();
updateView(findMinIndex(priceList)); //Find smallest val indx
}
private void updateView(int index){
View v = listView.getChildAt(index -
listView.getFirstVisiblePosition());
if(v == null)
return;
ImageView checkMark = (ImageView) v.findViewById(R.id.check_mark);
checkMark.setVisibility(View.VISIBLE); //Initially set Invisible
}
Edit, CustomAdapter:
public CustomList(Activity context,
ArrayList<Float> priceList) {
super(context, R.layout.list_single, priceList);
this.context = context;
priceList = priceList;
}
#Override
public View getView(int position, View view, ViewGroup parent) {
LayoutInflater inflater = context.getLayoutInflater();
View rowView = inflater.inflate(R.layout.list_single, null, true);
TextView price = (TextView) rowView.findViewById(R.id.new_price);
ImageView cheapest = (ImageView) rowView.findViewById(R.id.check_mark);
price.setText(priceList.get(position) + "");
return rowView;
}
Thank you
It is your priceList binded with the adapter?
First of all i would put a breakpoint to see if you are getting the right view in the updateView method.
try this way;
Create a Pojo class with imageview and it's state(Visibility) initially set all to invisible
Add your items to the ArrayList of Pojo Class type.
when user enters a new row based on your requirement set visibility state to true or false(visible or invisible) and call notifyDataSetChanged() to the adapter.
Doing this way you can have a easy track of the items.
I got it working :).
Problem is that adapter.notifyDataSetChanged(); is async, so while it's doing that, updateView(findMinIndex(priceList)); runs but doesn't find the new row as it should. Therefore, I add a runnable to the ListView object as so:
adapter.notifyDataSetChanged();
listView.post( new Runnable() {
#Override
public void run() {
updateView(findMinIdx(priceList));
}
});
Now it works perfectly!
I use a CursorAdapter with one single customized layout for my list, depending on the value of a field (true or false) of my table, the color of one of the textviews in the list item will be different.
public class CustomAdapter extends CursorAdapter {
DatabaseHelper myDB;
SQLiteDatabase db;
public CustomAdapter(Context context, Cursor cursor, int flags) {
super(context, cursor, 0);
myDB = new DatabaseHelper(context, DatabaseHelper.DB_NAME, null, DatabaseHelper.DB_VERSION_SCHEME);
db = myDB.getReadableDatabase();
}
#Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return LayoutInflater.from(context).inflate(R.layout.customize_cell_list, parent, false);
}
#Override
public void bindView(View view, Context context, Cursor cursor) {
TextView tv = (TextView) view.findViewById(R.id.tv);
TextView tv_date = (TextView) view.findViewById(R.id.tvDateCursor);
TextView tv_time = (TextView) view.findViewById(R.id.tvTime);
ImageView img = (ImageView)view.findViewById(R.id.img);
if(cursor != null) {
String tv = cursor.getString(2);
String date = cursor.getString(3);
String time = cursor.getString(8);
boolean active = cursor.getInt(12) > 0;
boolean go = cursor.getInt(13) > 0;
tv_date.setText(date);
tv.setText(tv);
if(go) { // OPTION 1
img.setImageResource(R.drawable.calle);
tv_time.setText(time);
} else { // OPTION 2
img.setImageResource(R.drawable.casa);
if(active){ // OPTION 2.1
tv_time.setText("Activado");
}else{ // OPTION 2.2
tv_time.setText("No activado");
tv_time.setTextColor(Color.RED);
}
}
}
}
}
The result: images display the correct one the same than the textviews, the problem is the color, when I have one item with the OPTION 2.2 and start scrolling up and down some of my tv_time (even from the option 1) begin to change the color to Red, then I scroll again and they become white and others change to red, it is something random.
Why is this happening? How can I keep the red color only when "go" and "active" are false?
Thanks
You need to remember the following things
1. The views in the ListView are limited.
2. Whenever you scroll the View whose visibility is gone will be updated with the later items and then it will come back to the view
So now you are setting the color based on some criteria. and whenever you are binding the view u are setting the color.
Make sure when unbinding the view you will make the view go back to the original color. if u don't do this the View when looses its visibility it still contains the color and when other data item is fetched into it that looks in red color.
So when binding first check whether it is colored red. If it is then check do you want to keep it red, and yes keep it and no then make it go back to original color
I use even and odd rows to set backgrond to my listview rows. In my efficientAdapter I set the row background as follows:
public View getView(int position, View convertView, ViewGroup parent) {
vi = convertView;
if (convertView == null) {
vi = inflater.inflate(R.layout.ecran_multiple_row, null);
holder = new ViewHolder();
holder.txIndex = (TextView) vi.findViewById(R.id.txIndex);
holder.txSTitle = (TextView) vi.findViewById(R.id.txSTitle);
holder.btOnOFF = (ImageView) vi.findViewById(R.id.btOnOFF);
vi.setTag(holder);
} else
holder = (ViewHolder) vi.getTag();
/*
* CHANGE ROW COLOR 0 WHITE 1 GRAY
*/
if ( position % 2 == 0) //0 even 1 odd..
vi.setBackgroundResource(R.drawable.listview_selector_odd);
else
vi.setBackgroundResource(R.drawable.listview_selector_even);
/*
* ONE ITEM IN ARRAY
*/
if (data.toArray().length==1){
holder.btOnOFF.setBackgroundResource(R.drawable.air_radio_button_rouge);
}else {
holder.btOnOFF.setBackgroundResource(R.drawable.air_deezer_check);
}
return vi;
}
and in my MainActivity.Class. I select an item using on itemclicklistener() as shown below:
**lvRMultiple.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
imgview = (ImageView) view.findViewById(R.id.btOnOFF);
//And change its background here
imgview.setBackgroundResource(R.drawable.air_radio_button_rouge);
}
});**
When i clicked on an item btnOff image change successfully but when i scroll down it change to default background. Secondly when i click on one item after the other both becomes the new image but i want only the row clicked by the user to change to new image and the previous image are set to default.
All row view of a ListView created by the getView() method of BaseAdpter class. When ever we scroll the ListView all, new viable row create by getView() using recycle. So getView() called again and again when new row is viable on scroll.
There are two solution of your question:-\
You can save the status of ListView
// Save ListView state
Parcelable state = listView.onSaveInstanceState();
// Set new items
listView.setAdapter(adapter);
// Restore previous state (including selected item index and scroll position)
listView.onRestoreInstanceState(state)
And other solution is create RowView at runtime and add it on a Parent Layout by using addView() method.
LayoutInflater inflater = LayoutInflater.from(context);
// You should use the LinerLayout instead of the listview, and parent Layout should be inside of the ScrollView
parentView = (LinerLayout)this.findViewById(R.id.parentView);
for(int i = 0; i<=numberOfRow;i++){
LinearLayout rowView = (LinerLayout)inflater.inflate(R.layout.rowView);
ImageView rowImageView = (ImageView)rowView.findViewById(R.id.rowImage);
rowImageView.setOnClickListener(new View.onClickListListener(){
#Override
public void onClick(){
rowImageView.setImageBitmap(onClickBitmapImage);
}
});
parentView.addView(rowView);
}
Please check this answer Maintain/Save/Restore scroll position when returning to a ListView
More Reference
http://developer.android.com/reference/android/widget/Adapter.html#getView(int,android.view.View, android.view.ViewGroup)
The item changes back to the default background because the view gets recycled. This is the same problems of checkboxes losing their checked state
Check out this answer too see how to handle it:
CheckBox gets unchecked on scroll in a custom listview
As for your second problem, I believe it's already answered here:
highlighting the selected item in the listview in android
Hope it helps
I facing below issue with item back ground on scrolling.
In my application I have a listview which require multi-selection. Also this is a custom list where selection needs to be represented by change in list item color instead of check-box based approach.
For this: In the OnClick I'm checking if the position is selected or not and then set the background for the item. However this has issue when I scroll the list. Taking an example:
suppose the list has 50 items. And 10 are visible at a time. I select say 5th item [thus changing the background]. And then I scroll the list. After scroll the visible part of the list corresponding to earlier 5th item ,say 15th item in item of the list but 5th index in visible portion,still has background corresponding to selected state. Whereas it should not have been set since I have not selected 15th item yet.
I tried:
a-In the getView method of adapter, if the item is not one of selected items I'm setting one background else different.Tried - setBackgroundColor as well setBackgrounddrawable.
b- In the xml have set the cacheColorHint to transparent
c- Have selector attached to items and the items responding to state [pressed,selected] in onlcick.
However still I'm not able to get rid of unwanted background color for item on scrolling.
Any help. I tried various suggestion mentioned in various post in SO but not succcessful yet.
I tried
thanks
pradeep
this is a normal behavior of ListView adapter in android, its getView() called on every scroll and for every new list item it call getView, if listview item currently not visible on UI then its convertView is equals to null: At a time listview take load of only visible list items, if it showing at a time 10 element out of 50, then listView.getChildCount() will return only 10 not 50.
In your case when you select 5, it reflected selection for 5+10(visible items count) = 15, 25, 35, 45 too.
To solve this problem you should have a flag associate with your each listItem data, for example if you have string array itemData[50] as array, then take an array of boolean isSelected[50] with initial value false for each.
Take a look for getView(), in adapter class:
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
string text = itemData[position]
if (convertView == null) {
rowLayout = (RelativeLayout) LayoutInflater.from(context)
.inflate(R.layout.list_view_item, parent, false);
holder = new ViewHolder();
holder.txtString= (TextView) rowLayout
.findViewById(R.id.txtTitle);
rowLayout.setTag(holder);
} else {
rowLayout = (RelativeLayout) convertView;
holder = (ViewHolder) rowLayout.getTag();
}
if(isSelected[position] == true){
holder.txtString.setText("Selected")
rowLayout.setBackGround(selected)
}else{
holder.txtString.setText("Not Selected")
rowLayout.setBackGround(notSelected)
}
public class ViewHolder {
public TextView txtString;
}
and in your Activity class on listView.setOnItemClickListener():
listView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> arg0, View arg1,
int position, long arg3) {
// TODO Auto-generated method stub
isSelected[position] = true // on selection
RelativeLayout rowLayout = (RelativeLayout) view;
rowLayout.setBackGround(Selected);
// also set here background selected for view by getting layout refference
}
});