Solve the countdown disorder of Android RecyclerView list

  • 2021-12-05 07:26:11
  • OfStack

Preface

In a twinkling of an eye, it has been a year ring since I wrote my blog last time, during which a lot of things happened; After leaving the company and looking for a job, the first version of the new company was launched. Now I finally have time to sort out the problems I encounter, and I will share more things when I have time later ~ ~

Scene

The problem shared today is that when the countdown is displayed in the list, the sliding list will have an abnormal time display. First of all, we need to pay attention to the following issues about countdown:

The problem of time jumping caused by the multiplexing of ViewHolder in RecyclerView.

The countdown will reset when sliding the list.

After exiting the page, the timer resource release problem, here I use the CountDownTimer that comes with the system

ps: What we are discussing here is a scenario where the countdown requirements are not very strict, and the operation of manually modifying the system time by users cannot be predicted; For the business scenario of Taobao Spike, it is suggested to constantly request the background to get the correct time in real time, and the corresponding interface should be designed as simply as possible to respond to data faster.

Next, learn more about it through the code:

Code


//  Adapter 
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
  // Server returns data 
  private List<TimeBean> mDatas;
  // Quit activity Turn off all timers to avoid resource leakage. 
  private SparseArray<CountDownTimer> countDownMap;

  // Record the time of each refresh 
  private long tempTime;

  public MyAdapter(Context context, List<TimeBean> datas) {
    mDatas = datas;
    countDownMap = new SparseArray<>();
  }

  @Override
  public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_common, parent, false);
    return new ViewHolder(view);
  }

  @Override
  public void onBindViewHolder(final ViewHolder holder, int position) {
    final TimeBean data = mDatas.get(position);
    // Record time point 
    long timeStamp = System.currentTimeMillis() - tempTime;

    long time = data.getLeftTime() - timeStamp;

    // Will be preceded 1 Cache cleanup 
    if (holder.countDownTimer != null) {
      holder.countDownTimer.cancel();
    }
    if (time > 0) { // Judge whether the countdown is over 
      holder.countDownTimer = new CountDownTimer(time, 1000) {
        public void onTick(long millisUntilFinished) {
          holder.timeTv.setText(getMinuteSecond(millisUntilFinished));
        }
        public void onFinish() {
          // End of countdown 
          holder.timeTv.setText("00:00");
        }
      }.start();

      countDownMap.put(holder.timeTv.hashCode(), holder.countDownTimer);
    } else {
      holder.timeTv.setText("00:00");
    }
  }

  @Override
  public int getItemCount() {
    if (mDatas != null && !mDatas.isEmpty()) {
      return mDatas.size();
    }
    return 0;
  }

  public class ViewHolder extends RecyclerView.ViewHolder {
    public TextView timeTv;
    public CountDownTimer countDownTimer;

    public ViewHolder(View itemView) {
      super(itemView);
      timeTv = (TextView) itemView.findViewById(R.id.tv_time);
    }
  }

  public void setGetTime(long tempTime) {
    this.tempTime = tempTime;
  }

  /**
   *  Convert milliseconds to  00:00  Form 
   */
  public static String getMinuteSecond(long time) {
    int ss = 1000;
    int mi = ss * 60;
    int hh = mi * 60;
    int dd = hh * 24;

    long day = time / dd;
    long hour = (time - day * dd) / hh;
    long minute = (time - day * dd - hour * hh) / mi;
    long second = (time - day * dd - hour * hh - minute * mi) / ss;

    String strMinute = minute < 10 ? "0" + minute : "" + minute;
    String strSecond = second < 10 ? "0" + second : "" + second;
    return strMinute + ":" + strSecond;
  }

  /**
   *  Empty resources 
   */
  public void cancelAllTimers() {
    if (countDownMap == null) {
      return;
    }
    for (int i = 0,length = countDownMap.size(); i < length; i++) {
      CountDownTimer cdt = countDownMap.get(countDownMap.keyAt(i));
      if (cdt != null) {
        cdt.cancel();
      }
    }
  }
}

The above is the core code of the whole problem; Where SparseArray < CountDownTimer > It is used to save the timer in the list and recycle the timer when exiting the page. SparseArray is a unique data structure of Android, so it is recommended to use it more; data. getLeftTime () is the time returned by the server, in milliseconds, that needs to be counted down.

Problem 1: Data disorder caused by ViewHolder reuse


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

Reset the countdown before setting the countdown every time.

Problem 2: The countdown will reset when sliding the list

This problem is caused by solving problem 1, because the list will be reused when leaving the screen when sliding. At this time, we will reset the timer. Before, I recorded the remaining time of the countdown in the countdown and reset the value, but there will still be problems; Here, the system time is borrowed to solve this problem, that is, the value of tempTime.

