RecyclerView倒计时优化

效果图

1.在RecyclerView中实现高质量倒计时需要解决哪些问题。

  • 当列表中有多个倒计时如何处理,开启多个还是统一管理?如果每个item都开启倒计时是否造成了资源浪费?不在展示区的倒计时又该如何取消?不在屏幕上的倒计时如何保证时间的准确性?只使用一个倒计时如何通知所有需要更新的item?如何保证不在屏幕上而不被通知的item的时间的准确性?能否在退出后台或者跳转到其他页面的时候暂停减少资源的浪费?

2.带着这些问题着手设计我们的倒计时方案。

  • 首先从大的逻辑上我们优选单任务倒计时方案。

  • 计时器的选择,考虑到可能频繁启停,我们选择Rxjava提供的interval方法。内部基于线程池管理,避免使用Timer类会造成线程开关的成本过高。

  • 敲黑板啦重点来了 计时器只负责定时通知相关item更新UI,不记录item剩余时间。由于前端获取的时间可能有时差或者被用户修改过是不可信的,网关下发的bean类中是剩余时间如2000秒。需要在bean类中手动增加一个变量 倒计时结束时间点 我们命名为endCountTime,在json映射为bean类的时候我们获取当前系统时间 让它加上 倒计时剩余时间 就是所需要的 endCountTime。 同时呢由于系统时间的不可信,也就是System.getCurrentMillions是不可信的,所以我们选择系统的开机时钟 SystemClock.elapsedRealtime()。这是我们能够得以实现随时暂停再开启倒计时 倒计时时间依然能够保证正确的关键因素。

  • 使用一个List来管理需要通知的Item位置pos,我们命名为countDownPositions。在bindViewHolder的时候我们将需要更新的itemPosition添加到countDownPositions中,当计时器任务通知时来遍历countDownPositions,然后进行notityitemchanged。在这我们会遇到两个选择,不在屏幕上的item,notifyitemchange的时候会不会造成浪费。另一种选择的是在notifyitemchange 的时候判断是否在屏幕上。 判断是否在屏幕上和直接notify成本哪个更高,虽然有查阅相关资料,也做过一些测试。并没有分辨出来哪个更好,如有了解的还请指教,在本次实践中 我是判断是否在屏幕上来决定是否notify。

  • 由于我们记录的item位置pos,当RecyclerView发生,增删的时候,我们记录的位置有可能会错位,所以我们给adaptert注册一个数据观察器,这样在数据发生变动的时候,可以保证需要更新的item不会产生错位

思路就是写一个Bean里面存储结束时间和是否需要倒计时的boole数值,一个集合存储需要倒计时的下标  然后在Recyclerview添加一个监听添加删除和变化,如果刷新整个列表就先删除所有的下标,如果是添加的话数据先add数据,然后循环所有下标数据>=插入的下标,当前数据下标赋值+=插入的数量,如果是删除先remove数据那就反向遍历,当前下标>=要删除的下标,那就将当前下标-=删除的总数量,否则并且满足>=要删除的第一个下标  的删除  这样就保证了id不会错乱

这时候,在onBindViewHolder判断是否需要倒计时,然后用系统时间减去后台给的结束时间值,如果大于1秒的显示在TexeView上,然后判断下标集合是否包含或者地址值和值是否相同  (详细参考Index的equals)

如果不满足上面的条件就添加进id集合,然后开启一次倒计时更新,循环遍历所有下标拿到recyclerView的viewHolder进行刷新rvAdapter.notifyItemChanged(countDownPosition.index)

 
 

3. 整体思路整理完毕,接下来代码实现

  • Helper类代码实现
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;
        }
    }
}
  • 使用代码样例
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