Category Archives: Uncategorized

第24章 与Web API通信

Published by:

移动技术再加上无所不在的网络,已经完全改变了我们生活的这个世界。如今坐在公园里就可以打理你的银行账户,或者在亚马逊书店搜索你正在阅读的图书的评论,或者查阅Twitter,看看世界上其他公园里的人们都在想些什么。手机只能打电话发短信的时代已经过去,它可以让你随时随地访问世界各地的数据。

虽然用手机浏览器可以访问互联网,但由于屏幕太小,而且速度受到限制,因此使用者会感觉不适。如果能够定制应用,有针对性地从网络上提取少部分信息,以适应手机终端的特点,就可以获比浏览器得更具吸引力的替代方案。

本章我们将领略从网络获取信息的各类应用,首先创建一个显示游戏排行榜的(图表)应用,然后以Yahoo财经频道的股票数据为例,讨论如何使用TinyWebDB从网上获取任意类型的信息(不只是图像),最后讨论如何创建属于自己的网络信息源,以用于App Inventor应用。

创新就是对这个世界的重组,以一种新奇的方式将旧的观念和内容组合在一起。埃米纳姆(Eminem,美国说唱歌手)的单曲Slim Shady追随了AC/DC(最著名的澳大利亚摇滚乐队)与Vanilla Ice(美国白人说唱歌手)的风格,并使这种混搭的音乐风行一时。这一类的“模仿”非常普遍,以至于许多艺术家,包括Girl Talk(专攻混搭及数字音乐的美国音乐家)及Negativland(来自美国加州的一个实验音乐乐队),都致力于将旧的内容融入某种新的风格。

