欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

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

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

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

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

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

背景介绍

需要在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相关参考文章:

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销毁时,别忘了调用countdownadapter的removetimer()方法来取消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:

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

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

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

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

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

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

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

小结&感想

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

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

上一篇:

下一篇: