RecyclerView倒計時解決方案

在開中難免遇到RecyclerView中帶有倒計時的場景,有時候不止一個屏镊。那么如何實現(xiàn)一個帶有倒計時的RecyclerView便是本文討論的課題

1.在RecyclerView中實現(xiàn)高質(zhì)量倒計時需要解決哪些問題。

  • 當列表中有多個倒計時如何處理符隙,開啟多個還是統(tǒng)一管理跳纳?如果每個item都開啟倒計時是否造成了資源浪費?不在展示區(qū)的倒計時又該如何取消卡儒?不在屏幕上的倒計時如何保證時間的準確性?只使用一個倒計時如何通知所有需要更新的item俐巴?如何保證不在屏幕上而不被通知的item的時間的準確性骨望?能否在退出后臺或者跳轉(zhuǎn)到其他頁面的時候暫停減少資源的浪費?

2.帶著這些問題著手設(shè)計我們的倒計時方案欣舵。

  • 首先從大的邏輯上我們優(yōu)選單任務(wù)倒計時方案擎鸠。

  • 計時器的選擇,考慮到可能頻繁啟停缘圈,我們選擇Rxjava提供的interval方法劣光。內(nèi)部基于線程池管理,避免使用Timer類會造成線程開關(guān)的成本過高糟把。

  • 敲黑板啦重點來了 計時器只負責定時通知相關(guān)item更新UI绢涡,不記錄item剩余時間。由于前端獲取的時間可能有時差或者被用戶修改過是不可信的糊饱,網(wǎng)關(guān)下發(fā)的bean類中是剩余時間如2000秒垂寥。需要在bean類中手動增加一個變量 倒計時結(jié)束時間點 我們命名為endCountTime,在json映射為bean類的時候我們獲取當前系統(tǒng)時間 讓它加上 倒計時剩余時間 就是所需要的 endCountTime另锋。 同時呢由于系統(tǒng)時間的不可信,也就是System.getCurrentMillions是不可信的狭归,所以我們選擇系統(tǒng)的開機時鐘 SystemClock.elapsedRealtime()夭坪。這是我們能夠得以實現(xiàn)隨時暫停再開啟倒計時 倒計時時間依然能夠保證正確的關(guān)鍵因素。

  • 使用一個List來管理需要通知的Item位置pos过椎,我們命名為countDownPositions室梅。在bindViewHolder的時候我們將需要更新的itemPosition添加到countDownPositions中,當計時器任務(wù)通知時來遍歷countDownPositions疚宇,然后進行notityitemchanged亡鼠。在這我們會遇到兩個選擇,不在屏幕上的item敷待,notifyitemchange的時候會不會造成浪費间涵。另一種選擇的是在notifyitemchange 的時候判斷是否在屏幕上。 判斷是否在屏幕上和直接notify成本哪個更高榜揖,雖然有查閱相關(guān)資料勾哩,也做過一些測試抗蠢。并沒有分辨出來哪個更好,如有了解的還請指教思劳,在本次實踐中 我是判斷是否在屏幕上來決定是否notify迅矛。

  • 由于我們記錄的item位置pos,當RecyclerView發(fā)生潜叛,增刪的時候秽褒,我們記錄的位置有可能會錯位,所以我們給adaptert注冊一個數(shù)據(jù)觀察器威兜,這樣在數(shù)據(jù)發(fā)生變動的時候销斟,可以保證需要更新的item不會產(chǎn)生錯位

3. 整體思路整理完畢,接下來代碼實現(xiàn)

  • Helper類代碼實現(xiàn)
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", "移除所有標記位置");
    }

    /**
     * 手動調(diào)用開始定時更新
     */
    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()));

        }
    }

    /**
     * 手動調(diào)用停止定時更新
     */
    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 已經(jīng)處理了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);
            }
        }
    }


}

結(jié)尾奉上demo源碼 歡迎圍觀多提意見

https://gitee.com/ehualu_chuan/Test.git

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牡属,一起剝皮案震驚了整個濱河市票堵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逮栅,老刑警劉巖悴势,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異措伐,居然都是意外死亡特纤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進店門侥加,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捧存,“玉大人,你說我怎么就攤上這事担败∥粞ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵提前,是天一觀的道長吗货。 經(jīng)常有香客問我,道長狈网,這世上最難降的妖魔是什么宙搬? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮拓哺,結(jié)果婚禮上勇垛,老公的妹妹穿的比我還像新娘。我一直安慰自己士鸥,他們只是感情好闲孤,可當我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著础淤,像睡著了一般崭放。 火紅的嫁衣襯著肌膚如雪哨苛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天币砂,我揣著相機與錄音建峭,去河邊找鬼。 笑死决摧,一個胖子當著我的面吹牛亿蒸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播掌桩,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼边锁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了波岛?” 一聲冷哼從身側(cè)響起茅坛,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎则拷,沒想到半個月后贡蓖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡煌茬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年斥铺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坛善。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡晾蜘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出眠屎,到底是詐尸還是另有隱情剔交,我是刑警寧澤,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布改衩,位于F島的核電站省容,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏燎字。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一阿宅、第九天 我趴在偏房一處隱蔽的房頂上張望候衍。 院中可真熱鬧,春花似錦洒放、人聲如沸蛉鹿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妖异。三九已至惋戏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間他膳,已是汗流浹背响逢。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留棕孙,地道東北人舔亭。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像蟀俊,于是被迫代替她去往敵國和親钦铺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,781評論 2 361

推薦閱讀更多精彩內(nèi)容