【Android】自定義倒計時控件 解決複用、線程安全、列表不展示時的回調等問題

前言

又用到了倒計時,好尷尬,不知道大家每次遇到倒計時後的時候都是怎麼做的。我想吧,這東西要寫真的很簡單,起一個線程不停的–就搞定的事情。但是如果我問這種情況你怎麼做?請聽題:如果我一個頁面有5個tab(甚至更多),每個tab都是一個RecyclerView,然後每個item裏面都有倒計時,只要有一個倒計時結束,就需要回調,刷新整個RecyclerView。要怎麼處理好呢?
我們簡單分析一下:

分析

1.如果是我們把倒計時封裝成一個view,那麼由於RecyclerView的重用機制,必定控件會重用,如果一個控件一個線程,那麼同時那麼多tab,那麼多item,會不會造成線程的不可控?
2.由於重用機制,也就是說每次滾動RecyclerView都會去綁定倒計時數據,這個時候怎麼處理呢?3.RecyclerView的重用機制,倒是列表後面的item其實並沒有被創建,也就是說比如列表的第10個item的倒計時結束了,怎麼通知回調呢?
對吧,簡單分析一下就發現了好多問題,主要問題就是用的地方太多了!,然後無圖無真相,線上效果。
這裏寫圖片描述

解決思路

還是通過自定義View的形式去管理對應的倒計時

爲了使用方便,我覺得還是通過自定義View的方式,讓對應控件去管理對應功能爲好,也方便使用,直接通過find的形式綁定數據就可以了。所以本次倒計時,我是還是喜歡通過自定義View。
那麼還有一個很重要的東西就是這個時間的概念,一個倒計時的時間應該包括小時、分鐘、秒鐘。有了這3個屬性後,那麼就是獲得顯示時間的文本的方法,所以我們先建一個基類,如下:

public abstract class BaseCountdownTime {
    protected int hour;
    protected int minute;
    protected int second;
    abstract String getTimeText();
}

然後創建CountdownTime,去集成我們的時間基類。
然後回憶一下這個問題,我們RecyclerView如果一頁只能顯示3個item,那麼當第10個item的倒計時結束了,要怎麼回調呢?
然後我想每次在綁定數據的時候去綁定監聽肯定是不現實的了,所以,我就動了一點歪腦筋。我在時間類上面添加了一個靜態監聽

/**
     * 這個真的不是我想這樣做,沒辦法,item不展示的時候也要回調,那就索性簡單粗暴點
     * */
    public static OnTimeCountdownOverListener onTimeCountdownOverListener;

然後到時候設置監聽就這樣玩:

CountdownTime.onTimeCountdownOverListener = new CountdownTime.OnTimeCountdownOverListener() {
            @Override
            public void onTimeCountdownOver() ->{
                Log.d("Blin QueueMangerOver","回調了");
            }
        };

雖然有點奇怪啊,不過這是我能想到的確簡單好用的辦法了,也不是各種麻煩了。具體完整的CountdownTime時間類,到時候在後面貼出。

解決線程不可控的問題

我們先看看爲什麼要說這個問題,簡單點,如果每個View裏面都有一個單獨的計時器管理自己的倒計時,的確可以簡單的實現自己管理自己的倒計時。但是一旦重用,或者一旦數量急劇增加後,那麼線程將會變得不可控。所以這個問題肯定是一個關鍵的地方。還有就是RecyclerView重用的時候,第十個不顯示的item怎麼來倒計時呢,這個時候item重用,計時器其實去管理另一個時間了。所以,最終我我決定用一個隊列來管理。
設計思路也是一樣,先看需要哪些功能,設計基類,然後再去子類實現它。

public abstract class BaseCountdownTimeQueueManager {
    protected ArrayList<CountdownTime> timeQueue;
    protected Timer timeTimer;
    protected TimerTask timeTask;
    public void removeTime(CountdownTime time){
        if(timeQueue != null){
            for(int i = 0;i<timeQueue.size();i++){
                if(TextUtils.equals(time.getId(),timeQueue.get(i).getId())){
                    timeQueue.remove(i);
                }
            }
        }
    }
    abstract void countdownTimeQueue();
    abstract void initCountdownTimeQueueManager();
    public abstract CountdownTime addTime(int time,String id,CountdownTime.OnCountdownTimeListener listener);
}

看一下,上面我有一個timeQueue,有一個計時器,然後就是一些必要的,去實現的方法。removeTime()主要是刪掉隊列裏面倒計時已經結束的對象,然後countdownTimeQueue()是每次刷新要做的事情。addTime()就是添加倒計時時間對象的方法了。
然後看看管理類:

