热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

Android列表倒计时的实现的示例代码(CountDownTimer)

实习一段时间了,一直想写点技术总结,但一直没找到合适的主题。刚好,最近版本中我负责的模块遇到了个线程相关问题(之前一直画界面,做点基础功能,

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

于是乎,我的第一篇android技术文章就诞生了。

【醒目】该demo用Kotlin语言实现。


背景介绍

需要在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相关参考文章:https://www.jb51.net/article/119729.htm

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

代码实现

先看核心,也就是CountDownAdapter类,这里就简化UI,每个item只有一个textView来显示倒计时,布局XML就不放了,直接放代码

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

  private val timeMap = HashMap()
  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>? = timeMap.entries
    var it: Iterator<*>&#63; = s!!.iterator()
    while (it!!.hasNext()) {
      try {
        val pairs = it.next() as MutableMap.MutableEntry<*, *>
        var cdt: MyCountDownTimer&#63; = pairs.value as MyCountDownTimer
        cdt!!.cancel()
        cdt = null
      } catch (e: Exception) {
      }

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

  fun removeTimer(){
    handler&#63;.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(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 

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

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

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

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

class ListActivity : AppCompatActivity() {

  private val list: ArrayList = ArrayList()
  private var countDownAdapter: CountDownAdapter&#63; = null

  override fun onCreate(savedInstanceState: Bundle&#63;) {
    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&#63;.cancelAllTimers()
    countDownAdapter&#63;.removeTimer()
    super.onDestroy()
  }
}

这里在销毁activity前,清除了服务器时间线程和所有item计时器,防止关闭页面后线程失控而导致的内存泄漏。但是并没有在打开其他页面时清除,因为如果清除了的话,那么从其他界面返回至此activity时,倒计时已停止。

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

countDownAdapter&#63;.cancelAllTimers()
countDownAdapter&#63;.removeTimer()

运行效果

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


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

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


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

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

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

小结&感想

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


推荐阅读
  • 在IIS上运行的WebApi应用程序在开发环境中能够正常进行文件的读写操作。然而,在尝试通过FTP访问实时服务器上的文件列表时,遇到了无法显示的问题,尽管服务器配置与开发环境相同。这可能涉及权限设置、FTP服务配置或网络连接等方面的问题。 ... [详细]
  • 探索偶数次幂二项式系数的求和方法及其数学意义 ... [详细]
  • Python 实战:异步爬虫(协程技术)与分布式爬虫(多进程应用)深入解析
    本文将深入探讨 Python 异步爬虫和分布式爬虫的技术细节,重点介绍协程技术和多进程应用在爬虫开发中的实际应用。通过对比多进程和协程的工作原理,帮助读者理解两者在性能和资源利用上的差异,从而在实际项目中做出更合适的选择。文章还将结合具体案例,展示如何高效地实现异步和分布式爬虫,以提升数据抓取的效率和稳定性。 ... [详细]
  • 在尝试为 Unity 编译一个简单的 Java 库时,运行 `ant jar` 命令后遇到了 Java I/O 异常。具体错误信息为“无法启动程序 ${aAPT},错误代码 2”,这通常表示指定的文件或目录不存在。此问题可能是由于环境配置不正确或路径设置有误导致的。建议检查相关路径和环境变量,确保所有依赖项都已正确安装和配置。 ... [详细]
  • 技术分享:深入解析GestureDetector手势识别机制
    技术分享:深入解析GestureDetector手势识别机制 ... [详细]
  • 开源实习机会 | Compiler SIG 正式发布实习任务,诚邀您加入申请!
    对编译技术充满兴趣却苦于无从入手?当前疫情形势下,外出实习变得困难重重?现在,Compiler SIG 正式发布了一系列实习任务,为有志之士提供了宝贵的机会。无论你是初学者还是有一定基础的学生,都能在这里找到适合自己的实践项目。我们诚挚邀请您的加入,共同探索编译技术的无限可能! ... [详细]
  • 推荐:利用Dapper.SimpleCRUD扩展Dapper功能以简化CRUD操作
    Dapper作为广受欢迎的ORM框架之一,虽然灵活性极高,但在处理基本的CRUD操作时仍需手动编写SQL语句,这无疑增加了开发工作量。为了解决这一问题,Dapper.SimpleCRUD扩展库应运而生。该扩展库通过提供简洁的方法,显著简化了数据访问层的开发流程,使开发者能够更加高效地进行读取、插入、更新和删除操作。此外,Dapper.SimpleCRUD还支持事务管理和批量操作,进一步提升了开发效率和代码可维护性。 ... [详细]
  • Syncnavigator激活工具及破解方法详解
    本文详细介绍了Syncnavigator激活工具的使用方法及其破解技巧。用户可以通过访问官方网站www.SyncNavigator.CN获取相关资源,并通过客服QQ 1793040获得技术支持和帮助。此外,文章还提供了详细的步骤说明和常见问题解答,以确保用户能够顺利激活并使用Syncnavigator软件。 ... [详细]
  • 《我的世界》Java版与Windows 10版(基岩版)有何不同?
    《我的世界》Java版与Windows 10版(基岩版)有何不同? ... [详细]
  • Golomb 编码是一种高效的变长编码技术,专门用于整数的压缩。该方法通过预定义的参数 \( M \) 将输入整数分解为商 \( q \) 和余数 \( r \) 两部分。具体而言,输入整数除以 \( M \) 得到商 \( q \) 和余数 \( r \),其中商 \( q \) 采用一元编码表示,而余数 \( r \) 则使用二进制编码。这种编码方式在数据压缩和信息传输中具有显著的优势,特别是在处理具有特定概率分布的数据时表现出色。 ... [详细]
  • Redis哈希数据结构入门指南
    Redis的哈希数据结构与Java中的HashMap类似,采用数组加链表的方式实现。数组用于存储哈希值的位置,而链表则用于处理哈希冲突的情况。此外,Redis的哈希数据结构还支持高效的字段操作和内存优化,适用于多种应用场景,如缓存和会话管理。 ... [详细]
  • 本文深入解析了Spring Cloud路由网关Zuul的核心功能及其典型应用场景。通过对方志朋老师教材的学习和实践,详细探讨了Zuul在微服务架构中的重要作用,包括请求路由、过滤器链管理以及服务动态扩展等关键特性。同时,结合实际案例,展示了Zuul在高并发和复杂业务场景下的应用优势,为读者提供了全面的技术参考。 ... [详细]
  • 深入解析 Linux 内核中的 signal.c 文件:从泛读到精析
    本文将对 Linux 内核中的 `signal.c` 文件进行深入解析,从泛读到精析,全面解读其核心功能与实现机制。通过详细分析代码,探讨信号处理的内部运作,帮助读者更好地理解 Linux 内核的信号处理机制。相关学习资料已整理在 GitHub 仓库:https://github.com/GreyZhang/little_bits_of_linux。 ... [详细]
  • 利用树莓派畅享落网电台音乐体验
    最近重新拾起了闲置已久的树莓派,这台小巧的开发板已经沉寂了半年多。上个月闲暇时间较多,我决定将其重新启用。恰逢落网电台进行了改版,回忆起之前在树莓派论坛上看到有人用它来播放豆瓣音乐,便萌生了同样的想法。通过一番调试,终于实现了在树莓派上流畅播放落网电台音乐的功能,带来了全新的音乐享受体验。 ... [详细]
  • 2016-2017学年《网络安全实战》第三次作业
    2016-2017学年《网络安全实战》第三次作业总结了教材中关于网络信息收集技术的内容。本章主要探讨了网络踩点、网络扫描和网络查点三个关键步骤。其中,网络踩点旨在通过公开渠道收集目标信息,为后续的安全测试奠定基础,而不涉及实际的入侵行为。 ... [详细]
author-avatar
家有吃货_魏ranran
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有