Search code examples
androidlistviewtimerhandlerrunnable

Android: Multiple timers in a ListView with handler and runnable. 2 Problems


I'm creating an app that contains a ListView with 2 columns. On the first column a countdown should be displayed and on the second column an additional text, that explains what is the countdown for. Below you see my code that works ... more or less. I have a listview with multiple rows an the timers are ticking. One problem is: the set.Text() in my runnable seems to override all rows. E.g. runnable for row 1 sets text also to row 2 and 3, runnable for row 2 sets text also for 1 and 3 and so on. This has the effect that the first column blinks (with correct values and values of other rows). How can I set text for a specific row in a listview?

Next problem: the runnable is running on and on even if I remove callbacks from handler. But when the activity is in background or closed the timer ticking is not needed and I don't want to waste system resources.

My Activity:

public class TimerActivity extends ListActivity {

MyTimerAdapter myTimerAdapter = null;
ArrayList<Long> timerList = new ArrayList<Long>();
ArrayList<String> textList = new ArrayList<String>();

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.listactivity);

    myTimerAdapter = new MyTimerAdapter(this, R.layout.row, R.id.tv_timer, R.id.tv_text);
    setListAdapter(myTimerAdapter);
}

@Override
protected void onResume() {
    super.onResume();
    refreshView();
}

@Override
protected void onPause() {
    myTimerAdapter.clear();
    myTimerAdapter.notifyDataSetChanged();
    super.onPause();
}

private void refreshView() {

    myTimerAdapter.clear();
    timerList.clear();
    textList.clear();

    // some code to read database and fill 
    // array timerList with a long value (used for displaying a countdown) 
    // and textList with some additional text

    myTimerAdapter.add(timerList, textList);
    myTimerAdapter.notifyDataSetChanged();
}

}

My Adapter:

public class MyTimerAdapter extends ArrayAdapter<String> {
private Activity mContext;
private ArrayList<Long> mTimer;
private ArrayList<String> mText;
private int mViewId;
private int mViewIdFieldTimer;
private int mViewIdFieldText;
private int listSize;
private ArrayList<Handler> handlerList = new ArrayList<Handler>();
private ArrayList<TimerRunnable> runList = new ArrayList<TimerRunnable>();

public MyTimerAdapter(Activity context, int textViewResId, int tv1, int tv2) {
    super(context, textViewResId);
    mContext = context;
    mViewId = textViewResId;
    mViewIdFieldTimer = tv1;
    mViewIdFieldText = tv2;
    listSize = 0;
}

public void add(ArrayList<Long> timer, ArrayList<String> text) {
    mTimer = timer;
    mText = text;
    listSize = mText.size();
    handlerList.clear();
    runList.clear();
}

@Override
public void clear() {
    super.clear();
    int i;
    for (i=0; i<listSize; i++) {
        handlerList.get(i).removeCallbacksAndMessages(runList.get(i));
        runList.get(i).stopHandler();
    }
}

@Override
public int getCount() {
    return listSize;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View v = convertView;
    if (v == null) {
        LayoutInflater vi = mContext.getLayoutInflater();
        v = vi.inflate(mViewId, null);
    }

    long timerLine = mTimer.get(position);
    if (timerLine != 0) {
        TextView tvTimer = (TextView) v.findViewById(mViewIdFieldTimer);

        if (tvTimer != null) {
            tvTimer.setTag(position);
            final Handler mTimerHandler = new Handler();
            TimerRunnable timerTask = new TimerRunnable(tvTimer, tvTimer.getTag().toString(), timerLine);
            mTimerHandler.post(timerTask);

            // save in array to stop later
            handlerList.add(mTimerHandler);
            runList.add(timerTask);
        }
    }

    String textLine = mText.get(position);
    if (textLine != null) {
        TextView tvText = (TextView) v.findViewById(mViewIdFieldText);
        if (tvText != null) {
            tvText.setText(textLine);
        }
    }

    return v;
}
}

My Runnable:

public class TimerRunnable implements Runnable {
private TextView tv;
final Handler mTimerHandler = new Handler();
String tag;
long endtime;
long sec;

public TimerRunnable (TextView tv, String tag, long endtime) {
    this.tv = tv;
    this.tag = tag;
    this.endtime = endtime;
}

public void run() {
    if (tv.getTag().toString().equals(tag)) {
        Calendar cal = Calendar.getInstance();
        sec = endtime - (cal.getTimeInMillis() / 1000); //endtime - aktuelle Zeit
        if (sec >= 0) {

            // some code formatting the time in seconds to something like hh:mm:ss (var String txt)

            tv.setText(txt);

            System.out.println(txt);  // only for tests; so I could see that runnable is still running

            mTimerHandler.postDelayed(this, 1000);
        } 
    }
}

public void stopHandler() {
    mTimerHandler.removeCallbacksAndMessages(null);
}
}

Solution

  • I would do as follows:

    1. Use a Timer (http://developer.android.com/reference/java/util/Timer.html) and let it run periodically every second or whatever.
    2. Create the Timer object in Activities onCreate() and cancel() the timer in Activities onDestroy() (or even better start it in onResume and cancel it in onPause()). So thats the simplest way how to avoid memory leaks and that your timer is running even if the activity is closed.
    3. update the ListView from the running timer. There are 2 options:

      • Simply call adapter.notifyDatasetChanged() from your timer. But this could lead to a "blinking" listview. However you could try and checkout if this could work in your application, since is the simplest implementation.
      • Update directly the TextViews that are currently visible in the ListView

        private void updateTime(){
        
        int firstVisibleItemIndex = listView.getFirstVisiblePosition();
        
        for (int i = 0; i < listView.getChildCount(); i++) {
            View v = listView.getChildAt(i);
        
                YourItem item = (YourItem)adapter
                        .getItem(firstVisibleItemIndex + i));
        
                ViewHolder vh = (ViewHolder) v.getTag();
                                    // Calculate the time somehow, i.e. call a methot on your data item
                                    vh.tvTimer.setText(item.getElapsedTime());
        
        
            }
        
        }
        

        }

    So I guess the second option would be the best. You may wonder what ViewHolder is. ViewHolder is a pattern that increases the performance of your ListView. http://developer.android.com/training/improving-layouts/smooth-scrolling.html#ViewHolder The problem of your adapter is, that findViewById() will be called everytime the user scrolls. findViewById() is a expensive method call, since it has to traverse all view childs to find the one with the given id. In your simple adapter it may not have to much impact (since you have only a single child view, the TextView). But ViewHolder is something you should use always in your Adapter implementation