I refresh a ListView with a CursorAdapter with notifyDataSetChanged(). the notify is called few times a second from a CountDownTimer (I display several count down timers, which are stored in DB). The UI updates correctly.
The problem is, that my list view has two buttons in each row, to pause/resume and cancel a timer. I attach the OnClickListeners in getView() of my custom adapter (the adapter itself implements OnClickListener and I handle the click events with a switch on view's ID). This works ok if I do not refresh the list view. When I start calling notifyDataSetChanged(), the click events are random. Clicking one button selects both buttons in a row, or a button in a different row and only some times I receive the correct click events. Those random click targets are not catchable (i.e. i do not receive an onClick()).
I also tried listView.invalidateViews(), getLoaderManager().restartLoader(...) re-adding a new adapter all with similar problem or even worse.
I think this could be a problem caused by recycling the "row views", but this behaviour is also with only one row.
The question is, how can I continuosly refresh a ListView and preserve click events of the buttons in it?
EDIT the adapter code:
public class TimersCursorAdapter extends SimpleCursorAdapter implements OnClickListener {
// helper interface to notify about timer has finished, so we can update the UI
public interface OnTimerFinishedLoaderReloadCallback {
public void onTimerFinishedLoaderRestart();
}
DecimalFormat m_Format = new DecimalFormat("00");
OnTimerFinishedLoaderReloadCallback m_TimerFinishedCallback = null;
public TimersCursorAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to, int flags) {
super(context, layout, cursor, from, to, flags);
}
public void setTimerFinishedCallback(OnTimerFinishedLoaderReloadCallback callback) {
m_TimerFinishedCallback = callback;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = super.getView(position, convertView, parent);
View btn;
view.setTag(position);
// pause
btn = view.findViewById(R.id.timers_list_button_pause_resume);
btn.setOnClickListener(this);
// cancel
btn = view.findViewById(R.id.timers_list_button_cancel);
btn.setOnClickListener(this);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cur) {
String result = "00:00:00";
long hours, minutes, seconds;
final long now = Calendar.getInstance().getTimeInMillis() / 1000;
long duration = cur.getLong(cur.getColumnIndex("duration"));
final String name = cur.getString(cur.getColumnIndex("name"));
long remaining = Math.max(0, cur.getLong(cur.getColumnIndex("start")) + duration - now);
if (remaining > 0) {
hours = remaining / 3600;
remaining %= 3600;
minutes = remaining / 60;
remaining %= 60;
seconds = remaining;
result = "" + m_Format.format(hours) + ":" + m_Format.format(minutes) + ":" + m_Format.format(seconds);
}
else if (m_TimerFinishedCallback != null) {
// the timer is up, so notify our listener, so the UI can be updated accordingly
m_TimerFinishedCallback.onTimerFinishedLoaderRestart();
}
// remaining time
TextView tv = (TextView)view.findViewById(R.id.timers_list_remaining_time);
tv.setText(result);
tv.setTypeface(TinyTimerActivity.font);
// timer's details
hours = duration / 3600;
duration %= 3600;
minutes = duration / 60;
duration %= 60;
seconds = duration;
final String details = name + " timer: " + m_Format.format(hours) + ":" + m_Format.format(minutes) + ":" + m_Format.format(seconds);
tv = (TextView)view.findViewById(R.id.timers_list_details);
tv.setText(details);
}
@Override
public void onClick(View v) {
final int position = (Integer)((ViewGroup)v.getParent()).getTag();
final CursorWrapper cw = (CursorWrapper)getItem(position);
switch (v.getId()) {
case R.id.timers_list_button_cancel:
Toast.makeText(TinyTimerActivity.AppContext, "canceling timer " + cw.getLong(cw.getColumnIndex("duration")), Toast.LENGTH_SHORT).show();
break;
case R.id.timers_list_button_pause_resume:
Toast.makeText(TinyTimerActivity.AppContext, "pausing timer " + cw.getLong(cw.getColumnIndex("duration")), Toast.LENGTH_SHORT).show();
break;
}
}
}
EDIT 2: i forget to add, that this happens on a real device with pure android 2.3.7 and also on an emulator with 4.0.0
EDIT 3: I also tried to always create a new view in getView, but the bahaviour is the same. the click events are always messed up. I think the only solution is to fill a linear layout from DB and update it.
public View getView(int position, View convertView, ViewGroup parent) {
View view = m_Inflater.inflate(R.layout.timers_list_item, null);
ViewHolder holder = new ViewHolder();
holder.buttonCancel = (Button)view.findViewById(R.id.timers_list_button_cancel);
holder.buttonPause = (Button)view.findViewById(R.id.timers_list_button_pause_resume);
holder.textDetails = (TextView)view.findViewById(R.id.timers_list_details);
holder.textRemaining = (TextView)view.findViewById(R.id.timers_list_remaining_time);
holder.buttonCancel.setOnClickListener(this);
holder.buttonPause.setOnClickListener(this);
holder.position = position;
view.setTag(holder);
// bind the view's content
final Cursor cur = getCursor();
cur.moveToPosition(position);
bindView(view, mContext, cur);
return view;
}