Android 列表倒計時的實現(xiàn)(CountDownTimer)

實習一段時間了,一直想寫點技術(shù)總結(jié)德绿,但一直沒找到合適的主題荷荤。剛好退渗,最近版本中我負責的模塊遇到了個線程相關(guān)問題(之前一直畫界面,做點基礎(chǔ)功能蕴纳,有點乏味)会油,列表項倒計時的實現(xiàn)。
于是乎古毛,我的第一篇android技術(shù)文章就誕生了翻翩。

該demo用Kotlin語言實現(xiàn)。
demo最終效果圖.gif

背景介紹

需要在ListView的item里實現(xiàn)倒計時稻薇,初看還挺簡單的嫂冻,但是真正做的時候也遇到了不少坑。
網(wǎng)上有不少類似文章塞椎,有用對TextView擴展實現(xiàn)的桨仿,也有用自帶的CountDownTimer實現(xiàn)的,本文就是用CountDownTimer案狠,只不過多了對服務器時間的刷新控制服傍,更貼近項目需求吧。

剛學了點kotlin莺戒,就拿這個來練練手伴嗡。所以這個demo的源碼就用koltin實現(xiàn)了,想了解學習kotlin的也可以來交流下从铲,剛學瘪校,代碼里可能有些細節(jié)語法用的不好。

要點分析:
  • 倒計時需要根據(jù)請求所得服務器時間結(jié)束時間確定(所以要一個線程來維持服務器時間的運行名段,而且還有n個線程來維持item項的倒計時刷新顯示)阱扬。
  • 既然是多線程,那么線程的控制就要注意

了解CountDownTimer

在看代碼前伸辟,先來了解下android自帶的CountDownTimer類用法

    private CountDownTimer timer = new CountDownTimer(30000, 1000) {  
        //根據(jù)間隔時間來不斷回調(diào)此方法麻惶,這里是每隔1000ms調(diào)用一次
        @Override  
        public void onTick(long millisUntilFinished) {  
            //todo millisUntilFinished為剩余時間,也就是30000 - n*1000
        } 
        
        //結(jié)束倒計時調(diào)用    
        @Override  
        public void onFinish() {  
            //todo
        }  
}; 

//開始倒計時
timer.start();

//取消倒計時(譯者:取消后信夫,再次啟動會重新開始倒計時)
timer.cancel();;

這里的入?yún)⒃俳忉屜?code>new CountDownTimer(30000, 1000)窃蹋。
第一個參數(shù)30000代表倒計時的總時間,單位為ms静稻,這里是30000ms警没,也就是30s。第二個參數(shù)1000就是刷新間隔振湾,也就是回調(diào)onTick方法的間隔杀迹,單位也是ms,這里就是1s回調(diào)一次押搪。

CountDownTimer相關(guān)參考文章:
http://blog.csdn.net/lilu_leo/article/details/6941724

OK树酪,基礎(chǔ)結(jié)束浅碾,接下來直接實現(xiàn)代碼了。

代碼實現(xiàn)

先看核心续语,也就是CountDownAdapter類垂谢,這里就簡化UI,每個item只有一個textView來顯示倒計時疮茄,布局XML就不放了埂陆,直接放代碼

class CountDownAdapter(private var activity: ListActivity, private var data: ArrayList<Date>, private var systemDate: Date) : BaseAdapter() {

    private val timeMap = HashMap<TextView, MyCountDownTimer>()
    private val handler = Handler()
    private val runnable = object : Runnable {
        override fun run() {
            if (systemDate != null) {
                systemDate.time = systemDate.time + 1000
                Log.i("xujf", "服務器時間線程===" + systemDate + "==for==" + this)
                handler.postDelayed(this, 1000)
            }
        }
    }

    init {
        handler.postDelayed(runnable, 1000)
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        var v: View
        var tag: ViewHolder
        var vo = data[position]
        if (null == convertView) {
            v = activity.layoutInflater.inflate(R.layout.item_count_down, null)
            tag = ViewHolder(v)

            v.tag = tag
        } else {
            v = convertView
            tag = v.tag as ViewHolder
        }

        //獲取控件對應的倒計時控件是否存在, 存在就取消, 解決時間重疊問題
        var tc: MyCountDownTimer? = timeMap[tag.tvTime]
        if (tc != null) {
            tc.cancel()
            tc = null
        }

        //計算時間差
        val time = getDistanceTimeLong(systemDate, vo)
        //創(chuàng)建倒計時,與控件綁定
        val cdu = MyCountDownTimer(position, time, 1000, tag.tvTime)
        cdu.start()

        //[醒目]此處需要map集合將控件和倒計時類關(guān)聯(lián)起來
        timeMap.put(tag.tvTime, cdu)

        return v
    }