First, set this value in the callback after the server request is successful, such as:


  private MyAdapter adapter;

  @Override
  public void onHttpRequestSuccess(String url, HttpContext httpContext)   {
    if ( Server returns data ) {
      adapter.setGetTime(System.currentTimeMillis());
  }

It is equivalent to getting the timestamp of the system at that time every time you refresh.

Then calculate it in adapter

long timeStamp = System.currentTimeMillis() - tempTime;

long time = data.getLeftTime() - timeStamp;

Among them, tempTime is the current timestamp of the system saved by us, and then onBindViewHolder will be called every time the list is slid, so timeStamp is the recorded distance how many seconds passed by the last refresh, and then the number of seconds passed by subtracting the countdown time returned by the server is the remaining countdown seconds. Finally, just set the timer.

Question 3: Release of resources

The following methods are called in the current activity.


@Override
protected void onDestroy() {
  super.onDestroy();
  if (adapter != null) {
    adapter.cancelAllTimers();
  }
}

Ok, that's all for today's sharing, because the code is relatively simple and the layout is an Textview, so it is not posted. If you need the code, you can leave a message ~ ~

Supplementary knowledge: Android custom countdown, support listview multi-item1 countdown

There are two kinds of countdown used in the project, one is CountDownTimer, but this way is not so easy to use in listview. When many item in listview need countdown, it can't be used. I think of using Thread plus handler to realize it. If you still have a good countdown method, you can leave a message to discuss oh, because the code is in the project, I will intercept a few pieces of code.

Type 1 CountDownTimer:

The main custom class inherits CountDownTimer, call start () when starting, and call canel () method after countdown.


time = new TimeCount(remainingTime, 1000);// Structure CountDownTimer Object 
time.start();// Start timing 

class TimeCount extends CountDownTimer {
    public TimeCount(long millisInFuture, long countDownInterval) {
      super(millisInFuture, countDownInterval);
    }

    @Override
    public void onFinish() {// Triggered when timing is finished 
      if (isDead) {
        remainingTime = 90000;
        ColorStateList colorStateList = getResources().getColorStateList(R.color.button_send_code_text2_selector);
        getCode.setTextColor(colorStateList);
        getCode.setText(R.string.register_tip7);
        getCode.setEnabled(true);
      }
    }

    @Override
    public void onTick(long millisUntilFinished) {// Timing process display 
      if (isDead) {
        getCode.setEnabled(false);
        getCode.setTextColor(getResources().getColor(R.color.grey5));
        remainingTime = millisUntilFinished;
        getCode.setText(millisUntilFinished / 1000 + " Retransmitted in seconds ");
      }
    }
  }

The second Thread plus handler

Create a new thread, subtract 1 time per second, and then refresh the interface once per second in handler to see the countdown effect.


 private Thread thread;

  // Entry countdown 
  public void start() {
    thread = new Thread() {
      public void run() {
        while (true) {
          try {
            if (list != null) {
              for (InvestProjectVo item : list) {

                if(item.remainOpenTime == 0){
                  item.status = 0;
                }

                if(item.remainOpenTime > 0){
                  item.remainOpenTime = item.remainOpenTime - 1;
                }
              }
            }
            sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    };
    thread.start();
  }

In the getview () method of adapter, whether the countdown time is greater than 0 is judged, and if it is greater than 0, the countdown time can be displayed continuously


          if (vo.remainOpenTime != 0 && vo.remainOpenTime > 0) {

            viewCache.showProjectFullIcon.setVisibility(View.GONE);
            viewCache.projectProgress.setVisibility(View.GONE);
            viewCache.showTimer.setVisibility(View.VISIBLE);

            long tempTime = vo.remainOpenTime;
            long day = tempTime / 60 / 60 / 24;
            long hours = (tempTime - day * 24 * 60 * 60) / 60 / 60;
            long minutes = (tempTime - day * 24 * 60 * 60 - hours * 60 * 60) / 60;
            long seconds = (tempTime - day * 24 * 60 * 60 - hours * 60 * 60 - minutes * 60);
            if (minutes > 0) {
              viewCache.timer.setText(minutes + " Points " + seconds + " Seconds ");
            } else {
              viewCache.timer.setText(seconds + " Seconds ");
            }
          }else{
            viewCache.showProjectFullIcon.setVisibility(View.GONE);
            viewCache.projectProgress.setVisibility(View.VISIBLE);
            viewCache.showTimer.setVisibility(View.GONE);
          }

Refresh the interface once per second in handler

mHandler.sendEmptyMessageDelayed(2586221,1000);


adapter.notifyDataSetChanged();
// Every interval 1 Millisecond update 1 Sub-interface, if only the countdown accurate to seconds is needed, it will be changed here to 1000 You can 
mHandler.sendEmptyMessageDelayed(2586221,1000);

Related articles: