So I wrote a custom cursor adapter to deal with a custom layout for a list row. The layout includes a ToggleButton, an ImageButton, and a TextView. The TextView data is derived from column called ALARM_TIME, and gets loaded just fine.
The ToggleButton is causing problems however. Its state is derived from a column called ALARM_ISACTIVE, which will either be 1 (corresponding to 'on') or 0 (corresponding to 'off'). Clicking the ToggleButton should update the corresponding row in the database accordingly.
However, for whatever reason, my actual behavior is something entirely different from that.
Clicking the ToggleButton triggers the OnCheckedChange listener for both the row I've clicked and the first two rows of the list, regardless of what row I click. Furthermore, if I check a row, scroll down, and scroll back up, then the row's state doesn't persist.
I'm not sure what's going on here, and have no idea how to explain or fix it.
Can anyone help me here?
For reference, here's my cursor adapter:
public class AlarmCursorAdapter extends SimpleCursorAdapter implements OnCheckedChangeListener {
private Context mContext;
private int mLayout;
private Cursor mCursor;
private int mIdIndex;
private int mAlarmIndex;
private int mIsActiveIndex;
private LayoutInflater mInflater;
public AlarmCursorAdapter(Context context, int layout, Cursor c,
String[] from, int[] to, int flags) {
super(context, layout, c, from, to, flags);
Log.d("alarmAdapter", "calling constructor");
this.mContext = context;
this.mLayout = layout;
this.mCursor = c;
this.mIdIndex = c.getColumnIndexOrThrow(DailyAlarmTable.ALARM_ID);
this.mAlarmIndex = c.getColumnIndexOrThrow(DailyAlarmTable.ALARM_TIME);
this.mIsActiveIndex = c.getColumnIndexOrThrow(DailyAlarmTable.ALARM_ISACTIVE);
this.mInflater = LayoutInflater.from(mContext);
Log.d("alarmAdapter", "finishing constructor");
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(mCursor.moveToPosition(position)) {
ViewHolder holder;
final int fPosition = position;
Log.d("AlarmCursorAdapter", "getView called for position " + fPosition);
// If the view isn't inflated, we need to create it
if(convertView == null) {
Log.d("AlarmAdapter", "creating position " + fPosition);
convertView = mInflater.inflate(mLayout, null);
holder = new ViewHolder();
holder.alarmView = (TextView) convertView.findViewById(R.id.alarmView);
holder.discardButton = (ImageButton) convertView.findViewById(R.id.alarmDiscard);
holder.isActiveToggle = (ToggleButton) convertView.findViewById(R.id.alarmToggle);
convertView.setTag(holder);
} else { // If the view is already inflated, we need to get it for the holder
Log.d("AlarmAdapter", "recycling position " + fPosition);
holder = (ViewHolder) convertView.getTag();
}
// Populate the views
String alarmString = mCursor.getString(mAlarmIndex);
int isActive = mCursor.getInt(mIsActiveIndex);
final int id = mCursor.getInt(mIdIndex);
holder.alarmView.setText(alarmString);
if(isActive == 1) {
holder.isActiveToggle.setChecked(true);
} else {
holder.isActiveToggle.setChecked(false);
}
holder.isActiveToggle.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton button, boolean isChecked) {
Log.d("ToggleListener", "Toggle for " + fPosition + " triggered");
if(isChecked) {
Toast.makeText(mContext, "row " + fPosition + " is checked", Toast.LENGTH_SHORT).show();
// Change the value of ALARM_ISACTIVE in the cursor to 1
ContentValues newValues = new ContentValues();
newValues.put(DailyAlarmTable.ALARM_ISACTIVE, 1);
String selection = "(" + DailyAlarmTable.ALARM_ID + " = " + id + ")";
int rowsUpdated = 0;
rowsUpdated = mContext.getContentResolver().update(AlarmProvider.CONTENT_URI,
newValues, selection, null);
} else {
Toast.makeText(mContext, "row " + fPosition + " is unchecked", Toast.LENGTH_SHORT).show();
// Change the value of ALARM_ISACTIVE in the cursor to 0
ContentValues newValues = new ContentValues();
newValues.put(DailyAlarmTable.ALARM_ISACTIVE, 0);
String selection = "(" + DailyAlarmTable.ALARM_ID + " = " + id + ")";
int rowsUpdated = 0;
rowsUpdated = mContext.getContentResolver().update(AlarmProvider.CONTENT_URI,
newValues, selection, null);
}
}
});
}
return convertView;
}
static class ViewHolder {
ToggleButton isActiveToggle;
TextView alarmView;
ImageButton discardButton;
}
@Override
public void onCheckedChanged(CompoundButton button, boolean isChecked) {
// TODO Auto-generated method stub
}
}
Here's the update() method from my content provider. I'm including this for the sake of thoroughness, and because the problem was way worse (the rows kept infinitely updating in response to each other as soon as I clicked any row) until I found that I shouldn't call notifyChange():
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int uriType = sUriMatcher.match(uri);
SQLiteDatabase db = database.getWritableDatabase();
int rowsUpdated = 0;
switch(uriType) {
case ALARMS:
rowsUpdated = db.update(DailyAlarmTable.TABLE_ALARM,
values,
selection,
selectionArgs);
break;
case ALARM_ID:
String id = uri.getLastPathSegment();
if(TextUtils.isEmpty(selection)) {
rowsUpdated = db.update(DailyAlarmTable.TABLE_ALARM,
values,
DailyAlarmTable.ALARM_ID + "=" + id,
null);
} else {
rowsUpdated = db.update(DailyAlarmTable.TABLE_ALARM,
values,
DailyAlarmTable.ALARM_ID + "=" + id + " and " + selection,
selectionArgs);
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
// getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
Turns out I needed to implement a boolean array in the custom cursor adapter to store the states of the toggle buttons.