    /**
     * 退出時清空所有item的計時器
     */
    fun cancelAllTimers() {
        var s: Set<MutableMap.MutableEntry<TextView, MyCountDownTimer>>? = timeMap.entries
        var it: Iterator<*>? = s!!.iterator()
        while (it!!.hasNext()) {
            try {
                val pairs = it.next() as MutableMap.MutableEntry<*, *>
                var cdt: MyCountDownTimer? = pairs.value as MyCountDownTimer
                cdt!!.cancel()
                cdt = null
            } catch (e: Exception) {
            }

        }
        it = null
        s = null
        timeMap.clear()
    }

    fun removeTimer(){
        handler?.removeCallbacks(runnable)
    }

    fun reSetTimer(date: Date) {
        removeTimer()
        systemDate = date
        handler.postDelayed(runnable, 1000)
    }

    override fun getItem(position: Int): Any = data[position]

    override fun getItemId(position: Int): Long = 0L

    override fun getCount(): Int = data.size

    internal inner class ViewHolder(view: View) {
        var tvTime = view.findViewById<TextView>(R.id.tv_time)
    }

    /**
     * 倒計時類娃豹,每間隔countDownInterval時間調(diào)用一次onTick()
     * index參數(shù)可去除,在這里只是為了打印log查看倒計時是否運行
     */
    private inner class MyCountDownTimer(internal var index: Int, millisInFuture: Long,
                                         internal var countDownInterval: Long, internal var tv: TextView
    ) : CountDownTimer(millisInFuture, countDownInterval) {

        override fun onTick(millisUntilFinished: Long) {
            //millisUntilFinished為剩余時間長
            Log.i("xujf", "====倒計時還活著===第 $index 項item======")
            //設(shè)置時間格式
            val m = millisUntilFinished / countDownInterval
            val hour = m / (60 * 60)
            val minute = (m / 60) % 60
            val s = m % 60
            tv.text = "倒計時  (${hour}小時${minute}分${s}秒)"
        }

        override fun onFinish() {
            tv.text = "倒計時結(jié)束"
            //todo 可以做一些刷新動作
        }
    }

    /**
     * 時間工具购裙,返回間隔時間長
     */
    fun getDistanceTimeLong(one: Date, two: Date): Long {
        var diff = 0L
        try {
            val time1 = one.time
            val time2 = two.time
            if (time1 < time2) {
                diff = time2 - time1
            } else {
                diff = time1 - time2
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }

        return diff
    }
}

這里主要的創(chuàng)建一個線程來保持服務器時間和N個item倒計時的“走”動懂版。

保持服務器時間沒什么好說的,就是Handler配合Runnable的循環(huán)調(diào)用躏率,注意的是躯畴,當activity銷毀時,別忘了調(diào)用CountDownAdapterremoveTimer()方法來取消handler的回調(diào)薇芝,防止內(nèi)存泄漏蓬抄。

重點就是item里的倒計時的線程控制,這里參照網(wǎng)上的一個比較好的方法夯到,就是用HashMap<TextView, MyCountDownTimer>()來讓MyCountDownTimer和item里的TextView關(guān)聯(lián)起來嚷缭,也就是每個item對應一個CountDownTimer,當關(guān)閉頁面時或者刷新list時耍贾,可利用cancelAllTimers()方法來清除所有關(guān)聯(lián)阅爽,避免內(nèi)存泄漏。

以下是ListActivity荐开,偽造一些時間數(shù)據(jù)

class ListActivity : AppCompatActivity() {

    private val list: ArrayList<Date> = ArrayList()
    private var countDownAdapter: CountDownAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_list)
        getDate()
        setDate()
    }

    private fun setDate() {
        if (countDownAdapter == null) {
            countDownAdapter = CountDownAdapter(this, list, Date())
            lv_count_down.adapter = countDownAdapter
            lv_count_down.onItemClickListener = AdapterView.OnItemClickListener { adapterView, view, i, l ->
                val intent = Intent(ListActivity@this, Main2Activity::class.java)
                startActivity(intent)
            }
        } else {
            //刷新數(shù)據(jù)時付翁,重置本地服務器時間
            countDownAdapter!!.reSetTimer(Date())
            countDownAdapter!!.notifyDataSetChanged()
        }
    }

    private fun getDate() {
        for (i in 1..20) {
            var date = Date(Date().time + i * 1000 * 60 * 30)
            list.add(date)
        }

    }

    override fun onDestroy() {
        countDownAdapter?.cancelAllTimers()
        countDownAdapter?.removeTimer()
        super.onDestroy()
    }
}

