實習一段時間了,一直想寫點技術(shù)總結(jié)德绿,但一直沒找到合適的主題荷荤。剛好退渗,最近版本中我負責的模塊遇到了個線程相關(guān)問題(之前一直畫界面,做點基礎(chǔ)功能蕴纳,有點乏味)会油,列表項倒計時的實現(xiàn)。
于是乎古毛,我的第一篇android技術(shù)文章就誕生了翻翩。
該demo用Kotlin語言實現(xiàn)。
背景介紹
需要在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)用CountDownAdapter
的removeTimer()
方法來取消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:
嗯,本地的服務器時間每秒一次再跑著越平,沒毛病频蛔。
再來看看item里的倒計時Log:
也沒毛病灵迫,只有顯示的那幾項再跑,沒出現(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ā)中遇到的坑剃盾。
有很多不足,望請各位大牛指出