public class CountdownTimeQueueManager extends BaseCountdownTimeQueueManager{
    private static CountdownTimeQueueManager manager;
    private CountdownTimeQueueManager(){}
    public static CountdownTimeQueueManager getInstance(){
        if (manager == null){
            manager = new CountdownTimeQueueManager();
            manager.initCountdownTimeQueueManager();
        }
        return manager;
    }
    @Override
    void initCountdownTimeQueueManager() {
        timeQueue = new ArrayList<>();
        timeTimer = new Timer(true);
        timeTask = new TimerTask() {
            @Override
            public void run() {
                countdownTimeQueue();
            }
        };
        timeTimer.schedule(timeTask,1000,1000);
    }

    @Override
    public CountdownTime addTime(int time, String id, CountdownTime.OnCountdownTimeListener listener) {
        CountdownTime countdownTime;
        if(timeQueue.size() >0)
            for(int i =0;i<timeQueue.size();i++){
                countdownTime = timeQueue.get(i);
                if(TextUtils.equals(countdownTime.getId(),id)){
                    countdownTime.setListener(listener);
                    return countdownTime;
                }
            }
        countdownTime = new CountdownTime(time,id,listener);
        timeQueue.add(countdownTime);
        return countdownTime;
    }

    @Override
    synchronized void countdownTimeQueue() {
        if(timeQueue != null&&timeQueue.size()>0){
            for(int i =0;i<timeQueue.size();i++){
                if(timeQueue.get(i).countdown())
                    i --;
            }
        }
    }
}

肯定要是一個單列的,對吧,然後呢,通過這個單列來管理所有的倒計時時間,並且只啓動一個計時器來倒計時。

倒計時控件的實現

這個倒計時控件就是簡單的顯示一個時間的概念,所以我繼承TextView

/**
     * 多了一個id參數,實際應用中可以是訂單id、流水id之類,可以保證唯一性即可
     * */
    public void setCountdownTime(int time,String id){
        nowId = id;
        if(time <= 0){
            if(countdownTime != null)
                countdownTime.setSeconds(0);
        }else{
            countdownTime = manager.addTime(time,id,this);
        }
        postInvalidate();
    }

這個就是我們找到view之後唯一要做的事情了,其實都交給view自己去處理。我添加了一個id,主要是爲了唯一定位到某個倒計時,因爲RecyclerView的複用的確帶來挺大的影響的,每次滑動都去調用setCountdownTime(),的確是一件很麻煩的事情。每次調用的時候,都會把自己傳進去,其實是爲了回調,通知繪製而已啦。結合我們上面的addTime();應該就能看懂了。

/**
     * 當前控件綁定的倒計時實踐對象id,由於重用,RecyclerView滾動的時候,
     * 會複用view,導致裏面顯示的時間其實是不一樣的
     * */
    private String nowId;

這邊就是由於重用的問題,在RecyclerView裏面可能一個View在不同的時刻需要顯示很多個不同的倒計時。
最終我們的CountdownView還需要去實現implements CountdownTime.OnCountdownTimeListener
爲的就是我們的管理類在倒計時的時候通知View去刷新UI。

最後就是把所有的東西整合,封裝,調試。

然後怎麼用呢?我們在,貌似就是非常簡單了,找到View以後直接綁定數據就可以。不過有一個小點要注意,我們從服務端返回的報文裏面item的倒計時是不一樣的,這個時候滾動RecyclerView的時候,會重新綁定數據,但是要知道那個時間是上次我們請求服務端的時間,所以需要計算一下請求回來的時間和綁定時間的時間差,減去就可以。我是通過時間戳來實現的,還是4個字,簡單粗暴!

總結

今天帶來的控件,應該說主要的不是說它帶來的功能,而是說在開發過程中,我們去實現一個功能時候的思路,我們需要思考的不僅僅是需求帶來的功能,更多的還是要思考代碼這種死板的東西,在配合我們實際大腦思考後會產生什麼問題?以及我們的一種開發思路,剛上手的朋友可能就是直接上手啊,想到什麼寫什麼,最終如果成功寫出來了,那沒問題,搞定。那要是最後發現這樣實現是不可行的,然後就悲催了。。。所以我還是想說,當我們在實現某個東西之前,還是要多思考,把我們需要的,不需要的都想清楚,然後再去一一實現。(是不是感覺我在說廢話……好吧,無視吧,哈哈哈,快清明瞭,大夥不要宅在家裏哦,多出去走走!喂,說你呢,別忘王者農藥了!)

啊哦,別忘了,乾貨在這裏!https://github.com/Blincheng/CountdownDemo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章