這里在銷毀activity前,清除了服務器時間線程和所有item計時器晃听,防止關(guān)閉頁面后線程失控而導致的內(nèi)存泄漏百侧。但是并沒有在打開其他頁面時清除,因為如果清除了的話能扒,那么從其他界面返回至此activity時佣渴,倒計時已停止。

當然如果你的需求允許返回界面時重新請求加載數(shù)據(jù)的赫粥,可以在onStop()中观话,只不過這樣體驗不好

countDownAdapter?.cancelAllTimers()
countDownAdapter?.removeTimer()

運行效果

這里就看下我跑模擬機運行demo打印的Log:

1

嗯,本地的服務器時間每秒一次再跑著越平,沒毛病频蛔。

再來看看item里的倒計時Log:

2.png

也沒毛病灵迫,只有顯示的那幾項再跑,沒出現(xiàn)失控線程晦溪。

關(guān)閉ListActivity頁面后所有線程全銷毀瀑粥。點擊item后進入新界面,所有計時線程都在運行三圆,然后返回ListActivity倒計時也是再跑的(模擬機跑demo的時候由于性能問題狞换,長時間可能會出現(xiàn)倒計時不統(tǒng)一,用真機會好很多舟肉。)

OK修噪,最后給出源碼地址:
https://github.com/xjf1128/ListCountDownDemo

小結(jié)&感想

剛接到這個需求時,感覺肯定不少坑路媚。最終做完再理一理思路黄琼,其實也還好。最初的思路正確的話整慎,能少踩點坑脏款。其實就是線程的控制和CountDownTimer的使用,難度也不大裤园。

我的第一篇技術(shù)文章寫完了撤师,算上demo的編寫,花了2個晚上拧揽。希望接下去我能堅持寫技術(shù)文章總結(jié)日常開發(fā)中遇到的坑剃盾。


有很多不足,望請各位大牛指出

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末强法,一起剝皮案震驚了整個濱河市万俗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌饮怯,老刑警劉巖闰歪,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蓖墅,居然都是意外死亡库倘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門论矾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來教翩,“玉大人,你說我怎么就攤上這事贪壳”ヒ冢” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長彪笼。 經(jīng)常有香客問我钻注,道長,這世上最難降的妖魔是什么配猫? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任幅恋,我火速辦了婚禮,結(jié)果婚禮上泵肄,老公的妹妹穿的比我還像新娘捆交。我一直安慰自己,他們只是感情好腐巢,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布品追。 她就那樣靜靜地躺著,像睡著了一般冯丙。 火紅的嫁衣襯著肌膚如雪诵盼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天银还,我揣著相機與錄音,去河邊找鬼洁墙。 笑死蛹疯,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的热监。 我是一名探鬼主播捺弦,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼孝扛!你這毒婦竟也來了列吼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤苦始,失蹤者是張志新(化名)和其女友劉穎寞钥,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陌选,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡理郑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了咨油。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片您炉。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖役电,靈堂內(nèi)的尸體忽然破棺而出赚爵,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布冀膝,位于F島的核電站唁奢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏畸写。R本人自食惡果不足惜驮瞧,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枯芬。 院中可真熱鬧论笔,春花似錦、人聲如沸千所。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淫痰。三九已至最楷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間待错,已是汗流浹背籽孙。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留火俄,地道東北人犯建。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像瓜客,于是被迫代替她去往敵國和親适瓦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,304評論 25 707
  • 809字|4分鐘 這一回看的我費盡了腦汁!紅樓夢雖然是白話小說疯攒,二十二回用了N多...
    寒晗Tylor閱讀 843評論 1 0
  • 今天學了跨域嗦随,迫不及待想跟大家分享!不妥之處希望大家指正敬尺。首先來明確一下“跨域”這個概念称杨。跨域指的是筷转,到外域去取數(shù)...
    z0nka1閱讀 261評論 0 0
  • 很久沒做噩夢了姑原,興許是年齡大了,膽子也大了呜舒《а矗可是,就在前幾天的一個夜里,我卻被鬼壓床了唤殴,折騰了好久般婆,才醒過來。 初...
    青石問心閱讀 586評論 17 9
  • 上帝是公平的啤咽,但世界是不公平的,除了時間渠脉∮钫可能大學畢業(yè)初入社會的時候同樣是一個起跑線,但因為時間規(guī)劃上芋膘,差異也會越...
    飄渺_d65f閱讀 334評論 0 0