-->

Multiple count down timers in RecyclerView flicker

2019-01-12 04:10发布

问题:

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

回答1:

This problem is simple.

RecyclerView reuses the holders, calling bind each time to update the data in them.

Since you create a countdown timer each time any data is bound, you will end up with multiple timers updating the same view holder.

The best thing here would be to move the countdown timer 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 view holder prior to starting another timer.



回答2:

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

          .... 
  }
}


回答3:

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

  1. Make Handle 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);
}