第二十二个项目——使用TimerTask及Handler实现秒表计时器

实验目的

  • 掌握TimerTimerTask实现定时任务;

  • 掌握HandlerLooper进行线程间消息通信的方式;

实验要求

  • 使用TimerTimerTaskHandler实现秒表计时器;

  • 使用HandlerLooper进行线程间通信;

实验内容

本次实验需要实现如图1. 秒表计时器App运行效果所示的秒表计时器App。 App主界面顶部为两个计时时间,分别为总计时时间和当前间隔;界面中部为所有间隔(计次、tick)计时时间列表; 底部包含三个操作按钮,分别为复位、开始(暂停)、间隔计时。

App启动后,用户点击开始按钮开始秒表计时,此时用户可点击底部最右侧的间隔计时按钮新增一次间隔计时记录,并且 App顶部的当前间隔计时时间清零;当用户点击暂停按钮时,秒表计时暂停,用户可点击底部最左侧的复位按钮清除所有 间隔计时记录,并复位秒表计时器。

图1. 秒表计时器App运行效果

要实现本实验App所要求的的功能,需要使用Timer计时器的schedule()方法定时运行某一TimerTask(计时器任务), 在TimerTask中通过HandlerUI主线程发送消息,并由UI主线程处理消息更新显示计时时间及当前间隔的UI视图元素。

步骤一,创建新工程

打开Android Studio,新建名为GGTimer的工程。

步骤二,设置activity_main.xml布局

在本实验中,秒表计时器App只有一个Activity,将其命名为MainActivity,其对应的布局文件名为activity_main.xml。 根据App运行效果图1. 秒表计时器App运行效果所示,顶部为两个TextView用于表示当前总计时时间以及当前间隔时间;中部由ListView用于表示所有间隔(计次、tick)计时时间列表;底部为三个操作按钮,具体布局代码详如下所示。

activity_main.xml所需使用到的stings.xmlcolors.xml分别如下所示。

strings.xml文件:

colors.xml文件:

步骤三,定义TimerLogAdapter及TimerLog类

1. 定义TimerLog类

首先看TimerLog类,用于记录每一次用户按下【间隔计时】按钮时,间隔计时时间以及总计时时间戳。 这类记录用户每次按下时均需要记录,因此TimerLog类包含了两个成员变量(其单位均为毫秒):

  • Long tick,表示秒表计时器开始工作以来的总计时时间戳;

  • Long interval,表示用户按下【间隔计时】后产生间隔计时周期时间戳;

TimerLog还包含了一个Integer类型的变量number用于记录其序号。此外,TimerLog类的 toString()方法进行了重写,调用了TimeUtils类的intervalToString()方法将Long类型的时间戳转换为字符串类型。

TimerLog类定义:

TimeUtils类定义:

2. 定义list_item.xml布局

list_item.xml布局用于ListView组件中显示每一组间隔计时时间记录信息,根据App运行效果图所示,需要显示的信息包括当前记录序号、当前记录间隔时间、以及当前记录对应的总时间戳等。其布局包含视图元素具体代码如下所示。

3. 定义TimerLogAdapter类

TimerAdapter用于ListView进行数据渲染的适配器,其继承于ArrayAdapter,使用方式与第七个项目中的NewsAdapter类似,这里不再进行赘述,具体代码如下所示。

步骤四,修改MainActivity类定义

关于activity_main.xml中控件对应的类对象定义、TimerLogAdapter等类对象定义不再赘述。

1. 注册ibPlayPause、ibTick、ibReset三个ImageButton点击事件

MainActivity中,定义了名为clickListenerView.OnClickListener接口对象,使用匿名类形式 对其进行初始化,在onClick()方法中对应的三个switch分支的作用分别是:

  • R.id.ib_play_pause分支,通过timerStatus布尔型变量判断App处于开始计时或暂停计时状态,从而调用startTimer()stopTimer()方法;并使能或禁用【间隔计时】、【复位】按钮;

  • R.id.ib_reset分支,表示执行复位操作,清除掉ListView中的间隔计时记录,并清零总计时时间及间隔计时时间;

  • R.id.ib_tick分支,表示用户重新开始一次间隔计时,需要新增一条间隔计时记录;

