第 4 章 开车不发短信
本章将创建一款“开车不发短信”的应用,让你在开车时能够自动回复收到的短信。一名计算机入门课上的新生首创了这款应用,与美国国家农场保险公司开发的一款装机量巨大的应用相类似。App Inventor可以利用Android手机中的某些强大功能,包括SMS短信处理、数据库管理、文本转成语音以及位置传感器等,本应用就是一个典型的例子。
美国国家安全委员会(NSC)于2010年1月发布的研究结果表明,每年有至少28%的交通事故——将近160万次车祸源于司机在开车时使用手机,其中至少20万次交通事故与发短信有关(http://www.nsc.org/pages/nscestimates16millioncrashescausedbydriversusingcellphonesandtexting.aspx)。因此,许多国家已经全面禁止司机开车时使用手机。
Daniel Finnegan,一个旧金山大学的学生,在2010年秋季学期的App Inventor编程课上,提出了一个了不起的想法,用一个应用来解决开车发短信泛滥的问题。如图4-1所示,他创建的应用可以对收到的任何短信进行自动回复,如回复“我正在开车,稍后与您联系”之类的内容。
图 4-1 “开车不发短信”
课堂上的头脑风暴增加了应用的功能,也丰富了本教程,这些内容发布在App Inventor网站上:
用户可以根据情况改变回复内容:例如,如果你正在开会或看电影,而不是开车,回复内容可以做相应的修改;
应用可以大声朗读来信内容:尽管有自动回复,但对来信的好奇心也会置你于死地;
回复内容可以包含您当前的位置信息:如果你的另一半正在家做晚饭,他或她可能想知道你何时能到家,但又担心回短信会危及安全。
就在本应用发布到App Inventor网站后的几周,State Farm Insurance创建了名为“On the Move”的Android应用,http://www.statefarm.com/aboutus/newsroom/20100819.asp【译者没找到On the Move,不过该公司的确有几个好的应用值得尝试】,其功能与“开车不发短信”类似。该公司将这项服务作为“掌上代理”应用的升级内容,免费向所有人开放,并在YouTube网站发布视频:http://www.youtube.com/watch?v=3xtjzO0-Hfw【译者访问该网址,提示“此视频未公开”】。
我们无法确认Daniel的应用或App Inventor网站的教程是否对“On the Move”产生了影响,但有趣的是,这让我们思考一种可能性:在一门编程基础课上创建的应用(竟然由一个创意写作的学生创建!)也许会促成软件的批量生产,或至少是对软件生产的生态系统有所贡献。当然,这也说明了App Inventor降低了软件开发的门槛,以至于无论是谁,只要有一个好主意,都可以快速、低成本地把想法变成一个实实在在的、可交互的应用。
学习内容
相比前几章来说,这是一个更加复杂的应用,因此我们每次只完成一项功能,从自动回复开始。以下是将要学习的内容:
Texting组件:具有收发短信功能;
TextBox组件:用于提交自定义回复信息(需要与Button组件配合使用);
TinyDB数据库组件:用于保存自定义信息,即使应用已经关闭,信息也不会丢失;
Screen.Initialize事件:在应用启动时加载自定义回复内容;
Text-to-Speech组件:用于大声朗读文字;
LocationSensor组件:报告司机的当前位置。
准备开始
本应用的运行,需要手机上Text-To-Speech(TTS)模块的支持。该模块包含在Android 2或更高的版本中,如果你的操作系统是Android 1.x,需要从Google Play下载。在手机上:
1. 打开Google Play应用;
2. 搜索TTS;
3. 选择应用Text-To-Speech Extended并安装。
TTS模块安装后,可以测试其功能。对于Android2以上版本,在"设置->辅助功能"中打开“文字转换语音输出”功能,根据需要设置默认语言及语速,然后选择“收听示例”。如果没听到任何声音,请确认手机音量已调高;还可以更改TTS引擎默认属性设置,来改变声音效果。
TTS模块设置完成后,登陆App Inventor网站开始新项目“NoTextingWhileDriving”(项目名中不能有空格),然后设置屏幕的标题为“开车不发短信”,最后通过WiFi与测试手机(AI伴侣)连接。
设计组件
应用的用户界面很简单:一个显示自动回复内容的Label,一个编辑自动回复信息的文本输入框和一个提交输入结果的按钮。还需要拖入一个Texting组件、一个TinyDB组件、一个TextToSpeech组件以及一个LocationSensor组件,这些组件都将出现在“不可视组件”区。你可以在图4-2中看到上述组件在设计器中的截图。
图 4-2 组件设计器中的“开车不发短信”应用
将表4-1中列出的组件拖到预览窗口中,创建出图4-2中所示的用户界面。
表4-1 “开车不发短信”应用中的全部组件
组件类型 | 面板中分组 | 命名 | 作用 |
---|---|---|---|
Label | User Interface | PromptLabel | 让用户了解应用的功能 |
Label | User Interface | ResponseLabel | 显示即将发送的自动回复信息 |
TextBox | User Interface | NewResponseTextbox | 用户在此处输入定制的回复信息 |
Button | User Interface | SubmitResponseButton | 用户点击来提交、保存回复信息 |
Texting | Social | Texting1 | 处理短信事务 |
TinyDB | Storage | TinyDB1 | 将自动回复信息保存在数据库中 |
TextToSpeech | Media | TextToSpeech1 | 大声朗读来信 |
LocationSensor | Sensors | LocationSensor1 | 感知电话所在位置 |
按照以下方式设置组件属性:
设置PromptLabel的Text属性为“当本应用正在运行时,将用下面的文字来回复所有收到的短信。”
设置ResponseLabel的Text属性为“我正在开车,稍后与您联系。”并勾选FontBold(粗体字)属性;
设置NewResponseTextbox的Text属性为“”(让文本框为空,等待用户输入);
设置NewResponseTextbox的Hint(提示)属性为“输入新的回复内容”,Width设为“Fill Parent”;
设置SubmitResponseButton的Text属性为“修改回复内容。”
为组件添加行为
从基本的自动回复行开始,再逐步添加更多功能。
编程实现自动回复
自动回复功能需要用到App Inventor的Texting组件,可以把它想象成一个藏在手机里的小矮人,它知晓如何收发短信。该组件用Texting.MessageReceived事件块来响应“收到短信”,拖出该事件块,并在块内放入一些命令块,看看当你收到短信时,会发生什么事情。这里我们希望能够自动回复预先编辑好的内容。
发送短信的操作需要调用Texting1.SendMessage块,将其放在Texting1.MessageReceived块内。Texting1.SendMessage块只能发送短信,至于要发什么内容,以及发给谁,需要在调用之前,由你来告诉它:表4-2列出了自动回复行为所需要的块,而图4-3显示了它们在块编辑器中的样子。
**表4-2 自动回复短信所需要的块
块的类型 | 所在抽屉 | 功能 |
---|---|---|
Texting1.MessageReceived | Texting | 手机收到短信时会启动该事件的处理程序 |
set Texting1.PhoneNumber to | Texting | 在发送短信前设置接收者电话号码 |
get number | Variables | 获取短信发送者的电话号码 |
set Texting.Message to | Texting | 设置要发送的短信内容 |
ResponseLabel.Text | ResponseLabel | 用户预输入的短信内容 |
call Texting1.SendMessage | Texting | 发送短信 |
图 4-3 对收到的短信进行自动回复
块是作用
手机收到短信将触发Texting1.MessageReceived事件。如图4-3所示,发送者的手机号保存在参数number中,短信内容保存在参数messageText中。自动回复就是要向发送者发送一条短信,为此要先设置Texting组件的两个关键属性:PhoneNumber及Message。PhoneNumber设置为发送者的手机号,Message设置为ResponseLabel中显示的内容:“我正在开车,稍后与您联系。”设置完成之后,调用Texting. SendMessage实现自动回复。
注释是编程工作的重要组成部分,它可以告诉其他程序员那些与代码有关的重要信息。在块上单击右键,在快捷菜单中选择“Add Comment”(添加注释),此时块的左上方会出现蓝色问号,点击蓝色问号,会弹出文本输入框,可供输入注释信息;点击蓝色问号还可以隐藏注释信息。注释在应用中不是必须的,这里添加注释是为了介绍每个块的功能。
很多人用注释来记录创建应用的过程;注释可以解释程序的功能,但不会改变程序的行为。无论是你自己今后要修改程序,还是其他人要对程序做个性化设置,注释都是非常重要的。“没有不变的软件”已经成为共识,因此,代码的注释是软件工程中非常重要的环节,尤其像App Inventor这样的开源软件。
测试: 需要用第二部手机来测试程序。如果没有,可以注册申请Google Voice或其他类似的服务,并从注册的服务中给你的手机发送短信。用第二部手机给正在运行本应用的手机发短信,第二部手机是否收到了回信?
输入一个定制的回复
下面来添加更多的程序块,允许用户输入自定义的回复内容。在组件设计器中,已经添加了名为NewResponseTextbox的TextBox组件,用于输入自定义回复信息,当用户点击SubmitResponseButton时,NewResponseTextbox中的内容被复制到ResponseLabel中,这就是自动回复短信的内容。表4-3列出了在ResponseLabel中显示新的回复内容所需的块。
表4-3 显示自定义回复所需的块
块的类型 | 所在抽屉 | 功能 |
---|---|---|
SubmitResponseButton.Click | SubmitResponseButton | 点击按钮提交新的回复信息 |
set ResponseLabel.Text to | ResponseLabel | 为该Label设置新的文本内容 |
NewResponseTextbox.Text | NewResponseTextbox | 用户在这里输入新的回复内容 |
块的作用
一个典型的输入表单 的作用是:首先在文本框中输入文字,然后单击提交按钮来通知系统做处理。图4-4显示了用户点击“修改回复内容”按钮时, SubmitResponseButton.Click事件被触发。
图 4-4 将用户输入的信息设置为自动回复内容
事件处理程序将用户在NewResponseTextbox中输入的文字复制到ResponseLabel中,而ResponseLabel保存的是自动回复信息,因此要确保新输入的信息显示在ResponseLabel中。
测试:输入一段自定义信息并提交,然后用第二部手机发送短信到测试手机上,看看这次自动回复的是新定制的内容吗?
将定制回复保存到数据库中
你创建了一个伟大的应用,却留下了一个陷阱:用户输入了定制回复,然后关闭应用,当再次启动应用时,定制回复却不见了(取而代之的是默认回复)。这种状况可不是用户所期望的,他们希望在重启应用时,定制的内容还在,为此需要信息的永久保存。
你可能认为数据放在ResponseLabel组件的Text属性中,也应该算作“储存”,但实际上组件属性中的数据是临时数据,就像人的短时记忆,只要应用关闭,数据就会被“忘记”。如果希望应用能永久记住某些数据,就需要将数据从短时记忆(组件的属性或变量)转移到永久记忆中(数据库)。
要永久地保存数据,需要使用TinyDB组件,它可以将数据存储在Android设备内置的数据库中。TinyDB提供两个功能: StoreValue(保存值)和getValue(获取值)。前者允许应用将信息存储在设备数据库中,而后者则允许应用重新读取已存储的信息。
对于多数应用,可以采取如下策略:
1. 每当用户提交新值,将其存储到数据库;
2. 应用启动时,从数据库中加载数据并将其赋给一个变量或属性。
为了实现数据的永久保存,必须修改SubmitResponseButton.Click事件处理程序,表4-4中列出了所需要的程序块。
表4-4 用TinyDB数据库存储定制回复所需要的块
块的类型 | 所在抽屉 | 功能 |
---|---|---|
TinyDB1.StoreValue | TinyDB1 | 将用户的定制信息保存在手机内置的数据库中 |
”responseMessage” | Text | 以此作为保存数据的标签 |
ResponseLabel.Text | ResponseLabel | 已设定的回复信息显示在这里 |
块的作用
TinyDB从ResponseLabel的Text属性中提取内容,并将其保存在数据库中。如图4-5所示,向数据库中保存数据时,要为数据设置一个tag(标签),本例中的tag是“responseMessage”。可以把tag想象成数据在数据库中的存放地址,是数据的唯一标识。在下节中你将看到,必须使用相同的tag(“responseMessage”)才能将数据从数据库中读取出来。
图 4-5 永久保存自定义回复信息
应用启动时读取定制信息
将定制回复信息保存在数据库中,以便用户再次启动应用时,保存的数据可以被重新读取出来。App Inventor提供了一个特殊的事件块:Screen1.Initialize,当应用启动时,将触发该事件(我们在第3章MoleMash中使用过)。将Screen.Initialize块拖出来,并将某些程序块放在其中,那么这些程序块会在应用启动时逐一执行。
在本应用中,Screen1.Initialize事件的处理程序会检查数据库中是否存放了自定义回复内容。如果是,则使用TinyDB.GetValue函数加载存储的内容。实现这一功能所需的块见表4-5。
表4-5 应用启动时用于加载数据的块
组件类型 | 所在抽屉 | 作用 | |
---|---|---|---|
Initialize global response to | Variables | 用于存放数据库中读出的定制回复信息 | |
“” | Text | 变量的初始值可以是任意值 |
|
Screen1.Initialize | Screen1 | 应用启动时会触发该事件 | |
set global response to | Variables | 用从数据库中读出的值为该变量赋值 | |
TinyDB1.GetValue | TinyDB1 | 从数据库中读取已存储的定制回复信息 | |
"responseMessage" | Text | 插入TinyDB.GetValue的tag插槽,与之前TinyDB.StoreValue使用相同文本 | |
If | Control | 判断读出的数据中是否包含文字 | |
> | Math | 检查读出的数据长度是否大于0 | |
Length(text) | Text | 检查文本类型数据的长度 | |
get global response | Variables | 从变量中读出的数据(定制回复信息) | |
数字0 | Math | 用于比较长度 | |
set ResponseLabel.Text to | ResponseLabel | 如果读出的数据有内容,放在label中 | |
get global response | Variables | 从变量中读出的数据(定制回复信息) |
块的功能
如图4-6所示,要想理解这些块的功能,必须设想用户的使用过程:首次打开应用,输入自定义回复,随时退出并再次打开应用。用户首次启动应用时,数据库中没有定制回复可供加载,因此ResponseLabel中显示的是默认回复。再次启动时,才有可能从数据库中加载定制回复,并将其显示在ResponseLabel中。
图 4-6 应用启动时从数据库中加载定制回复
应用启动时触发Screen1.Initialize事件,并用tag “responseMessage”来调用TinyDB1.GetValue,该tag与之前用户存储定制回复时采用的tag相同。读出的值放在变量response中,并对其进行检验,然后才能在ResponseLabel中显示。想想看,为什么从数据库中读出的数据,在向用户显示之前,要经过检验呢?如果数据库中不存在与指定tag相对应的数据,TinyDB将返回空文本;而第一次启动应用时,数据是不存在的,直到用户输入了自定义回复,数据才会有。由于变量response中保存了数据库返回值,因此可以用if块来检查其长度是否大于0。如果大于0 ,说明的确从TinyDB读出了定制回复信息,就会将信息显示在ResponseLabel中;如果长度不大于0,说明之前没有保存过定制回复信息,因此将不修改ResponseLabel的显示内容(保留默认自动回复内容)。
测试:上述功能无法进行实时测试,因为每次连接“AI伴侣”启动应用时,数据库都会被清空。因此需要选择“build->App(provide QR code)”,然后扫描条码,将应用下载安装到手机上。安装之后,在NewResponseTextbox中输入新的回复信息并单击SubmitResponseButton按钮;关闭应用并重新启动它,这次定制回复信息出现了吗?
大声读出收到的短信
本节将修改应用:收到短信后,手机将大声朗读发送者的电话号码以及短信内容。开车收到短信,虽然有自动回复功能,但你还是禁不住想知道短信的内容。使用text-to-speech功能,就可以手不离方向盘而收听到短信的内容。
Android设备提供了text-to-speech功能,而App Inventor提供了一个TextToSpeech组件,它可以读出任何text(文本信息 )(注意,此处“text”指的是一般意义上的字/word:一串字母、数字以及标点符号组成的文本,而不是短信文本 。)
在本章的“准备开始”部分,我们要求你从Android Market下载一个text-to-speech的模块。如果你还没做,现在该去做了。根据需要安装并配置完模块之后,就可以在App Inventor中使用TextToSpeech组件了。
TextToSpeech组件的使用非常简单,只需调用它的Speak函数并插入要朗读的文字即可。例如,图4-7中的函数会说“Hello World”。
图 4-7 会说“HelloWorld”的块
在本应用中,朗读的内容则更为复杂,既要包含短信发送者的电话号码,也要包含短信内容,而不只是像“Hello World”那样的静态文本。这里要用到极为重要的join块,它可以将若干文本片段(或数字以及其他字符)连接成单一的文本对象。
在之前的Texting.MessageReceived事件处理程序中,加入对TextToSpeech.Speak的调用。在之前的事件处理程序中,通过适当设置Texting组件的PhoneNumber和Message属性,然后发送回复信息。现在需要加入表4-6中所列出的块来扩展该事件的处理程序。
表4-6 朗读收到的短信所需的块
块的类型 | 所在抽屉 | 功能 |
---|---|---|
TextToSpeech1.Speak | TextToSpeech1 | 大声读出收到的短信 |
join | Text | 连接生成将被朗读的文字 |
"SMS text received from" | Text | 被读出的第一段话 |
get number | Variables | 获得短信发送者的电话号码 |
". The message is" | Text | 在读完电话号码之后稍加停顿,然后说"The message is" |
get messageText | Variables | 获得收到的短信文本 |
块的功能
在自动回复动作完成之后,将调用TextToSpeech1.Speak函数,如图4-8的下半部分所示。你可以在TextToSpeech1.Speak函数的消息槽中插入任何文本对象。在这种情况下,join块用来生成被读出的内容。它将几段文字串连在一起,最后生成类似这样的信息:“SMS text received from 111-222-3333. The message is:hello.”
图 4-8 大声读出收到的短信
测试:你需要第二部手机来测试应用。用第二部手机发送文字【必须是英文】到你的测试手机上。你的手机大声读出信息了吗?它是否照常发送自动回复?
在回复中加入位置信息
像Facebook的Places以及Google的Latitude等类型的应用,都是利用GPS信息来帮助人们跟踪彼此的位置信息。这样的应用最令人担忧的是隐私问题,原因之一是它引发了人们对“老大哥”的恐惧,这里的“老大哥”指的是那些设法跟踪其公民下落的集权政府。但是使用位置信息的应用的确非常有用,试想一个迷路的小孩,或者那些在森林里迷路的徒步旅行者。
在“开车不发短信”的应用中,位置跟踪让回复的短信再多一点内容,而不只是“我正在开车”,回复的信息可以是这样的:“我正在北京东直门内大街209号开车”。对于那些正在等待朋友或家人到来的人来说,这些额外的信息非常有益。
App Inventor提供了LocationSensor(位置传感器)组件,作为手机的GPS (Global Positioning System全球定位系统)功能的接口。除了纬度和经度信息,LocationSensor也可以接入到谷歌地图,为司机提供当前位置的地址信息。
值得注意的是,LocationSensor并不总在读取信息,因此务必要恰当地使用这一组件。具体地讲,应用只对LocationSensor.LocationChanged事件做出响应,而两种情况会触发LocationChanged事件:①当手机的位置传感器第一次收到位置信息时;②随着手机的移动,产生新的位置信息时。使用表4-7中列出的块,具体方法是:当LocationChanged事件触发时,将当前地址信息保存到变量lastKnownLocation中,再将变量值插入到自动回复信息中。
表4-7设置位置传感器的块
块的类型 | 所在抽屉 | 功能 |
---|---|---|
Initialize global lastKnownLocation to | Variables | 创建一个变量来保存最后读取的地址信息 |
"未知" | Text | lastKnownLocation的默认值 |
LocationSensor1.LocationChanged | LocationSensor1 | 位置传感器第一次读到位置信息,或每次位置信息变化时触发该事件 |
set global lastKnownLocation to | Variables | 设置变量值,稍后会用到 |
LocationSensor1.CurrentAddress | LocationSensor1 | 当前地址信息,如:"北京市东城区东直门内大街209号" |
块的功能
当位置传感器首次读取位置信息时,LocationSensor1.LocationChanged事件被触发,随着设备的移动,还会生成新的位置信息,事件将被再次触发。由于自动回复信息中要包括地址信息,因此,通过调用LocationSensor1.CurrentAddress函数 来获取地址信息,并将其保存在lastKnownLocation变量中,如图4-9所示。在后台,这个函数会调用谷歌地图(通过API 来调用,将在第24章学到),并根据传感器获得的经纬度信息来确定最近的街区地址。
图 4-9 每当传感器收到GPS位置信息时,用变量记录手机的位置
注意,这些块只完成了一半的工作,还要将位置信息插入自动回复信息中,再回复给发件人。
发送带有位置信息的回复
用变量lastKnownLocation中的值对Texting1.MessageReceived事件处理程序加以修改,向自动回复信息中添加位置信息。表4-8列出了所需要的块。
表4-8在自动回复中显示位置信息的块
块的类型 | 所在抽屉 | 功能 |
---|---|---|
join | Text | 多段文本的连接器 |
ResponseLabel.Text | ResponseLabel | 不包含位置信息的定制回复信息 |
“我最后的位置在:” | Text | 原定制信息之后的位置信息提示词 |
global lastKnownLocation | Variables | 地址信息,如:“北京市东城区东直门内大街209号” |
块的功能
向回复中添加位置信息需要LocationSensor1.LocationChanged事件与变量lastKnownLocation的协作。如图4-10所示,并非直接发送ResponseLabel.Text中的信息,而是使用join块将若干段信息整合起来:原有的自动回复信息+“ 我的最后位置在:”+变量lastKnownLocation。
图 4-10 在回复信息中加入位置信息
变量lastKnownLocation的默认值是“未知”,所以如果位置传感器尚未产生位置信息,则自动回复的第二部分内容为“我的最后位置在:未知”。如果已经产生了位置信息,则自动回复的第二部分内容有可能是这样:“我的最后位置在:北京市东城区东直门内大街209号”
测试:用第二部手机发送短信到运行应用的手机上,第二个手机是否接收到了带有位置信息的自动回复?如果没有,请确保你已经开启了第一部手机的GPS定位功能。
完整的应用:开车不发短信
图4-11中显示了“开车不发短信”最终的块的配置
图 4-11 完整的“开车不发短信”应用(同时显示所有注释)
改进
当应用已经付诸使用,你也许会想到做一些改进,例如:
编写另一个版本,允许用户针对某些特定来信号码定制回复内容。你需要增加一个条件(if)块,来检查这些来信的手机号。有关条件块(if)的更多信息,请参见第18章;
再编写一个版本,根据用户是否在某个纬度/经度范围内,来回复定制信息。这样,如果应用判断你在房间222,它会回复“鲍勃在222房间,现在不能回复短信。”有关LocationSensor以及确定边界的更多信息,请参见第23章;
另一个版本,当短信发送者的手机号码在某个“通知”列表中时,发出提醒铃声。关于如何使用列表,请参阅第19章。
小结
下面是本章涉及到的一些概念:
Texting组件:既可以用来发短信,也可以处理收到的信息。在调用Texting.SendMessage之前,需要为Texting组件设置PhoneNumber及Message属性。为了回复收到的短信,需要为Texting.MessageReceived事件编写处理程序;
TinyDB组件:用于将信息永久存储在手机数据库中,以便每次应用启动时都可以加载保存过的数据。有关TinyDB的更多信息,请参见第22章;
TextToSpeech组件:对于所提供的任何文本对象,都可以大声朗读出来(限英文);
join块:用于将若干片段的文字拼凑(或连接)成单一的文本对象中;
LocationSensor组件:可以报告手机的纬度、经度及当前的街道地址。为了确认它是否收到了位置信息,可以访问LocationSensor.LocationChanged事件处理程序中的相关参数。该事件在第一次收到位置信息时被触发,并在每次位置信息更新后被再次触发。有关LocationSensor的更多信息,请参见第23章。
背景知识
Text-To-Speech
缩写为TTS,直译为“从文本到语音”,是语音合成技术的一种应用,可以将文字信息实时转换为语音。在本章中使用了Android设备的此项功能,但遗憾的是,目前Google的TTS引擎尚不支持中文的转换,因此学员测试时还需使用英文。
英汉对照
text: 文本[名词]
text: 发短信[动词]
initialize: 初始化
speech: 讲话
location: 位置
sensor: 传感器
tiny: 微小的
DB: database数据库
social: 社交的
storage: 存储
prompt: 提示
response: 响应
receive: 接收
submit: 提交
hint: 提示
variable: 变量
phone: 电话
number: 数字,号码
call: 调用,呼叫
message: 消息
send: 发送
add: 增加
comment: 注释,评论
voice: 声音
store: 存放
value: 值
tag: 标签
global: 全球的,全局的
control: 控制
math: 数学
length: 长度
provide: 提供
code: 编码
GPS: 全球定位系统(Global Positioning System)
change: 改变
last: 最后的
current: 当前的,现在的