RecyclerView countdown optimization

renderings

 

1. What problems need to be solved to implement high-quality countdown in RecyclerView.

  • How to handle when there are multiple countdowns in the list, start multiple countdowns or manage them collectively? If every item turns on the countdown, will it cause a waste of resources? How to cancel the countdown that is not in the display area? How to ensure the accuracy of the time when the countdown is not on the screen? How to notify all items that need to be updated using only one countdown? How to ensure the accuracy of the time for items that are not on the screen and are not notified? Can it be paused to reduce resource waste when exiting the background or jumping to other pages?

2. Start designing our countdown plan with these questions in mind.

  • First of all, from a large-scale logic, we prefer the single-task countdown solution.

  • When choosing a timer, considering that it may start and stop frequently, we choose the interval method provided by Rxjava. The internal management is based on the thread pool to avoid using the Timer class, which will cause the cost of thread switching to be too high.

  • The key point here is to knock on the blackboard. The timer is only responsible for regularly notifying the relevant items to update the UI, and does not record the remaining time of the item. Since the time obtained by the front end may have a time difference or be modified by the user, it is not trustworthy. The bean class issued by the gateway shows the remaining time, such as 2000 seconds. We need to manually add a variable countdown end time point in the bean class and name it endCountTime. When json is mapped to the bean class, we get the current system time and add it to the remaining countdown time to get the required endCountTime. At the same time, because the system time is not trustworthy, that is, System.getCurrentMillions is not trustworthy, so we choose the system's boot clock SystemClock.elapsedRealtime(). This is the key factor that allows us to pause and restart the countdown at any time while still ensuring the correct countdown time.

  • Use a List to manage the Item positions pos that need to be notified, which we name countDownPositions. When bindViewHolder, we add the itemPosition that needs to be updated to countDownPositions, traverse countDownPositions when notified by the timer task, and then perform notityitemchanged. Here we will encounter two choices, items that are not on the screen, and whether notifyitemchange will cause waste. Another option is to determine whether it is on the screen when notifyitemchange. To determine whether the cost is higher, whether to notify on the screen or directly, I have consulted relevant information and done some tests. I haven't decided which one is better. If you know more about it, please give me some advice. In this practice, I judge whether it is on the screen to decide whether to notify or not.

  • Since the item position pos we record, when RecyclerView is added or deleted, the position we record may be misplaced, so we register a data observer for the adapter, so that when the data changes, we can ensure that the item that needs to be updated is not will produce misalignment

The idea is to write a Bean to store the end time and the boolean value of whether a countdown is needed, and a collection to store the subscripts that need to be counted down. Then add a listener in the Recyclerview to add deletions and changes. If the entire list is refreshed, delete all subscripts first. If so , If adding, add data first, then loop through all subscript data >= the inserted subscript, the current data subscript assignment += the number of insertions, if it is to delete the data, then remove the data first, then traverse in reverse, the current subscript >= to be deleted subscript, then set the current subscript -= the total number of deletions, otherwise it satisfies >= the deletion of the first subscript to be deleted.  This ensures that the ID will not be confused.

At this time, onBindViewHolder determines whether a countdown is needed, and then subtracts the end time value given by the background from the system time. If it is greater than 1 second, it is displayed on the TexeView, and then determines whether the subscript set contains or whether the address value and value are the same (detailed reference Index’s equals)

If the above conditions are not met, add it to the id collection, then start a countdown update, loop through all subscripts and get the viewHolder of recyclerView to refresh rvAdapter.notifyItemChanged(countDownPosition.index)

 
 

3. After the overall idea is sorted out, the next step is to implement the code

  • Helper class code implementation
public class RvCountDownHelper {

    private List<Index> countDownPositions = new ArrayList<>();
    private RecyclerView.Adapter rvAdapter;
    private RecyclerView recyclerView;

    private Disposable countDownTask;

    private OnTimeCollectListener mListener;

    public RvCountDownHelper(RecyclerView.Adapter rvAdapter, RecyclerView recyclerView) {
        this.recyclerView = recyclerView;
        this.rvAdapter = rvAdapter;
        rvAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
            @Override
            public void onChanged() {
                removeAllPosition();
                super.onChanged();
                Log.d("AdapterDataObserver", "onChanged");
            }

            @Override
            public void onItemRangeChanged(int positionStart, int itemCount) {
                super.onItemRangeChanged(positionStart, itemCount);
                Log.d("AdapterDataObserver", "onItemRangeChanged");
            }

            @Override
            public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
                super.onItemRangeChanged(positionStart, itemCount, payload);
                Log.d("AdapterDataObserver", "onItemRangeChanged");
            }

            @Override
            public void onItemRangeInserted(int positionStart, int itemCount) {
                for (Index countDownPosition : countDownPositions) {
                    if (countDownPosition.index >= positionStart) {
                        countDownPosition.index += itemCount;
                    }
                }
                super.onItemRangeInserted(positionStart, itemCount);
                Log.d("AdapterDataObserver", "onItemRangeInserted");
            }

