18

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 timer is working fine but when scrolled up it starts flickering. Searched a lot but did not got the good reference. Can any one help me?

This is my RecyclerView adapter

public class MyOfferAdapter extends RecyclerView.Adapter<MyOfferAdapter.FeedViewHolder>{
private final Context mContext;
private final LayoutInflater mLayoutInflater;
private ArrayList<Transactions> mItems = new ArrayList<>();
private ImageLoader mImageLoader;
private String imageURL;
private View mView;
private String mUserEmail;

public MyOfferAdapter(Context context) {
    mContext = context;
    mLayoutInflater = LayoutInflater.from(context);
    VolleySingleton mVolley = VolleySingleton.getInstance(mContext);
    mImageLoader = mVolley.getImageLoader();
}


public void addItems(ArrayList<Transactions> items,String userEmail) {
    int count = mItems.size();
    mItems.addAll(items);
    mUserEmail = userEmail;
    notifyItemRangeChanged(count, items.size());
}


@Override
public FeedViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    mView = mLayoutInflater.inflate(R.layout.my_feed_item_layout, parent, false);
    return new FeedViewHolder(mView);
}

@Override
public void onBindViewHolder(final FeedViewHolder holder, final int position) {
    holder.desc.setText(mItems.get(position).getDescription());//replace by title
    holder.scratchDes.setText(mItems.get(position).getScratchDescription());

    long timer = mItems.get(position).getTimerExpiryTimeStamp();
    Date today = new Date();
    final long currentTime = today.getTime();
    long expiryTime = timer - currentTime;


    new CountDownTimer(expiryTime, 500) {
        public void onTick(long millisUntilFinished) {
            long seconds = millisUntilFinished / 1000;
            long minutes = seconds / 60;
            long hours = minutes / 60;
            long days = hours / 24;
            String time = days+" "+"days" +" :" +hours % 24 + ":" + minutes % 60 + ":" + seconds % 60;
            holder.timerValueTimeStamp.setText(time);
        }

        public void onFinish() {
            holder.timerValueTimeStamp.setText("Time up!");
        }
    }.start();

}

@Override
public int getItemCount() {
    return mItems.size();
}

public static class FeedViewHolder extends RecyclerView.ViewHolder {
    TextView desc;
    TextView scratchDes;
    TextView timerValueTimeStamp;
    ImageView feedImage;
    CardView mCv;

    public FeedViewHolder(View itemView) {
        super(itemView);
        mCv = (CardView) itemView.findViewById(R.id.cv_fil);
        desc = (TextView) itemView.findViewById(R.id.desc_tv_fil);
        feedImage = (ImageView) itemView.findViewById(R.id.feed_iv_fil);
        scratchDes = (TextView) itemView.findViewById(R.id.tv_scratch_description);
        timerValueTimeStamp = (TextView) itemView.findViewById(R.id.tv_timer_value_time_stamp);

    }

}

And this is my xml file used in adapter

<LinearLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<android.support.v7.widget.CardView     xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cv_fil"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/card_margin"
    android:layout_gravity="center"
    app:cardUseCompatPadding="true"
    app:cardElevation="4dp"
    android:elevation="6dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/feed_iv_fil"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:layout_alignParentTop="true"
            android:scaleType="fitXY"
            android:tint="@color/grey_tint_color" />

        <TextView
            android:id="@+id/tv_scratch_description"
            style="@style/ListItemText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="casul shoes"
            android:fontFamily="sans-serif-light"
            android:padding="10dp" />


        <TextView
            android:id="@+id/tv_timer_value_time_stamp"
            style="@style/CardTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            />

        <TextView
            android:id="@+id/desc_tv_fil"
            style="@style/VendorNameText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/feed_iv_fil"
            android:textColor="#3f3e3f"
            android:padding="10dp"
            />

    </RelativeLayout>


</android.support.v7.widget.CardView>

And this is screen shot of my RecyclerView enter image description here

Hasan shaikh
  • 738
  • 1
  • 6
  • 16

3 Answers3

40

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.

Hanzala
  • 1,965
  • 1
  • 16
  • 43
  • 2
    @Lupsaa I noticed that other solutions use a Handler and Runnable methods and objects to update a new thread off the main thread. I wonder if those solutions would be better for large numbers of items in a RecyclerView list compared to your solution. Can you comment? See the answer by Gergely Kőrössy: http://stackoverflow.com/questions/32166375/set-counter-inside-recyclerview and the answer by H Raval: http://stackoverflow.com/questions/35860780/recyclerview-with-multiple-countdown-timers-causes-flickering. – AJW Oct 04 '16 at 02:39
  • 2
    @AJW Since the holders are being recycled, the large number of items in a list does not matter. You can have a list of 10.000 items, if only 10 are visible at a time, you will have maybe ~15 holder instances that are being reused. Here I was just providing an answer to this problem by explaining why he was encountering the flickering. I am sure there are multiple approaches but using Thread / Handler is overkill in my opinion. This solution will not update the holders at the same time since each timer is started when the view is bound. –  Oct 04 '16 at 06:31
  • @Lupsaa Ahh, so since the Viewholder does the recycling there are only the 15 holder instances during scrolling at any one time, so that should not overwhelm the main thread? And as a holder drops out of the View then the timer(s) for that View are cancelled so the main thread will only ever have a relatively small number of timers to handle? – AJW Oct 05 '16 at 00:58
  • 1
    @AJW That is correct. This is why you have to cancel the previous instance of the view holder timer (if present) and start a new one. –  Oct 11 '16 at 19:25
  • @Lupsaa Very good, I appreciate you checking back with a reply, cheers. – AJW Oct 11 '16 at 19:53
  • use save my day. Thx! – vlasentiy Dec 07 '17 at 08:21
  • Can anyone help me, I am stuck in a similar type of problem but little different. I am showing items Vertically with match_parent height and scrolling is stopped manually. onFinish() method of counter I am to switch to next position (using scrollToPostion(getAdapterPosition)) with reset timer. I really need help. Thanks in advance! – Chirag Jain Apr 19 '18 at 13:56
  • I am trying to create a similar timer in kotlin but the holder.timer object throws an object not initialised exception due to which i am not able to cancel the previously created timers. Any help would be appreiciated – vivek verma May 17 '18 at 16:59
  • saved my day Thanks – Mina Farid Jul 09 '18 at 23:26
  • scrolling time still we have the issue it was restarted – srinivas Oct 22 '18 at 14:10
  • When i use holder.timer.cancel in onBindViewHolder() just one item work with timer and other work when i scroll on them!!!! – milad salimi May 06 '19 at 12:22
6

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) {

          .... 
  }
}
2

There are two types of solution I can think of and they are without using CountDownTimer class

  1. Make Handler with 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!!");
    }
}
  1. 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);
}
Nadeem Iqbal
  • 2,357
  • 1
  • 28
  • 43