2. 声明并初始化Handler对象

Handler对象用于向其所绑定(attach、实例化)的线程发送消息,消息进入对应线程的Message Queue(消息队列), 并被对应线程的Looper进行处理。每个线程均对应一个Looper对象及一个Message Queue对象,但可拥有多个Handler对象。Android系统通过这个机制可以保证Handler对象发送的消息被对应线程的Looper进行处理。如果把A线程的Handler对象暴露给B线程,在B线程中通过Handler对象发送消息,则可实现线程间通信。

声明Handler对象,通常需要子类化Handler类并重写其handleMessage()方法。通常情况下使用匿名类形式对Handler对象进行初始化。handleMessage()方法在指定线程中进行执行,如果Handler对象在UI主线程进行初始化,则handlerMessage()方法将在主线程中执行。本实验正是通过这一机制,在TimerTask中运行子线程,并在子线程中调用UI主线程的Handler对象发送消息实现定时更新UI中的总计时时间及间隔计时时间等视图元素。

3. 定义startTimer()、stopTimer()方法

startTimer()通过Timer.schedule()方法启动TimerTask,该方法接受3个参数:

  • TimerTask t,需要实际执行的TimerTask对象;

  • long delay,需要延期delay毫秒执行TimerTask

  • long period,间隔period毫秒后重复执行TimerTask

TimerTask通常使用匿名类形式进行初始化,并需要重写其唯一的run()方法,在该方法内放入需要执行的代码端,这里仅调用UI主线程的Handler对象发送消息。

stopTimer()方法则只需要调用Timer.cancel()TimerTask.cancel()方法即可。

4. 使用SoundPool增加音效效果

在很多App中,当用户触发某个操作或App出现新状态时,通常会使用短促的音效进行提醒。在Android框架中提供了SoundPool组件进行音频播放功能,SoundPool使用MediaPlayer进行音频流解码,与MediaPlayer相比,其操作状态图更为简单。SoundPool可指定同时播放的音频流数量(通过SoundPool.Builder.setMaxStreams()方法)、重复播放次数、播放速率等属性,主要适用于播放短音频文件。

在本实验中,秒表计时器开始工作时,可以播放指定的计时音频文件(该文件是从网上获取的音频文件),实现秒表计时音效效果。

  • 首先,在Android Studio中导入准备好的计时音频文件,其名为tick.m4a。导入后该资源位于res/raw文件夹下。

  • 其次,初始化SoundPool组件,在initData()方法中使用SoundPool.Builder类构造SoundPool对象。此外还需要通过AudioAttributes类对象设置音频播放属性行为,包括:音频使用用途(通过setUsage()方法设置)、音频类型(通过setContentType()方法设置)等属性。AudioAttributes通过AudioAttributes.Builder类进行构造。完成SoundPool组件初始化后,通过其load()方法用于同步加载媒体资源,由于可以SoundPool可以同时播放多个音频文件,因此load()方法返回一个int型的参数表示音频流id,需要保存该该id以便后续对该音频流播放进行控制。

  • 最后,在startTimer()stopTimer()中调用SoundPool组件播放或暂停播放已加载的音频流。

步骤五,编译并运行App

编译并运行本项目,尝试点击【开始计时】、【间隔计时】、【复位】等按钮,看看App的逻辑是否正确。

仔细观察本项目中计时器与手机系统提供的计时器相比计时是否精确,有没有其他方法能提高计时精度?

实验小结

通过本次实验,你应该掌握了如下知识内容:

  • 使用TimerTimerTask执行定时任务;

  • 使用Handler进行线程间消息通信;

Last updated

Was this helpful?