            @Override
            public void onItemRangeRemoved(int positionStart, int itemCount) {
                for (int i = countDownPositions.size() - 1; i >= 0; i--) {
                    Index temp = countDownPositions.get(i);
                    if (temp.index >= positionStart + itemCount) {
                        temp.index = temp.index - itemCount;
                    } else if (temp.index >= positionStart) {
                        removeCountDownPosition(temp.index);
                    }
                }
                super.onItemRangeRemoved(positionStart, itemCount);
                Log.d("AdapterDataObserver", "onItemRangeRemoved");
            }

            @Override
            public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {

                Log.d("ItemMove", "frompos =" + fromPosition + " toPos =" + toPosition + " itemCount= " + itemCount);

                for (Index countDownPosition : countDownPositions) {
                    if (countDownPosition.index == fromPosition) {
                        countDownPosition.index = toPosition;
                    }else if (countDownPosition.index == toPosition) {
                        countDownPosition.index = fromPosition;
                    }
                }

                super.onItemRangeMoved(fromPosition, toPosition, itemCount);
                Log.d("AdapterDataObserver", "onItemRangeMoved");
            }
        });
    }

    public void setOnTimeCollectListener(OnTimeCollectListener listener) {
        this.mListener = listener;
    }

    /**
     * 新增一个需要倒计时的item位置
     * @param pos
     */
    public void addPosition2CountDown(int pos) {
        Index addPos = new Index(pos);
        if (!countDownPositions.contains(addPos)) {
            Log.d("CountDown", "新增pos-" + pos);
            countDownPositions.add(addPos);
            startCountDown();
        }
    }

    /**
     * 移除一个需要定时更新的item
     * @param pos
     */
    public void removeCountDownPosition(int pos) {
        boolean remove = countDownPositions.remove(new Index(pos));
        Log.d("CountDown", "移除pos-" + pos + "result = " + remove);

    }

    /**
     * 移除所有需要定时更新的item
     */
    public void removeAllPosition() {
        countDownPositions.clear();
        Log.d("CountDown", "移除所有标记位置");
    }

    /**
     * 手动调用开始定时更新
     */
    public void startCountDown() {
        if (countDownTask == null || countDownTask.isDisposed()) {
            countDownTask = Observable.interval(0, 1000, TimeUnit.MILLISECONDS)
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(aLong -> {
                        Log.d("倒计时--", "cur aLong= " + aLong);

                        if (countDownTask.isDisposed()) {
                            return;
                        }

                        if (countDownPositions.isEmpty()) {
                            countDownTask.dispose();
                            return;
                        }

                        for (Index countDownPosition : countDownPositions) {
                            RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
                            if (lm != null) {
                                View itemView = recyclerView.getLayoutManager().findViewByPosition(countDownPosition.index);
                                if (itemView != null) {
                                    if (mListener != null) {
                                        RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForPosition(countDownPosition.index);
                                        mListener.onTimeCollect(viewHolder, countDownPosition.index);
                                    } else {
                                        rvAdapter.notifyItemChanged(countDownPosition.index);
                                    }
                                }
                            }

                        }

                    }, throwable -> Log.e("倒计时异常", throwable.getMessage()));

        }
    }

    /**
     * 手动调用停止定时更新
     */
    public void stopCountDown() {
        if (countDownTask != null && !countDownTask.isDisposed()) {
            countDownTask.dispose();
        }
    }

    /**
     * 获取所有的item位置记录
     */
    public List<Index> getAllRecordPos() {
        return countDownPositions;
    }

    /**
     * 销毁
     */
    public void destroy() {
        stopCountDown();
        mListener = null;
        countDownTask = null;
        recyclerView = null;
        rvAdapter = null;
    }

    interface OnTimeCollectListener {
        void onTimeCollect(RecyclerView.ViewHolder vh,int pos);
    }

    static class Index {
        int index;

        public Index(int index) {
            this.index = index;
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if(!(obj instanceof Index)) {
                // instanceof 已经处理了obj = null的情况
                return false;
            }
            Index indObj = (Index) obj;
            // 地址相等
            if (this == indObj) {
                return true;
            }
            // 如果两个对象index相等
            return indObj.index == this.index;
        }

        @Override
        public int hashCode() {
            return 128 * index;
        }
    }
}
  • Use code samples
