ListView with Coundown timer. Timer flickers when scrolling the listview

1k views Asked by At

In my app, I have to show countdown timer for every item in listview. I have been able to do this using CountDownTimer. But, problem is when scrolling the listview up or down, timer starts flickering. Searched a lot but couldn't got a solution.

My Adapter class

public class BuyListingListAdapter extends BaseAdapter  {

private Context mContext;
private ArrayList<VehicleAttributes> mVehicleList = new ArrayList<VehicleAttributes>();
private Date currentDate;
//Saving position in map to check if timer has been    //started for this row
private HashMap<Integer, Boolean> timerMap = new HashMap<Integer, Boolean>();

public BuyListingListAdapter(Context context,
        ArrayList<VehicleAttributes> vehicleList, boolean showGridView) {
    this.mContext = context;
    this.mVehicleList = vehicleList;
    this.showGridView = showGridView;
    currentDate = new Date(System.currentTimeMillis());

    int listSize = mVehicleList.size();
    for (int i = 0; i < listSize; i++)
        timerMap.put(i, false);
}

@Override
public int getCount() {
    return mVehicleList.size();
}

@Override
public Object getItem(int position) {
    return mVehicleList.get(position);
}

@Override
public long getItemId(int position) {
    return 0;
}

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
    final ViewHolder holder;
    if (convertView == null) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        convertView = inflater.inflate(R.layout.row_buy_listing_grid,
                    parent, false);
        holder = new ViewHolder();
                    holder.timer_layout = (LinearLayout) convertView
                .findViewById(R.id.timer_layout);
                    holder.txt_remaining_days = (RobotoCondensedBoldTextView) convertView
                .findViewById(R.id.txt_remaining_days);
        holder.txt_remaining_hours = (RobotoCondensedBoldTextView) convertView
                .findViewById(R.id.txt_remaining_hours);
        holder.txt_remaining_mins = (RobotoCondensedBoldTextView) convertView
                .findViewById(R.id.txt_remaining_mins);
        holder.txt_remaining_secs = (RobotoCondensedBoldTextView) convertView
                .findViewById(R.id.txt_remaining_secs);
        holder.listing_status = (ImageView) convertView
                .findViewById(R.id.listing_status);

        convertView.setTag(holder);
    } else
        holder = (ViewHolder) convertView.getTag();

    final VehicleAttributes vehicleAttributes = mVehicleList.get(position);

    AuctionModel mAuctionModel = vehicleAttributes.getAuctionDetailModel();
    if (mAuctionModel != null) {
        holder.img_auction.setVisibility(View.VISIBLE);

        Date auctionStartDate = Util
                .getDateFromString(mAuctionModel.getStarted_at(),
                        "yyyy-MM-dd hh:mm:ss", "GMT");
        Date auctionEndDate = Util.getDateFromString(
                mAuctionModel.getEnded_at(), "yyyy-MM-dd hh:mm:ss", "GMT");
        long diff = currentDate.getTime() - auctionEndDate.getTime();

        if (diff < 0)
            diff = -diff;
        else
            diff = 0;

        if (timerMap.get(position) == null || !timerMap.get(position)) {

            timerMap.put(position, true);
            MyCountDown countDown = new MyCountDown(position, diff, 1000,
                    holder.txt_remaining_days, holder.txt_remaining_hours,
                    holder.txt_remaining_mins, holder.txt_remaining_secs);
            countDown.start();
        }
    } 
    return convertView;
}

private class MyCountDown extends CountDownTimer {
    RobotoCondensedBoldTextView txt_remaining_days;
    RobotoCondensedBoldTextView txt_remaining_hours;
    RobotoCondensedBoldTextView txt_remaining_mins;
    RobotoCondensedBoldTextView txt_remaining_secs;
    int position;

    public MyCountDown(int position, long millisInFuture, long countDownInterval,
            RobotoCondensedBoldTextView txt_remaining_days,
            RobotoCondensedBoldTextView txt_remaining_hours,
            RobotoCondensedBoldTextView txt_remaining_mins,
            RobotoCondensedBoldTextView txt_remaining_secs) {
        super(millisInFuture, countDownInterval);
        this.position = position;
        this.txt_remaining_days = txt_remaining_days;
        this.txt_remaining_hours = txt_remaining_hours;
        this.txt_remaining_mins = txt_remaining_mins;
        this.txt_remaining_secs = txt_remaining_secs;
    }

    @Override
    public void onFinish() {

    }

    @Override
    public void onTick(long millisUntilFinished) {
        updateTimerLabel(position, millisUntilFinished, txt_remaining_days,
                txt_remaining_hours, txt_remaining_mins, txt_remaining_secs);
    }
}

