I have implemented count down timer for each item of RecyclerView which is in a fragment activity. The count down timer shows the time remaining for expiry. The count down t
This problem is simple.
RecyclerView
reuses the holders, calling bind each time to update the data in them.
Since you create a CountDownTimer
each time any data is bound, you will end up with multiple timers updating the same ViewHolder
.
The best thing here would be to move the CountDownTimer
in the FeedViewHolder
as a reference, cancel it before binding the data (if started) and rescheduling to the desired duration.
public void onBindViewHolder(final FeedViewHolder holder, final int position) { ... if (holder.timer != null) { holder.timer.cancel(); } holder.timer = new CountDownTimer(expiryTime, 500) { ... }.start(); } public static class FeedViewHolder extends RecyclerView.ViewHolder { ... CountDownTimer timer; public FeedViewHolder(View itemView) { ... } }
This way you will cancel any current timer instance for that ViewHolder
prior to starting another timer.
There are two types of solution I can think of and they are without using CountDownTimer
class
postDelayed
method and call notifyDataSetChanged()
in that. In your adapter make calculation for timing. like below code.in Constructor of adapter class
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
notifyDataSetChanged();
handler.postDelayed(this, 1000);
}
}, 1000);
and in you onBindViewHolder
method
public void onBindViewHolder(final FeedViewHolder holder, final int position) {
updateTimeRemaining(endTime, holder.yourTextView);
}
private void updateTimeRemaining(long endTime, TextView yourTextView) {
long timeDiff = endTime - System.currentTimeMillis();
if (timeDiff > 0) {
int seconds = (int) (timeDiff / 1000) % 60;
int minutes = (int) ((timeDiff / (1000 * 60)) % 60);
int hours = (int) ((timeDiff / (1000 * 60 * 60)) % 24);
yourTextView.setText(MessageFormat.format("{0}:{1}:{2}", hours, minutes, seconds));
} else {
yourTextView.setText("Expired!!");
}
}
if you think notifyDataSetChanged()
every millisecond is wrong then here is second option. Create class with Runnable
implementation and use in in adapter with setTag()
and getTag()
method
public class DownTimer implements Runnable {
private TextView yourTextView;
private long endTime;
private DateFormat formatter = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
private Handler handler = new Handler();
public DownTimer(long endTime, TextView textView) {
this.endTime = endTime;
yourTextView = textView;
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public void setEndTime(long endTime) {
this.endTime = endTime;
}
public void start() {
if (handler != null)
handler.postDelayed(this, 0);
}
public void cancel() {
if (handler != null)
handler.removeCallbacks(this);
}
@Override
public void run() {
if (handler == null)
return;
if (yourTextView == null && endTime == 0)
return;
long timeDiff = endTime - System.currentTimeMillis();
try {
Date date = new Date(timeDiff);
yourTextView.setText(formatter.format(date));
}catch (Exception e){e.printStackTrace();}
}
}
and use in onBindViewHolder
like this
if (holder.yourTextView.getTag() != null) {
DownTimer downTimer = (DownTimer) holder.yourTextView.getTag();
downTimer.cancel();
downTimer.setEndTime(endTime);
downTimer.start();
} else {
DownTimer downTimer = new DownTimer(endTime, holder.yourTextView);
downTimer.start();
holder.yourTextView.setTag(downTimer);
}
As Andrei Lupsa said you should hold CountDownTimer reference in your ViewHolder, if you didn't want to timer reset when scrolling (onBindViewHolder) , you should check if CountDownTimer reference is null or not in onBindViewHolder :
public void onBindViewHolder(final FeedViewHolder holder, final int position) {
...
if (holder.timer == null) {
holder.timer = new CountDownTimer(expiryTime, 500) {
...
}.start();
}
}
public static class FeedViewHolder extends RecyclerView.ViewHolder {
...
CountDownTimer timer;
public FeedViewHolder(View itemView) {
....
}
}