public class MainActivity extends AppCompatActivity {
    MyRvAdapter myRvAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView rvMyRv = findViewById(R.id.rvMyRv);
//        rvMyRv.setItemAnimator(null);
        ((SimpleItemAnimator)rvMyRv.getItemAnimator()).setSupportsChangeAnimations(false);
        rvMyRv.setLayoutManager(new LinearLayoutManager(this));
        myRvAdapter = new MyRvAdapter(rvMyRv);
        rvMyRv.setAdapter(myRvAdapter);

    }

    @Override
    protected void onPause() {
        super.onPause();
        myRvAdapter.stopCountDown();
    }

    @Override
    protected void onResume() {
        super.onResume();
        myRvAdapter.startCountDown();
    }

    public void addClick(View view) {
        myRvAdapter.addItem();
    }

    public void removeClick(View view) {
        myRvAdapter.deleteItem();
    }

    public void exchangeClick(View view) {
        myRvAdapter.exchangeItem(4, 2);
    }

    static class MyRvAdapter extends RecyclerView.Adapter<MyViewHolder> {

        List<TestData> times;
        RvCountDownHelper countDownHelper;
        RecyclerView mRecyclerView;

        public MyRvAdapter(RecyclerView recyclerView) {
            this.mRecyclerView = recyclerView;
            times = new ArrayList<>();
            countDownHelper = new RvCountDownHelper(this, mRecyclerView);
//            countDownHelper.setOnTimeCollectListener((viewHolder,pos) -> {
//                if (viewHolder instanceof MyViewHolder) {
//                    long curMillions = SystemClock.elapsedRealtime();
//                    long endMillions = times.get(pos).countDownEndTime;
//
//                    long tmp = endMillions - curMillions;
//
//                    if (tmp > 1000) {
//                        ((MyViewHolder) viewHolder).tvShowTime.setText("倒计时  " + getShowStr(tmp));
//                    }
//                }
//            });

            long curMillions = SystemClock.elapsedRealtime();
            for (int i = 0; i < 50; i++) {
                if (i % 2 == 0) {
                    times.add(TestData.createRandomData(curMillions + (long) new Random().nextInt(30 * 60 * 1000)));
                } else {
                    times.add(TestData.createRandomData(-1));
                }
            }
        }

        public void addItem() {
            long curMillions = SystemClock.elapsedRealtime();
            times.add(0, TestData.createRandomData(curMillions + (long) new Random().nextInt(30 * 60 * 1000)));
            notifyItemInserted(0);
        }

        public void deleteItem() {
            times.remove(0);
            notifyItemRemoved(0);
        }

        public void exchangeItem(int fromPos, int toPos) {
            Collections.swap(times,fromPos,toPos);
            notifyItemRangeChanged(fromPos, toPos + 1 - fromPos);
        }

        @NonNull
        @Override
        public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
            View contentView = LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.item_layout, viewGroup, false);
            return new MyViewHolder(contentView);
        }

        @Override
        public void onBindViewHolder(@NonNull MyViewHolder viewHolder, int i) {
            TestData data = times.get(i);

            if (data.isCountDownItem) {
                long curMillions = SystemClock.elapsedRealtime();

                long tmp = data.countDownEndTime - curMillions;

                if (tmp > 1000) {
                    viewHolder.tvShowTime.setText("倒计时  " + getShowStr(tmp));
                    countDownHelper.addPosition2CountDown(i);

                } else {
                    viewHolder.tvShowTime.setText("倒计时  00:00:00");
                    countDownHelper.removeCountDownPosition(i);
                }
            }else {
                viewHolder.tvShowTime.setText("无倒计时");
            }

        }

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

        private String getShowStr(long mis) {
            mis = mis / 1000; //
            long h = mis / 3600;
            long m = mis % 3600 / 60;
            long d = mis % 3600 % 60;
            return h + ":" + m + ":" + d;
        }

        public void destroy() {
            countDownHelper.destroy();
        }

        public void stopCountDown() {
            countDownHelper.stopCountDown();
        }

        public void startCountDown() {
            countDownHelper.startCountDown();
        }
    }

    @Override
    protected void onDestroy() {
        myRvAdapter.destroy();
        super.onDestroy();
    }

    static class MyViewHolder extends RecyclerView.ViewHolder {
        TextView tvShowTime;

        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            tvShowTime = itemView.findViewById(R.id.tvShowTime);
        }
    }

    static class TestData {

        public TestData(boolean isCountDownItem, long countDownEndTime) {
            this.isCountDownItem = isCountDownItem;
            this.countDownEndTime = countDownEndTime;
        }

        boolean isCountDownItem;

        long countDownEndTime;

        static TestData createRandomData(long endTime) {
            if (endTime < 0) {
                return new TestData(false, endTime);
            } else {
                return new TestData(true, endTime);
            }
        }
    }


}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvMyRv"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <Button
            android:layout_width="80dp"
            android:layout_height="50dp"
            android:onClick="addClick"
            android:text="Add" />

        <Button
            android:layout_width="80dp"
            android:layout_height="50dp"
            android:onClick="removeClick"
            android:text="Delete" />

        <Button
            android:layout_width="80dp"
            android:layout_height="50dp"
            android:onClick="exchangeClick"
            android:text="Exchange" />


    </LinearLayout>



</FrameLayout>

item_layout.xml

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

    <TextView
        android:id="@+id/tvShowTime"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="-- :  --" />
</LinearLayout>

おすすめ

転載: blog.csdn.net/qq_15059163/article/details/124493832