无独有偶,在网络及移动世界中,网站及应用混合了来自各种渠道的数据及内容,而且很多网站在设计理念上遵循了互联互通原则(interoperability)。一个典型的混搭网站的例子就是Housing Maps(http://www.housingmaps.com),如图24-1,它从网站Craigslist(http://www.craigslist.org)上采集房屋租赁信息,并与谷歌地图API结合起来,提供一种新型的信息服务。

图24-1 住房地图(Housing Maps)应用将Craigslist的房屋信息与谷歌地图信息叠加起来

谷歌地图不仅仅是可供访问的网站,同时也提供相应的应用程序接口服务(web service API),这使得“住房地图”这类混搭应用成为可能。我们普通人只能通过浏览器访问http://maps.google.com来查看地图,但像“住房地图”这样的应用可以访问谷歌地图API来实现机器与机器之间的通信。混搭应用处理并组合来自不同站点(如Craislist及Google Maps)的数据,并将它们以一种更有意义的方式呈现出来。

现在,几乎所有流行的网站都提供这种备选方案:机器对机器的访问。提供数据的一方称为网络服务(web service),而客户端应用与网络服务之间的通信协议则称为应用程序接口,或API。事实上,术语API已经成为网络服务(web service)的代名词。

亚马逊网络服务(Amazon Web Service,即AWS)是最早的网络服务之一,由于亚马逊公司向第三方应用开放了它的业务数据,最终导致图书销量的增加。同样,当2007年Facebook发布了它的API时,也吸引了无数人的眼球。Facebook的数据不同于图书广告,那么为什么它甘愿让其他应用“偷走”它的数据,同时也可能拉走它的用户呢(还有广告收入!)?事实上,开放把facebook从一个网站变成了一个平台,这意味着像快乐农场这样的第三方程序,也可以运行在这个平台上,并利用平台的部分功能。现在,没有人能质疑Facebook的成功。到2009年Twitter发布时,API访问已经是意料之中的事情,果然,Twitter也如此行事。现在,如图24-2所示,大多数的网站都同时提供人机访问接口。

图24-2 大多数网站同时具备供人类访问的界面及供客户端应用访问的API

对于我们普通人来说,网络就是一个可供访问的为数众多的网站,而对于程序员来说,它却是一个世界上最大也最丰富的信息数据库。在网络世界里,机器对机器的通信量正在超过人机之间的通信量。

访问生成图像的网络API

提示:谷歌图表API现已废弃。在本例中仍可使用它,但总有一天将不可用。尽管如此,本例仍不失为解释URL(链接地址)即其参数的好例子。【原作者给出的网址确已废弃,译者给出了新的网址,现在可用。】

正如在第13章(亚马逊掌上书店)中所见,大多数API都会接受以URL形式发来的数据请求,并会返回数据(通常以标准格式返回数据,如XML[Extensible Markup Language,扩展的标记语言]、JSON[JavaScript Object,JavaScript对象表示法])。可以使用TinyWebDB组件与这些API进行通信,本章稍后将详细讨论这一重点话题。

不过也有些API返回的结果不是数据,而是图像。本节将讨论如何与生成图像的API进行通信,来拓展App Inventor的用户界面能力。

谷歌图表API就是这样一类服务。通过在URL地址中加入某些数据,向API发出请求,API将返回一个图表,你的应用负责显示这些图表。该服务可以生成多种图表,包括条状图、饼状图、地图及文氏图(Venn Diagram,用封闭曲线所包围的面积来表示集合及其关系的图形)。谷歌图表API成为网络服务(web service)互联互通原则的一个典范,它的目的在于增强其他网站的能力。由于App Inventor没有提供多少所谓的可视化组件,因此能够借用谷歌图表这样的API,对App Inventor来说是至关重要的。

首先要理解发给API的URL地址的格式。访问谷歌图表API网站(https://google-developers.appspot.com/chart/interactive/docs/gallery),你将看到如图24-3的页面。

图24-3 谷歌图表API生成的各类图表

网站提供了完整的说明文档及操作向导,可以交互式地创建图表,并探究如何书写URL地址。向导非常好用,可以通过表单来定义各种类型的图标,并能自动生成你需要的URL地址,你还可以反过来用自己的数据验证这个地址的有效性。让我们开始吧,访问网站,跟随向导来创建图表,然后仔细分析生成这些图表的URL地址的格式。看下面的例子,在浏览器中输入以下URL地址:

tp://chart.apis.google.com/chart?cht=bvg&chxt=y&chbh=a&chs=300×225&chco=A2C180&chtt=Vertical+bar+chart|(垂直条状图)&chd=t:10,50,60,80,40,60,30

你将获得图24-4所示的图表。

图24-4 谷歌图表API根据URL地址生成了这个图表

要想理解之前输入的URL地址,就需要了解URL地址的作用。你会发现其中包含了问号(?)及and符号(&)。其中的?标志着第一个参数的出现,而&号将后续的各个参数分隔开。每个参数都由名称、等号及值组成,因此在上面调用图表API(http://chart.apis.google.com/chart)的例子中,使用了七个参数,其具体内容如表24-1所示。

表24-1 图表API中使用的带参数的URL地址
参数 参数的含义
cht bvg 图标的类型为条状图(bar)、垂直的(verbical)、分组的(grouped)。
chxt y 在y轴上显示数字
chbh a 自动设置条的宽度及间隔
chs 300×225 整个图表尺寸(像素值)
chco A2C180 图表中条的颜色(16进制表示法)
chd t:10,50,60,80,40,60,30 生成图表的数据,简单的文本格式(t)
chtt Vertical+bar+chart|(%E5%9E%82%E7%9B%B4%E6%9D%A1%E7%8A%B6%E5%9B%BE) 图表的标题,“+”代表空格,“|”代表换行

译者提醒:表格中图表标题一项换行符“|”后的内容与浏览器中输入的“(垂直条状图)”不同,这是因为App Inventor对中文字符进行了编码的缘故。从浏览器地址栏中复制完整地址,然后粘贴到块编辑器的文本块中,就会自动将中文字变成表格中的字符。如果你强行在文本块中输入“(垂直条状图)”,最终在应用测试时,手机上应该显示中文字符的位置会显示“?”。提醒完毕。

通过修改参数,可以生成不同的图形。想了解更多的图表类型,请查阅下面的API文档:

  • http:// code.google.com/apis/chart/index.html

为图表API设置Image.Picture属性

在浏览器中输入上述例子中的URL地址,就可以看到图表API生成的图表,如果想在手机上显示该图表,就需要将Image组件的Picture属性设置为上述的URL。具体操作如下:

  1. 创建一个新应用,将Screen1的Title属性设置为“图表应用举例”;
  2. 添加Image组件,设置其Width属性为“Fill parent”,Height属性为300;
  3. 将Image1.Picture属性设置为上述URL(https://chart.apis.google.com/chart?cht=bvg&chxt=y&chbh=a&chs=300×225&chco=A2C180&chtt=Vertical+bar+chart|(%E5%9E%82%E7%9B%B4%E6%9D%A1%E7%8A%B6%E5%9B%BE)&chd=t:10,50,60,80, 40,60,30)。在组件デザイナー中无法Picture属性,因为这一属性只接受加载的文件,因此需要在块编辑器中进行设置,如图24-5所示,添加Screen1.Initialize事件处理程序,并在其中设置Image1.Picture属性。

图24-5 应用启动时,设置image组件的picture属性为一个图表API的URL

图24-6 手机应用中显示的图表

在手机或模拟器中将显示图24-6所示的图像。

动态生成图表API的URL地址

前面的例子显示了如何在应用中生成一个图表,不过例子中的URL使用的是固定数据(10,50,60,80,40,60,30)。通常我们需要用动态数据来生成图表,即,数据保存在变量中。例如,在一个游戏应用中,用户之前的成绩保存在变量Scores中,我们要显示这些成绩。

要创建这样的动态图表,同样需要为图表API生成一个URL,并将变量中的数据植入其中。前面例子的URL中,用于生成图表的数据是固定的,并用参数chd来声明(chd代表图表数据):

  • chd=t:10,50,60,80,40,60,30

要生成动态的成绩图表,参数定义的开头是一样的,chd=t;之后的数据要从Scores列表中读取,并将成绩用逗号逐个连接起来。如图24-7中显示的最终的方案。

图24-7 向图表API发送动态生成的URL

我们来详细研究一下些块暗藏机关的块,其中大部分我们之前都使用过。

  1. 为了便于理解,我们先编造一组数据,假设之前用户有三次游戏的成绩,保存在列表变量Scores中,分别为35、85、60。
  2. 定义了变量chdPara,用来保存URL中列表数据的部分。在showChartButton.Click事件处理程序中,第一行将变量chdPara初始化为“chd=t:”。
  3. 定义了变量scoreIndex,用于在foreach循环中跟踪当前正在处理的列表项,在Click事件处理程序中的第二行将其初始化为1;
  4. 随后是一个判断,看列表Scores中是否包含列表项(length of list > 0),如果包含列表项,则执行foreach循环:
    • 针对Scores列表中的每一项(成绩值),用参数chdPara的当前值与列表项连接;
    • 然后又是一个判断——检查当前正在处理的列表项是否不为列表的最后一项,如果不是最后一项,则在参数chdPara后面添加一个逗号,如果是最后一项,则不添加任何字符。
    • 在循环的最后一行,将变量scoreIndex的值+1,以便在下一次循环中用于判断列表的最后一项。
  5. 循环结束后,将Image1的Picture属性设置为最终的URL,其中第一部分为:http://chart.apis.google.com/chart?cht=bvg&chxt=y&chbh=a&chs=300×225&chco=A2C180&chtt=Vertical+bar+chart|(%E5%9E%82%E7%9B%B4%E6%9D%A1%E7%8A%B6%E5%9B%BE)&,第二部分为变量chdPara。
  6. 这里为了跟踪参数值,添加了一个名为chdParaLable的标签,用于显示最终生成的参数。

图24-8 应用在手机中运行的效果

到此为止,我们生成了动态的URL,这样的方式具有普遍的适用性,例如,假设用户在成绩列表中新增了若干项,那么这个程序也是好用的。图24-8显示了在手机中应用运行的结果。

你可以在任何游戏或应用中,采用本例中的方法来显示各种图表,也可以与其他API进行通信,将更多地内容植入到自己的应用中,其中的关键是App Inventor提供了可以获取网络图片的Image组件。

与网络数据API通信

提示:App Inventor现在提供了一个web组件,可以更容易地访问API数据,虽然下述的TinyWebDB方案仍然有效,但建议查看以下链接中使用web组件的例子:

  • http://www.appinventor.org/stockmarket-steps

谷歌图表API可以接受请求并返回图片,不过更常见的是返回数据的API,在应用中可以对这些数据进行处理,并根据需要加以利用。例如,在第13章“亚马逊掌上书店”的应用中,返回的数据是图书的列表,其中每项数据包含了书名、最低售价以及书号(ISBN)。

使用App Inventor应用于API通信,并不需要像在图表API的例子中那样,要自己来创建URL,而是更像使用一个网络数据库(见第22章):只需要在TinyWebDB.GetValue中使用相关的标签即可,实际上是TinyWebDB组件负责生成了访问API的URL。

不过,TinyWebDB并不能访问所有的API,即使是那些返回标准数据的API,如RSS。TinyWebDB只能访问那些“披着App Inventor外衣”的网络服务,并遵从特定的通信协议。幸运的是,已经创建了许多这样的服务,并且还会有更多的服务随之而来。网站http://appinventorapi.com上提供了一些这样的服务。

探索API的网络接口

本节将学习使用TinyWebDB获取股票价格信息,信息来源于一个App Inventor兼容的API,网址是http://yahoostocks.appspot.com。访问该网址,将看到一个如图23-12所示的web接口(人类可访问的)。

图24-9 App Inventor兼容的雅虎金融API的web接口

在Tag输入框中输入“IBM”或其他股票的代码,网页上将返回股票信息列表,每一项代表一个不同的信息,后面将解释这些数据的含义。

不过,在web页面上查找股票信息并不是什么新鲜事,它的真实目的是为程序员提供一个机器对机器的访问接口,从而实现与API之间的底层通信。

通过TinyWebDB访问API

图24-10 将ServiceURL属性设置为http://yahoostocks.appspot.com

创建股票查询应用的第一步是在组件デザイナー中拖入一个TinyWebDB组件,该组件只有一个属性可以设置,即ServiceURL,如图24-10所示,它的默认值为:http://appinvtinywebdb.appspot.com,指向默认的web数据库。而这里我们要访问的雅虎股票API,因此将其设置为http://yahoostocks.appspot.com,与你之前在浏览器地址栏中输入的URL相同。

下一步是调用TinyWebDB.GetValue,向网站请求数据。这个操作可以放在一个Button.Click事件中:当用户在手机的应用界面中输入股票代码并点击“提交”按钮时,执行此调用;或者将其放在Screen.Initialize事件中,在应用启动时,自动获取某个股票的信息。无论哪种情况,都需要为GetValue设置tag——某个股票的代码,如图24-11所示,就像在网站http://yahoostocks.appspot.com上的操作一样。

图24-11 请求股票信息

在第10章的“出题”应用中,我们已经讨论过数据库组件TinyWebDB,它的通信方式是异步的:应用中调用TinyWebDB.GetValue请求数据,之后程序将继续运行,必须为这次请求提供另一个事件TinyWebDB.GotValue的处理程序,当请求的数据从网络服务端返回时,来接收并处理这些数据。通过在用户界面http://yahoostocks.appspot.com上的操作,我们已经知道返回的数据为列表,每个列表项代表股票的不同信息(如,第二项代表股票的收盘价)。

客户端的应用可以利用网络所提供的部分或全部信息,如,如果你想显示股票的当前价格,并与开盘价进行比较,你就可以按照图24-12的方式来组织数据。

图24-12 使用GotValue时间来处理从Yahoo返回的数据

如果从网页http://yahoostocks.appspot.com上直接向API提交请求,你会看到返回列表的第2项的确是股票的当前价格,而第5想是当前价格与当天开盘价之间的差。这个例子只是简单地从API的返回值中提取部分信息,并用两个label显示出来:PriceLabel与ChangeLabel,如图24-13所示。

图24-13 股票应用的运行效果

创建自己的App Inventor兼容的API

在终端应用与网络之间,TinyWebDB起到了桥梁的作用。App Inventor程序员只需要依照GetValue内置的简单的tag-value协议,就可以实现应用与网络服务之间的通信。这种方式让程序员免于亲手处理那些标准格式的数据,如XML或JSON。

这种方便的代价是,用App Inventor开发的应用只能与少数网络服务通信,这些网络服务遵从TinyWebDB所设定的协议,协议中要求返回特殊格式的数据,因此API不得不提供对应格式的数据,如XML或JSON。如果找不到可用的与App Inventor兼容的API,那么就要靠那些有能力的程序员来创建。

从前,创建API是一件非常困难的事情,不但需要了解编程及网络协议,还要搭建服务器来运行自己创建的服务,另外还需要建立数据库来保存数据。但现在这件事变得容易多了,你可以借助于云计算工具,如谷歌公司的应用引擎(Google’s App Engine)以及亚马逊公司的弹性计算云(Amazon’s Elastic Compute Cloud),来部署自己创建的网络服务。这些平台不仅可以接受委托管理你的服务,还能在不必支付费用的情况下,让数以千计的用户访问你的服务。可以想象,这些平台为创新提供了巨大的支持。

定制的模板代码

编写API看似令人望而生畏,但令人欣慰的是你不必从零做起。利用某些现成的模板程序让创建App Inventor兼容的API变得非常容易。这些程序由python语言编写,并使用了谷歌应用引擎(App Engine)。模板程序提供了一段样板代码,可以将数据编辑成App Inventor所需要的格式,还提供了一个函数get_value,你可以按自己的需要进行修改。

下载模板程序及使用说明,并将其部署到谷歌应用引擎服务器上,网址是http://appinventorapi.com/using-tinywebdb-to-talk-to-an-api/。你会发现这个链接与第21章创建定制数据库时使用的网址都指向了相同的appinventorapi.com。实际上创建API类似与创建定制数据库,只是不必保存及提取数据,而是通过调用其他服务来获取所需要的数据。

为了创建自己的API,要先下载模板程序,并对几个关键代码做出修改,再上传到谷歌应用引擎。创建一个用TinyWebDB可以访问的API只是几分钟的事情。

以下是从模板程序中选出的一段代码,需要对其进行修改(不必理会那些“#”号后面的文字,它们就像App Inventor中的注释一样,用来说明接下来的代码的功能):

def get_value(self, tag):

#在这个简单的例子中,仅返回hello:tag,其中的tag来自于客户端应用

value=”hello:”+tag

value = “””+value+”””

# 如果value由多个单词组成,为其添加引号

if self.request.get(‘fmt’) == “html”:

WriteToWeb(self,tag,value )

else:

WriteToPhone(self,tag,value)

这段代码属于一个名为get_value的函数(与App Inventor中的procedure相同),当你使用TinyWebDB.GetValue函数调用某个API时,需要调用这个函数。tag是函数的参数,并于GetValue中发送的tag相对应。

黑体字的代码是需要修改的部分。默认情况下,该函数从发来的请求中提取tag,并返回“hello:tag”。(也就是说,如果在调用该函数时使用的tag为“joe”,那么函数将返回“hello:joe”)。通过设定变量value的值就可以实现这一点,随后value值将传递给另一个函数:如果请求来自于web,则传给函数WriteToWeb,如果请求来自手机,则传给WriteToPhone。

提示:即使你从未见过Python或其他程序的代码,根据使用App Inventor的经验,你也可以读懂上面的代码。其中第一行“def get_value”是对过程的定义,“vlue=…”行是为变量value赋值,“if…”后面的代码看起来很熟悉。是的,与App Inventor相比,它们的基本概念是相同的,只是用文字取代了块。

为了定制这段代码,需要将粗体字替换成你需要的某种计算,目的是为了给变量value赋值。通常你的API需要调用其他的API(被称为“封装”调用,更具体地说,就是get_value函数将调用其他的API)。

许多API过于复杂,拥有几百个函数以及复杂的用户认证方案。而另一些则相当简单,你可以找到一些例程,并在网络上访问它们,如下节所述。

封装雅虎金融API

本章所使用的App Inventor专用雅虎股票API就是通过对上述模板程序的修改而获得,该模板程序可以从网上搜索到。为了将雅虎股票API封装成App Inventor可以调用的API,开发者(Wolber教授)在网站http://www.gummy-stuff.org/Yahoo-data.htm【网站地址已经不存在了——译者注】上搜索”Python Yahoo Stocks API”,并发现了如下格式的URL:

  • http://download.finance.yahoo.com/d/quotes.csv?f=sl1d1t1c1ohgv&e=.cs v&s=IBM

上述URL将一个文本本件“quotes.csv”下载到本地计算机,文件中包含了如下格式的字符串:

  • “IBM”,183.76,”5/29/2014″,”4:02pm”,+0.68,183.68,183.78,182.33,2759978

之后Wolber教授又在网站http://www.goldb.org/ystockquote.html【该网站可访问,但代码已经更新,找不到本书中采用的代码了。——译者注】上发现了可以访问雅虎股票API的Python代码。通过几次快速的剪切粘贴及编辑,为App Inventor封装的API就创建出来了,具体修改方式如下:

def get_value(self, tag):

# Need to generate a string or list and send it to WriteToPhone/ WriteToWeb

# Multi-word strings should have quotes in front and back

# e.g.,

# value = “””+value+”””

# call the Yahoo Finance API and get a handle to the file that is returned

quoteFile=urllib.urlopen(“http://download.finance.yahoo.com/d/quotes.csv?f=sl1d1t1c1ohgv&e=.csv&s=”+tag)

line = quoteFile.readline() # there’s only one line

splitlist = line.split(“,”) # split the data into a list

# the data has quotes around the items, so eliminate them

i=0

while i<len(splitlist):

item=splitlist[i]

splitlist[i]=item.strip(‘”‘) # remove ” around strings

i=i+1

value=splitlist

if self.request.get(‘fmt’) == “html”:

WriteToWeb(self,tag,value )

else:

WriteToPhone(self,tag,value)

那行粗体的代码通过对urllib.urlopen函数的调用来访问雅虎API(这是Python语言访问API的方法之一)。在URL中有一个参数f,它表明你想获得的股票数据的类型(这个参数有点像谷歌图表API中的神秘参数)。数据保存在变量line中,其余的代码将返回值分解为列表,移除每个列表项中的引号,并将结果发给请求者(电脑上的web页面或手机上的App Inventor应用)。

小结

大多数网站以及许多移动应用并非孤岛,它们遵从互联互通原则,利用其它网站的功能来实现自己的目标。在App Inventor中,可以创建独立的应用,如游戏、测验等,但这还远远不够,你迟早会遇到访问web的问题。是否可以为我平时等车的公交车站写一个应用,来预计下一班车何时到达呢?是否可以让应用给我facebook中的部分好友发送短信呢?再有,应用是否能够发tweet呢?App Inventor中有两种方式可以连接到网络:①将Image.Picture属性设置为某个返回图像的URL;②使用TinyWebDB从某些专用的API上获取数据。

App Inventor不支持对任意API的访问,程序员需要创建遵从特定协议的“封装”API来实现对web的访问。一旦有了封装的API,App Inventor程序员就可以像访问数据库一样,使用TinyWebDB.GetValue来访问需要的API。实际上,相对于编写App Inventor应用来说,编写API对程序员来说是一个更大的挑战,但如果你有兴趣学习,可以查阅一些Python的书籍及课程(O’Reilly出版社有若干这类的书),然后就可以开练了。

第23章 传感器

Published by:

将你的手机指向天空,谷歌星空地图会显示出你正在观看的星群;倾斜手机,可以控制你的游戏;带着你的手机去散步,一款“面包渣儿”应用将记录下你的途经的路线。所有这些应用之所以能够实现,都是因为你所携带的移动设备装备了高科技的传感器,可以探测到位置、方向以及加速度。

本章将再次讨论App Inventor的位置传感器、方向传感器以及加速度传感器等组件,其中将学习全球定位系统(GPS)、方向测量(如倾斜、旋转及摇晃)以及与处理加速度读数相关的数学知识。

创建位置感知应用

在智能手机流行之前,计算仅限于桌面电脑。虽然便携式电脑算是移动设备,但与我们今天随身携带的微型设备相比,不可同日而语。计算已经摆脱了实验室及办公室,在地球上随时随地都在发生。

对计算的普遍性产生深刻影响的是一项新的、有趣的数据,它存在于上述的所有应用中,即:当前的位置信息。当人们在世界各地游走时掌握他们的行踪,这件事影响深远,它既有可能对我们的生活产生极大的帮助,但同时也存在侵犯隐私及损害人权的可能。

在“安卓,我的车在哪”的应用中(第7章)就是一个有益的位置感知应用的例子,让我们可以记住之前的地点,以便稍后还能找回来。这是一个个人应用——位置信息就保存在自己的手机数据库中。

同样的理念也适用于群组。例如,一个徒步旅行者小组可能希望在荒野中查看每个组员的去向,或者一个商务团队可能希望在一个大型会议上寻找自己的伙伴。这类应用已经出现在市场上,两个典型的应用就是“谷歌纵横(Latitude)”(www.google.com/latitude)以及Facebook的“签到(Place)”(www.facebook.com/ places)。由于公众对隐私的担忧,这些应用一经面世便备受争议。

另一类位置感知应用使用了增强现实工具。这类应用利用位置及手机的方向,在自然信息基础上,提供增强的叠加信息。因此当你用手机指向一栋建筑物时,你会看到它在房地产市场上的价格,或者你在植物园中欣赏异国花卉时,某个应用会告诉你这株植物的品种。这类应用的早期产品包括世界浏览器(Wikitude——一款增强现实的实景地图导航应用)、手机实景浏览器(Layar——第一款手机版的增强现实浏览器)以及谷歌星空地图。

世界浏览器甚至可以让用户通过网站http://wikitude.me在移动云上添加数据。在网站上,选定地图并标注上你的个人信息,稍后,当你或其他用户在这个位置使用该移动应用时,你发布的信息就会显示出来。

GPS

图23-1 位于赤道上的厄瓜多尔首都基多

创建一个位置感知应用,首先需要了解全球定位系统(GPS)的工作原理。GPS数据来自美国政府所保有的卫星系统,只要在视野开阔地带,至少能看到三颗卫星,你的手机就能获得读数。一份GPS读数包括位置的纬度、经度及海拔高度。纬度表示与赤道的距离,赤道以北为正值,以南为负值,范围从-90至90.如23-1显示了厄瓜多尔基多附近的谷歌地图,图中的纬度为-0.01,表示在赤道偏南一点点。

经度是距离本初子午线(零度经线)向东或向西偏离的距离,向东为正值,西为负值,零度经线穿过的最知名的地点就是格林威治,伦敦附近的一座小镇,皇家天文台的所在地。图23-2中的地图标出了格林威治,它的经度为0.0。

图23-2 格林威治的皇家天文台沿本初子午线射出一道光柱

图23-3 在俄罗斯与阿拉斯加边境附近的一点,经度为180

经度值从-180到180,图23-3显示了俄罗斯境内的一点,非常靠近阿拉斯加,它的经度为180.0,这个点可以理解为以格林威治(经度为0.0)为起点绕地球半圈所到达的位置。

用App Inventor感知位置

App Inventor为访问GPS信息提供了LocationSensor(位置传感器)组件,该组件具有Latitude(纬度)、Longitude(经度)及Altitude(海拔高度)三个属性,此外它可以与谷歌地图通信,因此还可以获得当前街道地址的信息。

图23-4中的LocationSensor. LocationChanged是位置传感器组件LocationSensor最关键的事件处理程序。

图23-4 LocationSensor1.LocationChanged事件处理程序

两种情况可以触发LocationChanged事件:传感器第一次收到读数时,以及当位置发生一定变化后收到新的读数时。其中第一次读数通常会延迟几秒钟,有时也会一直没有读数。例如,如果你在室内而且没有连接WiFi,设备将无法获得读数。手机中也有相关的设置,可以为了省电而关闭了GPS,这是无法获得读数的另一个可能的原因。除了这些原因,在LocationSensor.LocationChanged事件被触发之前,不能排除LocationSensor做了不合理的属性设置。

处理这种无法感知位置的情况,有一个方法是创建一个变量lastKnownLocation,并将其初始化为“未知”,然后让LocationSensor.LocationChanged事件处理程序来修改变量的值,如图23-5所示。

图23-5 变量lastKnownLocation的值会随位置的改变而改变

通过编写以上事件处理程序,在第一次获得读数之前显示“未知”,这样就可以始终显示当前位置,或将位置信息保存到数据库中。这一策略在第4章“开车不发短信”中使用过,即,在自动回复的短信中加入位置信息:“未知”或最后一次获得的读数。

图23-6 用HasLongitudeLatitude块测试传感器是否具有读数

也可以使用LocationSensor.HasLongitudeLatitude块,直接询问传感器是否具有读数。如图23-6所示。

检查边界

事件LocationChanged的一种通常的用法是检查设备是否在某个边界之内,或在某个设定区域内。例如,看图23-7中的代码,每次当传感器获得的读数显示某人离开零度经线的距离超过0.1度时,让手机产生震动。

图23-7 如果读书远离了零度经线,则手机发出震动

这种边界检查功能可以有很多应用,例如对于假释犯,如果他们离开家的距离接近规定的合法距离时,应用会发出警告;或者对教师及家长来说,可以监控孩子是否离开了操场。如果你想看到更为复杂的例子,参见第18章中关于条件块的讨论。

位置信息的来源:GPS, WiFi以及基站编码

有几种方法可以确定Android设备的位置,最精确的方法是通过卫星,美国政府维护的组成GPS系统的卫星,可精确到数米。但是如果在室内,并有高楼或其他物体遮挡,则无法获得读数。需要在开阔地区并且系统中至少要有三颗卫星。

如果无法使用GPS,或者用户的设备禁用了这一功能,也可以通过无线网络获得位置信息。设备需要在WiFi路由器附近,当然,你获得的经纬度读数是这台WiFi设备的位置信息。

判断设备位置的第三种方式是通过移动网络的基站编码(Cell ID),基站编码对手机位置的判断来源于手机与附近基站之间通信信号的强弱,这种方式通常不够精确,除非你周围有很多个基站。不过这种方式与GPS或WiFi连接相比,是最省电的。

使用方向传感器

游戏中会用到方向传感器(OrientationSensor),用户通过倾斜设备来控制物体的运动。方向传感器也可以用作指南针,确定手机所指的方向。

方向传感器有五个属性,除了航空工程师外,大多数人都不熟悉这些参数:

滚动参数Roll(左-右):当设备水平时,Roll的值为0°,当设备向左倾斜时,增加到90°,而当设备向右倾斜时,减少到-90°。

倾斜参数Pitch(前-后):当设备水平时,Pitch为0°,当设备头朝下时,Pitch值增加到90°,当设备翻转至面朝下时,增加到180°。同样,当设备的下端朝下时,Pitch值减小到-90°,当继续翻转至面朝下时,Pitch值为-180°。

方位角参数Azimuth(指南针):当设备顶端指向正北时,Azimuth的值为0°;指正东时,值为90°;指正南时,值为180°;指正西时,值为270°。

强度参数Magnitude(滚动球的速度):Magnitude参数的返回值在0-1之间,表示设备的倾斜程度。它的值表示在设备表面滚动的球体所能施加的力的大小。

角度参数Angle(滚动球的角度):Angle返回设备倾斜的方向。即,在设备表面滚动的球体所能施加的力的方向。

方向传感器同样提供了方向变化事件,每次当方向发生变化时,会触发该事件。为了进一步探索这些属性的意义,写一个应用来描述这些属性如何随设备的倾斜而变化。在用户界面中添加五个方向label,另外五个标签用于显示前面所述的属性当前的值。如图23-8所示添加相关的块。

图23-8 显示方向传感器数据的代码块

使用滚动参数Roll

现在通过用户对设备的倾斜,来实现图像在屏幕上的左右移动,就像在射击或赛车类游戏中那样。拖入一个Canvas组件,宽度设为“Fill parent”,高度为200像素。然后向Canvas上添加一个ImageSprite组件,并在Canvas下方添加一个名为RollLabel的Label,来显示Roll属性值。如图23-9所示。

图23-9 滚动操作如何控制图像移动的用户界面

方向传感器OrientationSensor的Roll属性表示手机的倾斜方向:向左或向右(即,如果你正握手机并稍向左倾斜,获得的读数为正值;反之向右倾斜则为负值)。因此,利用图23-10中的事件处理程序,用户可以实现对运动的控制。

图23-10 利用OrientionChanged事件来响应Roll属性的变化

图中的乘法块让roll属性乘以-1,因为向左倾斜时,roll的值为正,但我们希望物体向左移动(因此x坐标的值变小)。要了解动画应用中的坐标系统的工作原理,参见第17章。

需要注意的是,这段程序是针对纵向模式(正握手机时)编写的,而非横向。事实上,当你过度地倾斜手机时,屏幕会自动转成横向模式,而图像则被卡在屏幕的左边。这是因为当设备向一侧倾斜时,如向左倾斜时,获得的roll属性的读数一直是正值,因此图像的x坐标值也一直在变小,如图23-10所示。

如果App Inventor提供了解决上述问题的方法,应该是(1)在手机上取消屏幕的自动旋转功能;或者(2)区分手机的纵横模式,针对不同模式给出不同的物体运动公式。App Inventor未来会提供这样的支持,但现在你还需要向用户说明应用的运行方式。

控制运动的方向及速度

前面的例子中图像可以左右移动,如果想实现任意方向的运动,可以使用OrientationSensor的Angle(角度)及Magnitude(强度)属性,这正是第5章的游戏中让瓢虫移动的属性。

在图23-11中的块是一个测试程序,用户可以通过倾斜设备来实现任意方向的运动(需要两个Label及一个ImageSprite).

图23-11 用角度和强度来实现移动

试试看,强度属性的值介于0至1之间,代表设备的倾斜程度,在这段测试程序蒸南瓜,倾斜的程度越大,图像移动的越快。

手机用作指南针

指南针应用,以及像谷歌星空这样的应用,需要知道手机所指的方向(东南西北),谷歌星空就是根据手机的指向,将方向信息叠加在星座信息上。

属性Azimuth可以用于表示方向。Azimuth的取值介于0°至360°之间:正北为0,正东为90,正南为180,正西为270。因此当Azimuth值为45时,意味着手机指向东北,135时指向东南,225时指向西南,315时指向西北。

图23-12中的块创建了一个简易的指南针,可以用文字显示手机所指的方向(如西北)。

你会发现,程序只能显示四个方向之中的一个:东南、东北、西南、西北。你可以挑战一下自己,看能否修改程序,当手机的指向在某个范围内时,显示四个正方向(正北、正南、正东、正西)。

图23-12 编程实现一个简易的指南针

加速度传感器

加速度是速度随时间的变化率,如果你踩下油门,车会加速——车速会以一定的比率增加。

在Android手机中内置了加速度计,用于测量加速度,但测量的参照系不是静止的手机,而是自由下落中的手机:如果你让手机下落,它所记录的加速度读数为0。一句话,读数与重力有关。

如果感兴趣相关的物理知识,可以去查阅相关的书籍,但本小节中,我们将充分讨论加速度计,为你建立一个良好的开端,并仔细分析一个能够拯救生命的应用。

响应设备的摇晃

图23-13 手机摇晃时发出声音

如果学习过第1章(Hello猫咪),那么你已经使用过加速度传感器了:使用Accelerometer.Shaking事件,当手机摇晃时,设备发出猫叫声。如图23-13所示的块。

使用加速度传感器的读数

像其他传感器一样,加速度计也具备侦测读数变化的事件:AccelerometerSensor.AccelerationChanged,这个事件有三个参数,对应加速度在三个维度上的分量:

xAccel:当设备向右倾斜时,其值为正(即,左侧的边缘在上升);当设备向左倾斜时,其值为负(设备右侧边缘上升)。

yAccel:当设备的底部上升时,其值为正;当设备顶部上升时,其值为负。

zAccel:设备的显示屏朝上时,其值为正;显示屏朝下时,其值为负。

检测自由落体

我们知道,如果加速度的读数为0,那么设备一定在做自由落体运动,基于这一认识,我们可以在AccelerometerSensor.AccelerationChanged事件中,通过检测读数来模拟自由落体事件。这些代码经过反复测试,可以用于老年人的自动求救:一旦侦测到发生跌倒,就会自动向外发送短信。

图23-14中显示了这个应用使用的块,当发生自由落体运动时,给出一个简单的报告(用户可以点击“重置”按钮进行再次检测)。

图23-14 当自由落体发生时进行报告

每当传感器获得读数,这些块都要在x、y、z三个维度上进行检查,看是否这些值接近于0(即它们的绝对值小于1)。如果三者都接近于0,应用将改变label的属性,来表示设备正处于自由落体状态。当用户点击“重置”按钮时,显示状态的label又被重新设为初始值(“没发生跌落事件”)。

如果你想试用这个应用,可以从这里下载:http://examples.oreilly.com/ 0636920016632。

用校准值测定加速度

图23-15 校准加速度的读数

加速度传感器的读数用自由落体时的状态进行校准。如果你想测量设备平放在桌上时的加速度的相对值,则必须与标准读数进行校准。校准的意思是与标准值进行核对、判定或检测;在本例中,标准值就是将设备平放在桌上时的读数。

校准需要用户将设备平放在桌面上,然后点击“校准”按钮,这时 应用将读出平面上的加速度值,这些值会在稍后的AccelerationChanged事件中用来判断新读数的偏差,并显示设备是否在某个方向上进行了快速的移动。

图23-15中显示了一个样板应用,让用户校准读数并测试加速度。

可以在此下载并安装这一应用:http://examples.oreilly.com/0636920016632/。运行应用,并将手机放在桌上,点击校准按钮,将显示“在平台上的读数”,此时如果缓慢地拿起手机,“显著变化”区域的读数不会变化(显示“无”);但如果你快速提起手机,则Z-变化将由“无”变为“有”,如图23-15所示。同样,如果快速沿桌面移动手机,则X或Y也会有显著加速。在图23-16中显示了设置校准初始值的块。

图23-16 校准程序的初始设置

这些块从加速度传感器中获取读数,并显示在三个label中:XCalibLabel、YCalibLabel及ZCalibLabel,并初始化另外三个显示加速度变化结果的label。

当手机水平放置时,加速度计的zAccel读数大约为9.8,而xAccel及yAccel读数约等于0,这些值的偏差表明了加速度计的精确度。获得了基准读数之后,可以通过比较新的测量值与基准值之间的偏差,侦测到手机在x、y或z方向的加速度变化(这种方法与第18章中的边界检测程序相类似)。图23-17显示了这一方法的具体实现。

图23-17 用基准值来侦测加速度变化

当设备移动时,将触发这段程序。通过测量新的加速度值,并与静止时的基准值进行比较,从而判断加速度之是否产生了显著变化。假设ZCalib.Text记录的基准值为9.0,此时如果缓慢地拿起手机,那么新的读数将保持在9左右,并且不会报告有显著变化;但如果是快速地拿起手机,则读数会明显增大,此时程序将报告加速度“有”显著变化。

小结

传感器是移动应用中最富魅力的部分,因为它们实现了用户与环境之间实实在在的交互。无论是用户体验,还是应用开发,移动计算为我们带来了无限的商机。不过依然要精心地构思一个应用,来决定何时、何地以及如何使用这些传感器。很多人会担心隐私问题,如果应用中涉及到个人的敏感信息,他们可能会放弃使用。尽管如此,在游戏、社交网络、旅行以及其他众多的选项中,仍然有无限多种可能开发出有积极意义的应用来。

第22章 数据库

Published by:

Facebook的数据库中,有每位用户的账户信息、好友列表以及发布的信息,Amazon的数据库中有你能买到的任何东西,而Google的数据库中有互联网上的每个页面的信息。你自己的应用虽然没有那么大的规模,但一个正规的应用都会用到数据库组件。

在大多数的编程环境中,编写与数据库通信的应用是一种高级编程技术:要搭建数据库(软件)服务器,如Oracle或MySQL等,并编写程序与数据库建立连接。在大学里,这些内容通常要在软件工程或数据库这样的高级课程中才会涉及。

App Inventor承担了与数据库(以及许多其它有用的事情)有关的这部分繁琐的设置,在这个语言中,提供了数据库组件,将数据库通信简化为单纯的读写操作。应用可以直接将数据保存在Android设备上,也可以保存到集中式网络数据库中,从而实现在不同设备与其他人之间的数据共享。

保存在变量及组件属性中的数据属于临时存储:如果用户在表单中输入某些信息然后关闭应用,那么当应用重新打开时,这些信息将不复存在。想要长期保存信息,就需要将它们保存到数据库中。数据库中的信息被称为永久信息,因为当应用在关闭后重新打开时,数据依然存在。

作为例子,考虑第4章开车不发短信的应用,那个繁忙时自动回复短信的应用。这个应用允许用户输入一条个性化的信息,作为收到短信时的自动回复信息。如果用户将信息改为“我在睡觉,别来烦我”,然后关闭了应用,当重新打开应用时,定制的自动回复信息依然是“我在睡觉,别来烦我”。因此,定制信息必须保存到数据库中,在每次启动应用时,再将信息从数据库提取到应用中。

在TinyDB中永久保存数据

App Inventor提供了两个便于操作数据库的组件:TinyDB及TinyWebDB。TinyDB用于直接在Android设备上永久保存数据,它适合于那些极其私人化的应用,如开车不发短信,这类应用不需要让数据在不同设备及人群之间共享。而TinyWebDB则将数据保存到web数据库中,并可实现不同设备之间的共享。能够通过web数据库访问数据,这是多人游戏及应用的基础,用户可以借此分享信息(如第10章的出题应用)。

这两个数据库组件非常相似,但TinyDB更简单些,因此我们先来研究它。首先,不需要任何设置就可以直接使用它,此外,数据直接保存在设备上,并于应用相关联。

使用TinyDB.StroeValue块来实现数据的长期存储,如图22-1所示,这段代码来自于“开车不发短信”。

图22-1 TinyDB.StoreValue块将数据永久保存到设备中

数据库存储中用到了tag-value(标签-值)模式,在图22-1中,数据的标签是“responseMessage”,而值是用户输入的内的自动回复信息,比如“我在睡觉,别来烦我”。

标签是数据的名称,是信息查询的依据,而致才是数据本身。可以将标签理解为钥匙,必须用它从数据库中提取已经存储的数据。

同样,可以将App Inventor的TinyDB数据库理解为一个表,其中包含了许多tag-value对儿,在图22-1中的TinyDB.StoreValue块执行完成后,设备数据库将增加一条输入,如表22-1中所列。

表22-1 存储到数据库中的tag-value对:“responseMessage”-“我在睡觉,别来烦我”
tag value
responseMessage 我在睡觉,别来烦我

一个应用中可以有许多tag-value对,用来永久保存需要保留的各种数据项。标签必须是文本,而值既可以是单个的数据(一段文本或一个数字),也可以是一个列表。每个标签只能对应一个值,当你使用同一个标签保存一个新值时,将覆盖原来的值。

从TinyDB中提取数据

从数据库中提取数据要用到TinyDB.GetValue块。在调用GetValue块时,通过提供标签(tag)来请求特定的数据。在“开车不发短信”中,使用在保存数据时(StoreValue)用过的标签“responseMessage”来请求定制的回复信息。调用GetValue所获得的返回数据,必须插入到一个变量中。

通常要在应用打开时从数据库中提取数据。App Inventor提供了一个特别的事件处理程序Screen.Initialize,应用启动时会触发该程序。需要格外小心地处理数据库为空的情况(如,应用第一次启动时),因此当使用GetValue时,要指定一个“valueIfTagNotThere”参数,一旦数据库为空,则GetValue将返回该参数值。

图22-2中的块显示了在“开车不发短信”中,如何在应用初始化时,使用Screen.Initialize加载数据。

这里将GetValue的返回值写入到ResponseLabel组件中。如果数据库中已经存储过数据,则将读取的数据写入ResponseLabel中,如果没有与标签responseMessage相对应的数据,则将“我正在开车…”写入Label。

图22-2 应用启动时加载数据的一种模式

用TinyWebDB保存并共享数据

TinyDB组件将数据保存在Android设备的本地数据库中,这一点适用于那些不需要数据共享的个人应用,例如很多人都可以下载“开车不发短信”应用,但每个人都使用个性化的自动回复信息,这类信息不需要与其他人共享。

当然,更多的应用需要数据共享:像Facebook、Twitter以及像Words With Friends这样流行的多人游戏,这些应用的数据库必须运行在网络上,而非设备上。另一个例子是第10章的“出题/答题”应用,某人在手机上生成了一份测试,并将其保存到网络数据库中,这样其他人就可以在其他手机上加载测验,并回答问题。

TinyWebDB是TinyDB的web版本,可以让应用将数据保存到web上,方法与TinyDB类似,使用StoreValue与GetValue协议。

默认情况下,TinyWebDB组件使用由App Inventor团队创建的web数据库保存数据,从http://appinvtinywebdb.appspot.com可以访问到该数据库。该网站包括一个数据库,并能响应来自web的保存及提取数据的请求;此外,还提供了一个人类可读的web接口,可以让数据库管理员(也就是你)能够查看到在此保存的数据。

感兴趣的话,可以在浏览器中访问http://appinvtinywebdb.appspot.com,并检查保存在此的tag-value类型的数据。

这个默认的数据库仅用于开发,对于所有App Inventor程序员提供了有限的空间和权限。由于所有的App Inventor应用都可以使用该数据库,因此不能确保你的数据不被其它的应用所覆盖。

如果你只是在研究学习App Inventor,或者在项目的早期阶段,默认的web数据库就足够了,但如果你想创建正式发布的应用,从某种意义上讲,你需要建立自己的web数据库。由于我们正在学习,因此可以使用默认的数据库。在本章的后面将学习如何创建自己的web数据库,并配置TinyWebDB,来替代默认的数据库。

在这一节中,我们通过一个投票应用(如图22-3所示)来描述TinyWebDB的用法。该应用具有如下特性:

图22-3 投票应用:将投票结果保存到TinyWebDB中
  • 每次应用打开之后,提示用户输入自己的email地址,该地址既是用户名,又是保存到数据库中的投票信息的标签(tag);
  • 任何时候用户都可以提交新的投票内容,这种情况下,原有的投票内容将被覆盖;
  • 用户可以看到群组中每个人的投票结果;
  • 为简单起见,需要投票的议题在应用之外发布,如课堂上,教师宣布议题并要求每个学生进行电子投票。(注意,这个例子的功能可以扩展,在应用中允许用户输入并提示投票议题。)

用TinyWebDB保存数据

TinyWebDB.StoreValue的作用与TinyDB.StoreValue一样,只不过是将数据保存到Web上。在这个投票的例子中,假设用户会在文本框VoteTextBox中输入投票内容并点击按钮VoteButton发送投票结果。将投票结果保存到web数据库中,以便其他人也能看到它,我们将编写如图22-4所示的事件处理程序VoteButton.Click。

图22-4 用VoteButton.Click事件处理程序将投票结果保存到数据库中

用于识别数据的标签是用户的email地址,之前已经被保存到变量myEmail中(稍后将看到),而要保存的值是用户在VoteTextBox输入的内容。因此,如果用户的email地址是“wolber@gmail.com”,而他的投票是“Obama”,则作为整体存入数据库的信息如表22-2所示。

表22-2 记录在数据库中的标签(tag)及值(value)
tag value
wolber@gmail.com Obama

TinyWebDB.StoreValue块将这个tag-value对发送到位于http://appinvtinywebdb.appspot.com的web数据库服务器中。由于这里用的是默认的服务,会显示来自于各种应用的很多数据,因此在第一个显示窗口中,有可能看到,也有可能看不到你的数据。如果看不到,可以用页面上的GetValue链接用特定标签来搜索数据。

用TinyWebDB编程时,使用数据库服务器的web接口来测试是否按要求被保存起来。

用TinyWebDB来请求并处理数据

用TinyWebDB提取数据要比TinyDB复杂得多。由于TinyDB的GetValue操作是直接与Android设备上的数据库通信,因而可以立即获得返回值,但使用TinyWebDB的应用则需要跨越网络来请求数据,因此需要分两步来实现。

首先使用TinyWebDB的GetValue请求数据,稍后再来处理TinyWebDB.GotValue事件处理程序。实际上,TinyWebDB.GetValue应该叫做“RequestValue(请求值)”,因为他只是向web数据库发出请求,而请求实际上并不能立即“get(得到)”一个值。为了更清楚地了解二者之间的差别,可以对比图22-5中的TinyDB.GetValue与图22-6中的TinyWebDB.GetValue。

图22-5 TinyDB.GetValue块

图22-6 TinyWebDB.GetValue块

TinyDB.GetValue块立即得到返回值,因此该块的左侧有一个插头以便可以将返回值保存到一个变量或属性中;而TinyWebDB.GetValue块不能立即得到返回值,因此左侧没有插头。

对TinyWebDB而言,当web数据库实现了请求并将数据返回给设备时,将触发TinyWebDB.GotValue事件。因此整个提取数据过程分为两步,首先在一个地方调用TinyWebDB.GetValue,然后再编写TinyWebDB.GotValue事件处理程序,来处理实际接收到的数据。像TinyWebDB.GotValue这样的程序有时被称作回调过程,因为实际上是某些外部实体(这里是web数据库)在处理完你的请求之后,反过来调用你的程序。就像在一家繁忙的咖啡店点餐一样:你点餐,然后等待咖啡师喊你的名字,你才能真正拿到你的饮料。在同一时间,咖啡师会按顺序从每个人手里收取点餐单(而且所有人都在等待自己的名字被喊到)。

GetValue-GotValue连动

在我们的例子中,需要保存并提取一个投票者的列表,并最终显示所有人的投票结果。

最简单的方案是在应用启动时,在Screen.Initialize事件中发出请求来提取列表数据。如图22-7所示(在本例中,用“voterlist”为标签向数据库发出请求。)

图22-7 在Screen.Initialize事件中请求数据

当应用从数据库收到投票者列表的数据时,TinyWebDB.GotValue事件被触发,图22-8显示了处理这个返回列表的块。

图22-8 使用TinyWebDB.GotValue事件处理程序处理返回的列表

程序GotValue附带了参数valueFromWebDB,其中保存着向数据库请求的数据。像valueFromWebDB这样的事件附带的参数,只在该事件处理程序范围内有效(隶属于该事件处理程序),因此无法在其他事件处理程序中引用该参数。

这一点看似有些费解,但一旦你熟悉了这些保存局部数据的参数,你自然会联想到那些适用范围更大的数据(在整个应用中随处可用):变量。理解了这一点,也就理解了GotValue中的关键一步:将返回的数据valueFromWebDB转移到一个变量中。这里是将数据转移到变量voterList中,之后可以在其他的事件处理程序中使用该变量。

通常会在GotValue中同时使用if块,原因是,如果数据库中不存在被请求的数据,则返回值为空文本(“”),通常这种情况发生在第一次启动应用时。通过检查valueFromWebDB是否为列表,可以确定是否真的有数据返回。如果valueFromWebDB为空(if的测试结果为假),就不必将其写入变量voterList。

无论是TinyDB还是TinyWebDB,都是以相同的方式来获取数据、检查数据及设置数据(到变量中),不同的是,这里预期会收到一个列表,因此测试环节上略有差别。

更为复杂的GetValue/GotValue举例

在相对简单的应用中,图22-8中所示的代码是一种不错的提取数据的方式,但在投票的例子中,我们需要更为复杂的逻辑。说明如下:

  • 应用启动时,程序会提示用户输入Email地址。可以使用Notifier组件弹出窗口来实现这一功能。(Notifier在组件デザイナー组件面板的User Interface中。)用户输入email后,将其保存为变量;
  • 检查完用户的email之后,调用GetValue来提取投票人列表。你能说出为什么吗?

图22-9显示了向数据库请求数据的更为复杂的方案。

图22-9 在这个更为复杂的方案里,在获得用户的email之后调用GetValue

在应用启动时(Screen1.Initialize),Notifier组件提示用户输入他的email地址;用户输入后(Notifier.AfterTextInput),输入的信息保存到变量中,同时用label显示出来,然后调用GetValue来获得投票人列表。需要注意,这里没有在Screen1.Initialize中直接调用GetValue,因为需要首先设置用户的Email地址。

因此当应用初始化完成后,用这些块来提示用户的Email地址,然后以“voterlist”为标签调用GetValue。当从web上返回列表时,GotValue被触发,以下是后续功能的描述:

  • GotValue将检查到达的数据是否不为空(有人已经使用这个应用,并建立了投票人列表)。如果返回值中包含数据(投票人列表),则检查此用户的email是否已经在投票人列表中,如果没有,将其添加至列表,并将更新后的列表保存到数据库;
  • 如果数据库中没有投票人列表,我们将以此用户的email作为唯一的项来创建列表。

图22-10中显示了这一功能所需的块。

在这些块中,第一个if通过调用“is a list?”来检测从数据库返回的值,判断其是否不为空。如果不为空,返回的数据放入变量voterList中。切记,voterList中只有每个使用过该应用的用户的Email地址,但我们不确定当前用户是否也在此列表中,因此需要检查一下:如果此用户不在列表中,则用“add item to list”块将其添加至列表,并将更新后的列表保存到web数据库。

图22-10 使用GotValue块处理数据库返回的数据,根据不同的返回结果确定要执行的操作

如果数据库返回的结果不是列表,则执行ifelse块中的“else”分支;这说明还没有人使用过这个应用。此时需要创建一个新的列表voterList,将当前用户的Email地址作为列表的第一项,然后将这个只有一项的列表保存到web数据库中(同时也希望更多人的加入!)。

用不同的标签请求数据

到目前为止,投票应用值处理了一个用户列表,每个用户都可以看到其他用户的Email地址,但还不能提取并显示每个用户的投票结果。

此前设定在VoteButton的Click事件中,将用户的Email地址与投票结果以“email地址:投票结果”的方式组成tag-value对提交给web数据库。此时如果已经有两个人投票,那么相应的数据库实体中将包含表22-3中的数据。

表22-3 存储在数据库中的tag-value对
tag value
voterlist [wolver@gmail.com,joe@gmail.com]
wolber@gmail.com Obama
joe@gmail.com McCain

当用户点击“ViewVotes”按钮时,应用将从数据库中提取所有投票结果并加以显示。现在假设投票人列表已经提取并保存到变量voterList中,我们可以使用foreach来请求列表中每个人的投票结果,如图22-11所示。

图22-11 使用foreach块请求列表中每位成员的投票结果

这里对变量currentVotesList进行初始化,来清空列表,目的是为了将最新从数据库中获得的投票结果添加到列表中。在foreach中使用TinyWebDB.GetValue来处理列表中的每一个Email地址:以Email地址(voterEmail)为标签向数据库发送请求。需要注意的是,要等到一系列的请求数据返回时触发GotValue事件,才能将投票结果添加到currentVotesList中。

在TinyWebDB.GotValue中处理多标签

我们希望在应用中显示投票结果,事情变得更加复杂了。在点击ViewVotesButton按钮发出请求之后,在TinyWebDB.GotValue中将收到以每个Email地址为标签(tag)的数据,就像“voterlist”标签用于提取用户Email地址列表一样。当应用同时向数据库为不同标签请求多余一项的数据时,就需要在TinyWebDB.GotValue中编写代码来处理所有可能的请求。(你可能想到编写多个GotValue事件处理程序,来分别处理每个请求——知道为什么这样做行不通吗?)

为了处理这种复杂的情况,GotValue事件处理程序可以利用自带的参数tagFromWebDB,它会告诉你当前的返回值来自于哪一个请求。因此,如果标签是“voterlist”,我们可以像之前那样进行处理;如果不是“voterlist”,我们可以假设它是用户列表中某人的Email地址,来源于ViewVotesButton.Click事件处理程序中发出的请求。当这些请求返回时,我们希望将返回的数据——投票人及投票结果——添加到列表currentVotesList中,以便于向用户显示。

图22-12中显示了整个TinyWebDB1.GotValue事件处理程序。

图22-12 TinyWebDB1.GotValue事件处理程序

设置Web数据库

本章前面提到过,设立于http://appinvtiny webdb.appspot.com的默认web数据库仅供原型设计以及应用的测试,在向真正的用户发布应用之前,需要为应用创建一个专用的数据库。

访问网站http://appinventorapi.com/ program-an-api-python/,按照上面的说明就可以创建web数据库。该网站由本书的作者之一Wolber教授创建,网站提供了示例程序以及设置App Inventor web数据库及API(应用程序接口)的说明。按照说明,你可以下载相关的程序,并且只要对配置文件进行少量修改,就可以使用这些程序。经过设置的代码与之前使用的App Inventor默认数据库相同,它运行在Google的应用引擎上——一个云计算服务,运行在Google服务器上免费的web数据库。这样,你就建起了属于自己的web数据库(与App Inventor的协议兼容),几分钟就可以运行起来,并用它来创建web移动应用。

一旦创建并部署了属于自己的web数据库(因为只有你知道它的URL地址),你就可以用它来创建应用。不过还需要在应用中修改TinyWebDB组件的ServiceURL属性,以便组件可以用新的定制数据库来保存及提取数据。图22-13描述了如何操作。

图22-13 将ServiceURL属性修改为你的定制数据库的URL地址

在这个例子中,ServiceURL被设置为http://usfweb service.appspot.com,是本书的作者之一为他的学生们创建的一个web数据库(图22-13中”appsport.com”后面的部分被输入框遮挡住了)。设定了ServiceURL之后,所有的TinyWebDB.StoreValue及TinyWebDB.GetValue的调用都将执行这个特定的URL。

小结

通过TinyDB及TinyWebDB组件,App Inventor可以很容易地实现数据的永久存储。数据以标签-值(tag-value)对的方式存储,保存数据时使用的标签也用于之后对数据的提取。TinyDB用于将数据直接保存在设备上;当数据需要在手机之间分享时(如多人游戏或投票应用),就需要使用TinyWebDB。TinyWebDB更为复杂,尤其在获取数据的环节,除了用GetValue来请求数据,还要设置回调过程,即GotValue事件处理程序,同时还要设置web数据库服务。

一旦你可以得心应手地使用数据库——尤其是掌握了获取、检查及设置数据的要点,要不了多久,你就能创建更为复杂的应用了。

第21章 定义过程

Published by:

像App Inventor这类的编程语言通常会提供一组基本的内置功能,对于app inventor来说,就是一组基本块。编程语言还提供一种功能扩展的方法,即,向语言中添加新的子程序(块)。【在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram),是一个大型程序中的某一部份代码,由一个或多个语句块组成。它负责完成某项特定任务,而且与其他代码相比,具备相对的独立性。——译者注】在App Inventor中,通过定义过程(procedure),即,命名一些顺序执行的块,来实现功能的扩展。应用中可以像调用App Inventor中的预定义块一样,调用这些过程。本章中你将看到,创建这样抽象的过程的能力对于解决复杂问题是非常重要的,这是创建真正好应用的基石。

当家长对孩子说“睡觉前去刷牙”时,他们的实际含义是“从架子上拿起牙刷牙膏,向牙刷上挤一点牙膏,在每颗牙齿上刷10秒钟(哈哈!)”,等等。“刷牙”就是一种抽象:为一系列的低级指令起一个公认的名称。此处,家长要求孩子完成他们已经认可了的“刷牙”的一系列指令。

你也可以在编程中创建这样的有名字的一系列指令,有些编程语言称之为函数(function)或子程序(subprogram),在App Inventor中,被称为过程(procedure)。过程就是一组顺序执行的有名字的块,在应用中可以随时随地调用它。

图21-1就是一个过程的例子,它的功能是以英里为单位,计算两个GPS坐标之间的距离。

图21-1 计算两点间距离的过程

不必急于探究这个过程中的内部构件,只要知道对于你所使用的编程语言来说,这样的过程扩展了它的功能。如果每个家长每天晚上都要向他的孩子解释“刷牙”的步骤,那么这个孩子到了五年级可能还是不会刷牙。说“刷牙”是一种更有效的方式,而且每个人都会在睡觉之前去刷牙。

同样的道理,在设计或编写一个大型应用时,一旦定义好了distanceBetweenPoints这个过程,你就会忽略它的内部实现细节,而只是简单地使用(或调用)它的名字。这种抽象能力对于解决大型问题来说是至关重要的,可以将大型的软件项目分解成若干个便于管理的代码块。

过程还可以有助于减少错误,因为它们可以省去很多冗余的代码:只要在一处定义了过程,应用中就可以随处调用它。因此,假如应用中要计算你的当前位置与其他10个点之间的最近距离,你不必拷贝粘贴10次图21-1中的块,相反,你只需要定义这个过程,并在需要时调用它即可。此外,那种拷贝粘贴块的方法还非常容易引入错误,因为一旦你想修改程序,就必须找到所有的拷贝,并逐个以相同的方式修改它们。想象一下,你试图在一个有1000行或块的代码中,找到5-10个曾经粘贴过的代码块!与其被迫地拷贝粘贴这写块,不如用过程在一处将代码块封装起来。

最后,过程将有助于建立代码库,让这些代码在其他应用中可以被重用。即便是创建一个非常具体的应用,有经验的程序员总会在必要时设法考虑重用其他应用中的部分代码。有些程序员从未创建过应用,他们只是专注与创建可重用的代码库,以便其他程序员以此来创建他们自己的应用。

消除冗余

看一下图21-2中的代码块,能否发现其中的冗余。

图21-2 “随手记”应用中的冗余代码

这里的冗余代码指与foreach块有关(实际上是整个foreach块以及它上面的”set NotesLabel.Text to”块),例子中的三个foreach的作用都是显示笔记列表,只是使用的场合有所不同:当添加新项、删除某一项,以及应用启动从数据库加载列表时。

作为一个有经验的程序员,一旦看到这样的代码,脑子里会立即敲响警钟,甚至不必等到开始拷贝粘贴第一段程序中的代码,他们知道最好是将这些冗余的代码封装在一个过程里,这样既保证程序有很好的可读性,也可以使后来的修改变得容易。

因此,有经验的程序员会创建一个过程,将冗余代码块放在其中,并在原来使用冗余代码的地方调用这一过程。应用的执行结果完全一样,但更易于维护,也让其他程序员更容易地加以利用。这种代码(块)的重新整理的过程成为重构。

定义过程

我们来创建一个过程,实现图21-2中那些冗余代码的功能。在App Inventor中,定义过程几乎与定义变量一样简单:从Procedures抽屉中拖出一个“to procedure”块或“to procedure result”块。如果过程需要通过计算返回一个结果,则使用后者(我们将在本章稍后的部分讨论它)。

图21-3a 点击默认名称“procedure”

图21-3b 将过程名改为“displayList”

在拖出“to procedure”块后,可以修改过程名称:点击默认名称“procedure”并输入新名称。由于冗余代码块的作用是显示笔记列表,因此重构时将过程名设为“displayList”,如图21-3所示。

下一步是向过程中添加块,此时就用现有的冗余块,将它们从事件处理程序中拖出并放在displayList块中,如图21-4所示。

图21-4 封装了冗余代码的过程displayList

现在我们可以用过程来显示笔记列表了,在应用的任何一处,都可以很容易地调用它。

调用过程

像“displayList”和“刷牙”这样的过程是一个包含了某种功能的实体,它们只有在被调用时,才能体现出这种功能。因此,以上我们只是创建了过程,却并没有调用它。调用它意味着要运行它,或者说来实现它。

在App Inventor中,可以从Procedures抽屉中拖出一个以“call”开头的块来调用一个过程。每当定义了一个新的过程,procedures抽屉中就会显示一个新的块,即定义一个过程,就是向Procedures抽屉中添加一个新块,如图21-5所示。

图21-5 定义好一个过程后,Procedures抽屉中就会出现一个新的“call” 块

你一直都在用“call”块来调用App Inventor中的预定义函数,如Ball.MoveTo以及Texting.SendMessage。当你定义了一个过程,就相当于创建了自己的块,也相当于你扩展了App Inventor语言,新的“call”块让你可以使用自己的创造。

在“随手记”的例子中,三次拖出“call displayList”块来取代三个事件处理程序中的冗余代码,如,ListPicker1.AfterPicking事件处理程序(删除一条笔记)修改的结果如图21-6所示。

图21-6 使用“call displayList”来调用放在过程中的那些块

程序计数器

要理解“call”块的运行机制,要想象应用中有一个指针,它随着块的运行而移动。在计算机科学中,这个指针被称作程序计数器。

程序计数器随着事件处理程序中的块的运行而移动,当它遇到一个“call”块时,它会跳到所遇到的过程中,并开始随着过程中的块的执行而移动,;当过程执行完成,程序计数器再跳回到此前的位置(“call”块处),并从此处开始继续移动。以“随手记”为例,“remove list item”块执行完成后,程序计数器跳到displayList过程中,并随过程中的块(设置NotesLabel.Text属性为空,以及foreach循环)移动;最后程序计数器在回到TinyDB1.StoreValue块。

为过程添加参数

过程displayList将冗余代码重整到一处,这使得程序更加容易理解,你可以在更高层次上理解这些事件处理程序,而忽略掉如何显示列表的细节。这样做的另一个好处是,如果想要修改列表的显示方式,就只需修改一处代码(而不是三处)。

就过程的通用性而言,displayList是有局限的,因为该过程是针对特定的列表(notes)而设定的,而且用指定的label(NotesLabel)来显示列表内容,它不能用于显示其他列表,比如应用的用户列表,因为过程中的要素定义的过于具体。

App Inventor以及其他编程语言都提供了一种称为参数的机制,用于构造更为通用的过程。过程为了实现它的预设功能所必须的信息就由参数来提供,以睡前刷牙为例,有可能将牙膏的类型和刷牙时间设定为刷牙过程的参数。

通过点击过程块左上角的蓝色标记,就可以为过程设定参数。对于displayList过程,我们定义了一个名为“list”的参数,如图21-7所示。

图21-7 在过程中引入了list作为参数

即使是定义了参数,但foreach块中仍然直接引用特定列表“notes”(插入到foreach块的“in list”插槽中)。而我们希望在过程中使用我们传递的参数list,因此将对“global notes”的引用替换成对“get list”的引用。如图21-8所示。

图21-8 现在foreach中使用了传递来的参数“list”

新版本的过程更加通用:在调用displayList时,无论传入什么样的列表,displayList都能显示它。在向过程添加参数时,App Inventor会自动为“call”块添加一个对应的插槽,因此当displayList添加了参数list之后,“call displayList”块就变成图21-9中的样子。

图21-9 现在调用displayList时,需要指明要显示的列表

过程定义中引入的参数list被称为“形式参数”,而“call”块中与之相对应的插槽被称为“实际参数”。当在应用中的某处调用过程时,必须为过程中的每个“形式参数”提供一个“实际参数”。

对于“随手记”的应用来说,将列表“notes”作为实际参数添加到“call”块的list插槽中。ListPicker.AfterSelection的修改结果如图21-10所示。

图21-10 在调用displayList时,将notes作为实际参数传入

现在当displayList被调用时,列表notes被传递到过程中,来取代形式参数list。此时,程序计数器随着过程中的每个块的运行,它的指向是参数list,而实际上处理的是变量notes。

图21-11 过程displayList可用于显示任何列表,而不仅仅是notes

由于有了参数,过程displayList可以用于处理任何列表,而不仅仅是notes。例如,如果“随手记”应用可以在一组用户中共享,而你想查看一下用户列表,就可以调用displayList并传入userList参数。如图21-11所示。

过程的返回值

关于过程displayList的可重用性,还有一个问题需要讨论——你能猜到是什么吗?如前所述,它可以显示任何数据列表,但也只能在标签NotesLabel中显示。如果你想用其他的界面元素(如另一个label)来显示列表(如userList),该如何是好呢?

图21-12 “procedure result”块

一个方法就是重构过程——将它的功能从“用指定label显示列表”改为“只返回一个文本对象,它可以被显示在任何地方”。为此,需要使用“procedure result”块来取代“procedure”块,如图21-12所示。

你会发现与“procedure”块相比,“procedure result”块的底部有一个额外的插槽,将一个变量放入插槽,这个变量将被返回给调用者。因此,正如调用者可以向过程以参数的方式传入数据一样,过程也可以以值得方式将数据返回给调用者。

图21-13显示了上述过程的改写版本,现在使用的是“procedure result”块。注意,由于过程的作用变了,因此名称也由displayList改为convertListToText(将列表转换为文本)。

图21-13 过程convertListToText返回一个文本对象,调用者可以将其放在任何一个label中

在图21-13所示的块中,变量text用来保存foreach循环中通过遍历列表而生成的文本。用text变量取代之前使用的过于具体的NotesLabel组件。在foreach执行完毕后,变量text包含了列表中的所有项,而且项之间以换行符“n”分隔(即“item1nitem2nite3”)。最后,将变量text插入return插槽,返回给调用者。

在定义“procedure result”时,与“procedure”相比,对应的“call”块看起来略有不同,如图21-14中所做的比较。

图21-14 下面的有返回值的“call”必须插入到某个插槽中

不同的是在“call convertListToText”块的左侧有一个插头,这是因为当“call”块运行时,过程在执行一系列指令后将向“call”块返回一个值,必须有某个插槽可以接收这个返回值。

在这种情况下,调用块“call convertListToText”的返回值可以插入到任何一个label的Text属性中,以notes列表为例,需要显示列表的三个事件处理程序都可以调用这一过程,如图21-15所示。

图21-15 将列表notes的内容转换为文本,并用NotesLabel显示出来

更重要的是,由于过程的定义更具通用性,不需要引用任何特定list或label,因此应用中可以使用convertListToText在任何一个label蒸南瓜显示任何一个列表。像图21-16中的例子那样。

图21-16 这一过程再也不必与一个特定的Label组件捆绑在一起

在应用中重用块

通过过程的方式实现代码的重用不必只限于单独的应用,有许多过程,如convertListToText,可以用在你创建的任何应用中。事实上,有许多组织和编程社区都在为他们感兴趣的领域创建过程代码库,例如动画过程的代码库。

通常编程语言会提供一个“import(导入)”功能,可以在任何应用中引入其他的代码库。App Inventor目前没有这项功能,不过正在开发之中。同时,也可以在一个特定的“库应用”中创建一些过程,并复制该应用的代码,作为一个新建项目的基础代码。

第二个例子:求两点间距离

在displayList(convertListToText)例子中,我们将过程定义描述为一种消除冗余代码的方法:你开始写代码,随后发现代码存在冗余,于是整理代码消除冗余。无论如何,一个软件的开发人员或开发团队在应用开发的初期都会创建很多过程,同时也考虑到要重用部分代码。这样的规划可以在项目过程中节省大量时间。

考虑一项应用:确定离某人当前位置最近的本地医院,某些东西在紧急情况下会派上用场的。以下是这个应用的高层设计描述:

应用启动时,以英里为单位计算两点之间的距离,起点是当前所在位置,终点是发现的第一家医院。然后再寻找第二家医院,以此类推。在求得若干个距离后,判断最短距离的医院,并显示它所在位置的地址。

从以上描述中,你能断定应用中需要什么样的过程吗?

通常,一段描述中的动词提示了所需的过程。重读一遍描述,正如“等等”所提示的,这是另一个线索。这种情况下,“求出两点之间的距离”与“判断这些距离中最短的”成为两个必需的过程。

现在考虑设计一个过程distanceBetweenPoints(两点间距离)。在设计过程时,首先要确定过程的输入及输出:调用者需要向过程传递实现过程的功能所需的参数,而过程要向调用者返回执行结果。在这里,调用者需要向过程传递两个点的经度及纬度值,如图21-17所示;而过程的任务是以英里为单位返回两点之间的距离。

图21-17 调用者想过程传递了4个参数,并收到一个距离

图21-18中显示了我们在本章开始时提到的那个过程,使用公式求得两个GPS坐标点之间的近似英里数。

图21-18 过程distanceBetweenPoints

图21-19显示了对上述过程的两次调用,每次都会求出当前位置与指定医院之间的距离。

第一次调用中,起点为用户当前所在位置的LocationSensor(位置传感器)读数,终点是St. Mary’s hospital(圣玛利亚医院),计算的结果保存在变量distanceStMarys中;第二次调用也类似,只是将终点的数据改为CPMC Hospital(加州太平洋医疗中心医院)的经纬度。

接下来程序比较两个距离并返回最近的医院。但是如果还有更多的医院,那就需要在一个距离列表中进行比较,并找到最小值。依你所学,你能写出这个过程吗?将其命名为findMinimum,接受一个数值列表作为参数,并返回最短距离在列表中的索引值。

图21-19 两次调用distanceBetweenPoints过程

小结

像App Inventor这样的编程语言提供了一个内置功能的基本集,而过程是一种新功能的提取,它扩充了app inventor语言。App Inventor不提供显示列表的块,于是由你来做;那么是否需要一个计算两个GPS坐标间距离的块呢?答案是靠我们自己来创造。

想要建造大型的、可维护的软件,以及在解决复杂问题时免于不断地纠缠于细节之中,则定义高级过程的能力是至关重要的。过程是将代码块封装起来,并起一个名字。在编写过程时,你会关注这些块的细节,但对程序的其他部分而言,这个过程只是一个抽象的名字,你可以在更高层次上来引用它。

第20章 循环

Published by:

计算机最擅长做的事情就是“重复”——像儿童一样不厌其烦地重复做一件事,而且重复的速度很快,可以在1毫秒内列出你的全部Facebook好友。

本章将学习如何用有限的几个块来编写可以重复执行的程序,而不必反复拷贝粘贴同一段代码;还将学习与列表有关的操作,如给电话号码列表中的每个号码发送一条短信,以及为列表项排序。通过学习,你将了解到如何用循环块来有效地简化程序。

控制程序的执行:分支及循环

图20-1 让程序循环执行的重复块

在前几章中,我们学习了用一组事件处理程序来定义应用中的行为:事件以及对事件做出响应的函数。在这些响应函数中,程序通常不是按照线性的顺序执行,有些程序块只能在满足某些条件时才能执行。

重复块是程序的另一种非线性运行方式。就像if及ifelse块让程序产生分支一样,重复块让程序循环执行,换句话说,在执行完一组指令后,重新跳回到这组指令的起点并再次运行,如图20-1所示。在应用的运行过程中,内部的计数器会跟踪即将执行的下一步操作,因此,对于整个事件处理程序来说,从头至尾的每一步操作都在程序计数器的监控之下(有条件地)完成。程序计数器随着这些重复执行的块循环,不断地重复这些功能。

在App Inventor中有两种类型的重复块:foreach及while.foreach,其作用是对列表中的每一项实施某些特定的操作,如,向电话号码列表中的每个号码发送一条短信。

块while的应用比foreach要普遍,while块中的程序块会一直重复运行,直到某个条件不再满足。while块可用于数学公式的计算,如求n个连续自然数的和,或求n的阶乘,此外,while也可以用于同时处理两个列表;foreach每次只能处理一个列表。

使用foreach对列表实施迭代

在第18章里,我们讨论了一个“随机拨号”应用。这种随机拨打朋友电话的方式有时能拨通,但如果你有一个像我这样的朋友,这种呼叫却不总是能得到应答。可以采取另一种方式,给所有列表中的朋友发短信说“想你”,然后看谁最先回复你(或许还有更令人愉快的方式!)。

这个应用可以通过点击一次按钮向多个朋友发送短信,最简单的方法是,先写好发给一个人的代码块,然后拷贝粘贴并修改接收人的电话号码,如图20-2所示。

图20-2 拷贝并粘贴向不同号码发送短信的块

如果只有少量的块,用这种“强力”的拷贝粘贴方式也还说得过去,但是像朋友列表这样的数据表会时常变化,而你不希望每次添加或删除一个电话号码,都要动手去修改程序。

块foreach提供了一个更好的解决方案,可以定义一个包括所有电话号码的列表变量phoneNumberList,然后用foreach块将发送一次短信的块包围起来,从而实现群发功能,如图20-3所示。

图20-3 使用foreach块对列表中的每一项执行同一套指令

上述代码可以解读为:

对于phoneNumberList列表中的每一项(电话号码),设置Texting对象的PhoneNumber属性为列表中的项,并发送该条短信。

对于foreach块,一个必须的参数是一个列表,它所要处理的列表,将列表插入“in list”参数插槽。此时,从phoneNumberList变量的初始化块中拖出“get global phoneNumberList”块,并插入“in list”插槽,以便为即将发送的短信提供电话号码列表。

foreach块的第一行使用了foreach自带的占位符变量,在默认情况下,变量名为item,你可以修改它,也可以就用默认值,该变量代表了列表中正在被处理的当前项。

foreach中的所有块都将对列表中的每一项执行同样的操作,其中的占位符变量(例子中的phoneNumber)始终保存的是当前正被处理的项。如果列表中有三项,则foreach中包含的块将被执行三次,这些块可以说是从属于foreach块,或处于foreach块的内部,这些内部块执行到最后一行时,我们所说的程序计数器将要循环回第一行。

循环过程详细分析

我们来详细地分析一下foreach块的运行机制,因为理解循环是编程的基础。当点击TextGroupButton时,触发事件处理程序,首先执行的是“set Texting1.Message to”块,要将短信内容设置为“想你…”,这个块只执行一次。

然后开始执行foreach块。在foreach内部块开始执行前,占位符变量item被设置为列表phoneNumberList的第一项(111-1111),这一步是自动完成的,代替了你自己使用select list item来调出列表项。在完成将列表中的第一项赋给item之后,foreach内部的块开始第一次运行,Texting1.PhoneNumber属性被设为item的值(111-1111),并发出短信。

当运行到foreach中的最后一行时(Texting1.SendMessage块),程序将循环会到foreach的首行,并自动将列表中的下一项(222-2222)设为变量item的值,然后重复操作foreach内部的两个块,即发送短信“想你…”到号码222-2222。然后程序再次循环会首行,并将item的值设为列表中的第三项(333-3333),并执行第三次重复操作,第三次发送短信。

由于列表中最后一项,即本例子中的第三项已经被处理完毕,因此foreach循环到此结束,程序将跳出循环,这意味着程序计数器将继续下移来处理foreach下面的块。在本例中,foreach之后没有块,因此整个事件处理程序结束。

书写可维护的代码

在最终用户看来,使用foreach的方法还是“强力”的拷贝粘贴法,在最终结果上并无分别,但从程序员的角度来看,foreach方法让代码有更好的可维护性,即使数据(电话号码列表)是动态输入的,程序也可以适用。

可维护软件指的是可以很容易地对软件进行修改,而不会引入程序的漏洞。使用foreach方法,一旦需要修改短信接收人,只需要修改列表变量,而丝毫不需要修改程序的逻辑(事件处理程序)。相反,采用强力的方法,如果需要添加新的接收人,则需要在事件处理程序中添加新的块。任何时候,只要你改动了程序的逻辑,都会冒带来漏洞的风险。

更重要的是,即便电话列表是动态的,即,不仅是程序员,最终用户也可以向列表中添加新的号码,foreach方法也能奏效。在我们的例子中只有三个固定的号码,而且号码直接写在了代码中,与此相比,采用动态数据的应用,其信息来源可能是最终用户,或其他来源。如果你要重新设计应用,让最终用户来输入电话号码,你就必须使用foreach方法,因为在你写程序的时候,根本无法知道会有哪些号码,因此也就无从采用强力的拷贝粘贴法。

foreach的第二个例子:显示列表

显示列表项最简单的方式就是将列表变量插入Label的Text属性,如图20-4所示。

图20-4 列表的简单显示方法:将列表直接插入label

这样做的结果是,列表项在label中显示为一行,项之间以空格分隔,整个列表被一对括号包围:(111-1111 222-2222 333-3333)。

这些号码可能显示为多行或单行,取决于号码的多少。最终用户能看到这个数据,也可能将它们当做电话号码的列表,但这样的显示方式很不美观。通常会将列表项分行显示或用逗号分隔。

为了适当地显示列表,需要将每个列表项转换为一段带格式的单独的文本。文本对象通常有字母、数字、标点符号组成,但也可能包含特殊的控制字符,它们对应一些不可见的字符,如tab被表示为t(更多关于控制字符的内容,请查阅文本表示的统一码[Unicode]标准:http://www.unicode.org/standard/standard.html)。

为了逐行显示我们的电话号码列表,需要一个换行符“n”。当“n”出现在一段文本中,意味着“到下一行来显示后面的东西”。因此文本对象“111-1111n222-2222n333-3333”将显示为:

111-1111

222-2222

333-3333

要构造出这样的文本对象,需要用到foreach块,将每个列表项附加换行符后再添加到PhoneNumberLabel.Text属性中,如图20-5所示。

图20-5 使用foreach处理列表:在每个列表项后添加换行符

我们来跟踪一下这些块的作用。在第15章中讨论过在程序运行过程中跟踪变量及属性变化的相关内容,在foreach块中,我们考虑每一次迭代之后的值,所谓一次迭代,就是foreach循环执行一次。

在foreach之前,PhoneNumberLabel的Text属性被初始化为空文本;从foreach开始,程序会自动将列表的第一项赋给占位符变量phoneNumber。然后将PhoneNumberLabel.Text、n、phoneNumber连接起来之后,再将其设为PnoneNumberLabel.Text的属性值。这样,在完成foreach的第一次迭代后,相关的变量值如表20-1所示。

表20-1 第一次foreach迭代之后的变量值
phoneNumber PhoneNumberLabel.Text
111-1111 n111-1111

此时已经是foreach内的最后一行,程序进入第二次迭代,下一个列表项(222-2222)被设为占位符变量phoneNumber的值,并重复执行foreach内部的块:将PhoneNumberLabel.Text的原值(n111-1111)与“n”及phoneNumber(此时是222-2222)连接起来。第二次迭代后,变量及属性值如表20-2所示。

表20-2 第二次foreach迭代之后的变量值
phoneNumber PhoneNumberLabel.Text
222-2222 n111-1111n222-2222

列表中的第三项被设为phoneNumber的值,第三次重复运行foreach内部的块,在完成最后一次迭代后,最终结果如表20-3所示。

表20-3 第三次foreach迭代之后的变量值
phoneNumber PhoneNumberLabel.Text
333-3333 n111-1111n222-2222n333-3333

三次迭代完成之后,label包含了所有的电话号码,文本变得很长,在foreach执行完成后,PhoneNumberLabel.Text的显示如下:

111-1111

222-2222

333-3333

用while实现迭代

循环块while的使用比foreach要稍显复杂,但while块的优势在于它的通用性:foreach可以遍历一个列表,而while可以为循环设定任意的条件。随便举个例子,假设你想给电话号码表中每隔一个人发短信,foreach则做不到,但while中可以将每次循环中index的递增值设为2。

在第18章中,条件测试的结果将返回一个值:true或false,在while-do块中也包含了一个想if块一样的条件测试。如果while测试的结果为true,程序会执行while内部的块,然后返回并再次进行条件测试。只要测试结果为true,while内部的块就会重复运行。当测试值为false时,程序将跳出循环(如同foreach中一样)并继续执行while下面的块。

使用while同步处理两个列表

关于while的更具启发性的例子中,涉及到了一种常见的情形,即,需要同步处理两个列表。例如,在总统测试(第10章)应用中,有两个分别存放问题和答案的列表,以及一个变量index来跟踪当前的问题序号。为了同时显示问题-答案对,需要同步遍历两个列表,并从两个列表中获取序号为index的项。foreach只允许遍历一个列表,但在while循环中,则可以使用index从每个列表中抓取对应的项。图20-6中显示了用while块逐行显示问题-答案对的方法。

图20-6 使用while循环逐行显示问题-答案对

由于用while替代了foreach,因而需要直接初始化index、检查是否到达列表结尾、在每次循环中选择各个列表中对应的项,并使得index递增。

使用while做公式计算

这里是使用while循环的另一个例子:与列表无关的重复操作。想想看,图20-7中的块在做什么?高水平?要想弄清楚,就要跟踪每一个块(关于程序跟踪的更多内容见第15章),随着程序的进展,跟踪每个变量的值。

图20-7 你能说出这些块的功能吗?

当变量number的值小于或等于变量N时,while中的块将重复执行。在这个应用中,N值等于最终用户在界面上的文本框(NTextBox)中输入数字,假设用户输入3。当程序运行到while块时,程序中的变量如表20-4所示。

表20-4 程序运行到while块时,各个变量的值
N number total
3 1 0

在第一次循环中,while块询问:number值小于或等于(≤)N 吗?第一次询问得到的结果是true,于是执行while中的块:total值等于它现在的值(0)加上number(1),number值递增1。第一次while循环之后,各变量的值如表20-5所示。

表20-5 while中的块完成第一次循环使用,各个变量的值
N number total
3 2 1

第二次循环中,继续测试“number≤N”,结果仍然是true(2≤3),因而while内部的块再次运行。total值等于它自身(1)加上number(2),number继续递增。第二次迭代完成时,各变量的值如表20-6所示。

表20-6 两次循环结束时,各个变量的值
N number total
3 3 3

程序再次返回到条件测试,这次的结果仍然是true(3≤3),于是while内的块第三次运行。现在total值为它自身(3)加上number(3),结果为6;number递增到4,如表20-7所示。

表20-7 三次循环之后各个变量的值
N number total
3 4 6

在完成第三次迭代之后,程序再次返回测试“number≤N”,或“4≤3”,此时结果为false,因此while内部的块不再执行,事件处理程序完成。

现在该知道这些块的作用了吧?它们在做一个最基本的数学运算:数字计算。每当用户输入数字,程序就给出从1到N的自然数的和,这里的N就是输入的数。在这个例子中,我们假设用户输入了3,因此加和的结果是6;如果用户输入4,最后的结果为10。

小结

计算机擅长于做重复的事情。想象一下所有的银行账户都要做利息的累计核算,所有计算学生平均绩点的成绩处理,以及日常生活中计算机所做的各种无计其数的重复的工作。

App Inventor 提供了两种用于循环操作的块。foreach块适合于针对列表中的每一项实施一组相同的操作。与那些具体的数据相比,foreach更适合于处理抽象的列表,其编码更具可维护性,尤其是对于动态数据来说,foreach是必需的。

与foreach相比,while则更为通用:既可以处理单个列表,也可以同步处理两个列表,还能进行公式计算。在执行while循环时,只要条件测试结果为真,while内部的块就会顺次执行;在内部块运行完成后,程序将返回并重新进行条件测试,直到测试结果为false,则循环结束。

第19章 数据列表编程

Published by:

如你所见,应用就是处理事件以及作出决策,这一过程是计算机程序的基础,而同样构成程序基础的就是数据——程序所要处理的信息。程序中很少只用到像游戏中的成绩这样的单个数据,更普遍的是使用复杂数据——一些相互关联的数据项,必须像设计应用的功能一样,非常细心地组织这些数据。

本章将探讨App Inventor中处理数据的方式,并学习两种数据类型的基本编程方法,两种数据类型为静态数据(数据的值保持不变)及动态数据(数据由用户生成),然后将学习如何处理更为复杂的包含数据的数据,即数据项本身也是一组数据。

许多应用中都存在这样复杂的数据,如facebook中的好友列表,测试应用中的问题及答案列表等等,游戏中也会有角色的列表以及当前最高成绩的列表。

列表变量的使用如同普通的文本及数字变量一样,只是它们不仅仅代表单一的有名称的存储单元,而是表示一组相互关联的存储单元,例如,考虑表19-1中的电话号码列表。

表19-1 列表变量表示一系列的存储单元

使用索引值(index)来访问列表元素,因此在列表19-1中,index为1时表示第一项111-2222,index为2时表示第二项333-4444,而index为3时表示555-6666。

App Inventor提供了操作这些数据的块,包括数据的创建、为数据添加元素、从列表中选择指定的项以及对整个列表的操作,让我们从创建列表开始。

创建列表变量

在块编辑器中,使用“initialize global (name) to”块以及“make a list”块来创建列表变量。例如,假设你正在写一个“一键发送短信”的应用,通过点击一个按键向电话号码列表中的所有成员发送短信。用如下方式创建一个电话号码列表:

1. 从块编辑器的Variables抽屉中拖出一个“initialize global (name)to”块到应用中,如图19-1。

图19-1 初始化变量的块

2. 点击文本“name”,将其改为phoneNumbers,如图19-2所示。

图19-2 将变量重命名为phoneNumber

3. 从Lists抽屉中拖出“make a list”块插入初始化变量块,如图19-3所示。这是告诉应用,要存储的变量是一个列表,而非单个值。通过点击“make a list”块上的蓝色增项图标来指定所需存储槽的数量,来增加数据项,如图19-3所示。

图19-3 使用“make a list”块来定义列表变量phoneNumber

4. 最后,从Text抽屉中拖出文本块,输入需要的电话号码,插入到“make a list”块的数据项插槽中。

图19-4 当列表中添加了全部数据项,相当于开辟了一个新的存储空间

数据项的存储中可以插入任何类型的数据,但在本例中,这些数据项是文本类型的对象,而不是数字,因为电话号码中含有一个破折号“-”,这种非数字类型的符号无法输入到数字块中,也无法与数字进行任何运算(而数字运算必须用到数字块)。

图19-4中所示的块定义了一个名为phoneNumber的变量,在应用启动时,你定义的任何变量就在此时被创建,而像表19-1中的存储槽也被同时创建并被填写上初始值。一旦有了这个列表变量,就可以使用列表中的数据开始编程了。

选择列表项

应用中可以使用“select list item”块,并指定索引值(index)来访问列表中指定的数据项。index代表了该数据项在列表中的位置。因此,如果列表中有三个数据项,就可以用索引值1、2、3来访问这些项。图19-5中显示了选中列表第二项的块。

图19-5 选择列表中的第二项

使用选择块“select list item”需要提供两项参数,首先是要查询的列表,将其插入选择块的第一个插槽中,其次是索引值index,将其插入选择块的第二个插槽中。图19-5中的块是告诉应用从列表phoneNumber中选出第二个元素。如果列表的定义如表19-1的话,俺么选择的结果就是“333-4444”。

从列表中选择数据项,这仅仅是第一步,通过选择可以实现各种操作,下面将举例说明。

使用Index遍历列表

许多应用中,定义列表的目的是让用户可以遍历(逐个查看)它。第8章总统测验就是一个很好的例子:用户点击“下一题”按钮,程序从问题劣币哦啊中选择下一道题并显示出来。

但如何实现对下一项的选择呢?图19-5中选择的是列表phoneNumber中的第二项,而遍历列表时,每次选择的项目序号是不同的,会根据当前选中项在列表中的位置来确定。因此就需要定义一个变量来表示这个位置,通常用index来作为变量名,初始值通常设为1(列表中的第一个位置),如图19-6所示。

图19-6 变量index的初始值为1

当用户设法移动到下一项时,可以在当前的index值上加1来实现变量的递增,并使用递增后的值在列表中做选择。图19-7中显示了实现这一点使用的块。

图19-7 使index值递增,并用递增后的值选择列表项

举例:遍历画笔颜色列表

来看一个例子,用户可以通过点击按钮来为他的房子选择一种可能的粉刷颜色,每次点击,按钮的颜色都会变化。当用户查阅完全部颜色时,再重新回到第一种颜色。

例子中,可以使用基本色,也可以替换成任何一组颜色,关于颜色的更多信息,可以参见App Inventor文档(http://appinventor.googlelabs.com/learn/reference/ blocks/colors.html)。

图19-8 用一组颜色为colors列表做初始化

第一步是定义一个颜色列表变量,并以颜色为列表项来初始化列表,如图19-8中所示。

接下来,定义变量index来跟踪列表的当前项位置,初始值为1。可以给变量一个更有意义的名字,如currentColorIndex,但如果你的应用中不需要处理其他更多的列表,用index就好。如图19-9所示。

图19-9 使用index变量来跟踪列表当前项的位置,初始值为1

用户通过点击ColorButton从列表中浏览到下一项(颜色),此时,index值递增,而按钮的BackgroundColor变为当前选中的颜色,如图19-10所示。

图19-10 用户通过点击按钮浏览颜色列表——每次点击都会改变按钮颜色

先假设我们已经在组件デザイナー中将按钮的背景颜色设为红色(Red),第一次点击按钮时,index值从初始的1变为2,按钮颜色变为列表中的第二项——绿色;第二次点击按钮时,索引值从2变为3,按钮变为蓝色。

想象一下,下一次的点击会出现什么情况?

图19-11 当试图从一个只有三项的列表中选择第四项时,程序将提示错误信息

如果你说会出错,那么你对了!index值将变成4,程序将试图在列表中选择第4项,但列表中只有3项,因此程序强行关闭,或退出,用户将看到一条如图19-11中所示的错误信息。

显然,你不想让用户看到这样的信息,为了避免出现这样的问题,需要添加一个if块来检查是否到达了列表中的最后一项。如果是,将index值设回1,来显示第一种颜色,如图19-12所示。

图19-12 使用if块检查索引值index是否大于列表的长度,如果是,将index值重新设为1

用户点击按钮时,index值递增,然后检查它的值是否过大。与index值进行比较的是“length of list”,而不是3,因此,即便是列表中添加了更多的项,程序也能正常运行。通过检查index是否大于列表长度(而不是与固定的数字3进行比较),可以消除程序中的代码相关性。所谓“代码相关性”是一个编程术语,举例来说,如果你的应用中某些方面的程序写得过于具体,那么当你想对某处做出修改时(如,列表的数据项),你不得不找到应用中所有使用过这个list的地方,并对程序块进行逐一修改。

正如你所想象得,这种相关性会让程序在短时间内变得混乱不堪,也会产生更多的错误等待你去排查。事实上,在“粉刷彩色房屋”应用的设计中,就在我们刚刚完成的程序中,还存在另一个代码相关性问题,你能找出是什么吗?

如果将颜色列表中的第一项由红色改为其他颜色,应用的运行结果就不再正确,除非你能记得在组件デザイナー中修改ColorButton.BackgroundColor属性的初始设定。消除这种代码相关性的方法是,将ColorButton.BackgroundColor初始值设定为颜色列表中的第一项,而不是某个特定的颜色。由于这一修改涉及到程序启动时的行为,因此需要在应用启动时调用的Screen.Initialize事件处理程序中进行这一设定。如图19-13所示。

图19-13 应用启动时,将按钮的背景色设置为颜色列表中的第一项

创建输入表单及动态数据

前面的“粉刷彩色房屋”应用涉及到一个静态列表:程序员(也就是你)定义了列表中的元素,除非你亲自动手,没有人能修改这些列表项。不过,多数情况下,应用中要处理动态列表:最终用户输入新的数据项而导致数据的变化,或者从数据库或web信息源加载新数据。本节将讨论一个“随手记”应用:用户在应用中,通过表单输入笔记,并可预览之前所有输入过的内容。

定义动态列表

如果希望创建一个空列表,可以使用“create empty list”块来定义,例如,在“随手记”应用中,允许用户输入笔记,但在定义列表时,不应该有预定义的数据项,具体的定义方法见图19-14。

图19-14 动态列表的定义中不应该含有任何预定义数据项

添加数据项

图19-15 用输入表单想笔记列表中添加新项

当第一次启动应用时,notes列表是空的,当用户在表单中输入数据并点击“保存”按钮时,新的笔记内容将被添加到列表中。表单的设置非常简单,如图19-15所示。

当用户输入一段笔记并点击“保存”按钮,应用将调用“add items to list”函数将新输入的内容添加到列表中,如图19-16所示。

图19-16 用户点击“保存”按钮时,调用“add items to list”向列表中添加新内容

“add items to list”块将新的数据追加到列表的结尾,用户每次点击“保存”,就添加一条新笔记,在Lists抽屉中可以找到这个块。特别注意:还有另一个块“append to list”,它的功能是向一个列表中追加另一个列表,很少会用到这个块。

显示列表

对用户来说,列表变量notes的内容是不可见的,还记得之前讲过,应用中的变量是用来保存那些不需要被用户看到的信息。图19-16中的块实现了一点击按钮就添加新项的功能,但用户看不到任何反馈,除非你在程序中添加显示列表内容的功能。

图19-17 用NotesListLabel的Text属性显示笔记列表

在应用的用户界面中显示列表内容最简单的方法就是现实数字和文本的方法:将列表内写入Label组件的Text属性,如图19-17所示。

可惜这种简单的显示方法看起来不够美观,列表中所有的项被放置在一对小括号内,没有分行,项之间用空格分隔。如图19-18所示,用户输入了第一条笔记“忘记了让笔记显示出来”,然后又输入第二条“显示结果被一对括号包围着!”。

如果学习过第13章的“亚马逊掌上书店”,你对这个问题应该熟悉。在第20章中,将学习如何用更加复杂的方式来显示列表内容。

图19-18 列表内容的简单显示方法

删除列表项

图19-19 删除列表项

使用“remove list item”块可以从列表中删除某一项。如图19-19所示。

图19-19从列表notes中删除了第2项,但通常我们不希望只删除某个固定的项(如第2项),而是让用户来选择需要删除的项。

ListPicker是一个可以用于删除列表项的用户界面组件,它与一个按钮关联,当点击按钮时,ListPicker会显示列表项,并允许用户选择其中的一项。当用户选中后,应用就可以将其删除。

ListPicker有两个关键事件BeforePicking及AfterPicking,而且每个事件都有两个重要属性:Elements及Selection,如表19-2所示,只要理解了这两个事件及其属性,ListPicker组件的编程就很容易了。

表19-2 ListPicker组件的两个关键事件及其属性
事件 属性
BeforePicking:点击按钮时触发 Elements:选中的列表
AfterPicking:用户做出选择时触发 Selection:用户所选项

当用户点击ListPicker的关联按钮时,触发ListPicker.BeforePicking事件,此时用户尚未选择列表项;在ListPicker.BeforePicking事件处理程序中,可以将ListPicker.Elements属性设置为一个列表变量,例如,在“随手记”应用中,将Elements属性设置为列表notes,如图19-20。

这些块将列表notes的内容显示在ListPicker中,如果列表中有两条笔记,其显示如图19-21所示。

图19-20 ListPicker的Elements属性被设置为列表变量notes

图19-21 列表notes显示在ListPicker组件中

当用户从列表中选择一项时,将触发ListPicker.AfterPicking事件,在该事件的处理程序中,可以利用ListPicker.Selection属性来访问用户的所选项。

但是想到“remove item from list”块需要的是索引值(列表中的位置),不是具体的项,而Selection属性却是实际数据(一条笔记),不是索引值,ListPicker组件不直接提供对列表索引值的访问(在App Inventor的后续版本中将添加此功能)。

变通的方法是利用Lists抽屉中的另一个块:“index in list”。对于给定的文本,该块将返回列表中最先与该文本匹配的项的位置,使用“index in list”,ListPicker1.AfterPicking事件处理程序将删除用户选中的项。如图19-22所示。

图19-22 使用“index in list”块找出要删除项的索引值

AfterPicking事件被触发后,ListPicker1.Selection中包含了用户选中的文本(如“忘记了让笔记显示出来”)。我们的目标是找到选中项在列表中的索引值,以便将其删除。如果用户选择的是“忘记了让笔记显示出来”,则“index in list”块将返回1,因为这段文本是列表中的第1项。将索引值保存到变量removeIndex中,并将它用作“remove list item”块中的index值。

再继续阅读之前,先思考一个问题:这种方法是否总是有效呢?

回答是肯定的,但条件是列表中没有重复的项。比如说,用户输入的第2条和第10条笔记都是“今天过得太好了!”。如果此时用户点击“删除列表项”按钮(其实是ListPicker),并选中了第10项,那么被删除的将是第2项,而非第10项。“index in list”块只能返回第一个匹配项,然后就停在那里,因此也就找不出应该被删掉的内容相同的第10项。需要对列表进行遍历,并使用适当的条件判断(见第18章)来查看是否还有其他匹配项,并将其删除。

列表中的列表

列表项可以使数字、文本、颜色、布尔值(true/false),也可以是数据(维基:在计算及数据处理中,数据往往表示一种结构,如表格[由行和列组成]、树[一组有父子关系的节点]或者图形[一组连接起来的节点]。数据通常是测量的结果,可以被可视化成图形。),这是一种常见的数据结构。例如,一个数据的列表可以将第8章总统测试转变为一个多选题测验。我们来重温一下总统测试中数据的基本结构:一个问题列表和一个答案列表,如图19-23所示。

图19-23 一个问题列表和一个答案列表

每当用户回答完一个问题,程序通过与AnswerList中的当前项进行对比来判断回答是否正确。

为了实现多选测验,需要为每个问题提供一个可供选择答案的列表。多选列表可以表示为一个数据列表变量,将三个“make a list”块放在一个外层“make a list”块中,来定义这个变量,如图19-24所示。

图19-24 通过在外层“make a list”块中插入若干个“make a list”块来构造出一个数据列表

变量answerChoices中的每个数据项本身也是一个由三个数据项组成的列表,如果从answerChoices列表中选择一项,选择的结果将是一个列表。现在填好多选答案的双重列表,那么如何向用户显示这些数据呢?

在“随手记”应用中,使用了一个ListPicker来向用户显示选项。假如索引值为currentQuestionIndex,则事件处理程序ListPicker.BeforePicking将写成图19-25中显示的样子。

图19-25 使用ListPicker向用户展示多选列表

图19-26 向用户展示第1题的多选答案

这些块将选取并显示answerChoices中的当前项对应的子列表,供用户选择。如果currentQuestionIndex为1,ListPicker将显示图19-26中的列表。

用户选择之后,用图19-27中的块对答案进行检查。

这些块中,用户从ListPicker中选择的答案将与正确答案进行比较,而正确答案保存在另一个列表AnswerList中(注意answerChoices只提供选项而不代表答案)。

图19-27 检查用户选择的答案是否正确

小结

你能想到的几乎每个应用中都会用到数据,理解它们的运行机制是编程的基础,本章探索了一种最常用的编程模式:使用索引变量,从列表的第一项开始,通过变量的递增实现对每个列表项的处理。如果能理解并在自己的程序中加以运用,那么你的确是一名程序员了。

然后我们讲到了列表处理的其他方式,包括一个典型的让用户添加并删除列表项的表单。如此的编程还需要另一个层次的抽象能力,你必须假想数据的存在,因为直到用户输入某些数据之前,这些数据都是空的。如果你能理解这一点,你甚至可以考虑辞掉现在的日常工作了。

最后我们介绍了复杂的数据结构——数据列表。这显然是一个不太容易理解的概念,但我们利用一些固定的数据对问题进行了探索:多选测验中的可选择答案列表。如果你对此以及本章的其余部分都有所掌握,那么你的期末考试题是:使用数据列表创建一个应用,但要求使用动态数据!一个例子就是允许用户在应用中创建他们自己的多选测验,这个功能甚至比第10章的出题应用还要强大。祝你好运!

在你思考如何处理这些列表时,要知道我们的探索还没有结束。在下一章中,我们将继续讨论并重点讲解略有不同的列表循环:对列表中的每一项实施一些列的操作。

第18章 程序中的决策:条件块

Published by:

即使是像口袋里的手机这样小型的电脑,也可以在短短几秒钟内完成超过数千次的操作。更令人惊奇的是,它们可以基于内存中的数据以及程序员编写的逻辑进行决策。这种决策能力在人们所思考的人工智能问题中是极为关键的要素,当然也是创建有趣的智能应用的重要组成部分。本章将探索如何在应用中编写判断选择逻辑。

正如我们在第14章所讨论的,应用的行为由一系列的事件处理程序所定义。每个事件处理程序针对某个特定事件进行响应,并实现特定的功能。然而,这种响应的过程未必是按线性顺序来实现各项功能,有些功能只能在一定条件下才能执行。像游戏类的应用可能就会判断分数是否已经达到了100,而位置感知类的应用可能会问“某个手机是否在某个建筑物的范围之内”。你的应用也可以询问类似的问题,然后根据答案,继续执行不同的程序分支。

如图18-1,当事件(Event1)发生时,无论如何A功能都会被执行;然后进行一个检测判断:如果检测结果为真,则执行B1分支;如果结果为假,则执行B2分支;无论执行哪个分支,该事件处理程序的其余部分(C)都将被执行。

由于像图18-1这样的决策图看起来像一棵树,因此通常会将这种根据判断结果而选择执行的一段程序称为“分支”。在这种情况下,你会说, “如果测试结果为真,则执行包含B1的分支。 ”

图18-1 事件处理程序中,根据条件测试的结果执行不同分支

用if及ifelse进行条件测试

App Inventor提供了两类条件块(如图18-2):if块和ifelse块。可以从Control抽屉里拖出一个if块,然后点击上面的蓝色图标,弹出可扩充的块,可以根据需要添加任意多个“else”分支。

可以将任何逻辑表达式(Boolean)插入到if右侧的测试插槽中。逻辑表达式是一个用数学等式,它的返回值要么是真(true),要么是假(false)。如图18-3,逻辑表达式使用关系运算符(蓝色)以及逻辑运算符(绿色),对属性值或变量值进行检测。

图18-2 条件块if及ifelse

图18-3 用于条件判断的关系及逻辑运算符

无论是if块还是ifelse块,只有“if”后面的测试结果为真时,将执行“then”右侧插槽中的块。对于if块,如果测试结果为假,程序将跳出if块,继续执行if后面的块;而对于ifelse块,如果测试结果为假,将执行“else”右侧插槽中的块。

因此,对于一个游戏来说,可能会插入一个与成绩有关的逻辑表达式,如图18-4所示。

在本例中,如果成绩到达100,则播放一个声音文件。注意,如果测试结果为假,不执行任何块。如果需要在测试结果为假时执行某些操作,可以使用ifelse块。

图18-4 用于测试成绩值的逻辑表达式

编写一段二选一的决策程序

考虑这样一个应用,无聊的时候也许会用到它:在手机上点击一个按钮,就可以随机地拨打一个朋友的电话。如图18-5,使用一个random integer(随机整数)块来生成一个数字,然后用ifelse对生成的数字进行判断,来决定即将拨打的电话号码。

图18-5 用ifelse块判断随机生成的整数来选择要拨打的号码

在这个例子中,random integer的参数为1和2,意味着将以相等的几率产生1或2,所产生的随机数保存在变量randomNum中。

一旦取得了变量randomNum的值,在ifelse块中将变量值与1进行比较:如果randomNum的值为1,程序将执行第一个分支(then),将电话号码设置为“111-1111”;如果变量值不为1,测试结果为假,程序执行第二个分支(else),电话号码被设置为“222-2222”。无论测试结果如何,程序都将拔打电话,因为是在整个ifelse块的下面调用了MakePhoneCall过程。

多重条件判断

许多情况下不只是双重选择,即,可选择的结果不仅仅是两个。例如,也许你希望可以给更多的朋友随机拨打电话,因此就需要在原来的else分支中,再加入一个ifelse,如图18-6所示。

图18-6 外层条件判断的else分支中加入另一个ifelse条件判断

在这些块中,如果第一个检测条件结果为真,程序将执行第一个“then”分支并拨打号码“111-1111”;如果第一个测试结果为假,则执行外层的else分支,此时将立即进行另一个测试。因此,如果第一个测试结果(randomNum=1)为假,而第二个测试结果(randomNum=2)为真,则执行第二个(内层的)“then”分支,并拨打号码“222-2222”;如果前面两个测试的结果都为假,则执行最下面的内层的else分支,并拨打第三个号码333-3333。

注意,在修改过的程序中,随机整数生成器(random integer)中的参数2变成了3,因此,将以相等的几率生成结果1、2或3。

这种在一个条件判断中加入另一个判断的方式称为“嵌套”,在本例中,可以称为“嵌套的if-else块”,使用这种嵌套的逻辑,可以为随机拨打电话的程序提供更多的选择。一般来说,任何程序中都可以使用任意多层的嵌套。

复杂条件判断

除了嵌套,还可以设定更为复杂的检测条件,即,多于一个等式的检测条件。例如这样一个应用,当你(或你的手机)离开某栋建筑或某个边界时,手机会发出震动。这样的应用适用于那些受控人员,警告他们不要远离法定的边界;也可以用于家长监视孩子们的行踪;教师可以用它来做自动点名(条件是学生们都配有Android手机!)。

例如,我们提出这样的问题:手机是否在“旧金山大学哈尼科学中心”范围内?这样的应用要对4个不同的问题进行一个复杂的检测:

  • 手机所在的纬度低于边界纬度的最大值(37.78034)吗?
  • 手机所在的经度低于边界经度的最大值(-122.45027)吗?
  • 手机所在的纬度高于边界纬度的最小值(37.78016)吗?
  • 手机所在的经度高于边界经度的最小值(-133.45059)吗?

图18-7 放在if块测试插槽中的“and”块(选择“External Input/外展式输入”以免块的排列过宽)

本例中使用了位置传感器(LocatinSensor)组件,即便你没用过这个组件,也能够理解这些程序,在第23章中将有更多讲解。

使用逻辑运算符and、or及not可以构造出更为复杂的测试条件,可以从Logic抽屉中找到它们。在本例中,先拖出一个if块以及三个and块,并将and块放在if块的测试插槽中,如图18-7所示。

然后拖出几个块来组成第一个测试问题,并将其放在and块的第一个测试插槽中,如图18-8所示。

图18-8 and块中放入了第一个测试问题块

如法炮制出其他几个测试条件,填入其他几个and的测试插槽中,并将整个if块放入事件处理程序LocationSensor.LocationChanged中,这样就写成了一个检测边界的程序,如图18-9所示。

每次位置更新时,触发该事件处理程序,来检测是否在边界之内

这些块的功能是,在每次位置传感器读数更新时做出判断,如果手机的位置在边界之内,则发出震动。

OK,到目前为止,应用已经相当酷了,但现在我们来尝试更为复杂的功能,以便你能充分地了解程序中决策的威力。如何才能让手机仅在越出边界时才发出震动呢?继续学习之前,自己先想想如何来写这样的程序。

我们的方法是定义一个变量withinBoundary,目的是记住传感器上一次的读数是否在边界内,并根据每一次后续读数的测试结果对变量值进行修改。withinBoundary是一个布尔(Boolean)类型的变量,与保存数字或文本的变量相比,它保存的值为true(真)或false(假)。举例来说,如果将变量初始值设为false,如图18-10所示,这意味着设备不在旧金山大学的哈尼科学中心范围内。

图18-10 变量withinBoundary为初始化为false

对块做出修改,以便在每次位置信息变化时,对变量withinBoundary进行设置,并且只有当手机越出边界时,才会发出震动。说的更明确一些,手机产生震动的必备条件是(1)变量withinBoundary的值为真,即意味着上一次读数还在边界内;(2)新的传感器读数超出了边界。图18-11中是修改后的块。

图18-11 这些块的功能是:只有当手机从界内移动到界外时,手机才会震动

我们来仔细地分析一下。当位置传感器(LocationSensor)获得读数时,首先判断读数是否在边界内,如果是,将withinBoundary设置为true。由于我们希望只有在手机越出边界时才震动,因此在第一个分支中不发生震动。

如果执行的是else分支,我们知道新的读数已经超出了边界。此时,我们需要检查上一次的读数:尽管这次读数超出了边界,但我们希望仅当上次读数在边界内时,才让手机发出震动。withinBoundary变量会告诉我们上一次的读数,因此我们会检查这个变量,如果检查结果为真,则让手机震动。

一旦确认手机从界内移动到了界外,还有一件事必须要做,你能猜到是什么吗?对,需要重新设置withinBoundary为false,这样,在下一次收到传感器读数时,手机才不会再次震动。

关于布尔型变量,还有一点需要提示:检查一下这两个if测试,如图18-12,它们的效果一样吗?

图18-12 你能说出这两个if测试的结果一样吗?

答案是“一样”!唯一的差别在于下边的提问方式实际上更加老练,而上边的测试还要将一个布尔型的变量(其值只能是true或false)与true进行比较。如果withinBoundary的值为true,将true与true比较,结果一定是true;如果变量值为false,将false与true比较,结果为false。因此,只需要对withinBoundary的值进行检测,像右边那样,其结果相同,而且编码更加简洁。

小结

头晕了吗?尤其是最后的部分相当复杂!但这类决策方法是高级应用中必须具备的。如果你能一步一步(或者说一个分支一个分支)地实现这些行为,并做到边做边测试,我们敢断言,你会发现,即便是人工智能也不是不可能的。它让你头疼,也让你的大脑获得了些许逻辑思维的锻炼,但无疑也是充满乐趣的。

第17章 创建动画应用

Published by:

本章将讨论创建另一类应用的方法,应用中使用了简单的可移动的动画对象。你将学习使用App Inventor创建二维游戏的基本知识,并熟练使用图片精灵(image sprite)及处理两个物体碰撞一类的事件。

当你在电脑屏幕上看到一个平滑移动的物体时,你实际上看到的是一连串快速移动的图片,每次只移动一个极小的距离,它利用了人的视觉暂留,从这一点上,它无异于“手翻书”——一种通过快速翻页来看到动画效果的书(这也是那些精美绝伦的动画电影的制作方法)。

在App Inventor中,通过在Canvas组件上放置物体,并让这些物体随时间在Canvas内移动,从而产生出动画效果。本章将学习使用Canvas的坐标系统,学习利用Clock.Timer事件来触发运动,以及如何控制运动速度、如何响应两个物体的碰撞事件等等。

在应用中添加Canvas组件

图17-1 设置Canvas组件的Width属性

从组件面板的Drawing and Animation组中拖出Canvas组件,然后定义它的Width及Height属性。通常我们希望Canvas与屏幕等宽,为此将宽度设为“Fill parent”,如图17-1所示。

可以用同样的方式设定Height属性,但一般会将其设为一个数字(如300像素),以便为Canvas上面或下面的其他组件留出空间。

Canvas的坐标系统

Canvas上的图画实际上是一个许多像素构成的表格,像素是手机(或其他设备)屏幕上能够显示的最小的色块,每个像素都在Canvas上有它的位置(或者说单元格),位置由X-Y坐标系定义,如图17-2所示,X定义了水平方向上的位置(方向是从左到右),Y定义了垂直方向的位置(从上到下)。

图17-2 Canvas的坐标系统

坐标轴的方向定义可能与你的经验不一致,不过位于Canvas左上角的单元格的x、y坐标都为零,因此这个位置表示为(x=0,y=0)。(这与App Inventor列表中使用的索引值有所不同,索引值从1开始,看起来更容易理解。)向右移动时,x坐标增大;向下移动时,y值变大。位于左上角单元格右侧的单元格坐标为(x=1,y=0)。右上角单元格的x坐标等于canvas的宽度减1,多数手机屏幕的宽度都在300左右,但这里例子中显示的宽度是20,因此右上角的单元格坐标为(x=19,y=0)。

要改变canvas的外观有两种方法:①在上面绘画,或者②在上面放置移动的物体,本章所涉及的是后者,但我们首先要讨论如何绘画,以及如何通过绘画来创建动画(这也是本书第二章油漆桶中的主要内容)。

Canvas中的每一个单元格都对应显示为一个有颜色的像素。Canvas组件提供的Canvas.DrawLine及Canvas.Circle块可以用来在canvas上以绘制像素组成的图画。首先需要将Canvas.PaintColor属性设置为你需要的颜色,然后调用某个具体的绘画块来画出颜色。其中的DrawCircle块可以绘制直径为任意大小的圆,但如果你将半径设为1,如图17-3所示,那么只能画出一个单独的像素。

图17-3 用1个像素画圆,每次只能画1个单独的像素

在块编辑器Built-in组的Colors抽屉中,App Inventor提供了13种常用的颜色,可以用来绘制像素图(或设置组件背景色)。也可以使用颜色编码方案来获得更为丰富的颜色,颜色编码方案的解释请参见相关App Inventor文档:http://appinventor.googlelabs.com/learn/reference/blocks/colors.html。

改变canvas外观的第二种方法是在canvas上放置Ball和ImageSprite组件。sprite是一个被放置在场景中的图形对象,所谓的场景这里指的就是canvas。Ball和ImageSprite组件都属于sprites类型,只是外观不同而已。Ball为圆形,只能通过改变颜色和半径来改变它的外观,而ImageSprite可以是任何形状;ImageSprite和Ball都只能添加到Canvas中,不可能将它们拖入用户界面中Canvas以外的区域。

用计时事件制造动画

在App Inventor中,为应用添加动画的方法之一就是让物体对计时器事件做出响应,最常用的方法就是让sprite按照设定的时间间隔,在canvas上进行位置的移动。设定的时间间隔的方法是使用计时器事件最通用的方法。稍后我们还将讨论另一种方法,即,利用ImageSprite及Ball组件的Speed(速度)及Heading(方向)属性,通过编程来实现动画效果。

点击按钮以及其他用户触发的事件理解起来非常简单:用户做动作,应用通过执行某些操作来进行响应;但计时器事件则不然:这类事件不是由最终用户发起,而是由时间的流动来触发。你需要将应用中的这类手机时钟触发的事件与用户的行为区分开来。

定义计时器事件的第一步是在组件デザイナー中为应用拖入一个Clock组件。Clock组件有一个关联的TimerInterval(计时间隔)属性,用来以毫秒为单位定义计时器的计时间隔(1秒=1000毫秒)。如果将TimerInterval设为500,就意味着每隔半秒钟触发一次计时器事件。计时间隔越小,物体的移动也就越快。

在デザイナー中完成Clock的添加以及TimerInterval的设定后,就可以在块编辑器中拖出“when Clock.Timer”事件块,并在其中加入任何你需要的块,这些块将每个一个计时间隔执行一次。

产生运动

图17-4 让球在水平方向穿越屏幕

要让sprite随时间移动,就需要用MoveTo函数。在块编辑器的ImageSprite及Ball组件抽屉中可以找到这个函数。例如,要使一个球在水平方向上穿越屏幕,需要使用图17-4中的块。

MoveTo的作用是在canvas上将物体移动到一个绝对位置,而不是相对位置。因此,为了移动到这个绝对位置,需要将MoveTo函数的参数设定为当前位置与增量之和。这里我们要实现球的水平移动,只需要将参数x设定为当前的x值与增量20之和,而y值保持不变(Ball1.Y)。

如果想让球沿着对角线的方向移动,就需要同时设定x、y坐标的增量,如图17-5所示。

图17-5 设置x、y坐标的增量,实现球在对角线方向的移动

控制速度

在前面的例子中,球的移动有多快呢?速度取决于两个因素:Clock组件的TimerInterval属性值,以及MoveTo函数中的参数值。如果计时间隔设为1000毫秒,就意味着每秒钟触发一次计时事件,这样会让运动变得不流畅。为了得到更为平滑的运动,就需要缩短计时间隔。如果将TimerInterval设为100毫秒,则球每隔1/10秒移动20像素,或者每秒移动200像素,对于应用的使用者来说,这个速度看起来会平滑得多。除了改变计时间隔之外,还有一种方法也可以改变速度,你能想到是什么方法吗?(提示:速度与球移动的频次以及每次的移动量相关。)在保持计时间隔100毫秒不变的情况下,改变MoveTo中的算式也可以改变移动的速度:让球每次只移动2个像素,即2像素/100毫秒,这相当于20像素/秒。

高级动画功能

这种让物体在屏幕上移动的能力,适合于那些飘来飘去的动画类广告,但要制作游戏或其他的动画应用,就需要更为复杂的功能。幸运的是,App Inventor提供了几个的高级块,用于处理动画类事件,如物体到达屏幕边缘及两个物体的碰撞。

在这种情况下,用高级块来侦测两个sprite之间的碰撞这类事件,表明App Inventor已经深入到了程序的底层细节。其实你自己也可以利用Clock.Timer事件,通过检查每个sprite的xy坐标及Width、Height属性来检测到这类事件的发生,但这样的程序涉及到非常复杂的逻辑。由于这类事件在许多游戏及其他应用中很常见,因此App Inventor为你提供了这些功能。

抵达边界

图17-6 当球到达边缘时让它重回左上角

重新考虑前面的动画,物体在canvas上沿着对角线方向从左上角向右下角移动。依照前面的程序,物体沿对角线方向移动并将停在canvas的右下角(因为系统不允许sprite对象超出canvas的边界)。

如果想让物体在到达右下角后再重新出现在左上角,可以定义一个事件处理程序Ball.EdgeReached来响应到达边缘事件。

当Ball碰到canvas的任何一个边时,将触发EdgeReached事件(到达边缘事件,该事件只适用于Sprite及Ball组件)。这个事件,再加上前面提到的让球沿斜线移动的定时器事件,两个事件共同作用的结果就是,球从左上角向右下角移动,在到达彼岸猿猴再跳回到左上角,然后继续移动,并再次跳回,循环往复,永不停止(或者直到接到其他指令)。

注意到在EdgeReached事件中有一个参数,edge1,它代表球碰到的那个边,这里用数字来代表不同的方向:

  • North = 1
  • Northeast = 2
  • East = 3
  • Southeast = 4
  • South = -1
  • Southwest = -2
  • West = -3
  • Northwest = -4

CollidingWith事件与NoLongerCollidingWith事件

射击类、运动类游戏以及其他类型的动画应用通常都会涉及到两个或多个物体之间的碰撞(如,子弹击中靶子)。

例如,考虑这样一个游戏,当其中的物体与其他物体发生碰撞时,会改变颜色,并发出爆炸声,图17-7中显示了这样一个事件处理程序。

图17-7 当球与其他物体发生碰撞时,变色并发出爆炸声

图17-8 当碰撞的物体离开时,球变黑色并停止爆炸声

NoLongerCollidingWith事件是与CollidedWith相反的事件,当两个碰到一起的物体分开时,触发该事件。而在游戏中,可能用到图17-8中的块。

注意到CollidedWith及NoLongerCollidingWith事件都有一个参数other,它代表了被撞到的那个物体。这可以用来处理一个物体(如Ball1)与另一个指定物体之间的相互作用。如图17-9所示。

图17-9 只有当Ball1碰撞到ImageSprite1时才做相应

之前我们没有提到过这个“ImageSprite组件”块。如果需要对两个组件进行比较(得知究竟是哪一个与之碰撞),如本例中的情形,就必须指定被比较的具体对象。为此,每个组件都有一个指向它自己的块,而这个块就在ImageSprite1的抽屉里,排在最后一个的就是。

交互动画

到目前为止,我们所讨论的动画行为都没有最终用户的参与。毫无疑问,游戏都是交互的,最终用户扮演着核心的角色,通常他们使用按钮或其他界面对象来控制物体的速度及方向。

作为例子,我们来改变对角线移动的动画,用户可以让移动停止然后再启动。可以通过对Button.Click事件编程来实现这一点,具体方法是控制clock组件的启用与禁用属性。

在默认情况下,Clock组件的timerEnabled属性是被选中的,可以在事件处理程序中动态地设置它,如设为false。例如,在图17-10的事件处理程序中,在用户第一次点击按钮时,可以让Clock的计时作用停止运行。

图17-10 当按钮被第一次点击时,停止计时

在Clock1.TimerEnabled属性被设为false之后,Clock1.Timer事件不再被触发,因此球停止移动。

当然,只是在第一次点击时让运动停止,这样的操作并不能为游戏带来乐趣,需要在事件处理程序中添加一个ifelse块来控制计时功能的启用与禁用,从而实现对运动的双向控制(运动及停止)。如图17-11所示。

在点击按钮的事件处理程序中,第一次点击按钮,计时器停止计时,按钮上的文字由“停止”变为“开始”;第二次点击按钮,此时TimerEnabled的值为false,因此执行“else”分支,于是计时器被置于启用状态,使得物体重新开始移动,按钮上的文字改回“停止”。关于ifelse快的详细信息请参见第18章,另外,关于用方向传感器创建交互动画的例子,请参见第5章及第23章。

图17-11 添加ifelse块,通过点击按钮来控制运动的开始与停止

关于没有计时器的sprite动画

目前为止我们讲述的动画案例都是利用Clock组件的计时功能,计时器事件每触发一次,物体就移动一次。采用Clock.Timer事件的方案是设定动画最普遍的方案,除了可以移动物体,还可以随时间改变物体的颜色,改变某些文字(好像应用自己在输入文字一样),或者让应用以某个速度说话,等等。

App Inventor提供了另外一种不需要Clock组件而让物体的移动的方法。你可能已经注意到,ImageSprite及Ball组件都具有Heading(方向)、Speed(速度)及Interval(间隔)属性。与Clock.Timer方案中定义事件处理程序相比,这里可以在组件デザイナー及块编辑器中设置这些属性,来实现对sprite运动的控制。

图17-12 Heading属性的取值范围

为了便于描述,我们来重新考虑沿对角线移动的例子。Sprite或ball的Heading属性的取值范围为0-360度,如图17-2所示。

如果Heading属性设置为0,则球从左向右移动;如果设为90,则从底向上移动;如果设为180,则从右向左移动;如果设为270,则从上向下移动。

当然,可以将Heading设定为0-360之间的任何值。要想让球沿对角线从左上角向右下角移动,就需要将Heading设为315。

此外,还需要设置Speed属性,它可以是0以外的任何值。此处Speed属性对物体的移动作用与MoveTo函数的作用相同:定义了每个时间间隔(interval)物体移动的像素数,而时间间隔由物体的Interval属性来定义。

尝试设置这些属性,用Canvas及Ball创建一个测试应用,并点击“Connect AICompanion”,在手机(或设备)上查看应用。修改Heading、Speed以及Interval属性,看看球是如何运动的。

如果你想通过编程来实现球在左上角与右下角之间做连续往复运动,可以在组件デザイナー中将球的Heading属性初始值设为315,然后在块编辑器中添加Ball1.EdgeReached事件处理程序,当球到达边缘时,改变它的方向。如图17-13所示。

图17-13 当球到达边缘时改变它的方向

小结

动画是物体随时间的位置移动或某些属性的变化,App Inventor为提供了几个高级的组件及功能,让动画的实现变得简单易行。通过对Clock组件的Timer事件进行编程,可以创建任何类型的动画,包括物体的移动——这是任何类型游戏中最基本的活动。

Canvas组件在设备的屏幕上定义了一个区域,物体可以在其中移动,并产生交互。Canvas内部只接受两种类型的组件,即ImageSprite组件及Ball组件。这些组件为处理碰撞及到达边界这样的事件提供了高级功能。此外,这些组件的Heading、Speed及Interval属性也为运动的实现提供了替代方法。

第16章 应用中的存储

Published by:

就像人类需要记忆一样,应用需要存储。本章将探究如何在应用中实现信息的存储。

如果刚刚有人在电话里告诉你一家披萨店的电话号码,你的大脑中会留下一段记忆;这时,如果有人大声告诉你一串数字,并要你记住,你也会将它们保存到记忆中。在这种情况下,你未必能清楚地意识到,你的大脑是在保存或调用信息。

应用同样具备记忆功能,但它的内在机制并不像大脑那样神秘。本章你将学习如何设置应用的存储功能,如何利用它来保存信息,以及之后如何提取这些信息。

有名称的存储槽

应用的存储功能由一组有名称的存储槽(memory slots)组成。一旦组件被拖到应用中,就会自动创建了一组被称为“属性”的存储槽;也可以定义与特定组件无关的、有名称的存储槽,即变量。如果说属性通常与应用的外观呈现有关,那么变量则被认为是应用中不可见的“暂时”记忆。

属性

图16-1 在属性栏中修改存储槽来改变应用的外观

在应用中,组件,或者说像Button、TextBox以及Canvas这类可视组件,构成了完整的用户界面。而组件本身的外观则是又一组属性来确定,属性值就保存在存储槽中。

在组件デザイナー中,可以直接对属性的存储槽进行修改,如图16-1所示。

图16-1中的Canvas组件具有六个属性:BackgroundColor及PaintColor是保存颜色的存储槽,BackgroundImage保存了文件名(kitty.png),Visible属性保存了一个布尔值(true或false,依赖于是否勾选了方框),而Width及Height属性保存了一个数字或某个特定的设置(如,“Fill parent”)。

在组件デザイナー中设置组件的属性,相当于设置应用启动时的外观。应用的最终用户从未见过应用中有一个名字为Height、值为300的存储槽,他们只能看见用户界面上有一个300像素高的组件。

定义变量

像属性一样,变量也是被命名的存储槽,只是与特定的组件无关。在应用中,需要记住某个状态,如果无法用组件的属性来保存它,就需要定义一个变量来保存它。例如,一个游戏类的应用可能需要记住玩家到达的等级。如果等级数用Label组件来显示,就不需要定义变量,因为Label组件的Text属性可以用来保存这个等级。但是,如果等级数不需要显示给用户,就应该定义一个变量来保存它。

另一个使用变量的例子是第8章总统测验。在这个应用中,用户界面上一次只能显示一道测验题,而其他问题用户是看不见的,因此,就需要定义一个问题列表的变量来保存它们。

在组件デザイナー中拖入一个组件,它的属性就自动创建完成了,相比之下,变量的定义需要在块编辑器中直接拖出一个变量初始化块(initialize global name to),点击块中的“name”为变量命名,并为变量设置初始值,方法是拖出一个块放入变量初始化块中,可以是number块、text块、color块或者是make a list块。跟随下面的步骤就可以创建一个叫做score的初始值为0的变量。

1. 从块编辑器的Built-in分组中找到Variables,点击打开抽屉并拖出“initialize global name to”块,如图16-2所示。

图16-2 拖出变量初始化块

图16-3 为变量命名

2. 为变量命名:点击变量初始化块中的“name”,并输入“score”,如图16-3所示。

3. 为变量设置初始值:从Math抽屉中拖出数字块,将其插入变量初始化块的插槽中,如图16-4所示。

图16-4 为变量设初始值

图16-5 修改变量的初始值

4. 将变量初始值由默认值(0)改为123,如图16-5

定义一个变量,就是通知应用建立一个有名称的存储槽,来保存某个值。像属性一样,这些存储槽用户是看不见的。

变量的初始值在应用启动时就已经被放入存储槽中。可以用数字或文本对变量进行初始化,除此之外,也可以插入一个“make a list”块,它告诉应用这个变量是一个存储槽的列表,而不是一个单独的值。关于list的更多内容请参考第19章。

设置及读取变量

变量定义之后,App Inventor会生成两个属于这个变量的块:set块及get块,只要将鼠标悬停在变量初始化块中的变量名称之上,就可以呼出到这两个块。如图16-6所示。

图16-6 变量初始化块包含访问该变量的set块及get块

其中的“set global score to”块可以用来修改(设置)变量的值,例如图16-7中,将数字块5放在变量score的set块中。变量初始化块中的“global”一词意为“全局的”,指的是变量的适用范围,一个全局变量可以被程序中所有事件处理程序及过程所引用。新版的App Inventor中还可以定义一种“local”变量,这种变量可以在一个事件处理程序或某个过程的内部进行定义(这里暂不涉及)。

图16-7 将数字5赋给变量score

另一个“get global score”块用于从变量中读取变量值。例如,如果你想检查score的值是否大于或等于100,就可以将“get global score”块插入if块进行测试,如图16-8所示。

图16-8 使用get global score块来获取变量值

用表达式为变量赋值

可以用单一的数字5来为变量赋值,不过通常会用一个更为复杂的表达式来为变量赋值(“表达式”是一个计算机科学的术语,即公式)。例如,在总统测试的应用中,用户点击“下一题”按钮时,要让变量currentQuestionIndex的值增加1,来显示下一道题;又如在游戏类应用中,如果玩家失败,还有可能将他的成绩减10分;还有像第3章打地鼠的游戏中,通过改变变量x的值,实现地鼠在Canvas中水平位置的随机移动。因此可以用若干个块组成的表达式插入“set global score”块为变量score赋值。

变量的递增

一种最常见的表达式可能是变量的递增,或根据变量的当前值进行设定。例如,游戏中当玩家获胜一次,变量score就将增加5,如16-9显示了实现这一行为需要的块。

图16-9 分数变量递增5

如果能够理解这些块的含义,你就离程序员又近了一步。这些块可以理解为“让成绩在现有的值上加1”,这是变量递增的另一种说法。要理解这些块的工作机制,需要按照从内向外、而不是从左到右的顺序,最里面的块是“get global score”及数字“5”,它们是最基础的块,然后“+”块执行加法运算,并将结果设定为变量score的值。

假设存储槽中score的当前值为5,经过这些块的运算,程序执行了以下步骤:

  1. 从score的存储槽中读取当前值5;
  2. 加上5得到结果10;
  3. 3. 将10放回到score的存储槽中(来替代5)。

关于变量递增的更多内容请参见第19章。

构造复杂的表达式

在Math抽屉中(图16-10),App Inventor提供了许多数学函数,就像在电子表格或计算器中见到的一样。

图16-10 Math抽屉中的运算符及函数

你可以使用这些块来构造复杂的表达式,并将它们作为赋值表达式插入到“set global to”块中。例如,要想实现一个图片精灵(image sprite)在canvas范围内的随机水平移动,就需要使用一个乘法块(*)、一个减法块(-)一个Canvas.Width属性以及一个随机小数函数来组织表达式,如图16-11所示。

图16-11 使用数学(Math)块来构造上面的复杂表达式

正如在前面变量递增的例子中所说,程序对这些块的解释是遵循从内而外的顺序。假设Canvas的Width属性值为300,ImageSprite的Width为50,程序将执行以下步骤:

  1. 分别从Canvas1.Width及ImageSprite.Width的存储槽中读取300及50;
  2. 减法运算:300 – 50 = 250;
  3. 调用随机小数函数获得一个1-1之间的随机数(比如说0.5);
  4. 乘法运算:250 * 0.5 = 125;
  5. 将125放在ImageSprite.x属性的存储槽中。

显示变量

在前面的例子中,修改一个组件的属性,将直接影响到用户界面的外观,而变量则不然,改变一个变量并不会直接影响到应用的外观。如果你只是将变量score的值递增,而不设法修改用户界面的话,用户永远都不知道变化的存在,就像俗话说的“树木落入森林”一般:如果没有人知道它,怎么证明它的存在呢?

有时,当变量变化时,不希望在用户界面上立即显示出来,例如,在游戏中,可能会记录某些统计结果(如失败次数),只有游戏结束时才会显示其结果。

与组件的属性相比,这是使用变量来存储数据的优势:可以在需要的时间显示必要的数据,也可以使应用中的计算与用户界面分离,这样做的结果是更易于稍后对用户界面的修改。

例如,在游戏中,可以将成绩直接保存在Label的Text属性中,也可以保存在变量中。如果保存在Label中,得分时可以让Label的Text属性值递增,用户可以直接看到成绩的变化;如果成绩被保存到变量中,并用变量的递增记录得分,则需要另外设置块,将变量值显示到Label中。

尽管使用变量保存并显示数据要多出一些步骤,但当你决定要修改应用,以不同的方式在用户界面上显示成绩时,变量的方法让改变很容易实现。你不必对每个显示组件上的成绩进行修改,它们不需要修改,你只需要修改那些与显示有关的块。

使用Label而非变量的方法,会让应用变得难于修改,因为,比如说要用一个递增的值来控制label的宽度(Width),每一次递增都要执行一次对Width属性的修改。

小结

应用启动之后,开始执行一系列的操作,并对发生的事件进行响应。在事件响应过程中,应用有时需要记住一些东西,如,游戏中每个选手的成绩,或者某个对象的移动方向等。

应用可以用组件的属性来实现存储,但当你需要与组件无关的存储槽时,就需要定义变量。可以将值保存到变量中,也可以从变量中读取当前值,就像使用组件的属性一样。

无论是属性值,还是变量值,对用户来说都是不可见的。如果你想让用户看到保存在变量中的信息,只要添加块,就可以用Label或其他用户界面组件来显示这些信息。

第15章 软件工程与应用调试

Published by:

前面几章中讲过的Hello猫咪、打地鼠以及其他应用都是些非常小的软件项目,并不需要用引入软件工程的概念。工程的概念借用自其他行业,意为设计并建造,教程中的应用就像是用预制件拼装起来的房屋模型,而软件工程才是设计并建造真正用来居住的房子。这个例子虽然稍显夸张,但一般来讲,某些极其复杂的建造过程,的确需要大量的前期构思、规划以及技术分析,这些过程都可以归结为工程。

但凡接手过一个相对复杂的项目,你就会理解,只要在功能上稍稍增加一点复杂度,软件工程的复杂程度就会急剧增加,两者之间绝对不是线性的关系。对于我们大多数人来说,在真正开始面对这样残酷的现实之前,我们很少能够意识到将要面临的困顿。从这个意义上讲,你要准备学习更多的软件工程的原则及调试技巧。如果你已经认可这一点,或者,你是为数不多的、希望通过掌握一些技术来克服成长障碍的人,那么本章就是为你准备的。

软件工程原则

以下是本章所涵盖的一些基本原则:

  • 未来的软件使用者应该尽早,并尽可能多地参与到软件的设计及开发过程中来;
  • 建立一个初始的、简单的原型,并逐步完善;
  • 编码与测试同步进行,不要一次测试太多的代码(App inventor中的块);
  • 开始编码前进行逻辑设计:对功能做纵向切割,对技术或实施的复杂度做分层切割,并各个击破;
  • 对代码块进行注释,以便其他人(和你自己)能理解这些程序;
  • 学会用纸笔来跟踪记录块的执行过程,以便于理解它们的工作机制。

如果能够遵循上述原则,你就可以节省时间,避免挫折,从而制作出优秀的软件。但你很有可能做不到每次都依原则行事!有些原则看似违背常理。一种自然的倾向是,首先有了一个想法,并假设你了解用户的需求,然后开始把若干个块拼在一起,直到完成了想象中的任务。现在,让我们回到软件工程的第一个原则,在正式开始动手之前,看看如何了解用户的需求。

设计要面对真实的人、现实的问题

电影《梦幻成真》(Field of Dreams)中的男主角Ray听到了一个声音向他低语:“如果你建好了,他们就会来。”Ray听从了这个声音,在爱荷华州的农场中间建了一个棒球场,果然,在1919年,芝加哥白袜队(White Sox)和成千上万个球迷出现在这里。

不过你现在必须明白,那个低语的建议绝对不可以用于软件开发。事实上,必须正相反。在软件开发的历史中,充斥着各类“没问题”的伟大方案(如:“让我们写个软件,告诉人们开车到月球需要多长时间!”)。一个优秀的(同时也是极有可能获利的)软件的真正目的是解决现实中的问题。想知道问题出在哪里,就要找到有问题的人,并与他们交谈,这就是通常被称作“以用户为中心”的设计方法,这个方法同样也可以帮助你做出更好的应用。

如果遇到程序员,你可以问他们,在他们所写的软件中,有多少被真正交付到了最终用户的手中。结果会让你感到惊讶:即使是对那些伟大的程序员来说,这个比例也还是太小了!许多软件项目驶入了问题的泥沼而终无见天之日。

以用户为中心的设计理念意味着尽早并尽可能多地替未来的使用者着想,并与他们交流,这种思考与交流甚至应该在尚未确定目标之前就开始。大多数成功的软件都是针对某个具体的人,试图解决他的特定问题,也只有这样,最终才能发展成一个伟大的产品。

快速地创建软件原型,并展示给未来的使用者看

如果让最终用户阅读软件功能的说明文档,他们多半不会给出任何有效的回应,他们不会对文档做出反馈。真正有效的方法是,让他们体验未来软件的交互模式,即软件的原型。原型是一个不完整的、未经重构的软件版本,创建原型的目的在于充分体现软件所具有的核心价值,而不必注重细节、完整性或漂亮的用户界面。拿出原型让未来的使用者看,然后安静地倾听他们的反馈。

迭代式开发

在首次明确了软件的具体规格之后,采用迭代式开发。你可能很自然地倾向于将所有组件和块一股脑地添加到应用中,然后下载到手机上看看它是否好用。举例来说,“答题”应用,在缺乏指导的情况下,多数初学者会一次性添加所有的块:带有一长串问题及答案的块、浏览问题的块、检查用户答案的块,以及与每个逻辑细节有关的块,所有的块未经测试就全部罗列在应用中,这种开发方式在软件工程中被称为“大爆炸”方式。

几乎所有的初学者都会采用这种方式。在旧金山大学(USF)的课堂上,当学生忙于创建应用时,我经常会问他一个问题:“进展如何?”

“我想我做完了,”他说。

“好极了,能让我看看吗?”

“哦,还不行,我没带手机来。”

“那么你还从来没有运行过这个程序,对吗?”我问。

“嗯…”

我透过他的肩膀看到了30个左右色彩缤纷的块,但他居然连一个功能都没有测试过。

程序员们很容易着迷,他们沉湎于创建UI(用户界面)并在块编辑器中创建所需的行为。那些块天衣无缝地结合在一起,优雅地排布在屏幕上,这些让他们感到倾心,却忘记了创建一个让其他人也能使用的、完整的、通过测试的应用。这听起来像是洗发水的广告,但对于我的学生和那些有志成为程序员的人,这是我能给出的最好的建议:代码要随写随测,周而复始。

每次只写少量代码,并随时测试,这个过程本身会变成一种习惯,如此这般,在不久的将来,你会收获令人惊异且满意的成果(而且几乎杜绝了大而难缠的程序漏洞——bug)。

先设计,后编码

编程要分两步走:①理解应用的逻辑,②将这些逻辑翻译成某种形式的编程语言。在开始翻译之前,要在逻辑上花一些功夫:首先要明确应用中将会发生哪些事情,无论是用户引发的,还是应用内部的;其次在正式开始将逻辑翻译成代码块之前,要明确每个事件处理程序中的逻辑。

有许多专门讨论各种程序设计方法的书籍。有些人喜欢用流程图或结构图来做设计,有些则更愿意将设计或草图写在纸上,更有人认为所有的“设计”最终应该体现为代码的注释,而不是一个与代码分离的文档。对于初学者来说,关键是要理解所有的程序在本质上都是一套逻辑,而这种逻辑与具体的编程语言无关。当然,思考应用的逻辑和翻译为编程语言这两件事有时难免会同步进行,无论这种编程语言是否直观。因此,在整个逻辑思考阶段,应该远离电脑,想清楚应用最终要实现哪些功能,并以某种方式随时记录下你的想法,然后让设计文档与应用保持关联,以便其他人也可以从中获益。下面我们就来实践这一过程。

对代码进行注释

你已经学过了本书中的教程部分,应该见过块所附带的黄色方框(见图15-1),这就是“注释”。在App Inventor中,任何的块都可以添加注释,方法是在块上单击鼠标右键,并在快捷菜单中选择Add Comment。注释丝毫不影响程序的运行。

图15-1 为测试条件块添加注释——用简洁的语言描述块的作用

那么为什么要做注释呢?想想看,如果你的应用很成功,它的生命周期会很长,即便只是搁下一周的时间,你都有可能忘记当时的想法,想不起来这些块有什么用处。因此,尽管没有别人会看到你的代码块,你也应给添加这些注释。

假如你的应用很成功,毫无疑问它会传到很多人手里,人们想了解它、按自己的需要修改它,或者扩展它的功能,等等。在开源的世界里,很多项目会以现有项目为基础,做进一步的修改和完善,只要你亲身体验过那些没有代码注释的项目,你就会彻底明白为什么注释是必需的。

为程序添加注释并不是一种自觉地行为,我也从未见到过初学者重视它,然而,我也从未见到过一个经验丰富的程序员不重视它。

切割、分层、各个击破

当问题的规模大到难以应对时,解决之道在于将问题分解,分解的方法有两种:第一种方法我们非常熟悉,即,将问题分解为若干个部分(如A、B、C),然后各个击破;第二种则不太常见:将问题按照从简单到复杂的顺序逐层分解。对应到App Inventor的编程方法上,就是先添加少量的块来实现简单的功能,并测试其效果,再逐渐过渡到复杂的功能,以此类推。

让我们以第10章的“出题”应用为例来具体阐述这两种方法。在应用中,用户可以点击“下一题”按钮对问题进行浏览,也可以检查用户的答案是否正确。从设计角度,可以将应用分解为两个部分:问题浏览及答案核对,并针对两个部分单独编程。

但在每个部分中,还可以对整个过程按照从简单到复杂的顺序进行分解。例如,问题浏览环节,先创建代码来显示问题列表中的第一题,并测试其是否有效;然后编写代码来浏览到下一题,暂时不考虑到达最后一题时可能引起的错误;当测试结果证明可以从头至尾浏览所有问题时,再添加块来处理用户浏览到最后一题的“特殊情况”。

究竟是将问题分解为几部分,还是按照复杂性分解为若干层,这不是一个非此即彼的问题,但却是一个值得思考的问题,关键在于哪种方法更适合于你所创建的应用。

理解编程语言:用纸和笔跟踪记录

应用在运行过程中,仅有部分可见。最终用户只能看到它的外观——用户界面上显示的图形及数据,而软件的内部运作机制对外部世界来说是不可见的,就像人类大脑的内部机制一样(谢天谢地!)。应用在运行时,我们既看不到这些指令(块),也看不到跟踪当前正在执行的指令的程序计数器,更无法看到软件的内部存储单元(应用中的属性及变量)。不过说到底,这正是我们想要的:最终用户只能看到程序需要被显示的部分,但对于开发者来说,在开发及测试过程中,你需要了解所有正在发生的事情。

作为一个开发者,在开发过程中所看到的代码,都只是些静态视图,因此必须靠想象力来驱动软件的运行:事件发生了,程序计数器移动到下一个块,并执行这个块,内存单元中的值发生了变化,等等。

编程过程中需要在两种不同的场景之间切换:先从静态模式——代码块开始,并试着想象程序的实际运行效果;一切就绪后,切换到测试模式——以最终用户的身份测试软件,看它的运行结果是否与预期的结果相一致。如果不是,必须再切换回静态模式,调整程序,然后再试。如此循环反复,最终获得一个满意的结果。

初学者对于计算机程序的运作方式知之甚少,整个过程看起来就像魔术。依照本教程的指导,学习应该从简单的应用开始(如,点击按钮导致猫叫),再逐渐过渡到较为复杂的应用,而且随着学习的不断深入,或许还可以根据自己的需要,对教程中的例子做出修改。从初学者到入门者,对程序的内部运作机制有了一些了解,但依然感到对整个过程无法控制。他们经常会说:“这个不起作用,”或者“它不应该是这样的。”关键是要理解程序如何实现那些你主管想象出来的功能,而且要说:“我的程序正在做这件事”,以及“我的逻辑导致了程序的…”。

了解程序运行机制的方法就是剖析一个简单应用的执行过程,在纸上精确地描绘出每个块在执行时,设备的内部发生了什么。想象用户触发了某个事件处理程序,然后逐步跟踪并记录块的执行效果:应用中的变量及属性如何改变,用户界面上的组件如何改变。就像文学课上的“精读”环节,这样一步一步的跟踪可以促使你检查语言中的各个要素(即App Inventor中的块)。

对复杂性的描述几乎是完全抽象的,重要的是你要放慢思路,理清各个块之间的因果关系。最终你会明白,这些过程控制的规则,并不像最初想象的那样难以理解。

以第8章总统测验为例,如图15-2所示,思考图中的这些块(对原教程做了一点修改)。

图15-2 应用启动时,将QuestionLabel的Text属性设置为QuestionList列表的第一项

你能理解这些代码吗?你能跟踪这些代码,并说明每一步都发生了什么吗?

首先跟踪所有相关的变量及属性。画出存储单元的表格,这个例子中,表头分别为currentQuestionIndex和QuestionLabel.Text,如表15-1.

15-1 记录text属性及index值变化的表格
QuestionLabel.Text currentQuestionIndex

接下来,思考当应用启动时,发生了哪些事——不要以用户的视角来看,而是从应用的内部来分析它的初始化过程。如果你学过这些教程,你可能知道这个过程,但你可能没有从机制方面去思考过。当应用启动时:

  1. 完成了所有组件的属性设定,它们的值等于在组件デザイナー中设定的初始值;
  2. 完成了所有变量的定义及初始化;
  3. 执行了Screen.Initialize事件处理程序中的所有块。

对程序进行跟踪有助于理解程序的运行机制,那么在完成了应用的初始化之后,表格中应该填写什么内容呢?

如表15-2所示,currentQuestionIndex的值为1,因为应用启动时完成了变量的定义,并将其初始值设为1;而QuestionLabel.Text的值为第一题,因为在Screen.Initialize中选择了QuestionList列表中的第一项,并放入了QuestionLabel中。

表15-2 总统测验应用初始化后,QuestionLabel.Text与currentQuestionIndex的值
QuestionLabel.Text currentQuestionIndex
哪位总统在大萧条时期实施了“新政”? 1

下面再来跟踪用户点击“下一题”按钮时发生的事情。

图15-3 用户点击“下一题”按钮时执行的块

逐个检查每个块。首先是变量currentQuestionIndex的递增,说得更具体一些,变量当前值是1,经过+1的运算后,将结果2再赋给变量currentQuestionIndex。接下来看if语句,列表QuestionList的长度为3,显然currentQuestionIndex的值2小于3,因此if语句的结果是false(假),于是列表中的第2项(第二题)被写入QuestionLabel.Text中,如表15-3所示。

表15-3 点击“下一题”按钮后的变量及属性值
QuestionLabel.Text currentQuestionIndex
哪位总统在1979年实现中美建交? 2

跟踪“下一题”按钮的第二次点击。现在currentQuestionIndex已经递增到3,会发生什么呢?继续阅读之前,细心地检查一下,看你能否跟踪正确。

在if测试中,currentQuestionIndex的值(3)的确≥列表QuestionList的长度(3),于是currentQuestionIndex的值被设为1,第一题被写入label,如表15-4所示。

表15-4 “下一题”按钮被第二次点击时的值
QuestionLabel.Text currentQuestionIndex
哪位总统在大萧条时期实施了“新政”? 2

我们的跟踪揭露了一个错误:最后一题永远也无法显示!

通过类似的跟踪,最终使你成为一名程序员、工程师。你开始从机制上去理解编程语言,掌握代码中的语句和词汇,而不是对一些片段的模糊理解。诚然,编程语言是复杂的,但机器对每个“词”都有明确而且简单的解释,如果理解了块与变量或属性变化之间的对应关系,也就理解了如何编写或修复你的应用,当然也就实现了对应用的完全控制。

现在如果你告诉朋友们,“我正在学习如何让用户点击‘下一题’按钮来看到下一道题,这实在是太难了!”他们会以为你疯了。但这个过程的确很困难,困难不在于概念的复杂性,而在于你不得不有意让自己的脑子慢下来,来搞清楚计算机的每一步处理过程,包括那些你的大脑下意识完成的过程。

应用的调试

逐步跟踪不仅是理解编程的方法,同样在调试有问题的应用时,也是一个屡试不爽的方法。

像App Inventor这样的开发工具(通常被成为交互式开发环境,或IDEs-Interactive Development Environments)一般会提供了一种调试工具,相当于纸笔跟踪记录的高科技版本,能够自动完成某些跟踪过程,这极大地改善了应用开发的进程。这些工具提供了一个描述正在运行的应用的视图,程序员可以在其中:

  • 在任何一点暂停应用来检查其中的各个变量及属性;
  • 单独执行某些指令(块)来检查它们的执行效果。

监视变量

说明:监视变量是AI1(App Inventor version1.0)中的功能,目前尚未在AI2中实现。

单独测试块

除了可以用监视功能来检查应用运行过程中变量及属性的变化,还有另一个工具“Do It”,可以让你脱离开程序通常的运行顺序,单独测试某些块的运行。右键点击一个块,在快捷菜单中选择“Do It”,这个块就会开始执行,如果这个块是一个有返回值的表达式,App Inventor将在块的上方的方框内(在注释块中插入两行)显示返回值。如图15-4及15-5。

图15-4 右键点击事件处理程序中的任何一个块,会弹出快捷菜单

图15-5 在快捷菜单中选择“Do It”,可以执行该块,并查看返回值(如果有)

“Do It”在调试块的逻辑错误时非常有用。还是回到“总统测试”例子中的NextButton.Click事件处理程序,并假设程序中存在逻辑错误,无法浏览所有的问题。调试过程需要在开发环境及测试设备上同时进行。在用户界面上点击“下一题”按钮,然后回到块编辑器查看是否每次点击都显示了适当的问题。也可以监视变量index在每次点击时的变化。

但是这类测试只允许检查整个事件处理程序的执行效果,在运行完所有的块之前,你无法检查你要监视的变量或用户界面。(抓不到逐句的中间状态)

“Do It”允许你减缓测试过程,并检查任何一个块执行完成后的整个应用的状态。一般是从用户界面上的事件开始跟踪,直到发现问题所在。在发现无法显示最后一题之后,你可能在用户界面上点击“下一题”一次转到第二题,然后不再继续点击“下一题”,而是在块编辑器中让整个事件处理程序一步一步地运行。在NextButton.Click事件处理程序中,每次对一个块使用“Do It”让块执行,如图15-6中,先右键点击第一行的块(让变量index递增),并选择“Do It”。

此时index的值变为3,应用停止执行——“Do It”只能使被选中的块以及它所包含的子块运行,这可以让测试者检查被监视变量以及用户界面的变化。接下来,选择下一行要测试的块(if测试)并选择“Do It”来执行该行,其中的每一步都能看到每个块的执行效果。

图15-6 使用“Do It”工具,每次只执行一个块

使用“Do It”渐进式开发

有一点需要强调,这种逐行执行指令的方式不仅仅适用于程序的调试,它同样适用于开发过程中的随时测试。例如,如果你写了一个很长的公式来计算两个GPS坐标之间的距离,你可能要分步测试这个公式,来验证这些块的使用是否正确。

启用与禁用块

另一个有助于渐进式调试应用的方法是启用或禁用某些块,它允许应用中保留有问题的或未经测试的块,并让系统在运行过程中暂时忽略它们,然后充分调试那些启用状态的块,而不必担心那些有问题的部分。禁用块很简单,在块上点右键,在快捷菜单中选择Disable Block即可,被禁用的块呈现为灰色,在应用运行时,这些块被忽略;需要时,还可以重新启用这些块,方法是在块上点击右键并选择Enable Block。

小结

App Inventor的伟大之处在于它的易用性——可视化的特点让你可以直接开始一个应用,而不必担心那些低层的细节。但现实的问题是,App Inventor不可能知道你的应用要做什么,更不知道如何来做。尽管直接进入组件デザイナー与块编辑器创建应用是件让人着迷的事情,但这里要强调的是,花一些时间来思考并详细、准确地设计应用的功能,是非常重要的。这听起来有些烦,但如果你能听取用户的想法、创建原型、测试并跟踪应用的逻辑,那么创建出精彩应用的目标指日可待。

ツールバーへスキップ