美文网首页程序员Android进阶之旅Android开发实战总结
Android 列表倒计时的实现(CountDownTimer

Android 列表倒计时的实现(CountDownTimer

作者: 锋Plus | 来源:发表于2017-09-22 20:35 被阅读679次

    实习一段时间了,一直想写点技术总结,但一直没找到合适的主题。刚好,最近版本中我负责的模块遇到了个线程相关问题(之前一直画界面,做点基础功能,有点乏味),列表项倒计时的实现。
    于是乎,我的第一篇android技术文章就诞生了。

    该demo用Kotlin语言实现。
    demo最终效果图.gif

    背景介绍

    需要在ListView的item里实现倒计时,初看还挺简单的,但是真正做的时候也遇到了不少坑。
    网上有不少类似文章,有用对TextView扩展实现的,也有用自带的CountDownTimer实现的,本文就是用CountDownTimer,只不过多了对服务器时间的刷新控制,更贴近项目需求吧。

    刚学了点kotlin,就拿这个来练练手。所以这个demo的源码就用koltin实现了,想了解学习kotlin的也可以来交流下,刚学,代码里可能有些细节语法用的不好。

    要点分析:
    • 倒计时需要根据请求所得服务器时间结束时间确定(所以要一个线程来维持服务器时间的运行,而且还有n个线程来维持item项的倒计时刷新显示)。
    • 既然是多线程,那么线程的控制就要注意

    了解CountDownTimer

    在看代码前,先来了解下android自带的CountDownTimer类用法

        private CountDownTimer timer = new CountDownTimer(30000, 1000) {  
            //根据间隔时间来不断回调此方法,这里是每隔1000ms调用一次
            @Override  
            public void onTick(long millisUntilFinished) {  
                //todo millisUntilFinished为剩余时间,也就是30000 - n*1000
            } 
            
            //结束倒计时调用    
            @Override  
            public void onFinish() {  
                //todo
            }  
    }; 
    
    //开始倒计时
    timer.start();
    
    //取消倒计时(译者:取消后,再次启动会重新开始倒计时)
    timer.cancel();;
    

    这里的入参再解释下new CountDownTimer(30000, 1000)
    第一个参数30000代表倒计时的总时间,单位为ms,这里是30000ms,也就是30s。第二个参数1000就是刷新间隔,也就是回调onTick方法的间隔,单位也是ms,这里就是1s回调一次。

    CountDownTimer相关参考文章:
    http://blog.csdn.net/lilu_leo/article/details/6941724

    OK,基础结束,接下来直接实现代码了。

    代码实现

    先看核心,也就是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)
            //创建倒计时,与控件绑定
            val cdu = MyCountDownTimer(position, time, 1000, tag.tvTime)
            cdu.start()
    
            //[醒目]此处需要map集合将控件和倒计时类关联起来
            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时间调用一次onTick()
         * index参数可去除,在这里只是为了打印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======")
                //设置时间格式
                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 = "倒计时结束"
                //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
        }
    }
    

    这里主要的创建一个线程来保持服务器时间和N个item倒计时的“走”动。

    保持服务器时间没什么好说的,就是Handler配合Runnable的循环调用,注意的是,当activity销毁时,别忘了调用CountDownAdapterremoveTimer()方法来取消handler的回调,防止内存泄漏。

    重点就是item里的倒计时的线程控制,这里参照网上的一个比较好的方法,就是用HashMap<TextView, MyCountDownTimer>()来让MyCountDownTimer和item里的TextView关联起来,也就是每个item对应一个CountDownTimer,当关闭页面时或者刷新list时,可利用cancelAllTimers()方法来清除所有关联,避免内存泄漏。

    以下是ListActivity,伪造一些时间数据

    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 {
                //刷新数据时,重置本地服务器时间
                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计时器,防止关闭页面后线程失控而导致的内存泄漏。但是并没有在打开其他页面时清除,因为如果清除了的话,那么从其他界面返回至此activity时,倒计时已停止。

    当然如果你的需求允许返回界面时重新请求加载数据的,可以在onStop()中,只不过这样体验不好

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

    运行效果

    这里就看下我跑模拟机运行demo打印的Log:

    1

    嗯,本地的服务器时间每秒一次再跑着,没毛病。

    再来看看item里的倒计时Log:

    2.png

    也没毛病,只有显示的那几项再跑,没出现失控线程。

    关闭ListActivity页面后所有线程全销毁。点击item后进入新界面,所有计时线程都在运行,然后返回ListActivity倒计时也是再跑的(模拟机跑demo的时候由于性能问题,长时间可能会出现倒计时不统一,用真机会好很多。)

    OK,最后给出源码地址:
    https://github.com/xjf1128/ListCountDownDemo

    小结&感想

    刚接到这个需求时,感觉肯定不少坑。最终做完再理一理思路,其实也还好。最初的思路正确的话,能少踩点坑。其实就是线程的控制和CountDownTimer的使用,难度也不大。

    我的第一篇技术文章写完了,算上demo的编写,花了2个晚上。希望接下去我能坚持写技术文章总结日常开发中遇到的坑。


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

    相关文章

      网友评论

        本文标题:Android 列表倒计时的实现(CountDownTimer

        本文链接:https://www.haomeiwen.com/subject/efatextx.html