private void updateTimerLabel(int position, long millis,
        RobotoCondensedBoldTextView txt_remaining_days,
        RobotoCondensedBoldTextView txt_remaining_hours,
        RobotoCondensedBoldTextView txt_remaining_mins,
        RobotoCondensedBoldTextView txt_remaining_secs) {
    String days = String.format("%02d",
            TimeUnit.MILLISECONDS.toDays(millis));
    String hours = String.format(
            "%02d",
            TimeUnit.MILLISECONDS.toHours(millis)
                    - TimeUnit.DAYS.toHours(TimeUnit.MILLISECONDS
                            .toDays(millis)));
    String mins = String.format(
            "%02d",
            TimeUnit.MILLISECONDS.toMinutes(millis)
                    - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS
                            .toHours(millis)));
    String secs = String.format(
            "%02d",
            TimeUnit.MILLISECONDS.toSeconds(millis)
                    - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS
                            .toMinutes(millis)));

    txt_remaining_days.setText(days);
    txt_remaining_hours.setText(hours);
    txt_remaining_mins.setText(mins);
    txt_remaining_secs.setText(secs);
}

static class ViewHolder {
    NetworkImageView mVehicleImage;
    RobotoCondensedBoldTextView txt_remaining_days, txt_remaining_hours,
            txt_remaining_mins, txt_remaining_secs;
    LinearLayout timer_layout;
}
}
4

There are 4 answers

0
shadow-01 On BEST ANSWER

UPDATE

I updated my answer here have a look: (it's uses cardview with recyclerview but the same principle can be applied also for listview) How to change text based on time and date?

You have to take care when you create the countdowntimer instance and when you cancel it. If you don't cancel the timer it will update the wrong views.

2
bunbun On

You should be separating your listview and countdown timers into two fragments to avoid unintentional interference. Android is not designed for such an implementation.

Fragments is the way to go: http://developer.android.com/guide/components/fragments.html

Edit: For your case, you can run a separate database of timers for each list item, then just list your items in your listview. When a user needs to know the timing, they click on your listitem, which calls the database for a current time. This current time is then shown on the listitem. This is NOT live so the time will not change. Doing so will avoid the flickering problem.

2
Mohd Mufiz On

Ok lets talk about the problem first

if (convertView == null) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        convertView = inflater.inflate(R.layout.row_buy_listing_grid,
                    parent, false);
        holder = new ViewHolder();
                    holder.timer_layout = (LinearLayout) convertView
                .findViewById(R.id.timer_layout);
                    holder.txt_remaining_days = (RobotoCondensedBoldTextView) convertView
                .findViewById(R.id.txt_remaining_days);
        holder.txt_remaining_hours = (RobotoCondensedBoldTextView) convertView
                .findViewById(R.id.txt_remaining_hours);
        holder.txt_remaining_mins = (RobotoCondensedBoldTextView) convertView
                .findViewById(R.id.txt_remaining_mins);
        holder.txt_remaining_secs = (RobotoCondensedBoldTextView) convertView
                .findViewById(R.id.txt_remaining_secs);
        holder.listing_status = (ImageView) convertView
                .findViewById(R.id.listing_status);

        convertView.setTag(holder);
    } else
        holder = (ViewHolder) convertView.getTag();

The code above in the list view will reuse the item for components, for example when we load a list only 6 items are appear on the list, list view create these items but when you scroll up and down instead of creating new item it will reuse the same view for the next positions, so when you are passing your view to countDown timer to update the time same views are updating with different countdown timer

Now what will be the solution, you have to figure it out which item views are visible on the screen and stop the countdown timer for other views. or there is a very simple way but not recommended because it may cause memory issue

    LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    convertView = inflater.inflate(R.layout.row_buy_listing_grid,
                parent, false);
    holder = new ViewHolder();
                holder.timer_layout = (LinearLayout) convertView
            .findViewById(R.id.timer_layout);
                holder.txt_remaining_days = (RobotoCondensedBoldTextView) convertView
            .findViewById(R.id.txt_remaining_days);
    holder.txt_remaining_hours = (RobotoCondensedBoldTextView) convertView
            .findViewById(R.id.txt_remaining_hours);
    holder.txt_remaining_mins = (RobotoCondensedBoldTextView) convertView
            .findViewById(R.id.txt_remaining_mins);
    holder.txt_remaining_secs = (RobotoCondensedBoldTextView) convertView
            .findViewById(R.id.txt_remaining_secs);
    holder.listing_status = (ImageView) convertView
            .findViewById(R.id.listing_status);

    convertView.setTag(holder);

Simple remove if(convertview==null) and allow inflating a new view for every row.

0
Orit Malki On

What worked for me when I had the same problem of using individual timers for each viewholder, was setting the timer in onBindViewHolder() and just before that, checking to see if it wasn't null. In that case I would cancel it:

if (holder.mCountDownTimer != null) {
      holder.mCountDownTimer.cancel();
    }

    if (mPodEvents.get(position).getState() == PodEvent.State.FUTURE) {
      holder.mCountDownTimer = new CountDownTimer(podEvent.getMilliesTillTime(), 1000 / 60) {
        @Override
        public void onTick(long millisUntilFinished) {
          holder.hour.setText(String.format(Locale.getDefault(), "%d", (int) TimeUnit.MILLISECONDS.toHours(millisUntilFinished)));
          holder.minute.setText(String.format(Locale.getDefault(), "%d", (int) (TimeUnit.MILLISECONDS.toMinutes(millisUntilFinished) - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(millisUntilFinished)))));
        }

        @Override
        public void onFinish() {

        }
      }.start();
    }