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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With