Android uses RecyclerView to realize the countdown effect of list

Posted by ricta on Thu, 09 Dec 2021 18:31:34 +0100

Recently, I received a request. I need to set the countdown effect when the expiration time of the coupon in the list remains two days. I think it shouldn't be a problem when I get the demand.

There are two main methods to realize the countdown: 1. Set a timer for each item that starts the countdown, and then update the item; 2. Only start a timer, then traverse the data, and then update the item.

Since the previous countdown function has been encapsulated and used the CountDownTimer class, I choose the first method to implement it. It starts directly, and the countdown effect of the list is realized after a wave of operation. The following figure is a demo of the simulation effect, an informal project, as shown in the figure:

The implementation process is relatively smooth. The use of CountDownTimer class also perfectly solves the problem of time confusion of different items caused by item reuse in RecyclerView. I thought it was implemented in this way. In terms of function, it is indeed implemented. However, after exiting the page, it is found that the printed log is still running, which indicates that we did not cancel when exiting, This is the memory problem. Let's see how to solve it!

Here is a middle page. Click the button and jump to the countdown page. It is mainly to simulate whether you are still running in the background without canceling after exiting the page. Let's take a look at the main code.

Code implementation steps:

1. Analog data

The simulation data uses the data of the last 20 days of the current time, and the structure type is also datetime

        //Analog data, structure type 2021-12-11 15:28:23
        List<String> dataList = new ArrayList<>();
        //Gets the month and day of the current time
        Calendar now = Calendar.getInstance();
        int currentYear = now.get(Calendar.YEAR);
        int currentMonth = now.get(Calendar.MONTH) + 1;
        int currentDay = now.get(Calendar.DAY_OF_MONTH);

        for (int i = 0; i < 20; i++) {
            if ((currentDay + i) < CommonUtils.getCurrentMonthLastDay()) {
                dataList.add(currentYear + "-" + currentMonth + "-" + (currentDay + i) + " 23:59:" + i);
            } else {
                dataList.add(currentYear + "-" + (currentMonth + 1) + "-" + (currentDay + i - CommonUtils.getCurrentMonthLastDay()) + " 23:59:" + i);
            }
        }

        recycler_view.setAdapter(new TimeOutAdapter(this, dataList));

2. TimeOutAdapter class implemented by countdown function

class TimeOutAdapter extends RecyclerView.Adapter<TimeOutAdapter.ViewHolder> {

    private Context mContext;
    private List<String> dataList;
    private SparseArray<CountDownTimerUtils> countDownMap = new SparseArray<>();

    public TimeOutAdapter(Context context, List<String> dataList) {
        this.mContext = context;
        this.dataList = dataList;
    }

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

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

    @Override
    public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
        if (dataList != null && dataList.size() != 0) {
            holder.text_content.setText(dataList.get(position));

            try {
                //Converts the data to milliseconds, where CommonUtils is the tool class
                long residueTime = CommonUtils.residueTimeout(dataList.get(position));

                //Set when time is greater than 0
                if (residueTime > 0) {

                    //Where CountDownTimerUtils is the tool class for countdown
                    holder.countDownTimer = CountDownTimerUtils.getCountDownTimer()
                            .setMillisInFuture(residueTime)
                            .setCountDownInterval(1000)
                            .setTickDelegate(new CountDownTimerUtils.TickDelegate() {
                                @Override
                                public void onTick(long pMillisUntilFinished) {
                                    Log.i("TAG==", "==Output data update==" + pMillisUntilFinished);
                                    //Update data
                                    holder.text_content.setText(CommonUtils.stampToDate(pMillisUntilFinished));
                                }
                            })
                            .setFinishDelegate(new CountDownTimerUtils.FinishDelegate() {
                                @Override
                                public void onFinish() {
                                    //Countdown complete
                                }
                            });

                    holder.countDownTimer.start();
                    
                    //Set the hashcode of item as the key in SparseArray
                    countDownMap.put(holder.text_content.hashCode(), holder.countDownTimer);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

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

    public class ViewHolder extends RecyclerView.ViewHolder {

        private final TextView text_content;
        CountDownTimerUtils countDownTimer;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);

            text_content = itemView.findViewById(R.id.text_content);
        }
    }
}

Here, we can notice the cancelAllTimer() method, which is used to solve the memory problem.

Through the following line of code, set the hashcode in the item as the key into SparseArray, so that you can traverse it in the cancelAllTimer method to cancel the countdown.

countDownMap.put(holder.text_content.hashCode(), holder.countDownTimer);

3. Call the cancelAllTimer() method to cancel when exiting the page

        //Cancel processing
        if (timeOutAdapter != null) {
            timeOutAdapter.cancelAllTimers();
        }

This will solve the problem and end the work.