第 24 章 与Web API通信

最后更新于:2022-04-01 02:54:16

移动技术再加上无所不在的网络,已经完全改变了我们生活的这个世界。如今坐在公园里就可以打理你的银行账户,或者在亚马逊书店搜索你正在阅读的图书的评论,或者查阅Twitter,看看世界上其他公园里的人们都在想些什么。手机只能打电话发短信的时代已经过去,它可以让你随时随地访问世界各地的数据。 虽然用手机浏览器可以访问互联网,但由于屏幕太小,而且速度受到限制,因此使用者会感觉不适。如果能够定制应用,有针对性地从网络上提取少部分信息,以适应手机终端的特点,就可以获比浏览器得更具吸引力的替代方案。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e405104627f.png) 本章我们将领略从网络获取信息的各类应用,首先创建一个显示游戏排行榜的(图表)应用,然后以Yahoo财经频道的股票数据为例,讨论如何使用TinyWebDB从网上获取任意类型的信息(不只是图像),最后讨论如何创建属于自己的网络信息源,以用于App Inventor应用。 创新就是对这个世界的重组,以一种新奇的方式将旧的观念和内容组合在一起。埃米纳姆(Eminem,美国说唱歌手)的单曲Slim Shady追随了AC/DC(最著名的澳大利亚摇滚乐队)与Vanilla Ice(美国白人说唱歌手)的风格,并使这种混搭的音乐风行一时。这一类的“模仿”非常普遍,以至于许多艺术家,包括Girl Talk(专攻混搭及数字音乐的美国音乐家)及Negativland(来自美国加州的一个实验音乐乐队),都致力于将旧的内容融入某种新的风格。 无独有偶,在网络及移动世界中,网站及应用混合了来自各种渠道的数据及内容,而且很多网站在设计理念上遵循了互联互通原则(interoperability)。一个典型的混搭网站的例子就是Housing Maps([http://www.housingmaps.com](http://www.housingmaps.com/)),如图24-1,它从网站Craigslist([http://www.craigslist.org](http://www.craigslist.org/))上采集房屋租赁信息,并与谷歌地图API结合起来,提供一种新型的信息服务。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4051669a1d.png) **图 24-1 住房地图(Housing Maps)应用将Craigslist的房屋信息与谷歌地图信息叠加起来** 谷歌地图不仅仅是可供访问的网站,同时也提供相应的应用程序接口服务(web service API),这使得“住房地图”这类混搭应用成为可能。我们普通人只能通过浏览器访问[http://maps.google.com](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所示,大多数的网站都同时提供人机访问接口。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e405175765f.png) **图 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](https://google-developers.appspot.com/chart/interactive/docs/gallery)),你将看到如图24-3的页面。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40517c3afe.png) **图 24-3 谷歌图表API生成的各类图表** 网站提供了完整的说明文档及操作向导,可以交互式地创建图表,并探究如何书写URL地址。向导非常好用,可以通过表单来定义各种类型的图标,并能自动生成你需要的URL地址,你还可以反过来用自己的数据验证这个地址的有效性。让我们开始吧,访问网站,跟随向导来创建图表,然后仔细分析生成这些图表的URL地址的格式。看下面的例子,在浏览器中输入以下URL地址: [http://chart.apis.google.com/chart?cht=bvg&chxt=y&chbh=a&chs=300x225&chco=A2C180&chtt=Vertical+bar+chart|(垂直条状图)&chd=t:10,50,60,80,40,60,30](http://chart.apis.google.com/chart?cht=bvg&chxt=y&chbh=a&chs=300x225&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) 你将获得图24-4所示的图表。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4051849d5c.png) **图 24-4 谷歌图表API根据URL地址生成了这个图表** 要想理解之前输入的URL地址,就需要了解URL地址的作用。你会发现其中包含了问号(?)及and符号(&)。其中的?标志着第一个参数的出现,而&号将后续的各个参数分隔开。每个参数都由名称、等号及值组成,因此在上面调用图表API([http://chart.apis.google.com/chart](http://chart.apis.google.com/chart))的例子中,使用了七个参数,其具体内容如表24-1所示。 **表24-1 图表API中使用的带参数的URL地址** | 参数 | 值 | 参数的含义 | | --- | --- | --- | | cht | bvg | 图标的类型为条状图(bar)、垂直的(verbical)、分组的(grouped)。 | | chxt | y | 在y轴上显示数字 | | chbh | a | 自动设置条的宽度及间隔 | | chs | 300x225 | 整个图表尺寸(像素值) | | 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](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()。在组件设计器中无法Picture属性,因为这一属性只接受加载的文件,因此需要在块编辑器中进行设置,如图24-5所示,添加Screen1.Initialize事件处理程序,并在其中设置Image1.Picture属性。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4051db640a.png) **图 24-5 应用启动时,设置image组件的picture属性为一个图表API的URL** 在手机或模拟器中将显示图24-6所示的图像。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4051e17748.png) **图 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中显示的最终的方案。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4051e7531d.png) **图 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=300x225&chco=A2C180&chtt=Vertical+bar+chart|(%E5%9E%82%E7%9B%B4%E6%9D%A1%E7%8A%B6%E5%9B%BE)&](http://chart.apis.google.com/chart?cht=bvg&chxt=y&chbh=a&chs=300x225&chco=A2C180&chtt=Vertical+bar+chart|(%E5%9E%82%E7%9B%B4%E6%9D%A1%E7%8A%B6%E5%9B%BE)&),第二部分为变量chdPara。 6\. 这里为了跟踪参数值,添加了一个名为chdParaLable的标签,用于显示最终生成的参数。 到此为止,我们生成了动态的URL,这样的方式具有普遍的适用性,例如,假设用户在成绩列表中新增了若干项,那么这个程序也是好用的。图24-8显示了在手机中应用运行的结果。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40528dbebb.png) **图 24-8 应用在手机中运行的效果** 你可以在任何游戏或应用中,采用本例中的方法来显示各种图表,也可以与其他API进行通信,将更多地内容植入到自己的应用中,其中的关键是App Inventor提供了可以获取网络图片的Image组件。 ## **与网络数据API通信** 提示:App Inventor现在提供了一个web组件,可以更容易地访问API数据,虽然下述的TinyWebDB方案仍然有效,但建议查看以下链接中使用web组件的例子: [http://www.appinventor.org/stockmarket-steps](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](http://appinventorapi.com/)上提供了一些这样的服务。 ### **探索API的网络接口** 本节将学习使用TinyWebDB获取股票价格信息,信息来源于一个App Inventor兼容的API,网址是[http://yahoostocks.appspot.com](http://yahoostocks.appspot.com/)。访问该网址,将看到一个如图23-9所示的web接口(人类可访问的)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4052945ea9.png) **图 24-9 App Inventor兼容的雅虎金融API的web接口** 在Tag输入框中输入“IBM”或其他股票的代码,网页上将返回股票信息列表,每一项代表一个不同的信息,后面将解释这些数据的含义。 不过,在web页面上查找股票信息并不是什么新鲜事,它的真实目的是为程序员提供一个机器对机器的访问接口,从而实现与API之间的底层通信。 ### **通过TinyWebDB访问API** 创建股票查询应用的第一步是在组件设计器中拖入一个TinyWebDB组件,该组件只有一个属性可以设置,即ServiceURL,如图24-10所示,它的默认值为:[http://appinvtinywebdb.appspot.com](http://appinvtinywebdb.appspot.com/),指向默认的web数据库。而这里我们要访问的雅虎股票API,因此将其设置为[http://yahoostocks.appspot.com](http://yahoostocks.appspot.com/),与你之前在浏览器地址栏中输入的URL相同。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4052a0f402.png) **图 24-10 将ServiceURL属性设置为[http://yahoostocks.appspot.com](http://yahoostocks.appspot.com/)** 下一步是调用TinyWebDB.GetValue,向网站请求数据。这个操作可以放在一个Button.Click事件中:当用户在手机的应用界面中输入股票代码并点击“提交”按钮时,执行此调用;或者将其放在Screen.Initialize事件中,在应用启动时,自动获取某个股票的信息。无论哪种情况,都需要为GetValue设置tag——某个股票的代码,如图24-11所示,就像在网站[http://yahoostocks.appspot.com](http://yahoostocks.appspot.com/)上的操作一样。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4052f5964b.png) **图 24-11 请求股票信息** 在第10章的“出题”应用中,我们已经讨论过数据库组件TinyWebDB,它的通信方式是异步的:应用中调用TinyWebDB.GetValue请求数据,之后程序将继续运行,必须为这次请求提供另一个事件TinyWebDB.GotValue的处理程序,当请求的数据从网络服务端返回时,来接收并处理这些数据。通过在用户界面[http://yahoostocks.appspot.com](http://yahoostocks.appspot.com/)上的操作,我们已经知道返回的数据为列表,每个列表项代表股票的不同信息(如,第二项代表股票的收盘价)。 客户端的应用可以利用网络所提供的部分或全部信息,如,如果你想显示股票的当前价格,并与开盘价进行比较,你就可以按照图24-12的方式来组织数据。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4052fa7641.png) **图 24-12 使用GotValue时间来处理从Yahoo返回的数据** 如果从网页[http://yahoostocks.appspot.com](http://yahoostocks.appspot.com/)上直接向API提交请求,你会看到返回列表的第2项的确是股票的当前价格,而第5想是当前价格与当天开盘价之间的差。这个例子只是简单地从API的返回值中提取部分信息,并用两个label显示出来:PriceLabel与ChangeLabel,如图24-13所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4053012d2c.png) **图 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/](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。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4053071520.png) 提示:即使你从未见过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](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](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 章 传感器

最后更新于:2022-04-01 02:54:13

将你的手机指向天空,谷歌星空地图会显示出你正在观看的星群;倾斜手机,可以控制你的游戏;带着你的手机去散步,一款“面包渣儿”应用将记录下你的途经的路线。所有这些应用之所以能够实现,都是因为你所携带的移动设备装备了高科技的传感器,可以探测到位置、方向以及加速度。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404939ae9b.png) 本章将再次讨论App Inventor的位置传感器、方向传感器以及加速度传感器等组件,其中将学习全球定位系统(GPS)、方向测量(如倾斜、旋转及摇晃)以及与处理加速度读数相关的数学知识。 ## **创建位置感知应用** 在智能手机流行之前,计算仅限于桌面电脑。虽然便携式电脑算是移动设备,但与我们今天随身携带的微型设备相比,不可同日而语。计算已经摆脱了实验室及办公室,在地球上随时随地都在发生。 对计算的普遍性产生深刻影响的是一项新的、有趣的数据,它存在于上述的所有应用中,即:当前的位置信息。当人们在世界各地游走时掌握他们的行踪,这件事影响深远,它既有可能对我们的生活产生极大的帮助,但同时也存在侵犯隐私及损害人权的可能。 在“安卓,我的车在哪”的应用中(第7章)就是一个有益的位置感知应用的例子,让我们可以记住之前的地点,以便稍后还能找回来。这是一个个人应用——位置信息就保存在自己的手机数据库中。 同样的理念也适用于群组。例如,一个徒步旅行者小组可能希望在荒野中查看每个组员的去向,或者一个商务团队可能希望在一个大型会议上寻找自己的伙伴。这类应用已经出现在市场上,两个典型的应用就是“谷歌纵横(Latitude)”([www.google.com/latitude](http://www.google.com/latitude))以及Facebook的“签到(Place)”([www.facebook.com/places](http://www.facebook.com/places))。由于公众对隐私的担忧,这些应用一经面世便备受争议。 另一类位置感知应用使用了增强现实工具。这类应用利用位置及手机的方向,在自然信息基础上,提供增强的叠加信息。因此当你用手机指向一栋建筑物时,你会看到它在房地产市场上的价格,或者你在植物园中欣赏异国花卉时,某个应用会告诉你这株植物的品种。这类应用的早期产品包括世界浏览器(Wikitude——一款增强现实的实景地图导航应用)、手机实景浏览器(Layar——第一款手机版的增强现实浏览器)以及谷歌星空地图。 世界浏览器甚至可以让用户通过网站[http://wikitude.me](http://wikitude.me/)在移动云上添加数据。在网站上,选定地图并标注上你的个人信息,稍后,当你或其他用户在这个位置使用该移动应用时,你发布的信息就会显示出来。 ## **GPS** 创建一个位置感知应用,首先需要了解全球定位系统(GPS)的工作原理。GPS数据来自美国政府所保有的卫星系统,只要在视野开阔地带,至少能看到三颗卫星,你的手机就能获得读数。一份GPS读数包括位置的纬度、经度及海拔高度。纬度表示与赤道的距离,赤道以北为正值,以南为负值,范围从-90至90.如23-1显示了厄瓜多尔基多附近的谷歌地图,图中的纬度为-0.01,表示在赤道偏南一点点。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404994d600.png) **图 23-1 位于赤道上的厄瓜多尔首都基多** 经度是距离本初子午线(零度经线)向东或向西偏离的距离,向东为正值,西为负值,零度经线穿过的最知名的地点就是格林威治,伦敦附近的一座小镇,皇家天文台的所在地。图23-2中的地图标出了格林威治,它的经度为0.0。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404a3d909d.png) **图 23-2 格林威治的皇家天文台沿本初子午线射出一道光柱** 经度值从-180到180,图23-3显示了俄罗斯境内的一点,非常靠近阿拉斯加,它的经度为180.0,这个点可以理解为以格林威治(经度为0.0)为起点绕地球半圈所到达的位置。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404a497df7.png) **图 23-3 在俄罗斯与阿拉斯加边境附近的一点,经度为180** ## **用App Inventor感知位置** App Inventor为访问GPS信息提供了LocationSensor(位置传感器)组件,该组件具有Latitude(纬度)、Longitude(经度)及Altitude(海拔高度)三个属性,此外它可以与谷歌地图通信,因此还可以获得当前街道地址的信息。 图23-4中的LocationSensor. LocationChanged是位置传感器组件LocationSensor最关键的事件处理程序。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404a55d7e7.png) **图 23-4 LocationSensor1.LocationChanged事件处理程序** 两种情况可以触发LocationChanged事件:传感器第一次收到读数时,以及当位置发生一定变化后收到新的读数时。其中第一次读数通常会延迟几秒钟,有时也会一直没有读数。例如,如果你在室内而且没有连接WiFi,设备将无法获得读数。手机中也有相关的设置,可以为了省电而关闭了GPS,这是无法获得读数的另一个可能的原因。除了这些原因,在LocationSensor.LocationChanged事件被触发之前,不能排除LocationSensor做了不合理的属性设置。 处理这种无法感知位置的情况,有一个方法是创建一个变量lastKnownLocation,并将其初始化为“未知”,然后让LocationSensor.LocationChanged事件处理程序来修改变量的值,如图23-5所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404a5c51db.png) **图 23-5 变量lastKnownLocation的值会随位置的改变而改变** 通过编写以上事件处理程序,在第一次获得读数之前显示“未知”,这样就可以始终显示当前位置,或将位置信息保存到数据库中。这一策略在第4章“开车不发短信”中使用过,即,在自动回复的短信中加入位置信息:“未知”或最后一次获得的读数。 也可以使用LocationSensor.HasLongitudeLatitude块,直接询问传感器是否具有读数。如图23-6所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404a64ddb4.png) **图 23-6 用HasLongitudeLatitude块测试传感器是否具有读数** ## **检查边界** 事件LocationChanged的一种通常的用法是检查设备是否在某个边界之内,或在某个设定区域内。例如,看图23-7中的代码,每次当传感器获得的读数显示某人离开零度经线的距离超过0.1度时,让手机产生震动。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404a6db259.png) **图 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所示添加相关的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404a7653c6.png) **图 23-8 显示方向传感器数据的代码块** ### **使用滚动参数Roll** 现在通过用户对设备的倾斜,来实现图像在屏幕上的左右移动,就像在射击或赛车类游戏中那样。拖入一个Canvas组件,宽度设为“Fill parent”,高度为200像素。然后向Canvas上添加一个ImageSprite组件,并在Canvas下方添加一个名为RollLabel的Label,来显示Roll属性值。如图23-9所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404a7ea1b9.png) **图 23-9 滚动操作如何控制图像移动的用户界面** 方向传感器OrientationSensor的Roll属性表示手机的倾斜方向:向左或向右(即,如果你正握手机并稍向左倾斜,获得的读数为正值;反之向右倾斜则为负值)。因此,利用图23-10中的事件处理程序,用户可以实现对运动的控制。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404a861788.png) **图 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). ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404adc53af.png) **图 23-11 用角度和强度来实现移动** 试试看,强度属性的值介于0至1之间,代表设备的倾斜程度,在这段测试程序蒸南瓜,倾斜的程度越大,图像移动的越快。 ### **手机用作指南针** 指南针应用,以及像谷歌星空这样的应用,需要知道手机所指的方向(东南西北),谷歌星空就是根据手机的指向,将方向信息叠加在星座信息上。 属性Azimuth可以用于表示方向。Azimuth的取值介于0°至360°之间:正北为0,正东为90,正南为180,正西为270。因此当Azimuth值为45时,意味着手机指向东北,135时指向东南,225时指向西南,315时指向西北。 图23-12中的块创建了一个简易的指南针,可以用文字显示手机所指的方向(如西北)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404b0c045d.png) **图 23-12 编程实现一个简易的指南针** 你会发现,程序只能显示四个方向之中的一个:东南、东北、西南、西北。你可以挑战一下自己,看能否修改程序,当手机的指向在某个范围内时,显示四个正方向(正北、正南、正东、正西)。 ## **加速度传感器** 加速度是速度随时间的变化率,如果你踩下油门,车会加速——车速会以一定的比率增加。 在Android手机中内置了加速度计,用于测量加速度,但测量的参照系不是静止的手机,而是自由下落中的手机:如果你让手机下落,它所记录的加速度读数为0。一句话,读数与重力有关。 如果感兴趣相关的物理知识,可以去查阅相关的书籍,但本小节中,我们将充分讨论加速度计,为你建立一个良好的开端,并仔细分析一个能够拯救生命的应用。 ### **响应设备的摇晃** 如果学习过第1章(Hello猫咪),那么你已经使用过加速度传感器了:使用Accelerometer.Shaking事件,当手机摇晃时,设备发出猫叫声。如图23-13所示的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404b13ff71.png) **图 23-13 手机摇晃时发出声音** ### **使用加速度传感器的读数** 像其他传感器一样,加速度计也具备侦测读数变化的事件:AccelerometerSensor.AccelerationChanged,这个事件有三个参数,对应加速度在三个维度上的分量: xAccel:当设备向右倾斜时,其值为正(即,左侧的边缘在上升);当设备向左倾斜时,其值为负(设备右侧边缘上升)。 yAccel:当设备的底部上升时,其值为正;当设备顶部上升时,其值为负。 zAccel:设备的显示屏朝上时,其值为正;显示屏朝下时,其值为负。 ### **检测自由落体** 我们知道,如果加速度的读数为0,那么设备一定在做自由落体运动,基于这一认识,我们可以在AccelerometerSensor.AccelerationChanged事件中,通过检测读数来模拟自由落体事件。这些代码经过反复测试,可以用于老年人的自动求救:一旦侦测到发生跌倒,就会自动向外发送短信。 图23-14中显示了这个应用使用的块,当发生自由落体运动时,给出一个简单的报告(用户可以点击“重置”按钮进行再次检测)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404b1a4338.png) **图 23-14 当自由落体发生时进行报告** 每当传感器获得读数,这些块都要在x、y、z三个维度上进行检查,看是否这些值接近于0(即它们的绝对值小于1)。如果三者都接近于0,应用将改变label的属性,来表示设备正处于自由落体状态。当用户点击“重置”按钮时,显示状态的label又被重新设为初始值(“没发生跌落事件”)。 如果你想试用这个应用,可以从这里下载:[http://examples.oreilly.com/0636920016632](http://examples.oreilly.com/0636920016632)。 ### **用校准值测定加速度** 加速度传感器的读数用自由落体时的状态进行校准。如果你想测量设备平放在桌上时的加速度的相对值,则必须与标准读数进行校准。校准的意思是与标准值进行核对、判定或检测;在本例中,标准值就是将设备平放在桌上时的读数。 校准需要用户将设备平放在桌面上,然后点击“校准”按钮,这时 应用将读出平面上的加速度值,这些值会在稍后的AccelerationChanged事件中用来判断新读数的偏差,并显示设备是否在某个方向上进行了快速的移动。 图23-15中显示了一个样板应用,让用户校准读数并测试加速度。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404b227fb7.png) **图 23-15 校准加速度的读数** 可以在此下载并安装这一应用:[http://examples.oreilly.com/0636920016632/](http://examples.oreilly.com/0636920016632/)。运行应用,并将手机放在桌上,点击校准按钮,将显示“在平台上的读数”,此时如果缓慢地拿起手机,“显著变化”区域的读数不会变化(显示“无”);但如果你快速提起手机,则Z-变化将由“无”变为“有”,如图23-15所示。同样,如果快速沿桌面移动手机,则X或Y也会有显著加速。在图23-16中显示了设置校准初始值的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404b297f36.png) **图 23-16 校准程序的初始设置** 这些块从加速度传感器中获取读数,并显示在三个label中:XCalibLabel、YCalibLabel及ZCalibLabel,并初始化另外三个显示加速度变化结果的label。 当手机水平放置时,加速度计的zAccel读数大约为9.8,而xAccel及yAccel读数约等于0,这些值的偏差表明了加速度计的精确度。获得了基准读数之后,可以通过比较新的测量值与基准值之间的偏差,侦测到手机在x、y或z方向的加速度变化(这种方法与第18章中的边界检测程序相类似)。图23-17显示了这一方法的具体实现。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e404b315a6c.png) **图 23-17 用基准值来侦测加速度变化** 当设备移动时,将触发这段程序。通过测量新的加速度值,并与静止时的基准值进行比较,从而判断加速度之是否产生了显著变化。假设ZCalib.Text记录的基准值为9.0,此时如果缓慢地拿起手机,那么新的读数将保持在9左右,并且不会报告有显著变化;但如果是快速地拿起手机,则读数会明显增大,此时程序将报告加速度“有”显著变化。 ## **小结** 传感器是移动应用中最富魅力的部分,因为它们实现了用户与环境之间实实在在的交互。无论是用户体验,还是应用开发,移动计算为我们带来了无限的商机。不过依然要精心地构思一个应用,来决定何时、何地以及如何使用这些传感器。很多人会担心隐私问题,如果应用中涉及到个人的敏感信息,他们可能会放弃使用。尽管如此,在游戏、社交网络、旅行以及其他众多的选项中,仍然有无限多种可能开发出有积极意义的应用来。
';

第 22 章 数据库

最后更新于:2022-04-01 02:54:11

Facebook的数据库中,有每位用户的账户信息、好友列表以及发布的信息,Amazon的数据库中有你能买到的任何东西,而Google的数据库中有互联网上的每个页面的信息。你自己的应用虽然没有那么大的规模,但一个正规的应用都会用到数据库组件。 在大多数的编程环境中,编写与数据库通信的应用是一种高级编程技术:要搭建数据库(软件)服务器,如Oracle或MySQL等,并编写程序与数据库建立连接。在大学里,这些内容通常要在软件工程或数据库这样的高级课程中才会涉及。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403a0da79b.png) App Inventor承担了与数据库(以及许多其它有用的事情)有关的这部分繁琐的设置,在这个语言中,提供了数据库组件,将数据库通信简化为单纯的读写操作。应用可以直接将数据保存在Android设备上,也可以保存到集中式网络数据库中,从而实现在不同设备与其他人之间的数据共享。 保存在变量及组件属性中的数据属于临时存储:如果用户在表单中输入某些信息然后关闭应用,那么当应用重新打开时,这些信息将不复存在。想要长期保存信息,就需要将它们保存到数据库中。数据库中的信息被称为永久信息,因为当应用在关闭后重新打开时,数据依然存在。 作为例子,考虑第4章开车不发短信的应用,那个繁忙时自动回复短信的应用。这个应用允许用户输入一条个性化的信息,作为收到短信时的自动回复信息。如果用户将信息改为“我在睡觉,别来烦我”,然后关闭了应用,当重新打开应用时,定制的自动回复信息依然是“我在睡觉,别来烦我”。因此,定制信息必须保存到数据库中,在每次启动应用时,再将信息从数据库提取到应用中。 ## **在TinyDB中永久保存数据** App Inventor提供了两个便于操作数据库的组件:TinyDB及TinyWebDB。TinyDB用于直接在Android设备上永久保存数据,它适合于那些极其私人化的应用,如开车不发短信,这类应用不需要让数据在不同设备及人群之间共享。而TinyWebDB则将数据保存到web数据库中,并可实现不同设备之间的共享。能够通过web数据库访问数据,这是多人游戏及应用的基础,用户可以借此分享信息(如第10章的出题应用)。 这两个数据库组件非常相似,但TinyDB更简单些,因此我们先来研究它。首先,不需要任何设置就可以直接使用它,此外,数据直接保存在设备上,并于应用相关联。 使用TinyDB.StroeValue块来实现数据的长期存储,如图22-1所示,这段代码来自于“开车不发短信”。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403a20786c.png) **图 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加载数据。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403a775579.png) **图 22-2 应用启动时加载数据的一种模式** 这里将GetValue的返回值写入到ResponseLabel组件中。如果数据库中已经存储过数据,则将读取的数据写入ResponseLabel中,如果没有与标签responseMessage相对应的数据,则将“我正在开车...”写入Label。 ## **用TinyWebDB保存并共享数据** TinyDB组件将数据保存在Android设备的本地数据库中,这一点适用于那些不需要数据共享的个人应用,例如很多人都可以下载“开车不发短信”应用,但每个人都使用个性化的自动回复信息,这类信息不需要与其他人共享。 当然,更多的应用需要数据共享:像Facebook、Twitter以及像Words With Friends这样流行的多人游戏,这些应用的数据库必须运行在网络上,而非设备上。另一个例子是第10章的“出题/答题”应用,某人在手机上生成了一份测试,并将其保存到网络数据库中,这样其他人就可以在其他手机上加载测验,并回答问题。 TinyWebDB是TinyDB的web版本,可以让应用将数据保存到web上,方法与TinyDB类似,使用StoreValue与GetValue协议。 默认情况下,TinyWebDB组件使用由App Inventor团队创建的web数据库保存数据,从[http://appinvtinywebdb.appspot.com](http://appinvtinywebdb.appspot.com/)可以访问到该数据库。该网站包括一个数据库,并能响应来自web的保存及提取数据的请求;此外,还提供了一个人类可读的web接口,可以让数据库管理员(也就是你)能够查看到在此保存的数据。 感兴趣的话,可以在浏览器中访问[http://appinvtinywebdb.appspot.com](http://appinvtinywebdb.appspot.com/),并检查保存在此的tag-value类型的数据。 这个默认的数据库仅用于开发,对于所有App Inventor程序员提供了有限的空间和权限。由于所有的App Inventor应用都可以使用该数据库,因此不能确保你的数据不被其它的应用所覆盖。 如果你只是在研究学习App Inventor,或者在项目的早期阶段,默认的web数据库就足够了,但如果你想创建正式发布的应用,从某种意义上讲,你需要建立自己的web数据库。由于我们正在学习,因此可以使用默认的数据库。在本章的后面将学习如何创建自己的web数据库,并配置TinyWebDB,来替代默认的数据库。 在这一节中,我们通过一个投票应用(如图22-3所示)来描述TinyWebDB的用法。该应用具有如下特性: ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403a7dd1b8.png) **图 22-3 投票应用:将投票结果保存到TinyWebDB中** * 每次应用打开之后,提示用户输入自己的email地址,该地址既是用户名,又是保存到数据库中的投票信息的标签(tag); * 任何时候用户都可以提交新的投票内容,这种情况下,原有的投票内容将被覆盖; * 用户可以看到群组中每个人的投票结果; * 为简单起见,需要投票的议题在应用之外发布,如课堂上,教师宣布议题并要求每个学生进行电子投票。(注意,这个例子的功能可以扩展,在应用中允许用户输入并提示投票议题。) ### **用TinyWebDB保存数据** TinyWebDB.StoreValue的作用与TinyDB.StoreValue一样,只不过是将数据保存到Web上。在这个投票的例子中,假设用户会在文本框VoteTextBox中输入投票内容并点击按钮VoteButton发送投票结果。将投票结果保存到web数据库中,以便其他人也能看到它,我们将编写如图22-4所示的事件处理程序VoteButton.Click。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403a860909.png) **图 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](http://appinvtinywebdb.appspot.com/)的web数据库服务器中。由于这里用的是默认的服务,会显示来自于各种应用的很多数据,因此在第一个显示窗口中,有可能看到,也有可能看不到你的数据。如果看不到,可以用页面上的GetValue链接用特定标签来搜索数据。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403a8efe7a.png) 测试:用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。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403ae60839.png) **图 22-5 TinyDB.GetValue块** ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403aecdacb.png) **图 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”为标签向数据库发出请求。) ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403af37bba.png) **图 22-7 在Screen.Initialize事件中请求数据** 当应用从数据库收到投票者列表的数据时,TinyWebDB.GotValue事件被触发,图22-8显示了处理这个返回列表的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403b48bd60.png) **图 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显示了向数据库请求数据的更为复杂的方案。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403beede72.png) **图 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中显示了这一功能所需的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403c96d9a3.png) **图 22-10 使用GotValue块处理数据库返回的数据,根据不同的返回结果确定要执行的操作** 在这些块中,第一个if通过调用“is a list?”来检测从数据库返回的值,判断其是否不为空。如果不为空,返回的数据放入变量voterList中。切记,voterList中只有每个使用过该应用的用户的Email地址,但我们不确定当前用户是否也在此列表中,因此需要检查一下:如果此用户不在列表中,则用“add item to list”块将其添加至列表,并将更新后的列表保存到web数据库。 如果数据库返回的结果不是列表,则执行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所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403ca11457.png) **图 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事件处理程序。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403caa2061.png) **图 22-12 TinyWebDB1.GotValue事件处理程序** ## **设置Web数据库** 本章前面提到过,设立于[http://appinvtinywebdb.appspot.com](http://appinvtinywebdb.appspot.com/)的默认web数据库仅供原型设计以及应用的测试,在向真正的用户发布应用之前,需要为应用创建一个专用的数据库。 访问网站[http://appinventorapi.com/program-an-api-python/](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描述了如何操作。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403d146f65.png) **图 22-13 将ServiceURL属性修改为你的定制数据库的URL地址** 在这个例子中,ServiceURL被设置为[http://usfwebservice.appspot.com](http://usfwebservice.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 章 定义过程

最后更新于:2022-04-01 02:54:09

像App Inventor这类的编程语言通常会提供一组基本的内置功能,对于app inventor来说,就是一组基本块。编程语言还提供一种功能扩展的方法,即,向语言中添加新的子程序(块)。【在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram),是一个大型程序中的某一部份代码,由一个或多个语句块组成。它负责完成某项特定任务,而且与其他代码相比,具备相对的独立性。——译者注】在App Inventor中,通过定义过程(procedure),即,命名一些顺序执行的块,来实现功能的扩展。应用中可以像调用App Inventor中的预定义块一样,调用这些过程。本章中你将看到,创建这样抽象的过程的能力对于解决复杂问题是非常重要的,这是创建真正好应用的基石。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40316358b8.png) 当家长对孩子说“睡觉前去刷牙”时,他们的实际含义是“从架子上拿起牙刷牙膏,向牙刷上挤一点牙膏,在每颗牙齿上刷10秒钟(哈哈!)”,等等。“刷牙”就是一种抽象:为一系列的低级指令起一个公认的名称。此处,家长要求孩子完成他们已经认可了的“刷牙”的一系列指令。 你也可以在编程中创建这样的有名字的一系列指令,有些编程语言称之为函数(function)或子程序(subprogram),在App Inventor中,被称为过程(procedure)。过程就是一组顺序执行的有名字的块,在应用中可以随时随地调用它。 图21-1就是一个过程的例子,它的功能是以英里为单位,计算两个GPS坐标之间的距离。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40316b84d6.png) **图 21-1 计算两点间距离的过程** 不必急于探究这个过程中的内部构件,只要知道对于你所使用的编程语言来说,这样的过程扩展了它的功能。如果每个家长每天晚上都要向他的孩子解释“刷牙”的步骤,那么这个孩子到了五年级可能还是不会刷牙。说“刷牙”是一种更有效的方式,而且每个人都会在睡觉之前去刷牙。 同样的道理,在设计或编写一个大型应用时,一旦定义好了distanceBetweenPoints这个过程,你就会忽略它的内部实现细节,而只是简单地使用(或调用)它的名字。这种抽象能力对于解决大型问题来说是至关重要的,可以将大型的软件项目分解成若干个便于管理的代码块。 过程还可以有助于减少错误,因为它们可以省去很多冗余的代码:只要在一处定义了过程,应用中就可以随处调用它。因此,假如应用中要计算你的当前位置与其他10个点之间的最近距离,你不必拷贝粘贴10次图21-1中的块,相反,你只需要定义这个过程,并在需要时调用它即可。此外,那种拷贝粘贴块的方法还非常容易引入错误,因为一旦你想修改程序,就必须找到所有的拷贝,并逐个以相同的方式修改它们。想象一下,你试图在一个有1000行或块的代码中,找到5-10个曾经粘贴过的代码块!与其被迫地拷贝粘贴这写块,不如用过程在一处将代码块封装起来。 最后,过程将有助于建立代码库,让这些代码在其他应用中可以被重用。即便是创建一个非常具体的应用,有经验的程序员总会在必要时设法考虑重用其他应用中的部分代码。有些程序员从未创建过应用,他们只是专注与创建可重用的代码库,以便其他程序员以此来创建他们自己的应用。 ## **消除冗余** 看一下图21-2中的代码块,能否发现其中的冗余。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40317289f8.png) ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4031ca4a17.png) ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4031d1771d.png) **图 21-2 "随手记"应用中的冗余代码** 这里的冗余代码指与foreach块有关(实际上是整个foreach块以及它上面的"set NotesLabel.Text to"块),例子中的三个foreach的作用都是显示笔记列表,只是使用的场合有所不同:当添加新项、删除某一项,以及应用启动从数据库加载列表时。 作为一个有经验的程序员,一旦看到这样的代码,脑子里会立即敲响警钟,甚至不必等到开始拷贝粘贴第一段程序中的代码,他们知道最好是将这些冗余的代码封装在一个过程里,这样既保证程序有很好的可读性,也可以使后来的修改变得容易。 因此,有经验的程序员会创建一个过程,将冗余代码块放在其中,并在原来使用冗余代码的地方调用这一过程。应用的执行结果完全一样,但更易于维护,也让其他程序员更容易地加以利用。这种代码(块)的重新整理的过程成为重构。 ## **定义过程** 我们来创建一个过程,实现图21-2中那些冗余代码的功能。在App Inventor中,定义过程几乎与定义变量一样简单:从Procedures抽屉中拖出一个“to procedure”块或“to procedure result”块。如果过程需要通过计算返回一个结果,则使用后者(我们将在本章稍后的部分讨论它)。 在拖出“to procedure”块后,可以修改过程名称:点击默认名称“procedure”并输入新名称。由于冗余代码块的作用是显示笔记列表,因此重构时将过程名设为“displayList”,如图21-3所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4031d8bb62.png) **图 21-3a 点击默认名称“procedure”** ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4031dd4f51.png) **图 21-3b 将过程名改为“displayList”** 下一步是向过程中添加块,此时就用现有的冗余块,将它们从事件处理程序中拖出并放在displayList块中,如图21-4所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4031e2249b.png) **图 21-4 封装了冗余代码的过程displayList** 现在我们可以用过程来显示笔记列表了,在应用的任何一处,都可以很容易地调用它。 ## **调用过程** 像“displayList”和“刷牙”这样的过程是一个包含了某种功能的实体,它们只有在被调用时,才能体现出这种功能。因此,以上我们只是创建了过程,却并没有调用它。调用它意味着要运行它,或者说来实现它。 在App Inventor中,可以从Procedures抽屉中拖出一个以“call”开头的块来调用一个过程。每当定义了一个新的过程,procedures抽屉中就会显示一个新的块,即定义一个过程,就是向Procedures抽屉中添加一个新块,如图21-5所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4031e74bed.png) **图 21-5 定义好一个过程后,Procedures抽屉中就会出现一个新的“call” 块** 你一直都在用“call”块来调用App Inventor中的预定义函数,如Ball.MoveTo以及Texting.SendMessage。当你定义了一个过程,就相当于创建了自己的块,也相当于你扩展了App Inventor语言,新的“call”块让你可以使用自己的创造。 在“随手记”的例子中,三次拖出“call displayList”块来取代三个事件处理程序中的冗余代码,如,ListPicker1.AfterPicking事件处理程序(删除一条笔记)修改的结果如图21-6所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4031ebb2a8.png) **图 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所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4032e44ff3.png) **图 21-7 在过程中引入了list作为参数** 即使是定义了参数,但foreach块中仍然直接引用特定列表“notes”(插入到foreach块的“in list”插槽中)。而我们希望在过程中使用我们传递的参数list,因此将对“global notes”的引用替换成对“get list”的引用。如图21-8所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4032ea50c4.png) **图 21-8 现在foreach中使用了传递来的参数“list”** 新版本的过程更加通用:在调用displayList时,无论传入什么样的列表,displayList都能显示它。在向过程添加参数时,App Inventor会自动为“call”块添加一个对应的插槽,因此当displayList添加了参数list之后,“call displayList”块就变成图21-9中的样子。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4033914ed7.png) **图 21-9 现在调用displayList时,需要指明要显示的列表** 过程定义中引入的参数list被称为“形式参数”,而“call”块中与之相对应的插槽被称为“实际参数”。当在应用中的某处调用过程时,必须为过程中的每个“形式参数”提供一个“实际参数”。 对于“随手记”的应用来说,将列表“notes”作为实际参数添加到“call”块的list插槽中。ListPicker.AfterSelection的修改结果如图21-10所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4033967de0.png) **图 21-10 在调用displayList时,将notes作为实际参数传入** 现在当displayList被调用时,列表notes被传递到过程中,来取代形式参数list。此时,程序计数器随着过程中的每个块的运行,它的指向是参数list,而实际上处理的是变量notes。 由于有了参数,过程displayList可以用于处理任何列表,而不仅仅是notes。例如,如果“随手记”应用可以在一组用户中共享,而你想查看一下用户列表,就可以调用displayList并传入userList参数。如图21-11所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40339c7a81.png) **图 21-11 过程displayList可用于显示任何列表,而不仅仅是notes** ## **过程的返回值** 关于过程displayList的可重用性,还有一个问题需要讨论——你能猜到是什么吗?如前所述,它可以显示任何数据列表,但也只能在标签NotesLabel中显示。如果你想用其他的界面元素(如另一个label)来显示列表(如userList),该如何是好呢? 一个方法就是重构过程——将它的功能从“用指定label显示列表”改为“只返回一个文本对象,它可以被显示在任何地方”。为此,需要使用“procedure result”块来取代“procedure”块,如图21-12所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4033a22b26.png) **图 21-12 “procedure result”块** 你会发现与“procedure”块相比,“procedure result”块的底部有一个额外的插槽,将一个变量放入插槽,这个变量将被返回给调用者。因此,正如调用者可以向过程以参数的方式传入数据一样,过程也可以以值得方式将数据返回给调用者。 图21-13显示了上述过程的改写版本,现在使用的是“procedure result”块。注意,由于过程的作用变了,因此名称也由displayList改为convertListToText(将列表转换为文本)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e403589767e.png) **图 21-13 过程convertListToText返回一个文本对象,调用者可以将其放在任何一个label中** 在图21-13所示的块中,变量text用来保存foreach循环中通过遍历列表而生成的文本。用text变量取代之前使用的过于具体的NotesLabel组件。在foreach执行完毕后,变量text包含了列表中的所有项,而且项之间以换行符“\n”分隔(即“item1\nitem2\nite3”)。最后,将变量text插入return插槽,返回给调用者。 在定义“procedure result”时,与“procedure”相比,对应的“call”块看起来略有不同,如图21-14中所做的比较。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40359130d4.png) **图 21-14 下面的有返回值的“call”必须插入到某个插槽中** 不同的是在“call convertListToText”块的左侧有一个插头,这是因为当“call”块运行时,过程在执行一系列指令后将向“call”块返回一个值,必须有某个插槽可以接收这个返回值。 在这种情况下,调用块“call convertListToText”的返回值可以插入到任何一个label的Text属性中,以notes列表为例,需要显示列表的三个事件处理程序都可以调用这一过程,如图21-15所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4035a506c5.png) **图 21-15 将列表notes的内容转换为文本,并用NotesLabel显示出来** 更重要的是,由于过程的定义更具通用性,不需要引用任何特定list或label,因此应用中可以使用convertListToText在任何一个label蒸南瓜显示任何一个列表。像图21-16中的例子那样。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4035abaed5.png) **图 21-16 这一过程再也不必与一个特定的Label组件捆绑在一起** ## **在应用中重用块** 通过过程的方式实现代码的重用不必只限于单独的应用,有许多过程,如convertListToText,可以用在你创建的任何应用中。事实上,有许多组织和编程社区都在为他们感兴趣的领域创建过程代码库,例如动画过程的代码库。 通常编程语言会提供一个“import(导入)”功能,可以在任何应用中引入其他的代码库。App Inventor目前没有这项功能,不过正在开发之中。同时,也可以在一个特定的“库应用”中创建一些过程,并复制该应用的代码,作为一个新建项目的基础代码。 ## **第二个例子:求两点间距离** 在displayList(convertListToText)例子中,我们将过程定义描述为一种消除冗余代码的方法:你开始写代码,随后发现代码存在冗余,于是整理代码消除冗余。无论如何,一个软件的开发人员或开发团队在应用开发的初期都会创建很多过程,同时也考虑到要重用部分代码。这样的规划可以在项目过程中节省大量时间。 考虑一项应用:确定离某人当前位置最近的本地医院,某些东西在紧急情况下会派上用场的。以下是这个应用的高层设计描述: **应用启动时,以英里为单位计算两点之间的距离,起点是当前所在位置,终点是发现的第一家医院。然后再寻找第二家医院,以此类推。在求得若干个距离后,判断最短距离的医院,并显示它所在位置的地址。** 从以上描述中,你能断定应用中需要什么样的过程吗? 通常,一段描述中的动词提示了所需的过程。重读一遍描述,正如“等等”所提示的,这是另一个线索。这种情况下,“求出两点之间的距离”与“判断这些距离中最短的”成为两个必需的过程。 现在考虑设计一个过程distanceBetweenPoints(两点间距离)。在设计过程时,首先要确定过程的输入及输出:调用者需要向过程传递实现过程的功能所需的参数,而过程要向调用者返回执行结果。在这里,调用者需要向过程传递两个点的经度及纬度值,如图21-17所示;而过程的任务是以英里为单位返回两点之间的距离。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4035b49a4b.png) **图 21-17 调用者想过程传递了4个参数,并收到一个距离** 图21-18中显示了我们在本章开始时提到的那个过程,使用公式求得两个GPS坐标点之间的近似英里数。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40360bba20.png) **图 21-18 过程distanceBetweenPoints** 图21-19显示了对上述过程的两次调用,每次都会求出当前位置与指定医院之间的距离。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4036b49ae0.png) **图 21-19 两次调用distanceBetweenPoints过程** 第一次调用中,起点为用户当前所在位置的LocationSensor(位置传感器)读数,终点是St. Mary's hospital(圣玛利亚医院),计算的结果保存在变量distanceStMarys中;第二次调用也类似,只是将终点的数据改为CPMC Hospital(加州太平洋医疗中心医院)的经纬度。 接下来程序比较两个距离并返回最近的医院。但是如果还有更多的医院,那就需要在一个距离列表中进行比较,并找到最小值。依你所学,你能写出这个过程吗?将其命名为findMinimum,接受一个数值列表作为参数,并返回最短距离在列表中的索引值。 ## **小结** 像App Inventor这样的编程语言提供了一个内置功能的基本集,而过程是一种新功能的提取,它扩充了app inventor语言。App Inventor不提供显示列表的块,于是由你来做;那么是否需要一个计算两个GPS坐标间距离的块呢?答案是靠我们自己来创造。 想要建造大型的、可维护的软件,以及在解决复杂问题时免于不断地纠缠于细节之中,则定义高级过程的能力是至关重要的。过程是将代码块封装起来,并起一个名字。在编写过程时,你会关注这些块的细节,但对程序的其他部分而言,这个过程只是一个抽象的名字,你可以在更高层次上来引用它。
';

第 20 章 循环

最后更新于:2022-04-01 02:54:06

计算机最擅长做的事情就是“重复”——像儿童一样不厌其烦地重复做一件事,而且重复的速度很快,可以在1毫秒内列出你的全部Facebook好友。 本章将学习如何用有限的几个块来编写可以重复执行的程序,而不必反复拷贝粘贴同一段代码;还将学习与列表有关的操作,如给电话号码列表中的每个号码发送一条短信,以及为列表项排序。通过学习,你将了解到如何用循环块来有效地简化程序。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e402df53a62.png) ## **控制程序的执行:分支及循环** 在前几章中,我们学习了用一组事件处理程序来定义应用中的行为:事件以及对事件做出响应的函数。在这些响应函数中,程序通常不是按照线性的顺序执行,有些程序块只能在满足某些条件时才能执行。 重复块是程序的另一种非线性运行方式。就像if及ifelse块让程序产生分支一样,重复块让程序循环执行,换句话说,在执行完一组指令后,重新跳回到这组指令的起点并再次运行,如图20-1所示。在应用的运行过程中,内部的计数器会跟踪即将执行的下一步操作,因此,对于整个事件处理程序来说,从头至尾的每一步操作都在程序计数器的监控之下(有条件地)完成。程序计数器随着这些重复执行的块循环,不断地重复这些功能。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e402dfee119.png) **图 20-1 让程序循环执行的重复块** 在App Inventor中有两种类型的重复块:foreach及while.foreach,其作用是对列表中的每一项实施某些特定的操作,如,向电话号码列表中的每个号码发送一条短信。 块while的应用比foreach要普遍,while块中的程序块会一直重复运行,直到某个条件不再满足。while块可用于数学公式的计算,如求n个连续自然数的和,或求n的阶乘,此外,while也可以用于同时处理两个列表;foreach每次只能处理一个列表。 ## **使用foreach对列表实施迭代** 在第18章里,我们讨论了一个“随机拨号”应用。这种随机拨打朋友电话的方式有时能拨通,但如果你有一个像我这样的朋友,这种呼叫却不总是能得到应答。可以采取另一种方式,给所有列表中的朋友发短信说“想你”,然后看谁最先回复你(或许还有更令人愉快的方式!)。 这个应用可以通过点击一次按钮向多个朋友发送短信,最简单的方法是,先写好发给一个人的代码块,然后拷贝粘贴并修改接收人的电话号码,如图20-2所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e402e55aa3c.png) **图 20-2 拷贝并粘贴向不同号码发送短信的块** 如果只有少量的块,用这种“强力”的拷贝粘贴方式也还说得过去,但是像朋友列表这样的数据表会时常变化,而你不希望每次添加或删除一个电话号码,都要动手去修改程序。 块foreach提供了一个更好的解决方案,可以定义一个包括所有电话号码的列表变量phoneNumberList,然后用foreach块将发送一次短信的块包围起来,从而实现群发功能,如图20-3所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e402e5c0735.png) **图 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所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e402eb58ba3.png) **图 20-4 列表的简单显示方法:将列表直接插入label** 这样做的结果是,列表项在label中显示为一行,项之间以空格分隔,整个列表被一对括号包围:(111-1111 222-2222 333-3333)。 这些号码可能显示为多行或单行,取决于号码的多少。最终用户能看到这个数据,也可能将它们当做电话号码的列表,但这样的显示方式很不美观。通常会将列表项分行显示或用逗号分隔。 为了适当地显示列表,需要将每个列表项转换为一段带格式的单独的文本。文本对象通常有字母、数字、标点符号组成,但也可能包含特殊的控制字符,它们对应一些不可见的字符,如tab被表示为\t(更多关于控制字符的内容,请查阅文本表示的统一码[Unicode]标准:[http://www.unicode.org/standard/standard.html](http://www.unicode.org/standard/standard.html))。 为了逐行显示我们的电话号码列表,需要一个换行符“\n”。当“\n”出现在一段文本中,意味着“到下一行来显示后面的东西”。因此文本对象“111-1111\n222-2222\n333-3333”将显示为: > 111-1111 > 222-2222 > 333-3333 要构造出这样的文本对象,需要用到foreach块,将每个列表项附加换行符后再添加到PhoneNumberLabel.Text属性中,如图20-5所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e402ebb04f7.png) **图 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-1111\n222-2222 | 列表中的第三项被设为phoneNumber的值,第三次重复运行foreach内部的块,在完成最后一次迭代后,最终结果如表20-3所示。 **表20-3 第三次foreach迭代之后的变量值** | phoneNumber | PhoneNumberLabel.Text | | --- | --- | | 333-3333 | \n111-1111\n222-2222\n333-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块逐行显示问题-答案对的方法。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e402ec3a953.png) **图 20-6 使用while循环逐行显示问题-答案对** 由于用while替代了foreach,因而需要直接初始化index、检查是否到达列表结尾、在每次循环中选择各个列表中对应的项,并使得index递增。 ### **使用while做公式计算** 这里是使用while循环的另一个例子:与列表无关的重复操作。想想看,图20-7中的块在做什么?高水平?要想弄清楚,就要跟踪每一个块(关于程序跟踪的更多内容见第15章),随着程序的进展,跟踪每个变量的值。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e402ed16325.png) **图 20-7 你能说出这些块的功能吗?** 当变量number的值小于或等于变量N时,while中的块将重复执行。在这个应用中,N值等于最终用户在界面上的文本框(NTextBox)中输入数字,假设用户输入3。当程序运行到while块时,程序中的变量如表20-4所示。 **表20-4 程序运行到while块时,各个变量的值** | N | number | tota | | --- | --- | --- | | 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 章 数据列表编程

最后更新于:2022-04-01 02:54:04

如你所见,应用就是处理事件以及作出决策,这一过程是计算机程序的基础,而同样构成程序基础的就是数据——程序所要处理的信息。程序中很少只用到像游戏中的成绩这样的单个数据,更普遍的是使用复杂数据——一些相互关联的数据项,必须像设计应用的功能一样,非常细心地组织这些数据。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e401381a523.png) 本章将探讨App Inventor中处理数据的方式,并学习两种数据类型的基本编程方法,两种数据类型为静态数据(数据的值保持不变)及动态数据(数据由用户生成),然后将学习如何处理更为复杂的包含数据的数据,即数据项本身也是一组数据。 许多应用中都存在这样复杂的数据,如facebook中的好友列表,测试应用中的问题及答案列表等等,游戏中也会有角色的列表以及当前最高成绩的列表。 列表变量的使用如同普通的文本及数字变量一样,只是它们不仅仅代表单一的有名称的存储单元,而是表示一组相互关联的存储单元,例如,考虑表19-1中的电话号码列表。 **表19-1 列表变量表示一系列的存储单元** ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4013e250ed.png) 使用索引值(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。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4014369e9b.png) **图 19-1 初始化变量的块** 2\. 点击文本“name”,将其改为phoneNumbers,如图19-2所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40143abd4e.png) **图 19-2 将变量重命名为phoneNumber** 3\. 从Lists抽屉中拖出“make a list”块插入初始化变量块,如图19-3所示。这是告诉应用,要存储的变量是一个列表,而非单个值。通过点击“make a list”块上的蓝色增项图标来指定所需存储槽的数量,来增加数据项,如图19-3所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40143ed207.png) **图 19-3 使用“make a list”块来定义列表变量phoneNumber** 4\. 最后,从Text抽屉中拖出文本块,输入需要的电话号码,插入到“make a list”块的数据项插槽中。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e401494da9f.png) **图 19-4 当列表中添加了全部数据项,相当于开辟了一个新的存储空间** 数据项的存储中可以插入任何类型的数据,但在本例中,这些数据项是文本类型的对象,而不是数字,因为电话号码中含有一个破折号“-”,这种非数字类型的符号无法输入到数字块中,也无法与数字进行任何运算(而数字运算必须用到数字块)。 图19-4中所示的块定义了一个名为phoneNumber的变量,在应用启动时,你定义的任何变量就在此时被创建,而像表19-1中的存储槽也被同时创建并被填写上初始值。一旦有了这个列表变量,就可以使用列表中的数据开始编程了。 ## **选择列表项** 应用中可以使用“select list item”块,并指定索引值(index)来访问列表中指定的数据项。index代表了该数据项在列表中的位置。因此,如果列表中有三个数据项,就可以用索引值1、2、3来访问这些项。图19-5中显示了选中列表第二项的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4014996db7.png) **图 19-5 选择列表中的第二项** 使用选择块“select list item”需要提供两项参数,首先是要查询的列表,将其插入选择块的第一个插槽中,其次是索引值index,将其插入选择块的第二个插槽中。图19-5中的块是告诉应用从列表phoneNumber中选出第二个元素。如果列表的定义如表19-1的话,俺么选择的结果就是“333-4444”。 从列表中选择数据项,这仅仅是第一步,通过选择可以实现各种操作,下面将举例说明。 ### **使用Index遍历列表** 许多应用中,定义列表的目的是让用户可以遍历(逐个查看)它。第8章总统测验就是一个很好的例子:用户点击“下一题”按钮,程序从问题劣币哦啊中选择下一道题并显示出来。 但如何实现对下一项的选择呢?图19-5中选择的是列表phoneNumber中的第二项,而遍历列表时,每次选择的项目序号是不同的,会根据当前选中项在列表中的位置来确定。因此就需要定义一个变量来表示这个位置,通常用index来作为变量名,初始值通常设为1(列表中的第一个位置),如图19-6所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40149e05d5.png) **图 19-6 变量index的初始值为1** 当用户设法移动到下一项时,可以在当前的index值上加1来实现变量的递增,并使用递增后的值在列表中做选择。图19-7中显示了实现这一点使用的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4014a2b8e2.png) **图 19-7 使index值递增,并用递增后的值选择列表项** ### **举例:遍历画笔颜色列表** 来看一个例子,用户可以通过点击按钮来为他的房子选择一种可能的粉刷颜色,每次点击,按钮的颜色都会变化。当用户查阅完全部颜色时,再重新回到第一种颜色。 例子中,可以使用基本色,也可以替换成任何一组颜色,关于颜色的更多信息,可以参见App Inventor文档([http://appinventor.googlelabs.com/learn/reference/blocks/colors.html](http://appinventor.googlelabs.com/learn/reference/blocks/colors.html))。 第一步是定义一个颜色列表变量,并以颜色为列表项来初始化列表,如图19-8中所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4014a7ab57.png) **图 19-8 用一组颜色为colors列表做初始化** 接下来,定义变量index来跟踪列表的当前项位置,初始值为1。可以给变量一个更有意义的名字,如currentColorIndex,但如果你的应用中不需要处理其他更多的列表,用index就好。如图19-9所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4014ac4d34.png) **图 19-9 使用index变量来跟踪列表当前项的位置,初始值为1** 用户通过点击ColorButton从列表中浏览到下一项(颜色),此时,index值递增,而按钮的BackgroundColor变为当前选中的颜色,如图19-10所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4014b1561c.png) **图 19-10 用户通过点击按钮浏览颜色列表——每次点击都会改变按钮颜色** 先假设我们已经在组件设计器中将按钮的背景颜色设为红色(Red),第一次点击按钮时,index值从初始的1变为2,按钮颜色变为列表中的第二项——绿色;第二次点击按钮时,索引值从2变为3,按钮变为蓝色。 想象一下,下一次的点击会出现什么情况? 如果你说会出错,那么你对了!index值将变成4,程序将试图在列表中选择第4项,但列表中只有3项,因此程序强行关闭,或退出,用户将看到一条如图19-11中所示的错误信息。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e401505e3b1.png) **图 19-11 当试图从一个只有三项的列表中选择第四项时,程序将提示错误信息** 显然,你不想让用户看到这样的信息,为了避免出现这样的问题,需要添加一个if块来检查是否到达了列表中的最后一项。如果是,将index值设回1,来显示第一种颜色,如图19-12所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4015ac39f5.png) **图 19-12 使用if块检查索引值index是否大于列表的长度,如果是,将index值重新设为1** 用户点击按钮时,index值递增,然后检查它的值是否过大。与index值进行比较的是“length of list”,而不是3,因此,即便是列表中添加了更多的项,程序也能正常运行。通过检查index是否大于列表长度(而不是与固定的数字3进行比较),可以消除程序中的代码相关性。所谓“代码相关性”是一个编程术语,举例来说,如果你的应用中某些方面的程序写得过于具体,那么当你想对某处做出修改时(如,列表的数据项),你不得不找到应用中所有使用过这个list的地方,并对程序块进行逐一修改。 正如你所想象得,这种相关性会让程序在短时间内变得混乱不堪,也会产生更多的错误等待你去排查。事实上,在“粉刷彩色房屋”应用的设计中,就在我们刚刚完成的程序中,还存在另一个代码相关性问题,你能找出是什么吗? 如果将颜色列表中的第一项由红色改为其他颜色,应用的运行结果就不再正确,除非你能记得在组件设计器中修改ColorButton.BackgroundColor属性的初始设定。消除这种代码相关性的方法是,将ColorButton.BackgroundColor初始值设定为颜色列表中的第一项,而不是某个特定的颜色。由于这一修改涉及到程序启动时的行为,因此需要在应用启动时调用的Screen.Initialize事件处理程序中进行这一设定。如图19-13所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4016024bc4.png) **图 19-13 应用启动时,将按钮的背景色设置为颜色列表中的第一项** ## **创建输入表单及动态数据** 前面的“粉刷彩色房屋”应用涉及到一个静态列表:程序员(也就是你)定义了列表中的元素,除非你亲自动手,没有人能修改这些列表项。不过,多数情况下,应用中要处理动态列表:最终用户输入新的数据项而导致数据的变化,或者从数据库或web信息源加载新数据。本节将讨论一个“随手记”应用:用户在应用中,通过表单输入笔记,并可预览之前所有输入过的内容。 ### **定义动态列表** 如果希望创建一个空列表,可以使用“create empty list”块来定义,例如,在“随手记”应用中,允许用户输入笔记,但在定义列表时,不应该有预定义的数据项,具体的定义方法见图19-14。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4016077c12.png) **图 19-14 动态列表的定义中不应该含有任何预定义数据项** ### **添加数据项** 当第一次启动应用时,notes列表是空的,当用户在表单中输入数据并点击“保存”按钮时,新的笔记内容将被添加到列表中。表单的设置非常简单,如图19-15所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40160bdd27.png) **图 19-15 用输入表单想笔记列表中添加新项** 当用户输入一段笔记并点击“保存”按钮,应用将调用“add items to list”函数将新输入的内容添加到列表中,如图19-16所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e401704640f.png) **图 19-16 用户点击“保存”按钮时,调用“add items to list”向列表中添加新内容** “add items to list”块将新的数据追加到列表的结尾,用户每次点击“保存”,就添加一条新笔记,在Lists抽屉中可以找到这个块。特别注意:还有另一个块“append to list”,它的功能是向一个列表中追加另一个列表,很少会用到这个块。 ### **显示列表** 对用户来说,列表变量notes的内容是不可见的,还记得之前讲过,应用中的变量是用来保存那些不需要被用户看到的信息。图19-16中的块实现了一点击按钮就添加新项的功能,但用户看不到任何反馈,除非你在程序中添加显示列表内容的功能。 在应用的用户界面中显示列表内容最简单的方法就是现实数字和文本的方法:将列表内写入Label组件的Text属性,如图19-17所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40170a9708.png) **图 19-17 用NotesListLabel的Text属性显示笔记列表** 可惜这种简单的显示方法看起来不够美观,列表中所有的项被放置在一对小括号内,没有分行,项之间用空格分隔。如图19-18所示,用户输入了第一条笔记“忘记了让笔记显示出来”,然后又输入第二条“显示结果被一对括号包围着!”。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4017606aa0.png) **图 19-18 列表内容的简单显示方法** 如果学习过第13章的“亚马逊掌上书店”,你对这个问题应该熟悉。在第20章中,将学习如何用更加复杂的方式来显示列表内容。 ### **删除列表项** 使用“remove list item”块可以从列表中删除某一项。如图19-19所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e401765ea63.png) **图 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。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40185ac670.png) **图 19-20 ListPicker的Elements属性被设置为列表变量notes** 这些块将列表notes的内容显示在ListPicker中,如果列表中有两条笔记,其显示如图19-21所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40185f1b94.png) **图 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所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e401864ddd4.png) **图 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所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4018bb3c48.png) **图 19-23 一个问题列表和一个答案列表** 每当用户回答完一个问题,程序通过与AnswerList中的当前项进行对比来判断回答是否正确。 为了实现多选测验,需要为每个问题提供一个可供选择答案的列表。多选列表可以表示为一个数据列表变量,将三个“make a list”块放在一个外层“make a list”块中,来定义这个变量,如图19-24所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4018c1794b.png) **图 19-24 通过在外层“make a list”块中插入若干个“make a list”块来构造出一个数据列表** 变量answerChoices中的每个数据项本身也是一个由三个数据项组成的列表,如果从answerChoices列表中选择一项,选择的结果将是一个列表。现在填好多选答案的双重列表,那么如何向用户显示这些数据呢? 在“随手记”应用中,使用了一个ListPicker来向用户显示选项。假如索引值为currentQuestionIndex,则事件处理程序ListPicker.BeforePicking将写成图19-25中显示的样子。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4018c81c84.png) **图 19-25 使用ListPicker向用户展示多选列表** 这些块将选取并显示answerChoices中的当前项对应的子列表,供用户选择。如果currentQuestionIndex为1,ListPicker将显示图19-26中的列表。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4018cccc40.png) **图 19-26 向用户展示第1题的多选答案** 用户选择之后,用图19-27中的块对答案进行检查。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e401973921c.png) **图 19-27 检查用户选择的答案是否正确** 这些块中,用户从ListPicker中选择的答案将与正确答案进行比较,而正确答案保存在另一个列表AnswerList中(注意answerChoices只提供选项而不代表答案)。 ## **小结** 你能想到的几乎每个应用中都会用到数据,理解它们的运行机制是编程的基础,本章探索了一种最常用的编程模式:使用索引变量,从列表的第一项开始,通过变量的递增实现对每个列表项的处理。如果能理解并在自己的程序中加以运用,那么你的确是一名程序员了。 然后我们讲到了列表处理的其他方式,包括一个典型的让用户添加并删除列表项的表单。如此的编程还需要另一个层次的抽象能力,你必须假想数据的存在,因为直到用户输入某些数据之前,这些数据都是空的。如果你能理解这一点,你甚至可以考虑辞掉现在的日常工作了。 最后我们介绍了复杂的数据结构——数据列表。这显然是一个不太容易理解的概念,但我们利用一些固定的数据对问题进行了探索:多选测验中的可选择答案列表。如果你对此以及本章的其余部分都有所掌握,那么你的期末考试题是:使用数据列表创建一个应用,但要求使用动态数据!一个例子就是允许用户在应用中创建他们自己的多选测验,这个功能甚至比第10章的出题应用还要强大。祝你好运! 在你思考如何处理这些列表时,要知道我们的探索还没有结束。在下一章中,我们将继续讨论并重点讲解略有不同的列表循环:对列表中的每一项实施一些列的操作。
';

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

最后更新于:2022-04-01 02:54:02

即使是像口袋里的手机这样小型的电脑,也可以在短短几秒钟内完成超过数千次的操作。更令人惊奇的是,它们可以基于内存中的数据以及程序员编写的逻辑进行决策。这种决策能力在人们所思考的人工智能问题中是极为关键的要素,当然也是创建有趣的智能应用的重要组成部分。本章将探索如何在应用中编写判断选择逻辑。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400b361f83.png) 正如我们在第14章所讨论的,应用的行为由一系列的事件处理程序所定义。每个事件处理程序针对某个特定事件进行响应,并实现特定的功能。然而,这种响应的过程未必是按线性顺序来实现各项功能,有些功能只能在一定条件下才能执行。像游戏类的应用可能就会判断分数是否已经达到了100,而位置感知类的应用可能会问“某个手机是否在某个建筑物的范围之内”。你的应用也可以询问类似的问题,然后根据答案,继续执行不同的程序分支。 如图18-1,当事件(Event1)发生时,无论如何A功能都会被执行;然后进行一个检测判断:如果检测结果为真,则执行B1分支;如果结果为假,则执行B2分支;无论执行哪个分支,该事件处理程序的其余部分(C)都将被执行。 由于像图18-1这样的决策图看起来像一棵树,因此通常会将这种根据判断结果而选择执行的一段程序称为“分支”。在这种情况下,你会说, “如果测试结果为真,则执行包含B1的分支。” ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400b3eb8fe.png) **图 18-1 事件处理程序中,根据条件测试的结果执行不同分支** ## **用if及ifelse进行条件测试** App Inventor提供了两类条件块(如图18-2):if块和ifelse块。可以从Control抽屉里拖出一个if块,然后点击上面的蓝色图标,弹出可扩充的块,可以根据需要添加任意多个“else”分支。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400b45b75c.png) **图 18-2 条件块if及ifelse** 可以将任何逻辑表达式(Boolean)插入到if右侧的测试插槽中。逻辑表达式是一个用数学等式,它的返回值要么是真(true),要么是假(false)。如图18-3,逻辑表达式使用关系运算符(蓝色)以及逻辑运算符(绿色),对属性值或变量值进行检测。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400b4ba9e5.png) **图 18-3 用于条件判断的关系及逻辑运算符** 无论是if块还是ifelse块,只有“if”后面的测试结果为真时,将执行“then”右侧插槽中的块。对于if块,如果测试结果为假,程序将跳出if块,继续执行if后面的块;而对于ifelse块,如果测试结果为假,将执行“else”右侧插槽中的块。 因此,对于一个游戏来说,可能会插入一个与成绩有关的逻辑表达式,如图18-4所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400bd647bf.png) **图 18-4 用于测试成绩值的逻辑表达式** 在本例中,如果成绩到达100,则播放一个声音文件。注意,如果测试结果为假,不执行任何块。如果需要在测试结果为假时执行某些操作,可以使用ifelse块。 ## **编写一段二选一的决策程序** 考虑这样一个应用,无聊的时候也许会用到它:在手机上点击一个按钮,就可以随机地拨打一个朋友的电话。如图18-5,使用一个random integer(随机整数)块来生成一个数字,然后用ifelse对生成的数字进行判断,来决定即将拨打的电话号码。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400c2e71b6.png) **图 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所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400c36a391.png) **图 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)吗? 本例中使用了位置传感器(LocatinSensor)组件,即便你没用过这个组件,也能够理解这些程序,在第23章中将有更多讲解。 使用逻辑运算符and、or及not可以构造出更为复杂的测试条件,可以从Logic抽屉中找到它们。在本例中,先拖出一个if块以及三个and块,并将and块放在if块的测试插槽中,如图18-7所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400c90951a.png) **图 18-7 放在if块测试插槽中的“and”块(选择“External Input/外展式输入”以免块的排列过宽)** 然后拖出几个块来组成第一个测试问题,并将其放在and块的第一个测试插槽中,如图18-8所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400c96cd41.png) **图 18-8 and块中放入了第一个测试问题块** 如法炮制出其他几个测试条件,填入其他几个and的测试插槽中,并将整个if块放入事件处理程序LocationSensor.LocationChanged中,这样就写成了一个检测边界的程序,如图18-9所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400c9d0c09.png) **图 18-9 每次位置更新时,触发该事件处理程序,来检测是否在边界之内** 这些块的功能是,在每次位置传感器读数更新时做出判断,如果手机的位置在边界之内,则发出震动。 OK,到目前为止,应用已经相当酷了,但现在我们来尝试更为复杂的功能,以便你能充分地了解程序中决策的威力。如何才能让手机仅在越出边界时才发出震动呢?继续学习之前,自己先想想如何来写这样的程序。 我们的方法是定义一个变量withinBoundary,目的是记住传感器上一次的读数是否在边界内,并根据每一次后续读数的测试结果对变量值进行修改。withinBoundary是一个布尔(Boolean)类型的变量,与保存数字或文本的变量相比,它保存的值为true(真)或false(假)。举例来说,如果将变量初始值设为false,如图18-10所示,这意味着设备不在旧金山大学的哈尼科学中心范围内。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400ca48294.png) **图 18-10 变量withinBoundary为初始化为false** 对块做出修改,以便在每次位置信息变化时,对变量withinBoundary进行设置,并且只有当手机越出边界时,才会发出震动。说的更明确一些,手机产生震动的必备条件是(1)变量withinBoundary的值为真,即意味着上一次读数还在边界内;(2)新的传感器读数超出了边界。图18-11中是修改后的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400ca99623.png) **图 18-11 这些块的功能是:只有当手机从界内移动到界外时,手机才会震动** 我们来仔细地分析一下。当位置传感器(LocationSensor)获得读数时,首先判断读数是否在边界内,如果是,将withinBoundary设置为true。由于我们希望只有在手机越出边界时才震动,因此在第一个分支中不发生震动。 如果执行的是else分支,我们知道新的读数已经超出了边界。此时,我们需要检查上一次的读数:尽管这次读数超出了边界,但我们希望仅当上次读数在边界内时,才让手机发出震动。withinBoundary变量会告诉我们上一次的读数,因此我们会检查这个变量,如果检查结果为真,则让手机震动。 一旦确认手机从界内移动到了界外,还有一件事必须要做,你能猜到是什么吗?对,需要重新设置withinBoundary为false,这样,在下一次收到传感器读数时,手机才不会再次震动。 关于布尔型变量,还有一点需要提示:检查一下这两个if测试,如图18-12,它们的效果一样吗? ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400d51ddc8.png) **图 18-12 你能说出这两个if测试的结果一样吗?** 答案是“一样”!唯一的差别在于下边的提问方式实际上更加老练,而上边的测试还要将一个布尔型的变量(其值只能是true或false)与true进行比较。如果withinBoundary的值为true,将true与true比较,结果一定是true;如果变量值为false,将false与true比较,结果为false。因此,只需要对withinBoundary的值进行检测,像右边那样,其结果相同,而且编码更加简洁。 ## **小结** 头晕了吗?尤其是最后的部分相当复杂!但这类决策方法是高级应用中必须具备的。如果你能一步一步(或者说一个分支一个分支)地实现这些行为,并做到边做边测试,我们敢断言,你会发现,即便是人工智能也不是不可能的。它让你头疼,也让你的大脑获得了些许逻辑思维的锻炼,但无疑也是充满乐趣的。
';

第 17 章 创建动画应用

最后更新于:2022-04-01 02:53:59

本章将讨论创建另一类应用的方法,应用中使用了简单的可移动的动画对象。你将学习使用App Inventor创建二维游戏的基本知识,并熟练使用图片精灵(image sprite)及处理两个物体碰撞一类的事件。 当你在电脑屏幕上看到一个平滑移动的物体时,你实际上看到的是一连串快速移动的图片,每次只移动一个极小的距离,它利用了人的视觉暂留,从这一点上,它无异于“手翻书”—— 一种通过快速翻页来看到动画效果的书(这也是那些精美绝伦的动画电影的制作方法)。 在App Inventor中,通过在Canvas组件上放置物体,并让这些物体随时间在Canvas内移动,从而产生出动画效果。本章将学习使用Canvas的坐标系统,学习利用Clock.Timer事件来触发运动,以及如何控制运动速度、如何响应两个物体的碰撞事件等等。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4004573ca2.png) ## **在应用中添加Canvas组件** 从组件面板的Drawing and Animation组中拖出Canvas组件,然后定义它的Width及Height属性。通常我们希望Canvas与屏幕等宽,为此将宽度设为“Fill parent”,如图17-1所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4004b0d0c3.png) **图 17-1 设置Canvas组件的Width属性** 可以用同样的方式设定Height属性,但一般会将其设为一个数字(如300像素),以便为Canvas上面或下面的其他组件留出空间。 ### **Canvas的坐标系统** Canvas上的图画实际上是一个许多像素构成的表格,像素是手机(或其他设备)屏幕上能够显示的最小的色块,每个像素都在Canvas上有它的位置(或者说单元格),位置由X-Y坐标系定义,如图17-2所示,X定义了水平方向上的位置(方向是从左到右),Y定义了垂直方向的位置(从上到下)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4004b68ca3.png) **图 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所示,那么只能画出一个单独的像素。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40050bd698.png) **图 17-3 用1个像素画圆,每次只能画1个单独的像素** 在块编辑器Built-in组的Colors抽屉中,App Inventor提供了13种常用的颜色,可以用来绘制像素图(或设置组件背景色)。也可以使用颜色编码方案来获得更为丰富的颜色,颜色编码方案的解释请参见相关App Inventor文档:[http://appinventor.googlelabs.com/learn/reference/blocks/colors.html](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”事件块,并在其中加入任何你需要的块,这些块将每个一个计时间隔执行一次。 ### **产生运动** 要让sprite随时间移动,就需要用MoveTo函数。在块编辑器的ImageSprite及Ball组件抽屉中可以找到这个函数。例如,要使一个球在水平方向上穿越屏幕,需要使用图17-4中的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400511863c.png) **图 17-4 让球在水平方向穿越屏幕** MoveTo的作用是在canvas上将物体移动到一个绝对位置,而不是相对位置。因此,为了移动到这个绝对位置,需要将MoveTo函数的参数设定为当前位置与增量之和。这里我们要实现球的水平移动,只需要将参数x设定为当前的x值与增量20之和,而y值保持不变(Ball1.Y)。 如果想让球沿着对角线的方向移动,就需要同时设定x、y坐标的增量,如图17-5所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400516024b.png) **图 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为你提供了这些功能。 ### **抵达边界** ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40056b11ec.png) **图 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中显示了这样一个事件处理程序。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40056f38e9.png) **图 17-7 当球与其他物体发生碰撞时,变色并发出爆炸声** NoLongerCollidingWith事件是与CollidedWith相反的事件,当两个碰到一起的物体分开时,触发该事件。而在游戏中,可能用到图17-8中的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4005744bde.png) **图 17-8 当碰撞的物体离开时,球变黑色并停止爆炸声** 注意到CollidedWith及NoLongerCollidingWith事件都有一个参数other,它代表了被撞到的那个物体。这可以用来处理一个物体(如Ball1)与另一个指定物体之间的相互作用。如图17-9所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400578e840.png) **图 17-9 只有当Ball1碰撞到ImageSprite1时才做相应** 之前我们没有提到过这个“ImageSprite组件”块。如果需要对两个组件进行比较(得知究竟是哪一个与之碰撞),如本例中的情形,就必须指定被比较的具体对象。为此,每个组件都有一个指向它自己的块,而这个块就在ImageSprite1的抽屉里,排在最后一个的就是。 ### **交互动画** 到目前为止,我们所讨论的动画行为都没有最终用户的参与。毫无疑问,游戏都是交互的,最终用户扮演着核心的角色,通常他们使用按钮或其他界面对象来控制物体的速度及方向。 作为例子,我们来改变对角线移动的动画,用户可以让移动停止然后再启动。可以通过对Button.Click事件编程来实现这一点,具体方法是控制clock组件的启用与禁用属性。 在默认情况下,Clock组件的timerEnabled属性是被选中的,可以在事件处理程序中动态地设置它,如设为false。例如,在图17-10的事件处理程序中,在用户第一次点击按钮时,可以让Clock的计时作用停止运行。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4006bd34fb.png) **图 17-10 当按钮被第一次点击时,停止计时** 在Clock1.TimerEnabled属性被设为false之后,Clock1.Timer事件不再被触发,因此球停止移动。 当然,只是在第一次点击时让运动停止,这样的操作并不能为游戏带来乐趣,需要在事件处理程序中添加一个ifelse块来控制计时功能的启用与禁用,从而实现对运动的双向控制(运动及停止)。如图17-11所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4006c4a16b.png) **图 17-11 添加ifelse块,通过点击按钮来控制运动的开始与停止** 在点击按钮的事件处理程序中,第一次点击按钮,计时器停止计时,按钮上的文字由“停止”变为“开始”;第二次点击按钮,此时TimerEnabled的值为false,因此执行“else”分支,于是计时器被置于启用状态,使得物体重新开始移动,按钮上的文字改回“停止”。关于ifelse快的详细信息请参见第18章,另外,关于用方向传感器创建交互动画的例子,请参见第5章及第23章。 ## **关于没有计时器的sprite动画** 目前为止我们讲述的动画案例都是利用Clock组件的计时功能,计时器事件每触发一次,物体就移动一次。采用Clock.Timer事件的方案是设定动画最普遍的方案,除了可以移动物体,还可以随时间改变物体的颜色,改变某些文字(好像应用自己在输入文字一样),或者让应用以某个速度说话,等等。 App Inventor提供了另外一种不需要Clock组件而让物体的移动的方法。你可能已经注意到,ImageSprite及Ball组件都具有Heading(方向)、Speed(速度)及Interval(间隔)属性。与Clock.Timer方案中定义事件处理程序相比,这里可以在组件设计器及块编辑器中设置这些属性,来实现对sprite运动的控制。 为了便于描述,我们来重新考虑沿对角线移动的例子。Sprite或ball的Heading属性的取值范围为0-360度,如图17-2所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4006cc7fe6.png) **图 17-12 Heading属性的取值范围** 如果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所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4006d31ede.png) **图 17-13 当球到达边缘时改变它的方向** ## **小结** 动画是物体随时间的位置移动或某些属性的变化,App Inventor为提供了几个高级的组件及功能,让动画的实现变得简单易行。通过对Clock组件的Timer事件进行编程,可以创建任何类型的动画,包括物体的移动——这是任何类型游戏中最基本的活动。 Canvas组件在设备的屏幕上定义了一个区域,物体可以在其中移动,并产生交互。Canvas内部只接受两种类型的组件,即ImageSprite组件及Ball组件。这些组件为处理碰撞及到达边界这样的事件提供了高级功能。此外,这些组件的Heading、Speed及Interval属性也为运动的实现提供了替代方法。
';

第 16 章 应用中的存储

最后更新于:2022-04-01 02:53:57

就像人类需要记忆一样,应用需要存储。本章将探究如何在应用中实现信息的存储。 如果刚刚有人在电话里告诉你一家披萨店的电话号码,你的大脑中会留下一段记忆;这时,如果有人大声告诉你一串数字,并要你记住,你也会将它们保存到记忆中。在这种情况下,你未必能清楚地意识到,你的大脑是在保存或调用信息。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4001dd4349.png) 应用同样具备记忆功能,但它的内在机制并不像大脑那样神秘。本章你将学习如何设置应用的存储功能,如何利用它来保存信息,以及之后如何提取这些信息。 ## **有名称的存储槽** 应用的存储功能由一组有名称的存储槽(memory slots)组成。一旦组件被拖到应用中,就会自动创建了一组被称为“属性”的存储槽;也可以定义与特定组件无关的、有名称的存储槽,即变量。如果说属性通常与应用的外观呈现有关,那么变量则被认为是应用中不可见的“暂时”记忆。 ### **属性** 在应用中,组件,或者说像Button、TextBox以及Canvas这类可视组件,构成了完整的用户界面。而组件本身的外观则是又一组属性来确定,属性值就保存在存储槽中。 在组件设计器中,可以直接对属性的存储槽进行修改,如图16-1所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4001ee5b90.png) **图 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所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4001f44638.png) **图 16-2 拖出变量初始化块** 2\. 为变量命名:点击变量初始化块中的“name”,并输入“score”,如图16-3所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4001f84fd3.png) **图 16-3 为变量命名** 3\. 为变量设置初始值:从Math抽屉中拖出数字块,将其插入变量初始化块的插槽中,如图16-4所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e4001fd0322.png) **图 16-4 为变量设初始值** 4\. 将变量初始值由默认值(0)改为123,如图16-5所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40020277dc.png) **图 16-5 修改变量的初始值** 定义一个变量,就是通知应用建立一个有名称的存储槽,来保存某个值。像属性一样,这些存储槽用户是看不见的。 变量的初始值在应用启动时就已经被放入存储槽中。可以用数字或文本对变量进行初始化,除此之外,也可以插入一个“make a list”块,它告诉应用这个变量是一个存储槽的列表,而不是一个单独的值。关于list的更多内容请参考第19章。 ## **设置及读取变量** 变量定义之后,App Inventor会生成两个属于这个变量的块:set块及get块,只要将鼠标悬停在变量初始化块中的变量名称之上,就可以呼出到这两个块。如图16-6所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40020783f5.png) **图 16-6 变量初始化块包含访问该变量的set块及get块** 其中的“set global score to”块可以用来修改(设置)变量的值,例如图16-7中,将数字块5放在变量score的set块中。变量初始化块中的“global”一词意为“全局的”,指的是变量的适用范围,一个全局变量可以被程序中所有事件处理程序及过程所引用。新版的App Inventor中还可以定义一种“local”变量,这种变量可以在一个事件处理程序或某个过程的内部进行定义(这里暂不涉及)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40020c3e28.png) **图 16-7 将数字5赋给变量score** 另一个“get global score”块用于从变量中读取变量值。例如,如果你想检查score的值是否大于或等于100,就可以将“get global score”块插入if块进行测试,如图16-8所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400211f802.png) **图 16-8 使用get global score块来获取变量值** ### **用表达式为变量赋值** 可以用单一的数字5来为变量赋值,不过通常会用一个更为复杂的表达式来为变量赋值(“表达式”是一个计算机科学的术语,即公式)。例如,在总统测试的应用中,用户点击“下一题”按钮时,要让变量currentQuestionIndex的值增加1,来显示下一道题;又如在游戏类应用中,如果玩家失败,还有可能将他的成绩减10分;还有像第3章打地鼠的游戏中,通过改变变量x的值,实现地鼠在Canvas中水平位置的随机移动。因此可以用若干个块组成的表达式插入“set global score”块为变量score赋值。 ### **变量的递增** 一种最常见的表达式可能是变量的递增,或根据变量的当前值进行设定。例如,游戏中当玩家获胜一次,变量score就将增加5,如16-9显示了实现这一行为需要的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400218b136.png) **图 16-9 分数变量递增5** 如果能够理解这些块的含义,你就离程序员又近了一步。这些块可以理解为“让成绩在现有的值上加1”,这是变量递增的另一种说法。要理解这些块的工作机制,需要按照从内向外、而不是从左到右的顺序,最里面的块是“get global score”及数字“5”,它们是最基础的块,然后“+”块执行加法运算,并将结果设定为变量score的值。 假设存储槽中score的当前值为5,经过这些块的运算,程序执行了以下步骤: 1\. 从score的存储槽中读取当前值5; 2\. 加上5得到结果10; 3\. 将10放回到score的存储槽中(来替代5)。 关于变量递增的更多内容请参见第19章。 ### **构造复杂的表达式** 在Math抽屉中(图16-10),App Inventor提供了许多数学函数,就像在电子表格或计算器中见到的一样。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e40021e2083.png) **图 16-10 Math抽屉中的运算符及函数** 你可以使用这些块来构造复杂的表达式,并将它们作为赋值表达式插入到“set global to”块中。例如,要想实现一个图片精灵(image sprite)在canvas范围内的随机水平移动,就需要使用一个乘法块(*)、一个减法块(-)一个Canvas.Width属性以及一个随机小数函数来组织表达式,如图16-11所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e400228111d.png) **图 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 章 软件工程与应用调试

最后更新于:2022-04-01 02:53:55

前面几章中讲过的Hello猫咪、打地鼠以及其他应用都是些非常小的软件项目,并不需要用引入软件工程的概念。工程的概念借用自其他行业,意为设计并建造,教程中的应用就像是用预制件拼装起来的房屋模型,而软件工程才是设计并建造真正用来居住的房子。这个例子虽然稍显夸张,但一般来讲,某些极其复杂的建造过程,的确需要大量的前期构思、规划以及技术分析,这些过程都可以归结为工程。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ffe5a75d5.png) 但凡接手过一个相对复杂的项目,你就会理解,只要在功能上稍稍增加一点复杂度,软件工程的复杂程度就会急剧增加,两者之间绝对不是线性的关系。对于我们大多数人来说,在真正开始面对这样残酷的现实之前,我们很少能够意识到将要面临的困顿。从这个意义上讲,你要准备学习更多的软件工程的原则及调试技巧。如果你已经认可这一点,或者,你是为数不多的、希望通过掌握一些技术来克服成长障碍的人,那么本章就是为你准备的。 ## **软件工程原则** 以下是本章所涵盖的一些基本原则: * 未来的软件使用者应该尽早,并尽可能多地参与到软件的设计及开发过程中来; * 建立一个初始的、简单的原型,并逐步完善; * 编码与测试同步进行,不要一次测试太多的代码(App inventor中的块); * 开始编码前进行逻辑设计:对功能做纵向切割,对技术或实施的复杂度做分层切割,并各个击破; * 对代码块进行注释,以便其他人(和你自己)能理解这些程序; * 学会用纸笔来跟踪记录块的执行过程,以便于理解它们的工作机制。 如果能够遵循上述原则,你就可以节省时间,避免挫折,从而制作出优秀的软件。但你很有可能做不到每次都依原则行事!有些原则看似违背常理。一种自然的倾向是,首先有了一个想法,并假设你了解用户的需求,然后开始把若干个块拼在一起,直到完成了想象中的任务。现在,让我们回到软件工程的第一个原则,在正式开始动手之前,看看如何了解用户的需求。 ### **设计要面对真实的人、现实的问题** 电影《梦幻成真》(Field of Dreams)中的男主角Ray听到了一个声音向他低语:“如果你建好了,他们就会来。”Ray听从了这个声音,在爱荷华州的农场中间建了一个棒球场,果然,在1919年,芝加哥白袜队(White Sox)和成千上万个球迷出现在这里。 不过你现在必须明白,那个低语的建议绝对不可以用于软件开发。事实上,必须正相反。在软件开发的历史中,充斥着各类“没问题”的伟大方案(如:“让我们写个软件,告诉人们开车到月球需要多长时间!”)。一个优秀的(同时也是极有可能获利的)软件的真正目的是解决现实中的问题。想知道问题出在哪里,就要找到有问题的人,并与他们交谈,这就是通常被称作“以用户为中心”的设计方法,这个方法同样也可以帮助你做出更好的应用。 如果遇到程序员,你可以问他们,在他们所写的软件中,有多少被真正交付到了最终用户的手中。结果会让你感到惊讶:即使是对那些伟大的程序员来说,这个比例也还是太小了!许多软件项目驶入了问题的泥沼而终无见天之日。 以用户为中心的设计理念意味着尽早并尽可能多地替未来的使用者着想,并与他们交流,这种思考与交流甚至应该在尚未确定目标之前就开始。大多数成功的软件都是针对某个具体的人,试图解决他的特定问题,也只有这样,最终才能发展成一个伟大的产品。 ### **快速地创建软件原型,并展示给未来的使用者看** 如果让最终用户阅读软件功能的说明文档,他们多半不会给出任何有效的回应,他们不会对文档做出反馈。真正有效的方法是,让他们体验未来软件的交互模式,即软件的原型。原型是一个不完整的、未经重构的软件版本,创建原型的目的在于充分体现软件所具有的核心价值,而不必注重细节、完整性或漂亮的用户界面。拿出原型让未来的使用者看,然后安静地倾听他们的反馈。 ### **迭代式开发** 在首次明确了软件的具体规格之后,采用迭代式开发。你可能很自然地倾向于将所有组件和块一股脑地添加到应用中,然后下载到手机上看看它是否好用。举例来说,“答题”应用,在缺乏指导的情况下,多数初学者会一次性添加所有的块:带有一长串问题及答案的块、浏览问题的块、检查用户答案的块,以及与每个逻辑细节有关的块,所有的块未经测试就全部罗列在应用中,这种开发方式在软件工程中被称为“大爆炸”方式。 几乎所有的初学者都会采用这种方式。在旧金山大学(USF)的课堂上,当学生忙于创建应用时,我经常会问他一个问题:“进展如何?” “我想我做完了,”他说。 “好极了,能让我看看吗?” “哦,还不行,我没带手机来。” “那么你还从来没有运行过这个程序,对吗?”我问。 “嗯...” 我透过他的肩膀看到了30个左右色彩缤纷的块,但他居然连一个功能都没有测试过。 程序员们很容易着迷,他们沉湎于创建UI(用户界面)并在块编辑器中创建所需的行为。那些块天衣无缝地结合在一起,优雅地排布在屏幕上,这些让他们感到倾心,却忘记了创建一个让其他人也能使用的、完整的、通过测试的应用。这听起来像是洗发水的广告,但对于我的学生和那些有志成为程序员的人,这是我能给出的最好的建议:**代码要随写随测,周而复始。** 每次只写少量代码,并随时测试,这个过程本身会变成一种习惯,如此这般,在不久的将来,你会收获令人惊异且满意的成果(而且几乎杜绝了大而难缠的程序漏洞——bug)。 ### **先设计,后编码** 编程要分两步走:①理解应用的逻辑,②将这些逻辑翻译成某种形式的编程语言。在开始翻译之前,要在逻辑上花一些功夫:首先要明确应用中将会发生哪些事情,无论是用户引发的,还是应用内部的;其次在正式开始将逻辑翻译成代码块之前,要明确每个事件处理程序中的逻辑。 有许多专门讨论各种程序设计方法的书籍。有些人喜欢用流程图或结构图来做设计,有些则更愿意将设计或草图写在纸上,更有人认为所有的“设计”最终应该体现为代码的注释,而不是一个与代码分离的文档。对于初学者来说,关键是要理解所有的程序在本质上都是一套逻辑,而这种逻辑与具体的编程语言无关。当然,思考应用的逻辑和翻译为编程语言这两件事有时难免会同步进行,无论这种编程语言是否直观。因此,在整个逻辑思考阶段,应该远离电脑,想清楚应用最终要实现哪些功能,并以某种方式随时记录下你的想法,然后让设计文档与应用保持关联,以便其他人也可以从中获益。下面我们就来实践这一过程。 ### **对代码进行注释** 你已经学过了本书中的教程部分,应该见过块所附带的黄色方框(见图15-1),这就是“注释”。在App Inventor中,任何的块都可以添加注释,方法是在块上单击鼠标右键,并在快捷菜单中选择**Add Comment**。注释丝毫不影响程序的运行。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fff5c9ac1.png) **图 15-1 为测试条件块添加注释——用简洁的语言描述块的作用** 那么为什么要做注释呢?想想看,如果你的应用很成功,它的生命周期会很长,即便只是搁下一周的时间,你都有可能忘记当时的想法,想不起来这些块有什么用处。因此,尽管没有别人会看到你的代码块,你也应给添加这些注释。 假如你的应用很成功,毫无疑问它会传到很多人手里,人们想了解它、按自己的需要修改它,或者扩展它的功能,等等。在开源的世界里,很多项目会以现有项目为基础,做进一步的修改和完善,只要你亲身体验过那些没有代码注释的项目,你就会彻底明白为什么注释是必需的。 为程序添加注释并不是一种自觉地行为,我也从未见到过初学者重视它,然而,我也从未见到过一个经验丰富的程序员不重视它。 ### **切割、分层、各个击破** 当问题的规模大到难以应对时,解决之道在于将问题分解,分解的方法有两种:第一种方法我们非常熟悉,即,将问题分解为若干个部分(如A、B、C),然后各个击破;第二种则不太常见:将问题按照从简单到复杂的顺序逐层分解。对应到App Inventor的编程方法上,就是先添加少量的块来实现简单的功能,并测试其效果,再逐渐过渡到复杂的功能,以此类推。 让我们以第10章的“出题”应用为例来具体阐述这两种方法。在应用中,用户可以点击“下一题”按钮对问题进行浏览,也可以检查用户的答案是否正确。从设计角度,可以将应用分解为两个部分:问题浏览及答案核对,并针对两个部分单独编程。 但在每个部分中,还可以对整个过程按照从简单到复杂的顺序进行分解。例如,问题浏览环节,先创建代码来显示问题列表中的第一题,并测试其是否有效;然后编写代码来浏览到下一题,暂时不考虑到达最后一题时可能引起的错误;当测试结果证明可以从头至尾浏览所有问题时,再添加块来处理用户浏览到最后一题的“特殊情况”。 究竟是将问题分解为几部分,还是按照复杂性分解为若干层,这不是一个非此即彼的问题,但却是一个值得思考的问题,关键在于哪种方法更适合于你所创建的应用。 ### **理解编程语言:用纸和笔跟踪记录** 应用在运行过程中,仅有部分可见。最终用户只能看到它的外观——用户界面上显示的图形及数据,而软件的内部运作机制对外部世界来说是不可见的,就像人类大脑的内部机制一样(谢天谢地!)。应用在运行时,我们既看不到这些指令(块),也看不到跟踪当前正在执行的指令的程序计数器,更无法看到软件的内部存储单元(应用中的属性及变量)。不过说到底,这正是我们想要的:最终用户只能看到程序需要被显示的部分,但对于开发者来说,在开发及测试过程中,你需要了解所有正在发生的事情。 作为一个开发者,在开发过程中所看到的代码,都只是些静态视图,因此必须靠想象力来驱动软件的运行:事件发生了,程序计数器移动到下一个块,并执行这个块,内存单元中的值发生了变化,等等。 编程过程中需要在两种不同的场景之间切换:先从静态模式——代码块开始,并试着想象程序的实际运行效果;一切就绪后,切换到测试模式——以最终用户的身份测试软件,看它的运行结果是否与预期的结果相一致。如果不是,必须再切换回静态模式,调整程序,然后再试。如此循环反复,最终获得一个满意的结果。 初学者对于计算机程序的运作方式知之甚少,整个过程看起来就像魔术。依照本教程的指导,学习应该从简单的应用开始(如,点击按钮导致猫叫),再逐渐过渡到较为复杂的应用,而且随着学习的不断深入,或许还可以根据自己的需要,对教程中的例子做出修改。从初学者到入门者,对程序的内部运作机制有了一些了解,但依然感到对整个过程无法控制。他们经常会说:“这个不起作用,”或者“它不应该是这样的。”关键是要理解程序如何实现那些你主管想象出来的功能,而且要说:“我的程序正在做这件事”,以及“我的逻辑导致了程序的...”。 了解程序运行机制的方法就是剖析一个简单应用的执行过程,在纸上精确地描绘出每个块在执行时,设备的内部发生了什么。想象用户触发了某个事件处理程序,然后逐步跟踪并记录块的执行效果:应用中的变量及属性如何改变,用户界面上的组件如何改变。就像文学课上的“精读”环节,这样一步一步的跟踪可以促使你检查语言中的各个要素(即App Inventor中的块)。 对复杂性的描述几乎是完全抽象的,重要的是你要放慢思路,理清各个块之间的因果关系。最终你会明白,这些过程控制的规则,并不像最初想象的那样难以理解。 以第8章总统测验为例,如图15-2所示,思考图中的这些块(对原教程做了一点修改)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fff63f6e1.png) **图 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 | 下面再来跟踪用户点击“下一题”按钮时发生的事情。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fff6af4a8.png) **图 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。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fff727f17.png) **图 15-4 右键点击事件处理程序中的任何一个块,会弹出快捷菜单** ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fff7ba7f5.png) **图 15-5 在快捷菜单中选择“Do It”,可以执行该块,并查看返回值(如果有)** “Do It”在调试块的逻辑错误时非常有用。还是回到“总统测试”例子中的NextButton.Click事件处理程序,并假设程序中存在逻辑错误,无法浏览所有的问题。调试过程需要在开发环境及测试设备上同时进行。在用户界面上点击“下一题”按钮,然后回到块编辑器查看是否每次点击都显示了适当的问题。也可以监视变量index在每次点击时的变化。 但是这类测试只允许检查整个事件处理程序的执行效果,在运行完所有的块之前,你无法检查你要监视的变量或用户界面。(抓不到逐句的中间状态) “Do It”允许你减缓测试过程,并检查任何一个块执行完成后的整个应用的状态。一般是从用户界面上的事件开始跟踪,直到发现问题所在。在发现无法显示最后一题之后,你可能在用户界面上点击“下一题”一次转到第二题,然后不再继续点击“下一题”,而是在块编辑器中让整个事件处理程序一步一步地运行。在NextButton.Click事件处理程序中,每次对一个块使用“Do It”让块执行,如图15-6中,先右键点击第一行的块(让变量index递增),并选择“Do It”。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fff83fa1b.png) **图 15-6 使用“DoIt”工具,每次只执行一个块** 此时index的值变为3,应用停止执行——“Do It”只能使被选中的块以及它所包含的子块运行,这可以让测试者检查被监视变量以及用户界面的变化。接下来,选择下一行要测试的块(if测试)并选择“Do It”来执行该行,其中的每一步都能看到每个块的执行效果。 ### **使用“Do It”渐进式开发** 有一点需要强调,这种逐行执行指令的方式不仅仅适用于程序的调试,它同样适用于开发过程中的随时测试。例如,如果你写了一个很长的公式来计算两个GPS坐标之间的距离,你可能要分步测试这个公式,来验证这些块的使用是否正确。 ### **启用与禁用块** 另一个有助于渐进式调试应用的方法是启用或禁用某些块,它允许应用中保留有问题的或未经测试的块,并让系统在运行过程中暂时忽略它们,然后充分调试那些启用状态的块,而不必担心那些有问题的部分。禁用块很简单,在块上点右键,在快捷菜单中选择Disable Block即可,被禁用的块呈现为灰色,在应用运行时,这些块被忽略;需要时,还可以重新启用这些块,方法是在块上点击右键并选择Enable Block。 ## **小结** App Inventor的伟大之处在于它的易用性——可视化的特点让你可以直接开始一个应用,而不必担心那些低层的细节。但现实的问题是,App Inventor不可能知道你的应用要做什么,更不知道如何来做。尽管直接进入组件设计器与块编辑器创建应用是件让人着迷的事情,但这里要强调的是,花一些时间来思考并详细、准确地设计应用的功能,是非常重要的。这听起来有些烦,但如果你能听取用户的想法、创建原型、测试并跟踪应用的逻辑,那么创建出精彩应用的目标指日可待。
';

第 14 章 理解应用的结构

最后更新于:2022-04-01 02:53:52

本章将从程序员的视角来探讨应用的结构问题。先从一个经典的比喻开始,将应用理解为一份菜谱,然后再从组件的角度,理解其对事件的响应,从而对应用产生新的认识。本章还将探讨应用如何提问、重复、记忆以及与Web交互,所有这些在后面章节均有更详细的叙述。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff5674ca3.png) 大多数人是从用户的角度来描述一个应用,但是在程序员看来,应用要复杂得多。我们必须充分理解应用的内部结构,才能更加有效地创建应用。 通常从两个方面来描述应用的内部结构:组件及行为,这大致与App Inventor的两个主要窗口相对应:组件设计器及块编辑器。前者用来设定应用中的对象(组件),而后者用来编写程序,实现对用户及外部事件的响应(应用程序的行为)。 在图14-1中,对应用的架构给出了总体描述,本章将对这种架构进行深入细致的探讨。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff58c3963.png) **图 14-1 App Inventor应用的内部结构** ## **组件** 应用中的组件分为两大类:可视组件及非可视组件。可视组件是在应用启动后能够看到的组件,如Button、Textbox及Label等,这些通常被视为应用的用户界面。 非可视组件是不可见的,因此它们不是用户界面的组成部分,通常用于访问设备的内置功能,如,Texting组件用于收发短信,LocationSensor组件用于确定设备的位置,而TextToSpeech组件用于朗读文字。非可视组件是设备的技术核心,是服务于应用程序的小精灵。 两类组件都是由一组属性来定义,属性相当于组件信息的存储空间。如可视组件的Width、Height及Alignment属性,它们共同定义了组件的外观。因此,最终用户所看到的如图14-2所示的“Submit”按钮,实际上是在组件设计器中由一组属性所定义的,如表14-1所示。 **表14-1 按钮属性** | Width | Height | Alignment | Text | | --- | --- | --- | --- | | 50 | 30 | center | Submit | ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff60a387e.png) **图 14-2 提交按钮** 可以将属性理解为上面表格中的内容,在组件设计器中,用它们来定义组件的初始外观。如果将Width属性从50改为70,那么无论是在设计器中,还是在实际应用中,按钮看起来都会变宽。注意,最终用户不会看到70这个数字,他们只能看到按钮变宽。 ## **行为** 一般来说,人们很容易了解组件的用途:文本框(Textbox)用于输入信息,按钮(Button)用来点击等等。但是对于应用中的行为,则往往是抽象的和复杂的。行为定义了应用对事件的响应,无论是用户发起的事件(如点击按钮),还是外部事件(如手机收到短信)。定义这些交互行为的难度恰恰是编程的挑战性所在。 幸运的是,App Inventor提供了一种非常适合于定义行为的可视化“块”语言,本节为理解块语言提供了一种模型。 ### **应用如菜谱** 人们习惯于把软件与菜谱相对比。像菜谱一样,传统的应用由一系列的顺序排列的指令构成,如图14-3所示,而计算机(厨师)则按顺序执行这些指令。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff65ee08c.png) **图 14-3 传统的软件由一系列顺序执行的指令构成** 一项典型的银行业务可能按如下顺序执行: 1\. 启动某项业务; 2\. 执行某些计算并修改客户账目; 3\. 在屏幕上显示新的余额信息。 ### **应用就是一系列的事件处理程序** 然而,如今的绝大多数应用,无论是手机的,还是Web或桌面电脑的,都不再适合采用这种菜谱模式了。应用不再是顺序地执行一系列的指令,相反,更为普遍的是对事件的响应,事件的触发者是最终用户。例如,当用户点击按钮时,程序会做出响应,执行某些操作(如发送短信)。对于使用触屏的手机或设备,当你的手指在屏幕上拖动时,将触发另一类事件,在应用中可以利用这类事件,在手指最初的接触点与最终的抬起点之间画一条线,作为对该事件的响应。 这种类型的应用更适合于概括为“对事件做出响应的组件的集合”。这类应用中依然包含了“菜谱”——一些顺序执行的指令,但每个菜谱只限于对某些特定事件做出响应,如图14-4所示的“菜谱”。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff663eba6.png) **图 14-4 拥有多个“事件菜谱”的应用** 因此,当事件发生时,应用通过调用一系列的函数进行回应。这里的函数是指①利用某些组件来完成某些操作,如发送短信;②对某些组件属性进行操作,如在用户界面上修改label的text属性。调用函数意味着让函数运行,让它产生作用。我们把事件,连同对事件进行响应的一系列函数统称为事件处理程序(Event Handler)。 许多事件由最终用户触发,但还有些不是。应用可以对手机内部的事件进行响应,如方向传感器的变化以及时钟的行走(即时间的流逝),也可以对手机以外的事件做出响应,如来自于其他手机的事件,或收到来自web的数据,等等。如图14-5所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff6ca6244.png) **图 14-5 应用可以兼顾对内部及外部事件的响应** 之所以称App Inventor编程为“直观”编程,是因为这种编程完全基于一种事件响应模式,而“事件处理程序”则是该语言中最重要的词汇(在其他语言中情况未必如此)。想要定义某个行为,首先要拖出一个事件块,事件块在形式上是这样的:“When do”。假设有这样一个“朗读”应用,当用户点击按钮时,应用大声读出用户输入的文字,这个应用只需要一个事件处理程序,如图14-6所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff6d10ab6.png) **图 14-6 “朗读”应用中的事件处理程序** 这些块的作用是,当用户点击“SpeakItButton”按钮时,TextToSpeech组件将朗读用户在输入框TextBox1中输入的文字。在这里,事件是SpeakItButton.Click,对事件的响应是调用TextToSpeech1.Speak函数,事件处理程序中包括了图14-6中的所有块。 在App Inventor中,所有活动都发生在对事件的响应之中,应用中不可能存在事件块“when-do”之外的块,如图14-7这样的单摆浮搁的块是毫无意义的。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff6d5db59.png) **图 14-7 事件处理程序之外的散在的块毫无用处** ### **事件类型** 可以引发活动的事件被分类列在表14-2中。 **表14-2 能够引发活动的事件** | 事件类型 | 举例 | | --- | --- | |用户发起的事件 | 当用户点击Button1时,执行... | | 初始化事件 | 当应用启动时,执行... | | 计时器事件 | 当20毫秒过去时,执行... | | 动画事件 | 当两个物体碰撞时,执行... | | 外部事件 | 当电话收到短信时,执行... | #### **用户引发的事件** 用户引发的事件是一种最常见的事件类型,在输入表单中,通常点击按钮事件会引发应用的响应。图形化的应用更多的是对触摸及拖拽事件做出响应。 #### **初始化事件** 有时需要在应用启动时实现某些功能,这既不同于响应最终用户引发的事件,也不是对其他类型事件的响应,那么如何让这种情况也适合于事件处理模式呢? 在App Inventor这种基于事件处理的语言中,应用的启动也被视为一种事件。如果你想在应用打开的同时实现某些功能,可以拖出Screen1.Initialize事件块,并将某些函数调用块放在其中。 例如,在第三章打地鼠游戏中,在应用启动的同时,通过调用MoveMole过程,将地鼠放在一个随机的位置,如图14-8所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff72af6b2.png) **图 14-8 应用启动时,使用Screen1.Initialize事件块来放置地鼠** #### **计时器事件** 应用中的某些活动是由时间的流逝而触发的,比如动画,可以理解为计时器事件触发了角色的移动。App Inventor有一个Clock组件,用于触发计时器事件。例如,如果想让一个球在一定时间间隔内,在屏幕上水平移动10个像素,就可以像图14-9那样来设置块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff72ee15e.png) **图 14-9 一旦Clock1.Timer开始运行(计时),使用计时器事件块来移动球** #### **动画事件** 在canvas范围内的图形对象(sprites精灵),它们的活动将触发动画事件,具体地说,当两个sprites发生碰撞,或一个sprites到达canvas的边界时,将触发动画事件。因此可以编写游戏或其他交互式动画程序,利用动画事件来定义游戏或动画的情节。更多信息请参见第17章。 #### **外部事件** 当手机从接收到来自GPS卫星的位置信息时,将触发一个外部事件;同样,当手机收到短信时,也会触发此类事件(图14-10)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff734a0cd.png) **图 14-10 当手机收到短信时,触发Texting1.MessageReceived事件** 这类向设备输入外来信息的行为都被视为外部事件,用户点击按钮也属于此类事件。 因此你所创建的应用,从本质上讲是一系列的事件处理程序:一个是对应用的初始化,有些是响应最终用户的输入,有些由事件触发,有些则有外部事件触发。你的任务是以事件处理的方式构思应用,然后设计对每个事件的响应方式。 ### **事件处理程序可以提问** 对事件的响应不总是单线条的菜谱,程序可以提问,也可以重复某些操作。“提问”意味着就应用中的数据进行提问,并根据答案决定下一步的行进方向(分支)。我们把应用中的提问称为“条件分支”,如图14-11所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff7391af6.png) **图 14-11 事件处理程序可以根据对条件问句的回答来执行不同的分支** 测试条件会如此设计:“分数到达100了吗?”或者“我刚收到的短信来自小明吗?” 测试也可以是的更为复杂的规则,包含多种关系操作符(小于、大于、等于)及逻辑运算符(and、or、not)。 在App Inventor中可以使用if块、ifelse块来设定条件行为,例如,在图14-12中,当玩家的分数为100点时,程序将显示“你赢了!”。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff78de448.png) **图 14-12 一旦玩家成绩达到100点,用if块来报告获胜** 本书第18章中将详细讨论条件块。 ### **事件处理程序可以重复执行某些块** 应用不但可以提问并依据答案执行不同分支,还可以多次重复执行某些操作。App Inventor提供了两种用于重复执行的块:foreach及while do。两种块中都会包含其它块。对于列表中的每一项,都会执行一次foreach块中的所有块,例如,如果你想给电话号码列表中的每个人都发送同一条短信,你可以利用图14-13中的块来实现。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3ff794432d.png) **图 14-13 foreach块中的块将对列表中的每一项都执行一次操作** foreach块中的块会重复多次,例如,3次,因为PhoneNumbers列表中只有三项。因此“想你...”这样的短信将发往这三个号码。本书第20章将详细讨论重复块。 ### **事件处理程序可以实现存储功能** 在事件处理程序运行过程中,通常会记录某些信息,有些信息可以保存在内存中,被称作变量。变量在块编辑器中定义,与组件的属性类似,但与任何组件无关。例如,在游戏类应用中,可以定义一个叫做“score(分数)”的变量,当用户执行了某些操作时,事件处理程序会相应地修改score的值。变量用于在应用运行过程中临时保存数据,一旦退出应用,数据将不复存在。 有时不仅需要在运行中保存某些数据,而且要求当退出又重新打开应用时,数据依然存在。例如,你想保存一个游戏的历史最高得分,就需要长期保存数据,以便下次有人再玩游戏时,可以看到这个分数。在应用关闭后依然保存下来的数据成为永久性数据,这些数据被保存在某种类型的数据库中。 在本书的第15章及第22章,我们将分别讨论临时存储(变量)及永久存储(数据库)的使用问题。 ### **事件处理程序可以与Web对话** 有些应用只能使用电话或设备的内部信息,但有些则能通过向web service API发送请求的方式与网络进行通信,这类应用被称为“网络应用”。 Twitter是一个很好的例子,它提供web service供App Inventor的应用访问。你可以编写一个应用,来请求并显示朋友们刚刚发布的“推文”,也可以随时更新自己的Twitter状态。能够与多个web service进行对话的应用被称为聚合类应用,我们将在第24章进行探讨。 ## **小结** 应用开发者必须以两种视角来观察一个应用,一个是最终用户的视角,另一个则是自内向外的程序员的视角。利用App Inventor来开发应用,首先要设计应用的外观,然后设计应用的行为——一套事件处理程序,让应用按照你的意图去运行。首先,通过在事件处理程序中拼装配置某些块,来实现对事件的响应,这些块可能是函数、条件分支、循环操作、web调用、数据库操作,等等;然后在手机中运行应用,来测试你的程序。当你编写了若干个程序之后,应用的内部结构与它的物理外观之间的关系将逐渐清晰,此时,你将成了一名真正的程序员。
';

第 13 章 亚马逊掌上书店

最后更新于:2022-04-01 02:53:50

![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe1ae9725.png) 假设你正在一家你很喜欢的书店里翻书,你想了解某一本书在Amazon.com(网上书店)的售价,那么这款“亚马逊掌上书店”应用就可以帮你实现这一愿望。通过扫描书上的条码,或输入书上的ISBN,应用将告诉你这本书当前在Amazon.com的最低售价。你也可以按照主题进行书籍的搜索。 “亚马逊掌上书店”演示了如何使用App Inventor来创建与web service进行交互的应用(web service又称作API 或应用程序接口)。本应用将从一个web services上获取数据,该web services是由本书的作者之一创建的。由本章结束时,你将为自己创建一款定制的应用,来访问亚马逊网上书店。 该应用界面非常简单,用户可以输入关键字或书的ISBN 码,应用将列出书名、ISBN以及新书在亚马逊上的最低售价。也可以使用条码扫描组件,让户可不必输入文本,而是通过扫描来进行搜索(从技术上讲,是扫描仪替你输入了书的ISBN!)。 ## **学习要点** 本章将学习以下内容: * 在应用中使用条形码扫描仪功能; * 用TinyWebDB组件访问Web信息源(Amazon API); * 学会处理从web信息源返回的复杂数据。所谓复杂数据,具体来说,就是图书列表中的每本书都是一个列表,其中包含三个列表项:书名、价格及ISBN。 此外我们将介绍用Python语言及谷歌的App引擎编写的源代码,并用它来创建自己的web service API。 ## **什么是API?** 在开始设计组件和编写应用之前,我们来解释一下什么是API(应用程序接口),以及它如何工作。可以把API想象为一个网站,只是它不与人类交互,而是与其他计算机程序交互。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe2079a70.png) **图 13-1 模拟器中的“亚马逊掌上书店”** API通常被称作“服务器端”程序,因为它们的特点就是为“客户端”程序提供信息。“客户端”程序负责实现与人类的接口——如App Inventor应用。如果你曾经在手机上使用Facebook 应用,你实际上是通过Facebook的客户端程序与Facebook的服务器端程序(API)进行通信。 本章将创建一个Android客户端应用,与Amazon API进行通信。应用将向Amazon API请求书的信息(书名、书号及价格等),而API将向应用返回最新的信息列表,用户将在应用中浏览到书的相关信息。 我们即将使用的Amazon API是App Inventor专用的API。这里我们不想过多地解释细节,但有关配置的知识是非常有用的,正是由于有了这些配置,我们才能用TinyWebDB组件与Amazon进行沟通。好在你已经学会了使用TinyWebDB!调用TinyWebDB.GetValue来请求信息,然后在TinyWebDB.GotValue事件处理程序中处理返回的信息,就像在用Web数据库一样。(如果忘记了,去复习一下第10章的“出题”应用。) 在创建应用之前,需要先了解一下Amazon API协议,协议规定了请求数据的方式以及返回数据的格式。就像不同的族群有不同的礼仪(两人相遇时,是握手、鞠躬还是点头?),计算机之间的互相则需要有协议 。 Amazon API为调用者提供了一个Web页面,来说明API的使用方法。虽然设计API的目的是为了与其它计算机交互,但在这个页面上,你可以看到这种交互的过程。 按照下列步骤,你可以尝试调用一个指定tag参数的GetValue,并在页面上看到返回的数据,这与你在App Inventor中使用TinyWebDB组件请求数据的结果完全一致: 1\. 在浏览器中访问网站[http://aiamazonapi.appspot.com/](http://aiamazonapi.appspot.com/),你会看到如图13-2所示的页面(页面中的中文为译者添加)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe2d344b1.png) **图 13-2 App Inventor专用的Amazon API的说明及测试页面** 2\. 本页面允许你对与此API的GetValue功能进行测试:在tag输入框中输入搜索词(如“natural computing”),然后单击“Get value”按钮。页面将显示从Amazon API返回的排在前五位的书籍列表,如图13-3所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe2da1495.png) **图 13-3 调用Amazon API来搜索与tag(或关键字)“natural computing”有关的书籍** 返回值是一个书的列表,每本书的信息由一对方括号包围[像这样],提供了书名、售价及ISBN。如果仔细观察,你会发现每本书其实是另一主列表的子列表。主列表(natural computing)由外层的方括号包围,每个子列表(或书)被封闭在单独的一对方括号内。所以此API的返回值实际上是一个列表的列表,每个子列表提供一本书的信息。我们来细致地观察一下。 数据中的每个左括号([)标志列表的开头。第一左括号标志外层列表(书籍列表)的开始,紧挨着它的左括号是第一个子列表,即第一本书的开头: > [['"Natural Computing: DNA, Quantum Bits, and the Future of Smart Machines"', '$5.77', '0393336832'], 子列表包含三个部分:书名、该书在亚马逊书店的最低售价及这本书的ISBN。当你的App Inventor应用取得这些信息时,就可以使用select list item块来访问其中的每个部分,用索引值1访问书名,索引值2访问价格,索引值3访问ISBN。(如果淡忘了有关列表及索引的使用方法,请复习第十章的“出题”应用。) 3\. 除了搜索关键字,你还可以通过ISBN来精确地搜索一本书,只要在tag后面直接输入书号即可,试试输入“1449397484”,如图13-4所示。返回结果如下: > [['"App Inventor: Create Your Own Android Apps"', '$21.64', '1449397484']] 返回结果中的双括号([[)表示返回的仍然是一个列表的列表,虽然列表中只有一本书。这似乎有点奇怪,但这一点对需要访问这类信息的应用来说非常重要。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe2e0d8b2.png) **图 13-4 用ISBN替代关键字在AmazonAPI中查询书籍** ## **设计组件** “掌上书店”应用的用户界面比较简单:一个用于输入搜索词或ISBN的文本框,一个用于启动搜索(关键字或ISBN)的按钮,另一个启动扫描书的条码的按钮(稍后会用到),一个搜索结果标题的label,另一个显示搜索结果的label,还有两个非可视组件:TinyWebDB和条码扫描仪。表13-1列出了图13-5中所示的所有组件,对照检查你的结果。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe2e65a99.png) **图 13-5 组件设计器中“亚马逊掌上书店”的用户界面** **表13-1 “亚马逊掌上书店”应用的组件列表** | 组件类型 | 面板中分组 | 命名| 作用 | | --- | --- | --- | --- | | Textbox | User Interface | SearchTextBox |用户在此输入关键字或ISBN | | HorizontalArrangement| Layout| HorizontalArrangement1 |将按钮放置在同一行内| | Button | User Interface | SearchButton |点击启动关键字或ISBN搜索 | | Button | User Interface | ScanButton |点击开始扫描书的ISBN条码 | | Label | User Interface | Label1 | 搜索结果的标题| | Label | User Interface | ResultLabel | 显示搜索结果 | | TinyWebDB | Storage | TinyWebDB1 | 与Amazon.com交互 | | BarcodeScanner | Sensors | BarcodeScanner1 | 扫描条码 | 组件的属性设置如下: 1\. 设置SearchTextBox的Hint属性为“输入关键字或ISBN”; 2\. 设置Button及Label的Text属性:如图13-5所示; 3\. 设置TinyWebDB组件的serviceURL属性为[http://aiamazonapi.appspot.com/](http://aiamazonapi.appspot.com/) 。 ## **设计行为** 在块编辑器中设定下列行为: * 按关键字搜索:用户输入关键字并点击SearchButton来调用Amazon搜索。通过调用TinyWebDB.GetValue来实现这一点; * 按ISBN搜索:用户输入一个ISBN并点击SearchButton; * 条码扫描:用户点击ScanButton启动扫描仪,扫描ISBN完成后将启动Amazon搜索; * 处理图书列表:首先采用默认方式显示Amazon返回的数据。稍后再加以修改,采用有组织的方式来显示每本书的书名、售价及ISBN。 ### **按关键字或ISBN搜索** 用户点击SearchButton时,从SearchTextbox中获取搜索内容,并以此为tag,使用TinyWebDB.GetValue块向Amazon API请求搜索数据。 当Amazon返回结果时,将触发TinyWebDB.GotValue事件。现在用ResultsLabel直接显示返回结果,如图13-6所示,当见到这些真实的数据后,可以采取更为复杂的方式来显示数据。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe2f3c718.png) **图 13-6 向API发送搜索请求,并将结果显示在ResultsLabel中** #### **块的作用** 当点击SearchButton时,TinyWebDB1.GetValue开始向API发出请求。随请求一同发送的tag正是用户在 SearchTextBox中输入的内容。 从第十章“出题”应用中得知,TinyWebDB.GetValue发出的请求并不能立即获得结果,而是在收到从API返回的数据时,触发TinyWebDB1.GotValue事件。在GotValue块中,首先检查返回值是否为列表,如果是,则数据被写入ResultsLabel。(如果Amazon API离线或搜索的关键字不存在,将没有数据返回。)再输入ISBN“1118717376”试试看。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe2f9d120.png) **图 13-7 搜索“Android”返回的结果** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe300e6f6.png) 测试:在输入框中输入搜索词,如“Android”,然后点击“搜索”按钮。你将得到类似于图13-7的结果。注意:手机上返回的结果与网页上的结果略有不同:方括号变成了圆括号。(这个界面看起来很糟糕,稍后我们会处理。) ### **消除用户的困惑** 在前几章中了解到,使用TinyWebDB请求数据的过程需要一点时间,在收到返回的数据之前,用户界面上悄无声息,这时用户会感到困惑,因此从用户友好的角度考虑,添加一点提示是非常必要的。这里我们用ResultLabel来显示提示信息。如图13-8所示。但如果网络速度很快,数据很快返回并触发GotValue事件,则几乎看不到提示,数据就显示出来了。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe3561153.png) **图 13-8 添加提示信息消除用户的困惑** ### **扫描一本书** 现实情况是:在手机上输入字符通常不那么容易,而且总会出一些小错。如果能够在应用中使用条码扫描,那么问题会变得简单(并且几乎不会出错)。这是Android手机内置的另一项强大的功能,你可以用App Inventor轻而易举地实现它。 函数BarcodeScanner.DoScan用于启动扫描仪,可以在ScanButton被点击时调用它。一旦扫描操作完成,将触发BarcodeScanner.AfterScan事件。该事件带有一个参数result,其中保存了扫描所获得的信息。在本例中,result即是用于搜索的ISBN,如图13-9所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe35aa84a.png) **图 13-9 用户扫描书上的条码获得ISBN并启动搜索** #### **块的作用** 点击ScanButton将启动扫描仪,即执行BarcodeScanner1.DoScan。扫描完成时触发AfterScan事件。该事件带有result参数,在本例中为书的ISBN。用ResultLabel告诉用户正在进行搜索,并在SearchTextBox中显示result(扫描获得的ISBN),最后调用TinyWebDB.GetValue来启动搜索。仍然使用之前定义的TinyWebDB.GotValue事件处理程序来处理返回的书籍信息。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe300e6f6.png) 测试:点击ScanButton并扫描一本书的条码。应用中是否显示了该图书的信息? ### **改进信息的显示** 我们所创建的这类客户端应用,可以按需要来处理收到的数据,可以与其他网上商店进行价格比较,也可以用书名信息来搜索其他图书馆中的同类书籍。 通常是将加载自API的信息保存到变量中再做处理,而目前只是在TinyWebDB.GotValue事件处理程序中,将Amazon API返回的所有信息直接写入到ResultsLabel中。 下面我们来处理(或这说安排)这些数据,方法是(1)将返回数据中的每本书的书名、售价以及ISBN分别保存到单独的变量中;(2)以一种有序的方式显示这些项目。 到目前为止,你已熟知了全局变量(global variable),与之对应的是局部变量(local variable),这是本章引入的一个新的概念。一个变量有它的有效范围:全局变量直接在块编辑器中定义,单独占一行,不属于任何一组块,而所有的块都可以调用全局变量;局部变量则定义在某个程序块内部,它将包含其它块,而且只对其包含的块有效,其余的所有块都无法访问到它。 下面将使用这些变量,并将它们显示出来,试试看按需要创建这些变量,并组织一些块来分行显示每一项搜索结果,完成之后与图13-10进行比较。 #### **块的作用** 这里定义了四个变量:resultList、title、cost及ISBN,用来保存Amazon API返回数据中的每一条数据。从API返回的数据保存在参数valueFromWebDB中,这里将它另存为resultList。其实程序可以直接使用valueFromWebDB,但通常会将它另存为一个变量,以便在该事件处理程序之外也可以使用这一数据。(像valueFromWebDB这样的参数仅在事件处理程序内有效,事件处理程序之外无法访问该参数值。) foreach循环用来遍历返回结果中的每个数据项。回想一下,从Amazon返回的数据是一个列表的列表,每个子列表代表一本书的信息,因此foreach块中的项变量被命名为bookItem,它保存了当前正在被遍历的书的信息。 现在我们要清醒地面对项变量bookitem,它是一个列表,其中第一项是书名,第二项是售价,第三项是ISBN,因此,select list item块利用索引值(index)将这些数据逐项提取出来,并保存在事先定义的变量中(title、cost及ISBN)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe3b54e22.png) **图 13-10 在遍历过程中提取每本书的书名、售价及ISBN,并逐行显示它们** 一旦数据被拆解成这种方式,就可以随意地摆布它们。程序中的局部变量只是作为join块的组成部分,来逐行显示书名、售价及ISBN。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe300e6f6.png) 测试:尝试搜索其他书,看看返回的信息是如何显示的。应该类似于图13-11。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fe503e5d4.png) **图 13-11 用更有条理的方式显示搜索结果** ### **定制化API** 我们所连接的API([http://aiamazonapi.appspot.com](http://aiamazonapi.appspot.com/))是由Python(编程语言)和谷歌应用引擎(App Engine)创建的。开发者可以将应用引擎上创建并发布网站或服务(API)。只有当你的网站或服务非常受欢迎时(这意味着你使用了大量的谷歌服务),才需要向App Engine付费。 这里使用的API只能访问全部Amazon API中的有限的部分:最多只能查询到五本书。如果想提供更多灵活的访问,例如,不仅是找书。你可以从[http://appinventorapi.com/amazon/](http://appinventorapi.com/amazon/)下载源代码,并按照自己的需要来修改它。 这种修改确实需要有Python编程的知识,所以要小心!但是加入你已经通过本书完成了App Inventor的学习,那么是该考虑迎接新的挑战了。想要学习Python,可以查看网上的文章《如何像计算机科学一样思考:学会使用Python》([http://openbookproject.net//thinkCSpy/](http://openbookproject.net//thinkCSpy/)),并查看本书第24章的“创建App Inventor API”部分。 ## **改进** 一旦应用运行起来,你可能会探索做一些改进。如: * 如果用户的搜索没有任何返回值(比如当用户输入了一个无效的ISBN),程序没有任何反馈,修改或添加块,当没有返回值时通知用户; * 修改程序,只查找低于10美元的书籍; * 修改程序,当扫描一本书后,用声音来报告Amazon的最低售价(使用第七章“Android,我的车在哪儿”中用过的TextToSpeech组件); * 从[http://examples.oreilly.com/0636920016632/](http://examples.oreilly.com/0636920016632/)下载[http://aiamazonapi.appspot.com](http://aiamazonapi.appspot.com/)API,修改它,使之能返回更详细的信息。例如,返回每本书在Amazon上的网址(URL),并随每本书的信息一同显示;用户点击URL可以打开相应的页面。正如前面提到的,修改API需要Python编程和谷歌App Engine的一些知识。更多信息请参见第24章。 ## **小结** 以下是本章涵盖的内容: * 使用TinyWebDB组件以及指定的API,在应用中访问互联网API。将TinyWebDB组件的serviceURL属性设置为API的地址(URL),并调用TinyWebDB.GetValue来发出请求。请求的数据不会立即返回,可以在TinyWebDB.GotValue事件处理程序中访问到该数据; * BarcodeScanner.DoScan可以启动扫描仪。当用户完成条码扫描时,将触发BarcodeScanner.AfterScan事件,并将扫描获得的数据保存在参数result中; * 在App Inventor中,复杂数据可以表示为列表以及列表的列表。如果知道从API返回的数据的格式,就可以使用foreach及select list item来分别提取没条信息,并将其保存到变量,这样就能以你需要的方式,对变量进行处理或显示。
';

第 12 章 遥控机器人

最后更新于:2022-04-01 02:53:48

> **作者介绍** > > **Liz Looney** > > ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd386f57b.png) > > Liz Looney是本书的合著者之一。她是谷歌的软件工程师,也是谷歌“机器人工作小组”(Robotics Task Force)的成员,作为App Inventor团队的初创成员,她领导了乐高头脑风暴机器人(LEGO MINDSTORMS)组件的开发工作。Liz Looney是一位卓越的软件工程师,有着超过25年的从业经历,曾先后在Borland、Oracle及Google公司工作。 本章将创建一个应用,将Android手机变成LEGO MINDSTORMS NXT 机器人的遥控器。应用中用按钮来控制机器人前后移动、左右转动和停止,如果机器人遇到障碍物,它还会自动停止。应用中使用具有蓝牙功能的手机与机器人通信。 LEGO MINDSTORMS机器人不只是玩具,更是教具。After-school program 使用机器人来教小学和初中的孩子们掌握解决问题的能力,并引导他们了解工程和计算机编程。NXT机器人也用于FIRST LEGO League 机器人竞赛,这项比赛允许9-14岁的孩子参加。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd3e321ac.png) NXT可编程机器人套件中有一个“NXT智能积木”主单元,它可以控制三个电机及四个输入传感器。你可以用乐高的构造元件、齿轮、车轮、电机和传感器来组装机器人。该套件自带的软件可以对机器人进行编程,但现在我们将用App Inventor来创建Android应用,通过蓝牙连接来控制NXT机器人。 应用中参与协作的机器人具有超声波传感器以及用于移动的车轮,如Shooterbot 机器人。图中所示,这款机器人通常是人们利用LEGO MINDSTORMS NXT 2.0套件建造的第一个机器人。它的左车轮与输出端口C相连,右车轮与输出端口B相连,颜色传感器与输入端口3相连,超声波传感器与输入端口4相连。 ## **学习要点** 本章用到了以下组件和概念: * BluetoothClient组件:用于建立Android设备与NXT机器人之间的蓝牙连接; * ListPicker组件:为用户提供机器人选择列表,选中后开始建立机器人到Android的连接; * NxtDrive组件:用于驱动机器人的轮子; * NxtUltrasonicSensor组件:利用机器人的超声波传感器探测障碍物; * Notifier组件:显示错误消息。 ## **准备开始** 本章的应用需要Android 2.0或以上版本。此外,出于安全原因,蓝牙设备必须首先配对才能彼此连通。在开始构建应用之前,需要按以下步骤使Android设备与NXT机器人配对: 1\. 在NXT上单击向右箭头,直到显示“Bluetooth”,然后按下橙色方块; 2\. 点击向右的箭头,直到显示“Visibility”,然后按下橙色方块; 3\. 如果“Visibility”值已设定为可见,继续步骤4;如果不可见,请单击向左或向右箭头设置其值为可见; 4\. 在Android设备上,进入设置→无线与网络; 5\. 确保打开蓝牙功能; 6\. 点击“蓝牙”; 7\. 在“可用设备”中查找名为“NXT”的设备; 8\. 如果机器人名字下显示“已配对但未连接”字样,则配对成功!否则,继续执行步骤9; 9\. 如果机器人名字下显示“与此设备配对”,则点击它; 10\. 在NXT上,要求输入密码,按下橙色方块接受1234为密码; 11\. 在Android上,也会要求输入PIN码,输入1234,然后按确定; 12\. 现在应该看到“已配对但未连接。”,说明配对成功! > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd43aca5c.png) 注意:如果你曾经修改过机器人的名字,则寻找机器人现在的名字,而非“NXT”。 连接到App Inventor网站,创建新项目“NXTRemoteControl”,将设置屏幕的标题为“遥控机器人”,并连接测试手机。 ## **设计组件** 在这个应用中,我们需要分别创建可见组件及不可见组件,并分别定义它们的行为。 ### **不可见组件** 在创建用户界面之前,先来创建表12-1中的不可见组件,如图12-1所示,用来控制NXT。 **表12-1 NXT“机器人遥控”应用中的不可见组件** | 组件类型 |面板中分组 | 命名 | 作用 | | --- | --- | --- | --- | | BluetoothClient| Connectivity | BluetoothClient1 |建立Android与NXT的连接 | | NxtDrive | LEGO® MINDSTORMS® | NxtDrive1 |驱动机器人的轮 | | NxtUltrasonicSensor | LEGO® MINDSTORMS® |NxtUltrasonicSensor1 | 检测障碍物 | | Notifier | User Interface | Notifier1 | 显示错误信息 | ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd490a4db.png) **图 12-1 在组件设计器底部显示的不可见组件** 按以下方式设置组件的属性: 1\. 设置NxtDrive1及NxtUltrasonicSensor1的BluetoothClient属性为 BluetoothClient1;(说明轮子的驱动与障碍物的侦测都需要依赖蓝牙通信——译者注) 2\. 勾选NxtUltrasonicSensor1的BelowRangeEventEnabled属性(近距离侦测障碍物功能可用); 3\. 设置NxtDrive1的DriveMotors属性 * 如果机器人的左轮电机与输出端口C连接,右轮电机与输出端口B连接,则保持默认设置“CB”; * 如果机器人的配置与上述不同,则将DriveMotors属性设置为两个字母的文本,其中第一个字母是连接左轮电机的输出端口,第二个字母是连接右轮电机的输出端口。 4\. 设置NxtUltrasonicSensor1的SensorPort属性 * 如果机器人的超声波传感器与输入端口4连接,则保持默认值“4”; * 如果机器人的配置与上述不同,则将SensorPort设置为与超声波传感器连接的输入端口。 ### **可视组件** 现在创建用户界面组件,如图12-2所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd4e979bf.png) **图 12-2 组件设计器中的应用** 建立蓝牙连接时,Android设备需要访问NXT机器人具有唯一性的蓝牙地址,但蓝牙地址由8个用冒号分隔的2位数的十六进制数(二进制数的另一种表示方式)组成,输入起来异常麻烦,而且每次运行应用都要在手机上输入该地址。为了减少麻烦,使用ListPicker来显示已经与手机配对的机器人列表(列表项的值为机器人的名称及蓝牙地址),并从中选择一个。 使用按键来驱动机器人的前进、后退、左右转动、停止和断开连接,使用VerticalArrangement来放置除ListPicker以外的所有组件,用HorizontalArrangement来放置左右转向及停车按钮。 按照表12-2中列出的组件来创建图12-2所示的用户界面。 **表12-2 NXT机器人控制器应用中的可见组件** | 组件类型 | 面板中分组 | 命名 | 作用 | | --- | --- | --- | --- | | ListPicker | User Interface | ConnectListPicker |选择要连接的机器人 | | VerticalArrangement | layout | VerticalArrangement1 |布局容器,容纳除ListPicker之外的组件 | | Button | User Interface | ForwardButton | 前进 | | HorizonalArrangement | layout | HorizonalArrangement1 |布局容器,容纳左转、右转、停止按钮 | | Button | User Interface | LeftButton | 左转 | | Button | User Interface | StopButton | 停止 | | Button | User Interface | RightButton | 右转 | | Button| User Interface | BackwardButton | 后退 | | Button | User Interface | DisconnectButton| 与NXT断开连 | 按照图12-2所示来设置可视组件布局:将LeftButton、StopButton和RightButton放在HorizontalArrangement1中,将ForwardButton、HorizontalArrangement1、BackwardButton和DisconnectButton放在VerticalArrangement1中。 按下列方式设置组件属性: 1\. 取消勾选Screen1的Scrollable属性(滚屏功能); 2\. 设置ConnectListPicker和DisconnectButton的宽度为“Fill parent”; 3\. 设置VerticalArrangement1、ForwardButton、HorizontalArrangement1、LeftButton、StopButton、RightButton及BackwardButton的Width与Height为“Fill parent”; 4\. 设置ConnectListPicker的Text属性为“连接”; 5\. 设置ForwardButton的Text属性为“∧”; 6\. 设置LeftButton的Text属性为“<”; 7\. 设置StopButton的Text属性为“—”; 8\. 设置RightButton的Text属性为“>”; 9\. 设置BackwardButton的Text属性为“∨”; 10\. 设置DisconnectButton的Text属性为“断开连接”; 11\. 设置ConnectListPicker和DisconnectButton的FontSize属性为30; 12\. 设置ForwardButton、LeftButton、StopButton、RightButton及BackwardButton的FontSize属性为40。 在这类应用中,当手机与NXT建立蓝牙连接之前,应该隐藏用户的操作界面,为此取消勾选VerticalArrangement1的Visible属性。不要担心,当NXT连通后,将重新显示用户界面。 ## **为组件添加行为** 本节将编程来设置应用的行为,包括: * 用户从列表中选择机器人,并与之建立连接; * 断开机器人与应用的连接; * 使用控制按钮来操控机器人; * 在机器人侦测到障碍物时,让它停下来。 ### **连接到NXT机器人** 添加第一个行为:连接到NXT。点击 ConnectListPicker将显示已配对的机器人列表,选中一个,将在应用与机器人之间建立蓝牙连接。 ### **显示机器人列表** 使用ConnectListPicker组件来显示机器人列表。ListPicker的外表像按钮,被点击后则显示列表项,并允许进行单选。 使用BluetoothClient1.AddressesAndNames块来提供列表,列表项是已经与Android设备配对的蓝牙设备的名称及地址。由于NXT已经将轮驱动及超声波组件的BluetoothClient属性设定为BluetoothClient1,因此AddressesAndNames属性列表中的设备会自动限定为这类机器人,其他类型的蓝牙设备(如耳机)将不会出现在列表中。表12-3列出了所需要的块。 **表12-3 在应用中添加ListPicker列表所需要的块** |块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | ConnectListPicker.BeforePicking | ConnectListPicker |当ConnectListPicker被点击时,触发该事件 | | set ConnectListPicker.Elements to | ConnectListPicker |为ConnectListPicker设置可供选择的列表项 | #### **块的作用** 点击ConnectListPicker将触发ConnectListPicker.BeforePicking事件,并显示可选项列表。将ConnectListPicker.Elements属性设置为 BluetoothClient1.AddressesAndNames块,来设定可选项;ConnectListPicker将显示已经与Android设备配对的机器人列表。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd4f13d9d.png) **图 12-3 显示机器人列表** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd4f79e4c.png) 测试:在手机上点击“连接”,看看会发生什么,你会看到所有已经与手机配对的机器人列表。 如果只见黑屏,说明手机尚未与任何机器人配对;如果见到其他蓝牙设备,如蓝牙耳机,说明 NxtDrive1与 NxtUltrasonicSensor1的BluetoothClient属性设置有误。 ### **建立蓝牙连接** 从列表中选择一个机器人,应用将通过蓝牙与机器人连接。如果连接成功,用户界面将发生变化:隐藏ConnectListPicker,并显示用户界面的其余部分。如果机器人开关没有打开,则连接失败,会弹出错误信息。 使用call BluetoothClient1.Connect块与机器人进行连接。ConnectListPicker.Selection属性提供了选中机器人的地址和名称信息。 使用ifelse块来测试连接是否成功。ifelse块需要连接三个不同的块:“if”、“then”及“else”。“if”与BluetoothClient1.Connect块连接,“then”区域放置连接成功时要执行的块;“else”区域放置连接失败时要执行的块。 如果连接成功,使用Visible属性来隐藏 ConnectListPicker并显示VerticalArrangement1(其中放置了除ConnectListPicker之外的所有组件)。如果连接失败,则使用Notifier1.ShowAlert块来显示错误信息。表12-4列出了设置上述行为所需的块。 **表12-4与机器人建立蓝牙连接所需的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | ConnectListPicker.AfterPicking | ConnectListPicker |当从ConnectListPicker选中一个机器人时触发| | ifelse | Control | 检验蓝牙连接是否成功 | | call BluetoothClient1.Connect | BluetoothClient1 |连接到机器人 | | ConnectListPicker.Selection | ConnectListPicker |选中的机器人的地址及名称 | | set ConnectListPicker.Visible to | ConnectListPicker|隐藏ConnectListPicker按钮 | | false | Logic | 插入set ConnectListPicker.Visible to块 | | set VerticalArrangement1.Visible to | VerticalArrangement | 显示“连接”按钮之外的所有组件 | | true | Logic | 插入set VerticalArrangement1.Visible to块 | | Notifier1.ShowAlert | Notifier1 | 用来弹出错误信息 | | “无法建立蓝牙连接。” | Text | 错误信息。 | #### **块的作用** 选中机器人后将触发ConnectListPicker.AfterPicking事件,见图12-4,BluetoothClient1.Connect块用于建立与机器人之间的蓝牙连接。如果连接成功,执行“then”块:隐藏ConnectListPicker按钮并显示VerticalArrangement1内的所有组件,即,设置ConnectListPicker.Visible属性为false,设置VerticalArrangement1.Visible属性为true。如果连接失败,执行“else”块:用Notifier1.ShowAlert块弹出错误信息。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd54c9699.png) **图 12-4 建立蓝牙连接** ### **与NXT断开连接** 让Android设备与NXT机器人连接着实让人兴奋,不过“断开连接”是我们下面要添加的行为,这样便于对连接与断开进行连续测试。 当点击DisconnectButton时,应用将关闭蓝牙连接,用户界面将发生变化:ConnectListPicker按钮将重新出现,而用户界面上的其余组件将被隐藏。 表12-5列出了构建BluetoothClient1.Disconnect(断开蓝牙连接)所需的块。设置Visible属性来显示 ConnectListPicker按钮并隐藏VerticalArrangement1中包含的所有组件。 **表12-5 与机器人断开连接所需的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | DisconnectButton.Click | DisconnectButton |当点击DisconnectButton时触发该事件 | | BluetoothClient1.Disconnect | BluetoothClient1 |断开与机器人的蓝牙连接 | | set ConnectListPicker.Visible to | ConnectListPicker|显示ConnectListPicker(“连接”按钮) | | true | Logic | 插入set ConnectListPicker.Visible to块 | | set VerticalArrangement1.Visible to | VerticalArrangement | 隐藏用户界面上的其余组件 | | false | Logic | 插入set VerticalArrangement1.Visible to | #### **块的作用** 点击DisconnectButton将触发DisconnectButton.Clicked事件,如图12-5所示,断开蓝牙连接要用BluetoothClient1.Disconnect块,之后设置ConnectListPicker.Visible属性为true来显示 ConnectListPicker,设置VerticalArrangement1.Visible属性为false来隐藏VerticalArrangement1。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd552a414.png) **图 12-5 与机器人断开连接** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd4f79e4c.png) 测试:请确保机器人已经打开,点击手机上的“连接”按钮,并选择要连接的机器人。建立蓝牙连接需要一点时间。一旦连接成功,用户界面将显示机器人的控制按钮,以及“断开连接”按钮。 单击“断开连接”按钮:控制机器人的按钮会消失,“连接”按钮则重新出现。 ### **操控机器人** 下面是真正有趣的部分:添加前进、后退、左右转动及停止行为。不要忘记“停止”,否则你手中的机器人会失去控制! NxtDrive组件提供了五个块,用来驱动机器人的电机: * MoveForwardIndefinitely块:驱动两个电机前进; * MoveBackwardIndefinitely块:驱动两个电机后退; * TurnCounterClockwiseIndefinitely块:驱动机器人左转:让右侧电机向前而左侧电机后退; * TurnClockwiseIndefinitely块:驱动机器人右转:让左侧电机向前而右侧电机后退; * Stop将停止电机。 每个移动及转向块都有一个Power参数,需要与数字块配合使用,来设定机器人电机的输出功率,取值范围可以从 0到100。但如果设置的功率太小,电机会发出吱吱声而不运转。在本例中建议使用90(百分比)。表12-6中列出了所需的块。 **表12-6 用于控制机器人的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | ForwardButton.Clic | ForwardButton|点击ForwardButton时触 | | NxtDrive1.MoveForwardIndefinitely | NxtDrive1 |驱动机器人前进 | | 数字90 | Math | 功率值 | | BackwardButton.Click | BackwardButton |点击BackwardButton时触发 | | NxtDrive1.MoveBackwardIndefinitely | NxtDrive1 |驱动机器人后退 | | 数字90 | Math | 功率值 | | LeftButton.Click | LeftButton | 点击LeftButton时触发 | | NxtDrive1.TurnCounterClockwiseIndefinitely | NxtDrive1 |驱动机器人逆时针转动 | | 数字90 | Math | 功率值 | | RightButton.Click | RightButton | 点击RightButton时触发 | | NxtDrive1.TurnClockwiseIndefinitely | NxtDrive1 |驱动机器人顺时针转动 | | 数字90 | Math | 功率值 | | StopButton.Click | StopButton | 点击StopButton时触发 | | NxtDrive1.Stop | NxtDrive1 | 让机器人停止 | #### **块的作用** 如图12-6所示,点击ForwardButton按钮时触发ForwardButton.Clicked事件,此时调用NxtDrive1.MoveForwardIndefinitely块,让机器人以90%的功率前进,其余按钮的事件处理程序与此类似,并以相同的功率驱动机器人后退及左右转动。点击StopButton时触发StopButton.Clicked事件,调用NxtDrive1.Stop块让机器人停止运动。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd6ae8611.png) **图 12-6 操控机器人** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd4f79e4c.png) 测试:按照此前的“测试”说明,先连接NXT机器人。不要将机器人放在桌子上,以免跌落,然后测试以下行为: > > 1\. 点击前进按钮,机器人应该向前移动; > > 2\. 点击后退按钮,机器人应该向后移动; > > 3\. 点击左转按钮,机器人应逆时针转动; > > 4\. 点击右转按钮,机器人应顺时针转动; > > 5\. 点击停止按钮,机器人应停止。 > > 如果机器人不动并发出吱吱声,可能需要加大电机的功率,可以用最大功率100。 ### **用超声波传感器探测障碍物** 使用超声波传感器的机器人可以侦测到30厘米范围内的障碍物,遇到障碍物时机器人会像罪犯一样停下来,如图12-7所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd75ab082.png) **图 12-7 为NXT机器人设置障碍** NxtUltrasonicSensor组件用于侦测障碍物,两个属性BottomOfRange和TopOfRange用来定义侦测范围(以厘米为单位)。默认设定BottomOfRange为30厘米,TopOfRange为90厘米。 NxtUltrasonicSensor组件具有三个事件BelowRange、WithinRange及boveRange,当侦测到障碍物在BottomOfRange(下限)距离以内时,会触发BelowRange事件;当障碍物的距离在BottomOfRange与TopOfRange (上下限)之间时,会触发WithinRange事件;当障碍物的距离超过TopOfRange(上限)时,将触发AboveRange事件。 这里使用NxtUltrasonicSensor1.BelowRange事件块,用来侦测30厘米以内的障碍物,如果你想尝试侦测不同距离的障碍物,可以调整BottomOfRange属性。当BelowRange时间发生时,使用NxtDrive1.Stop块让机器人停下来。表12-7中列出了所需的块 **表12-7 使用NxtUltrasonicSensor需要的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | NxtUltrasonicSensor1.BelowRange | NxtUltrasonicSensor1 |超声波传感器在30厘米内遇到障碍物时触发 | | NxtDrive1.Stop | NxtDrive1 | 让机器人停下来 | #### **块的功能** 当机器人的超声波传感器侦测到30厘米以内的障碍物时,NxtUltrasonicSensor1.BelowRange事件被触发,如图12-8所示,此时NxtDrive1.Stop块让机器人停下来。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd7bba9bd.png) **图 12-8 侦测障碍物** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd4f79e4c.png) 测试:按照此前的“测试”说明,先连接NXT机器人。引导机器人朝着障碍物(如猫)的方向前进,机器人将在距离猫30厘米时停下来。如果机器人没停下来,可能是猫已经远离了机器人,它们之间的距离一直大于30厘米。可以换一个静止的障碍物来进行测试。 ## **改进** 应用运行起来,想必你已经花了大量时间来操控这个机器人,不过还是想继续其他的尝试: * 调节驱动电机的输出功率: * 可以修改插入到前进(MoveForwardIndefinitely)、后退(MoveBackwardIndefinitely)、左转(TurnCounterclockwiseIndefinitely)及右转(TurnClockwiseIndefinitely) 块中的数字块的值。 * 当侦测到障碍物时,使用NxtColorSensor让红灯闪烁: * 可以使用NxtColorSensor组件及其GenerateColor属性; * 需要将DetectColor属性设置为false(或在组件设计器取消勾选该属性),因为颜色传感器无法同时检测和产生颜色。 * 使用Android的方向传感器OrientationSensor来控制机器人。 * 使用乐高的构造元件建立手机与机器人之间的物理连接,创建应用实现机器人的自主性。 ## **小结** 以下是本章涵盖的内容: * ListPicker组件:让用户可以从已配对的机器人列表中进行选择; * BluetoothClient组件:使Android设备与机器人建立连接; * Notifier组件:用来显示错误消息; * Visible属性:用于隐藏或显示用户界面中的组件; * NxtDrive组件:可以控制机器人的移动、转向及停止; * NxtUltrasonicSensor组件:用于侦测障碍物。 ### **外部链接** [LEGO MINDSTORMS EV3控制程序](http://blog.sina.com.cn/s/blog_6210c9730101jqui.html) [Android应用控制LEGO EV3(Video)](http://blog.sina.com.cn/s/blog_62218b990101k4yr.html)
';

第 11 章 广播中心

最后更新于:2022-04-01 02:53:45

FrontlineSMS 是一款工具软件,用于联络那些无法访问互联网但可以用手机通信的人,通常用于互联网尚未普及地区的选举监督、天气预报广播等。软件作者Ken Banks借助于移动通信技术为人们提供帮助,他的贡献大概无人能及。 FrontlineSMS运行在连接了手机的电脑上。电脑和手机共同构成一个短信中转站,为群内人员提供文本通信服务。无法上网的人可以发送一个特殊代码来加入群,随后他们会收到来自中转站的各种广播消息。这个中转站我们称之为“广播中心”,对于那些没有网络的地方,广播中心成为与外界联系的重要手段。 使用App Inventor可以创建自己的短信处理应用。有趣的是,应用需要运行在一部android设备上,但应用的用户却不必使用Android手机,他们可以用任何手机,智能的或非智能的,与应用之间进行短信的交流。应用虽然具有图形化的用户界面(GUI),但GUI仅供应用的管理者使用,用来监控应用中的各种活动。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fcddb45ff.png) 本章将创建一个与FrontlineSMS功能类似的广播中心,不过是运行在Android手机上。一台具有中转枢纽作用的移动设备,意味着管理者可以在移动中保持交流,这一点在某些场合下尤其重要,如选举监督和医疗争议谈判。 假想有一个“快闪舞蹈团”(FlashMob Dance Team,缩写为FMDT),他们可以召之即来,随时随地表演舞蹈,然后瞬间解散,消失得无影无踪,他们用你创建的广播中心来组织表演活动。人们只要向中心发送短信“joinFMDT”(参加快闪舞蹈团),即可完成入团注册,每个注册成功的人都可以向舞蹈团中的其他人广播消息。 广播中心用下面的方式处理收到的短信: 1\. 如果发信人不在广播中心的成员名单中,则回复短信邀请他加入,并告知他申请代码; 2\. 如果收到“joinFMDT”,则接收发信人为广播中心成员;【如果组员发送“joinFMDT”呢?】 3\. 如果发信人已经是广播中心的成员,则转发该消息给全体广播中心成员。 我们来分步实现这些功能模块。首先,用自动回复来邀请人们加入广播中心。整个应用完成之后,对于创建这类“以短信为用户界面的应用”,你将有透彻的了解。 ## **学习要点** 本章包括下列App Inventor概念,其中有些你可能已经熟悉了: * Texting组件:发送短信及处理收到的短信; * 列表变量:在本例中用来记录电话号码清单; * foreach块:对列表中的数据进行逐项重复操作。在本例子中,使用foreach块向电话号码列表中的所有手机广播消息; * TinyDB组件:实现数据的永久存储,以保证当应用关闭并再次打开时,电话号码列表不丢失。 ## **准备开始** 你需要一部可以接收和发送短信的手机来测试程序,因为App Inventor自带的模拟器没有这个功能。您还需要招呼一些朋友给你发送短信,来充分地测试应用。 连接到App Inventor网站,创建新项目“BroadcastHub”,设置Screen1.Title属性为“广播中心”,并连接测试手机。 ## **设计组件** 广播中心有利于手机之间的通信:这些手机不需要安装应用,甚至不必是智能手机。因此在本例中不必为用户提供操作界面,只需为群管理员提供操作界面。 管理员的操作界面包括两个简单的部分,一是显示当前的“广播列表”,即已注册成员的电话号码清单,二是记录所有收到并被广播出去的短信。 为了创建这个界面,要添加表11-1中列出的组件。 **表11-1 广播中心操作界面中的组件** | 组件类型 | 面板中分组 | 命名 | 作用 | | --- | --- | --- | --- | | Label | User Interface | Label1 | 电话号码清单的标题 | | Label | User Interface | BroadcaseListLabel |显示所有已注册的电话号码 | | Label | User Interface | Label2 | 日志信息的标题 | | Label | User Interface | LogLabel |显示收到及广播短信的记录 | | Texting | Social | Texting1 | 处理短信 | | TinyDB | Storage | TinyDB1 | 保存已注册的手机号码清单 | 添加组件之后,还要设置以下属性: 1\. 设置每个Label的Width属性为“Fill parent”,让组件在水平方向上充满手机; 2\. 设置标题Label的FontSize属性(Label1和Label2)为18,并勾选FontBold框; 3\. BroadcastListLabel和LogLabel的Height设置为200像素,用于显示多行; 4\. 设置BroadcastListLabel的Text属性为“广播列表...”; 5\. LogLabel的Text属性设置为空。 图11-1显示了应用在组件设计器中的布局。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fcde4ac19.png) **图 11-1 广播中心组件设计** ## **为组件添加行为** 在这个应用中,促使程序运行的事件是其他手机发来的短信,而不是用户在界面上的输入或点击,因此应用的任务是处理这些短信,并将发信人手机号码保存到列表中,具体操作如下: * 如果短信发送者不在广播列表中,则回复一个邀请参加的短信; * 如果收到短信“joinFMDT”,则将发送者注册为广播列表的一员; * 如果短信发送者已经在广播列表中,则将该短信广播到列表中的所有手机。 现在开始创建第一个行为:收到短信时,回复发送者,邀请他注册,方法是向你发送短信“joinFMDT”。表11-2中列出了需要的块。 **表11-2 邀请人们通过发短信来加入群组,需要下面的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | Texting.MessageReceived | Texting1 |当手机收到短信时,触发该事件 | | set Texting1.PhoneNumber to | Texting1 |设置短信接收者的电话号码 | | 参数number | Variables |MessageReceived事件的参数:发送者手机号 | | Set Texting1.Message | Texting1 | 设置要发送的邀请短信 | | “想加入快闪舞蹈团,请发送‘joinFMDT’到此号码。” | Text|邀请短信的内容 | Texting1.SendMessage | Texting1 | 发送短信 | #### **块的作用** 根据在第4章“开车不发短信”中的经验,你应该很熟悉这些块。当手机收到短信时会触发Texting1.MessageReceived事件。如图11-2,在事件处理程序中设置Texting1组件的PhoneNumber及Message属性,然后发送短信。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fce3b8e15.png) **图 11-2 收到短信后回复邀请短信** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fce926712.png) 测试:需要用第二部手机来测试这一功能;你不能给自己发短信,否则会永远循环下去!如果没有其他手机,可以注册Google Voice或类似的服务,从这些服务中给自己的手机发短信。用第二部手机发送“你好”到测试手机,则第二部手机会收到一个邀请加入“舞蹈团”的短信。 ### **将某人加入广播列表** 现在创建第二个行为:收到短信“joinFMDT”后,将发信人添加到广播列表中。首先定义列表变量BroadcastList来保存注册的电话号码。从Variables中拖出一个“initialize global name to”块,将name改为“BroadcastList”,并用make a list块初始化列表,此时列表为空。如图11-3(稍后将实现向列表中添加数据项的功能)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fce9872d8.png) **图 11-3 变量BroadcastList用于存储注册的电话号码【也可用create empty list块】** 下面修改Texting1.MessageReceived事件处理程序,如果收到短信“joinFMDT”,则将发信人手机号码添加到BroadcastList中。判断短信内容需要使用Ifelse块(在第十章“出题”应用中使用过),将新号码添加到列表中需要使用add item to list块。整个设置所需的块见表11-3。在电话号码添加完之后,用BroadcastListLabel来显示新列表。 **表11-3 检查来信内容,并将发信人添加到广播列表中,需要如下块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | initialize global BroadcastList to | Variables |定义广播列表变量 | | ifelse | Control | 根据收到短信的内容决定做什么事 | | = | Math | 判断短信内容是否等于“joinFMDT” | | get messageText | Variables | 将来信内容插入“=”块(左边 | | “joinFMDT” | Text | 将固定文本插入“=”块(右边) | | add items to list | Lists | 向广播列表中添加发信人电话号 | | get number | Variables| 将发信人手机号码插入“add items to list” | | set BroadcaseListLabel.Text to | BroadcaseListLabel |显示新列表 | | get global BroadcastList | Variables | 将其插入set BroadcaseListLabel.Text to块 | | set Texting1.Message to | Texting1 |设置短信内容,准备用Texting1回复发信人 | | “恭喜你成功加入…” | Text | 祝贺发信人加入群组成功。 | #### **块的作用** 如图11-4所示,对刚收到的短信进行回复,第一行的块将发信人手机号码设置为接收人手机号码,即设置Texting1.PhoneNumber为number。然后判断messageText是否为特殊代码“joinFMDT”:如果是,则将发送者手机号添加到BroadcastList并发短信祝贺;如果不是,则回复邀请短信。在Ifelse块之后,回复短信被发出(最后一行)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fcf3cf34d.png) **图 11-4 如果收到短信“joinFMDT”,则将发信人手机号添加到BroadcastList** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fce926712.png) 测试:用第二部手机发送短信“joinFMDT”到测试手机,在测试手机收到短信的同时,第二部手机的号码出现在“已注册的电话号码”下面,第二部手机会收到祝贺短信。尝试发一个其他内容的短信,检查邀请短信是否能正常发送。 ### **广播消息** 下面来添加广播行为:当广播列表BroadcastList中的成员向广播中心发来短信时,将此信息转发给列表中的所有手机。这一功能稍显复杂,需要更多的控制块:增加一个Ifelse块和一个foreach块。新增的Ifelse块用于检查发送短信的手机号是否在广播列表中,而foreach块用于向列表中的所有手机广播这条短信。另外还要将之前的Ifelse块移动到新Ifelse块的“else”部分。表11-4列出了需要新增的块。 **表11-4 向列表中的成员广播某个成员发来的短信需要新增的块** | 块的类型 | 所在抽屉| 作用 | | --- | --- | --- | | ifelse | Control |根据发信人是否已在广播列表中来决定做不同的事| | is in list?|Lists | 检查某数据是否在列表中 | | get global BroadcastList | Variables | 将其插入is in list?的list插槽中 | | get number | Variables | 将其插入is in list?的thing插槽 | | set Texting1.Message to | Texting1 |设置将被广播出去的短信内容(列表成员的来信) | | get messageText | Variables | 即将被广播出去的列表成员来 | | foreach | Control | 向列表中的所有成员发送同一条短信 | | get global BroadcastList | Variables |将其插入foreach的list插槽 | | set Texting1.PhoneNumber to| Texting1 |设置接收短信的手机号码 | | get item | Variables |BroadcaseList中当前正在操作的项/变量:保存的是手机号 | #### **块的作用** 这里使用了嵌套的ifelse块,使得程序更加复杂,如图11-5所示。嵌套的ifelse块指的是在一个ifelse块的“then”或“else”插槽中嵌入了另一个ifelse块。在本例中,外层的ifelse负责检查发信人的手机号是否已在广播列表中。如果在,则将该短信转发给列表中的所有人;如果不在,则执行内层ifelse判断:短信内容messageText是否为“joinFMDT”,并依据判断结果,执行不同的分支操作。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fcf9970d3.png) **图 11-5 检查发信人是否已在广播列表中,如果是,则广播此短信** 从理论上,if块和ifelse块可以做任意层级的嵌套,来实现更加复杂的行为(更多关于条件语句块的内容请参见第18章)。 在外层ifelse块的then分支中,使用foreach块来广播短信。foreach遍历BroadcastList列表中的每一项,并把短信发送给列表中的每个电话号码。在foreach执行循环时,BroadcastList中的每个电话号码依次被保存在item中(item是一个变量,代表了foreach当前正在处理的项)。在foreach块内,设置Texting.PhoneNumber的值为当前项item,并向其发送短信。有关foreach的更多信息,请参见第20章。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fce926712.png) 测试:首先要有两部不同的手机通过发送“joinFMDT”到测试手机,实现成功注册。然后,从一部手机向广播中心发一条短信,这时两部手机都应该收到这条短信(包括发送短信的那一个)。 ### **整理列表的显示** 广播短信的功能已经实现,但管理员的界面尚需改进。首先,电话号码列表的显得很乱:用Label显示列表时,列表项之间用空格分隔,并且尽可能占满一行,像下面这样: > (+861303318989 +861581235590 +8618902018909 +8613301103355 +8613801237890) 为了改善这种局面,使用表11-5列出的块创建一个过程displayBroadcastList,来实现每行只显示一个号码。请务必在add items to list块的下面调用该过程,以便显示更新后的列表。 **表11-5 改进电话号码列表显示所需的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | to procedure(“displayBroadcastList”) | Procedures|创建过程displayBroadcastList| | set BroadcaseListLabel.Text to | BroadcaseListLabel |用来显示列表 | | “” | Text | 空文本 | | foreach |Control | 对电话号码列表进行遍历 | | pnumber | foreach内置 |变量pnumber为遍历过程中正在访问的 | | get global BroadcaseList | Variables | 插入foreach块的in list插槽 | | set BroadcaseListLabel.Text to | BroadcaseListLabel |显示电话号码列表 | | join | Text | 将多个文本片段连接为一个文本对象 | | BroadcaseListLabel.Text | BroadcaseListLabel |每次循环都以既有label内容为基础追加新项 | | “\n” | Text | 换行,以便下一个号码显示在下一行 | | get pnumber| foreach内置 |遍历时列表中正在访问的项(手机号码) | #### **块的作用** 过程displayBroadcastList中的foreach块逐行地将每个手机号码添加到label的末尾,如图11-6所示,用换行符(\ n)来分隔每个号码,使得每个号码各占一行。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fcff8a202.png) **图 11-6 逐行显示手机号码** 不过displayBroadcastList过程不会主动做任何事,除非调用它。在Texting1.MessageReceived事件处理程序中,紧接着add item to list块调用它。过程的调用取代了列表BroadcastList在 BroadcastListLabel.Text中的默认显示。块call displayBroadcastList归属在Procedures抽屉中。 图11-7显示了Texting1.MessageReceived事件处理程序中相关的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd0006433.png) **图 11-7 调用displayBroadcastList过程** 关于用foreach来显示列表的详细信息请参见第20章,关于创建和调用过程的详细信息请参见第21章。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fce926712.png) 测试:重新启动应用来清除列表,然后用至少两个不同的手机进行注册(再次)。手机号码是否逐行显示了? ### **录广播过的短信** 在收到短信并向其他手机发出广播之后,程序应该记录此类事件,以便管理员可以对活动进行监督。在组件设计器中,已经添加的LogLabel组件就是用于这一目的。下面编写程序,每当收到新的短信时,改变LogLabel的显示。 要创建像这样的一段文本:“来自+8613901231234的短信已经广播。”字符“+8613901231234”不是固定数据,而是MessageReceived事件自带的参数值。因此,要创建的文本包括三个部分:①“来自”;②手机号码,为参数number;③“的短信已经广播”。正如在前几章中所做的一样,用join将三个部分连接起来,表11-6列出了需要的块。 **表11-6 构建广播日志所需要的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | set LogLabel.Text to | LogLabel | 在此显示日志 | | join | Text | 由多个文本片段创建成一个文本对象 | | “来自” | Text | 每条日志信息的第①部 | | get number | Texting1.MessageReceived事件内置参数 |日志信息的第②部分:短信发送者的手机号码 | | “的短信已经广播。\n” | Text| 日志信息的第③部分 | | LogLabel.Text| LogLabel | 在原有日志前插入一条新的日志 | #### **块的作用** 在收到短信后,向BroadcastList列表中的所有号码广播此短信,再修改LogLabel,记录刚才的广播操作,如图11-8所示。需要注意的是,我们将消息添加到列表的开始,而不是结尾,因此最后发出的消息将显示在最顶端。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd0190b15.png) **图 11-8 向广播日志中添加一条新消息** join块创建了一条新记录:来自+8613901231234的短信已经广播。 每次短信广播之后,这条记录将被添加到LogLabel.Text的第一行,使最新的记录一直出现在顶部。join块中各个文本片段的顺序决定了日志中记录的顺序。在本例子中,新消息被编排在前三个插槽中,而LogLabel.Text,已经保存的现有记录,将插入最后一个插槽。 “的短信已经广播。\n”中的“\n”称为换行符,它让每条记录单独占一行,像这样: > 来自+8613030123668的短信已经广播。 > > 来自+8613901231234的短信已经广播。 关于使用foreach来显示列表的详细信息,请参见第20章。 ### **将BroadcastList保存在数据库中** 现在应用算是大功告成了,但通过前几章的学习,你可能猜到了一个问题:如果管理员将应用关闭再重新启动时,广播列表中的数据将会丢失,每个人都得重新注册。为了解决这个问题,要使用TinyDB组件实现BroadcastList列表在数据库中的存储和检索。 这里将使用与“出题”应用(第10章)中相类似的方案: > 每次添加新项时,将列表保存到数据库中; > > 应用启动时,从数据库中加载列表,并保存到一个变量中。 用表11-7中所列的块,将列表存储到数据库中。TinyDB组件中的tag作为数据的标识,将保存在数据库中的不同数据区分开来。在本例中,你可以将数据标记为“broadcastList”。在Texting1.MessageReceived中,将这些块添加到add items to list块之下。 **表11-7 用TinyDB来存储列表所需的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | TinyDB1.StoreValue| TinyDB1 | 将数据保存到数据库中 | | “broadcastList” | Text| 将其插入StoreValue的tag插槽中 | | get global BroadcastList | Variables|将其插入StoreValue的value插槽中 | #### **块的功能** 当应用收到短信“joinFMDT”,并将新成员的手机号码添加到列表时,调用TinyDB1.StoreValue将BroadcastList保存到数据库中。tag(“broadcastList”)的使用是为了便于之后对数据的检索。如图11-9,被StoreValue调用的值(valueToStore)是变量BroadcastList。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd0251977.png) **图 11-9 调用TinyDB来存储BroadcastList列表** ### **从数据库加载广播列表(BroadcastList)** 每次应用启动时都要加载广播列表,按照表11-8中列出的块来实现这一功能。应用的启动将触发Screen1.Initialize事件,因此将在该事件的处理程序中实现加载。使用存储时的tag(“broadcastList”)来调用TinyDB.GetValue。就像前几章一样,我们需要检查是否的确有数据返回,这里将检查返回值是否为列表,因为如果列表中没有数据,那么它也就不是列表。 #### **块的作用** 应用启动将触发Screen1.Initialize事件。如图11-10所示,使用TinyDB1.GetValue块向数据库请求数据,返回的数据临时保存在已定义的变量valueFromDB中。 **表11-8 应用启动时加载广播列表所需要的块** | 块的类型 | 所在抽屉 | 作用| | --- | --- | --- | | initialize global valueFromDB to | Variables |用于保存并检查数据库返回值的临时变量 | | “” | Text | 设valueFromDB初始值为空 | | Screen1.Initialize | Screen1 | 应用启动时触发该事件 | | set global valueFromDB to | Variables |将数据库返回值暂时存放在其中 | | TinyDB1.GetValue | TinyDB1 | 向数据库请求数据 | | “broadcastList” | Text | 将其插入GetValue的tag插槽 | | if | Control | 判断数据库中是否有数据 | | is a list | Lists|如果数据库返回值是一个列表,则返回值不为空 | | get global valueFromDB | Variables | 将其插入is a list? | | set global BroadcaseList to| Variables | 将变量值设置为数据库的返回值| | get global valueFromDB | Variables |数据库返回值不为空时,将返回值写入广播列表 | | call displayBroadcastList | Procedures |加载数据成功后,显示数据| ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd02ce42f.png) **图 11-10 从数据库中加载广播列表BroadcastList** 事件处理程序中的if块是必需的,因为在首次启动应用时,数据库将返回空文本(“”),这时还没有生成广播列表。通过判断valueFromDB是否为列表,可以确定是否真的有数据返回。如果没有,则跳过那些保存返回数据以及显示数据的块。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fce926712.png) 测试:对于涉及数据库操作的应用,不适合做实时测试,因为每次连接测试设备时,都会清空数据库。为了测试数据库以及Screen.Initialize事件处理程序,需要将应用打包并下载到手机【点击“buildApp(provide QR code for .apk)”,下载并安装应用】。在手机上启动应用,用另外两部手机发送“joinFMDT”加入群组,再退出应用。当重启应用时,如果那些电话号码还在,说明数据库部分工作正常。 ## **完整的广播中心应用** ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fd03b9343.png) **图 11-11 完整的广播中心应用中的块** ## **改进** 在庆祝一个如此复杂的应用完工的时候,你也许想要做进一步的改进。例如: * 在广播短信环节,广播中心向所有人发出短信,也包括发送这条短信的列表成员。修改此功能,将短信群发给除了发送者之外的所有成员; * 允许列表成员退出群组,用手机发送短信“quitabc”给广播中心,请求从列表中删除自己。需要使用remove from list块; * 管理员可以在操作界面上添加或删除广播列表中的成员(手机号); * 管理员可以指定某些不允许加入列表的手机号; * 细化应用的功能,让任何人都可以加入到列表并接收广播,但只有管理员可以广播消息; * 进一步细化应用,让任何人都可以加入到接收广播,但只有一个固定列表中的电话号码可以向全体成员广播消息(这正式赫尔辛基事件 的成功之处); * 应用中的广播列表可以永久保存,但日志却不能。每次关闭该应用再重新打开时,日志也从头开始。改进一下,让日志也能永久保存。 ## **小结** 以下是本章涵盖的内容: * 应用不仅可以响应用户发起的事件,也可以响应非用户发起的事件,像收到短信这样的事件。这意味着应用的用户也可以是其他手机; * ifelse与foreach块的嵌套使用可以构造出复杂的行为。有关条件语句if和循环语句foreach的详细信息,请参见第18章及第20章; * join块可以用来创建一个多重内容组成的文本对象; * TinyDB可以用于数据库操作:存储及检索数据。通用方案是,在数据发生变化时,调用StoreValue更新数据库;在应用启动时,调用GetValue从数据库中读取数据。
';

第 10 章 出题及答题

最后更新于:2022-04-01 02:53:43

第8章的“总统测验”可以被定制成各种测验,但这种定制只对App Inventor程序员有用。只有程序员可以修改问题和答案,而对于父母、老师或其他用户来说,他们无法创建一个测验或变换问题(除非他们也学App Inventor!)。 本章将构建一个“出题”应用,“老师”可以在输入表单中创建试题。试题和答案将被存储在Web数据库中,以便“学生”可以单独访问“答题”应用并参加考试。通过创建这两个应用,你会在概念上产生更大的飞跃,并学习如何创建一个应用,让用户自行生成数据,并实现用户之间跨应用的数据共享。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc623c4bd.png) “出题”与“答题”这两个应用协同工作,让“老师”可以为“学生”出题。父母可以在长途旅行中做一些旅行花絮类的应用,以增加孩子们的乐趣;小学教师可以创建“数学突击”一类的小测验;而大学生们可以创建一系列的测验,帮助他们的学习小组来准备期末考试。本章建立在第8章“总统测验”的基础上,如果你还没学过,在继续本章之前,请先学习第8章。 本章将设计两个应用:针对“老师”的“出题”应用(见图10-1)以及针对“学生”的“答题”应用。在“出题”应用中: * 用户在输入表单中输入问题及答案; * 显示输入的一对问答; * 将问题及答案存储在数据库中。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc62b60da.png) **图 10-1 出题应用** “答题”应用的功能与之前的“总统测验”类似。事实上,是以“总统测验”为起点创建“答题”应用,不同的是,这里的问题是使用“出题”应用输入并保存在数据库中的。 ## **学习要点** “总统测验”是一个使用静态数据的应用范例:不管用户做多少次测验,问题都是一样的,因为问题被写在程序中(称为“硬编码”)。新闻应用、博客以及像Facebook和Twitter这类的社交网络应用采用的是动态数据,这意味着数据随时在改变。通常这种动态信息由用户生成,这类应用允许用户输入、修改并共享信息。在“出题”与“答题”应用中,将学习创建一个应用,来处理用户生成的数据。 在第9章“木琴”应用中,我们首次引入动态列表概念:用户输入的音符被记录在列表中。由用户生成数据的应用更为复杂,而且使用的块也更抽象,因为没有预设的静态数据可供参照。尽管可以定义列表变量,但不能设置具体的项。在编写程序的同时,需要设想最终用户输入的数据被添加到列表中。 本章涵盖了App Inventor中的如下内容: * 输入表单:允许用户输入信息; * 显示来自多个列表的数据项; * 永久保存数据:“出题”应用将问题和答案保存到网络数据库中,“答题”应用将从同一个数据库中加载它们; * 数据共享:使用TinyWebDB组件(而不是之前的TinyDB)将数据存储在Web数据库中。 ## **准备开始** 登陆App Inventor网站,创建新项目“MakeQuiz”,屏幕标题设为“出题”,并连接到测试手机或模拟器。 ## **设计组件** 使用组件设计器来创建用户界面,如图10-2所示(图的后面有更详细的说明),组件清单列于表10-1中。从Palette中拖出组件,将名称改为表中的命名。注意,标题Label的名称(Label1 – Label3)不必改,就用它们的默认值(因为在编辑器中不会使用这些名称)。 **表10-1 “出题”应用中的所有组件** | 组件类型 | 面板中分组 | 命名 | 作用 | | --- | --- | --- | --- | | TableArrangement | Layout | TableArrangement1 | 格式化表单,包括问题及答案| | Label | User Interface | Label1 | 提示“问题:” | | TextBox | User Interface | QuestionText | 用户在此输入问题 | | Label | User Interface | Label2 | 提示“答案:” | | TextBox | User Interface | AnswerText | 用户在此输入答案 | | Button | User Interface | SubmitButton | 用户点击提交问题-答案对儿 | | Label | User Interface | Label3 | 显示“测验的问题及答案。” | | Label | User Interface | QuestionAnswersLabel |显示之前输入的成对的问题答案| | TinyWebDB | Storage | TinyWebDB1 | 用数据库保存并提取数据 | ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc6836cf4.png) **图 10-2 组件设计器中的“出题”应用** 按以下方式设置组件属性: 1\. 设置Text属性:Label1为“问题:”,Label2为“答案:”,Label3为“试题及答案”; 2\. 设置Label3的字号为18,并勾选FontBold属性; 3\. 设置QuestionText的Hint属性为“输入问题”,AnswerText的Hint属性为“输入回答”; 4\. 设置SubmitButton的Text属性为“提交”; 5\. 设置QuestionsAnswersLabel的Text属性为“试题及答案”; 6\. 将QuestionText、AnswerText以及与它们相关的Label移入TableArrangement1。 ## **为组件添加行为** 在“总统测验”中,首先定义了两个全局列表变量QuestionList和AnswerList,本章中无需为这两个变量提供预设的问题和答案,如图10-3所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc68ac8fa.png) **图 10-3 列表变量初始化** 需要注意,与“总统测验“不同的是,这两个列表没有定义列表项,因为“出题”及“答题”应用中,所有数据都将由用户创建(即动态的、用户生成的数据)。 ### **记录用户的输入** 首先来处理用户的输入行为。具体来说,当用户输入问题和答案并点击提交时,程序要向列表中添加数据项来更新QuestionList和AnswerList,如下图所示: ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc6925888.png) **图 10-4 向列表中添加新项** #### **块的作用** 向列表中添加项,意味着向列表的末尾追加新项。如图10-4,程序从QuestionText和AnswerText文本框中获取用户输入的内容,并分别被追加到相应的列表中。 向列表中添加的项更新了列表变量QuestionList和AnswerList,但用户看不到任何变化。第三行的块用来显示这个变化:用冒号将两个列表的内容连接起来。默认情况下,App Inventor用小括号来包围列表内容,列表项之间用空格间隔,像这样:(item1 item2 item3)。当然,这不是显示列表的理想方式,只是暂时用来测试程序的行为。稍后我们将用更高级的方式来显示列表,即,每对问题答案各占一行。 ### **清空问题及答案** 回忆一下在“总统测验”中,当移动到下一题时,要清空上一题的回答结果。在本应用中,当用户提交了一对问题-答案后,同样要清空QuestionText及AnswerText文本框,以便准备下一题的输入,如下图所示: ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc6991297.png) **图 10-5 提交问题-答案之后清空文本框** #### **块的作用** 用户提交的问题-答案,将分别被添加到各自的列表中,并显示出来,这时QuestionText和AnswerText中的文本被清空,如图10-5所示。请注意,可以复制一个有内容的文本块(如上图中的“:”块),通过删除块中的文本,来获得一个空的文本块。 ### **用多行文本显示问题-回答** 现在是以App Inventor的默认格式来显示问题及答案。假如有一个有关州首府的测验,已经输入了两对问题-答案,则显示成: (加州首府在哪? 纽约州首府在哪?):(萨克拉门托 奥尔巴尼)。 可以想像,如果测验中的问题很多,结果会显得非常混乱。理想的显示方式,应该是每行只显示一对问题-答案: > 加州首府在哪? 萨克拉门托 > > 纽约州首府在哪? 奥尔巴尼 第20章讲述了单个列表中项的逐行显示技术,在继续学习之前,可以去阅读一下。 这里的任务稍显复杂,因为涉及到两个列表。为了应对这种复杂性,需要创建过程displayQAs,并从SubmitButton.Click事件处理程序中调用该过程。 逐行显示问题-答案,需要做到以下几点: * 使用foreach块遍历QuestionList中的每个问题; * 使用变量answerIndex,在遍历问题的同时,获取与问题对应的答案; * 使用join块连接每对问题-答案,并用换行符(\n)来分开每对问题-答案,如下图所示: #### **块的作用** 过程displayQAs封装了所有用于显示数据的块,如图10-6所示,在需要显示列表时,可直接调用displayQAs,而不必再重复使用过程内部的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc6a31ce3.png) **图 10-6 创建displayQAs过程** 由于foreach块只能遍历一个列表,而本应用中有两个列表,因此要求在遍历问题列表的同时,为每个问题选择对应的答案。这需要定义一个索引变量,就像第8章“总统测试”中的currentQuestionIndex一样,这里定义了answerIndex,当foreach遍历QuestionList时,用来跟踪对应的答案在列表AnswerList中的位置。 在foreach开始遍历之前,设answerIndex的值为1;在foreach遍历过程中,answerIndex用来从AnswerList中选择当前问题的答案,然后递增1。在foreach的每次迭代中,当前的问题-答案被添加到QuestionsAnswersLabel的最后一行,问题与答案之间以冒号分隔。 ### **调用新建的过程** 已经创建了显示问题-答案的过程displayQAs,但在调用它之前,它起不到任何作用。修改SubmitButton.Click事件处理程序,用displayQAs替代对QuestionsAnswersLabel.Text的简单设置,来显示所有的问题-答案。更新后的块如图10-7所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc6aab152.png) **图 10-7 在SubmitButton.Click中调用displayQAs过程** ### **将数据永久保存到Web数据库** 到目前为止,用户输入的问题-答案只是保存在列表中,如果此时用户退出应用,会怎么样呢?正如“开车不发短信”(第4章)或“Android,我的车在哪儿?”(第7章)中所学到的,如果数据不能存储到数据库中,那么当用户退出并重新打开应用时,数据将丢失。只有永久存储数据,才能让出题者在每次打开应用时,都能看到最新版本的数据,并对数据内容进行编辑。同时,永久保存数据也是必要的,因为在“答题”应用中也需要访问这些数据。 之前我们学习过用TinyDB组件在数据库中存储并检索数据,本章将使用TinyWebDB组件。两者的区别是:TinyDB将数据存储在手机上,而TinyWebDB将数据存储在Web数据库中。 本应用在设计上之所以选择在线数据库,而非手机数据库,关键在于存这些数据要供两个应用访问,如果出题者把问题和答案都存储在个人的手机上,那么答题者将无法获取数据并参加考试!而TinyWebDB将数据保存在互联网上,答题者可以使用不同于出题者的设备来访问试题及答案。(在线数据存储通常被称作云。) 下面是永久保存列表数据(如问题及答案)的通用方案: * 每当向列表中添加新项时,将数据保存到数据库; * 应用启动时,从数据库中加载数据,并保存在列表变量中。 首先考虑数据的保存:每次用户输入新的问题-答案时,将QuestionList和AnswerList保存到数据库中。 #### **块的功能** TinyWebDB1.StoreValue块将数据存储在Web数据库中。StoreValue有两个参数:tag用做数据的标识,value是要保存的实际数据。如图10-8所示,QuestionList在存储时以“questions”为tag(标签),而AnswerList则用“answers”为tag(标签)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc6b31fef.png) **图 10-8 将问题和答案保存到数据库中** 建议在个人应用中使用更有特点的tag(如DavesQuestions和DavesAnswers)来代替questions和answers,这非常重要,因为你正在使用App Inventor的默认Web数据库,所以你的数据(列表questions和answers)可能会被别人的数据覆盖,也包括那些正在学习本教程的人。 这里要提醒各位,App Inventor默认的Web服务会在各个程序员和各种应用之间共享,因此它仅适用于测试。当你打算正式发布一款应用时,需要建立自己私有的数据库服务。幸运的是,做到这一点很简单,而且不需要编程(见第22章)。 ### **从数据库加载数据** 本应用需要永久保存数据,一方面因为出题者可以随时关闭应用,并随时启动应用,以便对之前输入问题和答案进行补充、修改或删除。这就需要在每次启动应用时,从数据库中加载那些已存储的数据。(另一方面,答题者可以访问数据库总的问题及答案,稍后会涉及到。) 正如我们之前所学,在应用启动时需要进行的操作,要通过Screen.Initialize事件处理程序来实现。在本应用中,需要用TinyWebDB组件向Web数据库请求questions及answers这两个列表,因此Screen1.Initialize将两次调用TinyWebDB.GetValue。块的设置如下图: ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc70a394a.png) **图 10-9 在应用启动时从数据库中请求列表数据** #### **块的功能** 图10-9中使用的TinyWebDB.GetValue块与之前用过的TinyDB.GetValue的运行机制不同,后者会立即返回一个值,而前者只负责向Web数据库发送请求,不会立即收到返回值。当应用收到Web数据库返回的数据时,会触发TinyWebDB.GotValue事件,因此需要另外编写一个GotValue事件的处理程序来接收返回的数据。 当TinyWebDB.GotValue事件发生时,所请求的数据封装在参数valueFromWebDB中,所请求的数据标签(tag)则封装在参数tagFromWebDB中。 如图10-9所示,在Screen1.Initialize事件处理程序中发出了两次GetValue请求,分别请求questions和answers,因此GotValue也将被触发两次。为了避免把questions的数据写入AnswerList中(或反过来),需要对tag进行检查,来判断收到的是哪个请求的返回值,然后再把返回值写到相应的列表中(QuestionList或AnswerList)。现在,你该意识到这些tag的真正用途了吧! #### **块的功能** 应用中两次调用TinyWebDB1.GetValue来请求存储过的数据:分别是为QuestionList及AnswerList。当收到Web数据库返回的数据时,触发TinyWebDB1.GotValue事件,如图10-10。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc75ea534.png) **图 10-10 当收到来自web的数据时触发GotValue事件** 从数据库中返回的数据封装在GotValue事件的valueFromWebDB参数中。在GotValue的事件处理程序中,外层的if块用来判断数据库的返回值valueFromWebDB是否为空。设想用户首次启动应用,数据库中没有任何数据。通过询问参数valueFromWebDB是否“is a list?”,可以得知是否真的有数据返回。如果没有数据返回,则会跳过对GotValue事件的处理。 如果有数据返回(is a list?为真),再继续判断收到的是哪个请求。识别数据的标记tag封装在tagFromWebDB中:即可能是“questions”,也可能是“answers”,如果tag是“questions”,则将valueFromWebDB保存到变量QuestionList,否则(else块),保存到AnswerList。(如果你使用的tag不是“questions”和“answers”,请替换成你自己的tag再做判断。) 我们希望当两个列表都已收到时(GotValue被触发两次)再来显示这些数据。想想看,如何判断从数据库收到了两个列表的数据?是的,用if块来检测两个列表的长度是否相同,因为只有两个列表都收到了,检测结果才能为真。如果为真,你可以轻松调用之前编写的displayQAs过程来显示加载的数据。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc767a859.png) **图 10-11 “出题”应用中的块** ## **答题:从数据库中读取试题的应用** “出题”应用已经就绪,下面来创建“答题”应用,一个可以动态加载测验的应用,相当简单。只是在“总统测验”的基础上稍加修改(如果你还没学过,现在就去学,然后再继续)。 打开“总统测验”,选择“save project as”将应用另存为“TakeQuiz”,这保证在不修改“总统测验”的情况下,以此为基础来构建“答题”应用 。 ### **在组件设计器中调整组件** 在组件设计器中做如下改变: 1\. 这个版本的“出题/答题”应用不需要为问题搭配图片,因此首先删除所有与图片相关的部分:在组件设计中,从Media区域中选择并删除所有图片,然后再删除Image1组件,这将删除块编辑器中对它的所有引用(块编辑器中的global PictureList需手工删除); 2\. 由于“答题”应用中会用到数据库中的数据,因此添加一个TinyWebDB组件; 3\. 在试题被加载完成之前,不希望用户来回答问题或点击“下一题”按钮,因此取消勾选“提交”和“下一题”按钮的Enabled属性。 ### **在快编辑器中编程:从数据库加载测验** 首先,修改列表变量的初始化设置:这里不需要预置问题及答案,因此用create empty list块替代QuestionList和AnswerList初始化时用到的make a list块,结果如图10-12所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc7c38015.png) **图 10-12 应用开始时,问题及答案列表为空** 其次,删除PictureList,这里不需要图片;修改Screen1.Initialize,两次调用TinyWebDB.GetValue来加载列表,与“出题”应用中相同,如图10-13所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc7c86ab0.png) **图 10-13 从Web数据库中请求问题及答案列表** 最后,拖出一个TinyWebDB.GotValue事件处理程序。此事件处理程序与“出题”应用中的程序类似,但这里只显示第一个问题,而且没有答案。先尝试自己做些修改,然后对照图10-14,看看你的方案是否与图中的相符。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc7ce00be.png) **图 10-14 用GotValue处理来自Web的数据** #### **块的作用** 应用启动时触发Screen1.Initialize事件,应用从Web数据库请求数据(questions及answers)。每次(共两次)收到数据都会触发TinyWebDB.GotValue事件。首先使用“is a list?”来判断valueFromWebDB中是否真的含有数据:如果有,则使用tagFromWebDB来判断是哪个请求返回的,并将valueFromWebDB值写入相应的列表中。如果QuestionList已经加载,则从QuestionList选择第一题并显示;如果AnswerList已经加载,则启用“提交”及“下一题”按钮,以便用户可以开始答题。 ### **完整的答题应用** ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fc7d4931a.png) **图 10-15 “答题”应用中块的最终设置** ## **改进** 在“出题”与“答题”应用开始运行之后,你也会会尝试做一些改进。例如: * 允许出题者为每个问题指定一个图片。当然,你(作为开发者)不能预加载这些图像,并且目前应用的用户也无法做到这一点。因此,图片必须是一些Web URL,出题者需要在“出题”应用的表单中输入这些URL,这些URL构成了应用的第三个列表。请注意,你可以将Image组件的Picture属性设置为一个URL。 * 允许出题者从问题和答案列表中删除项。用户可以使用ListPicker组件选择一个问题,并用remove list item块来删除列表项(记住要同时从两个列表中删除,并更新数据库)。想获得ListPicker和列表删除的相关帮助,请参见第19章。 * 让出题者为他的测验设定名称。测验名称也要以一个不同的tag保存到数据库中,并在“答题”应用中,与整个测验一同加载。名称加载完成后,可以将其设置为Screen1的Title属性,这样当用户答题时,测验名称也将显示出来。 * 允许创建多个不同名称的测验。需要建一个测验的列表,并且用测验的名称作为保存问题和答案时的tag(一部分)。 ## **小结** 以下是本章涵盖的内容: * 动态数据是指由用户输入的、或从数据库中加载的信息。用动态数据编程会更加抽象。更多信息请参见第19章; * 可以使用TinyWebDB组件在Web数据库中永久保存数据; * 要从Web数据库中检索数据,需要用TinyWebDB组件的GetValue方法。当Web数据库返回数据时,会触发TinyWebDB.GotValue事件。用TinyWebDB.GotValue事件处理程序,可以把数据存储在列表中,或以其他方式进行处理; * TinyWebDB数据可以在多部手机和应用之间共享。关于(Web)数据库的更多信息,请参见第22章。
';

第 9 章 木琴

最后更新于:2022-04-01 02:53:41

很难相信,利用技术来记录和播放音乐只能追溯到1878年,也就是爱迪生获得留声机专利时。时至今日,我们已经有了长足的进步,音乐合成器、光盘、采样和混音、播放音乐的手机,甚至是拥塞的远程互联网。在本章中,通过创建一个可以录制和播放音乐的木琴应用,你也将成为这种进步的推动者。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbd88b3f5.png) ## **作品描述** 如图9-1所示,这个应用(最初由App Inventor团队的Liz Looney创建)可以做到: ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbd90c6ea.png) **图 9-1 木琴应用的用户界面** * 通过触摸屏幕上的彩色按钮播放八个不同的音符; * 按“播放”按钮,回放之前弹奏的音符; * 按“重置”按钮清除之前弹过的音符,以便输入新曲。 ## **学习要点** 本章程涵盖了以下概念: * 使用单一的声音组件来播放不同的音频文件; * 使用Clock组件来计算并实现两个音符之间的延迟; * 在创建一个过程时做判断; * 创建能够自我调用的过程; * 列表的高级应用,包括添加、删除及读取项。 ## **准备开始** 登陆App Inventor网站,创建新项目“Xylophone”,屏幕标题设置为“木琴”,并连接到测试手机或模拟器。 ## **设计组件** 应用中有13个不同的组件(其中8个Button组成了乐器键盘),见表9-1。要在编程之前一次性创建这么多组件,显得有些乏味,因此我们按功能将应用划分为若干部分,分步来创建组件,这需要在组件设计器与块编辑器之间反复切换,就像第五章“瓢虫快跑”应用中一样。 **表9-1木琴应用的所有组件** | 组件类型 | 面板中分组 | 命名 | 作用| | --- | --- | --- | --- | | Button| User Interface| Button1 | 播放低音C | | Button | User Interface | Button2 | 播放D | | Button | User Interface | Button3 | 播放E| | Button | User Interface | Button4|播放F | | Button | User Interface | Button5| 播放G| | Button | User Interface| Button6 | 播放A | | Button | User Interface |Button7 | 播放B | | Button | User Interface | Button8 | 播放高音C | | Sound | Media|Sound1 | 播放音符 | | Button | User Interface | PlayButton | 回放曲子 | | Button | User Interface | ResetButton | 清除保存的曲子,开始新曲 | | HorizontalArrangement | Layout | HorizontalArrangement1 |放置“播放”及“重置”按钮 | | Clock | User Interface | Clock1 | 记录音符之间的延迟| ## **创建键盘** 用户界面中包含了从低音C到高音C的大调五声(七音符)音阶的八个音符键盘,本节将创建这样的音乐键盘。 ### **创建第一个音符按钮** 首先创建前两个木琴键,用按钮来实现: 1\. 从面板(palette)的user interface组中拖出一个按钮,保留Button1的名称,我们希望它像木琴的键一样,是一个洋红色(Magenta)的长条,因此做如下设置: * BackgroundColor属性:为洋红色(Magenta); * Text属性:为“C”; * Width属性:为“Fill parent”,使其占满屏幕; * Height属性:为40像素。 2\. 重复上述步骤创建第二个按钮,名为Button2,放在Button1下面。Width及Height属性值同Button1,但BackgroundColor属性设为红色,Text属性设置为“D”。 (稍后将重复步骤2来创建其余六个音符按钮。)在组件设计器中看起来如图9-2所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbd9560b2.png) **图 9-2 用按钮来充当音符按键** 在手机上的显示看起来与此相似,只是两个彩色按钮之间没有空白。 ### **添加Sound组件** 我们不能让木琴没有声音,创建一个Sound组件,名字为Sound1。MinimumInterval(最小间隔)属性设置为0(默认值为500毫秒)。这可以让我们的演奏要多快有多快,而不必等半秒钟(500毫秒)。不必设置Source属性,稍后我们会在块编辑器中设置。 下载1.wav和2.wav,并加载到项目中。与前几章不同,这里的声音文件必须保持原有文件名,不能修改,理由稍后就会明晰。后面还有六个声音文件需要加载。 ### **声音与按钮的连接** 当某个按钮被点击时,用程序来实现播放声音的行为,即:如果Button1被点击,则播放1.wav,如果Button2被点击,播放2.wav,等等。切换到块编辑器,如图9-3所示,进行以下设置: 1\. 从Screen1项下的Button1抽屉里拖出Button1.Click块; 2\. 从Sound1抽屉里拖set Sound1.Source块,放置在Button1.Click块中; 3\. 输入“text”来创建一个文本块(而不是从Built-in项下的Text抽屉里拖出,这样更便捷。)设置文本值为“1.wav”,并与Sound1.Source块连接; 4\. 添加Sound1.Play块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbda102b3.png) **图 9-3 点击按钮时播放声音** 对Button2进行同样设置,如图9-4(只改了文件名),代码几乎完全重复。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbda56b2b.png) **图 9-4 添加更多的声音** 重复的代码提示我们最好是创建一个过程,像在第3章“打地鼠”和第5章“瓢虫快跑”中那样。具体来说,我们将创建一个带数字参数的过程,将Sound1的Source属性设置为相应的声音文件,并播放该声音文件。这是对程序进行重构改进而又不改变程序行为的又一个例子,这一概念在“打地鼠”一章中首次引入。用join块将数字(如1)与文本“.wav”连接起来,创造出正规的文件名(如“1.wav”)。下面是创建这个过程的步骤: 1\. 在块编辑器中打开Procedures抽屉,拖出“to procedure”块; 2\. 单击procedure将过程名改为playNote; 3\. 点击procedure块左上角的蓝色方块呼出内部组件,将一个input x块插入“inputs”块; 4\. 将input x块中的x改为number; 5\. 将set Sound1.Source to块从Button1.Click事件处理程序中拖出,放在PlayNote过程内“do”的右边,Sound1.Play块也将随之移动; 6\. 将1.wav块拖入垃圾桶; 7\. 从Text抽屉中拖出join块放到set Sound1.Source to的插槽内; 8\. 将鼠标悬停在playNote的number参数上,呼出并拖动get number块,并将其放入join块的第一个插槽中; 9\. 从Text抽屉中拖出空文本块,放在join块的第二个插槽中; ##将文本值设置为“.wav”。(切记不要输入引号); ##从Procedures抽屉中拖出call PlayNote块,放到空的Button1.Click内; ##在number插槽中插入文本“1”。 现在,当Button1被点击时,过程PlayNote将以数字1为参数被调用。该过程将Sound1.Source属性设为“1.wav”,并播放该声音。 创建一个Button2.Click块,调用参数为2的PlayNote过程。(可以复制现有的PlayNote块,将其移动到Button2.Click块内,并将参数更改为2;也可以复制整个Button1.Click块,然后将Button1改为Button2,再将参数1改为2。)程序如图9-5所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbda9fecc.png) **图 9-5 创建一个过程来演奏音符** 告诉Android加载声音 此时在手机上测试程序会让你失望:第一次按键时,不但没听到预想的声音,手机还弹出错误提示:“Error 703:Unable to play 1.wav”(不能播放1.wav);第二次再按同一个键时,才听到声音。这是因为Android系统是在程序运行时才加载声音文件(只需加载一次),加载过程需要一点时间。第一次按键,当call Sound1 play块开始执行时,set Sound1.Source to块的加载任务尚未完成,因此系统给出错误提示;等到第二次按键时,声音文件已经加载完成,因此可以正常播放。为什么前几章没有出现过这个问题?因为我们在组件设计器中预先设置了Sound组件的Source属性为某个声音文件,当程序启动时,声音文件会自动加载。而这里,直到程序启动之后,我们也没有对Sound1.Source进行设置,因此没有对声音做初始化。我们必须在程序启动时直接加载声音文件,如图9-6所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbdaf4021.png) **图 9-6 在应用启动时加载声音文件** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbe054257.png) 测试:在手机中重新启动应用,按键之后立刻播放声音。(如果你没有听到声音,请确保手机上的媒体音量没有被设置为静音。) ### **实现其余的音符** 两个按钮已经实现了演奏音符的功能,现在需要回到组件设计器,加载其余六个声音文件3.wav、4.wav、5.wav、6.wav、7.wav和8.wav,并添加其余六个音符。首先创建六个新Button组件,重复此前的步骤,但Text及backgroundColor属性的设置有所不同,具体设置如下: * Button3(“E” Pink / 粉红色) * Button4(“F”,Orange / 橙色) * Button5(“G”,Yellow / 黄色) * Button6(“A”,Green / 绿) * Button7(“B”,Cyan / 青色) * Button8(“C”,Blue / 蓝) Button8的TextColor属性需要改为白色,这样更加醒目,如图9-7所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbe09e578.png) **图 9-7 在组件设计器中放置其余的声音按钮** 回到块编辑器中,为每个新按钮创建Click块并以相应的参数调用PlayNote过程。同样,在Screen.Initialize中加载新的声音文件,如图9-8所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbe67f850.png) **图 9-8 对按钮单击事件编程,使得键盘与音调相对应** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbe054257.png) 测试:现在所有按钮都已经就绪,点击不同按钮会演奏不同的音符。 ## **记录并回放音符** 用按键来弹奏音符的确有趣,但如果能录制并播放歌曲岂不更好。为了实现回放功能,需要记录弹奏的音符并加以保存。除了要记录弹奏的音高(声音文件),还要记录两个音符之间的时间长度,否则将无法表现两个连续快弹音符与两个间隔10秒的音符之间的差别。 我们需要维护两个列表,每弹奏一个音符,两个列表中都会各自添加一条记录: * notes:包含与演奏的音符相对应的声音文件名,按照演奏顺序排列; * times:记录音符演奏时的时间点。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbf16f77e.png) 提示:在继续之前,不妨复习一下在“总统测验”中所学到的关于列表的知识。 我们可以从Clock组件中得到计时信息,因此也可以用来正确地设定音符的回放速度。 ### **添加组件** 在设计器中添加一个Clock组件及“播放”和“重置”按钮,按钮放在HorizontalArrangement中: 1\. 拖入一个Clock组件,它将出现在“不可见组件”区域,取消勾选TimerEnabled属性,因为我们希望在回放期间,计时器听从我们的调遣,适时地启动并完成计时; 2\. 从layout组中拖出一个HorizontalArrangement组件放在按钮下面,Width属性设为“Fill parent”; 3\. 从User Interface组中拖动一个按钮,改名为PlayButton,Text属性设为“播放”; 4\. 拖出另一个按钮并放在PlayButton右侧,改名为ResetButton,Text属性设为“重置”。 图9-9中显示了应用在设计视图中的外观。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbf6c3373.png) **图 9-9 记录并回放声音的组件被添加到设计器中** ### **记录音符及时间** 回到块编辑器中,为组件添加正确的行为。我们需要维护两个列表:notes与times,每次用户按下一个按钮,就向列表中添加一项: 1\. 从Variables抽屉中拖出一个initialize global name to块来定义一个新的变量; 2\. 单击“name”将变量命名为“notes”; 3\. 打开Lists抽屉,拖动一个make a list块,将其放置在变量notes的插槽中; 这样就定义了一个名为“notes”的空列表。重复上述步骤定义另一个变量,命名为“times”。块的样子如图9-10所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbf770280.png) **图 9-10 设置变量来记录音符** #### **块的功能** 每演奏一个音符,需要保存两项数据:声音文件名(保存到notes列表),以及演奏瞬间的时刻(保存到times列表)。用Clock1.Now块来记录时刻,它返回当前时刻的时间值(例如,2011年3月12日上午8时33分14秒),精确到毫秒。这些数据可以通过Sound1.Source和Clock1.Now块获得,将分别被添加到notes及times列表中,如图9-11所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbf7c5a1a.png) **图 9-11 将演奏的声音添加到列表中** 例如,如果你演奏“哆来咪哆咪哆咪”[CDECECE],你的列表中最终会有七条记录,可能是: * notes:1.wav,2.wav,3.wav,1.wav,3.wav ,1.wav,3.wav * times[日期省略]:12:00:01,12:00:03,12:00:04,12:00:05,12:00:06,12:00:07,12:00:08 当用户按下“重置”按钮时,我们希望清空这两个列表。由于用户看不到清空带来的任何变化,因此添加一个Sound1.Vibrate块,通过振动来告知用户按键生效了,这种设置对用户来说是非常友好的。图9-12显示了这一功能用到的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbf877f54.png) **图 9-12 为用户的“重置”操作提供反馈** ### **音符的回放** 作为一个思想实验,先来考虑如何实现音符的回放,而暂时忽略回放速度。我们可以(但不会)通过创建图9-13中的那块来实现这个暂时的目标: * 变量count用来跟踪notes列表中当前正在播放的音符的索引(位置); * 新过程 PlayBackNote,用来播放当前音符,并移动到下一个音符; * 编写PlayButton.Click事件处理程序,设置count为1,只要列表中有保存的音符,就调用PlayBackNote。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbf8c7c6d.png) **图 9-13 回放被记录下来的音符** #### **块的功能** 这可能是你第一次看到能自我调用的过程。这件事乍一看好像不可能,但实际上这是计算机科学中一个非常重要的概念:强大的递归。 为了更好地了解递归的工作原理,我们来一步一步地探究,当用户演奏了三个音符( 1.wav、 3.wav和6.wav),然后按下“播放”按钮时,都发生了什么。PlayButton.Click首先判断列表中是否保存了音符:由于notes列表长度3>0,列表不空,因此设定count等于1,并调用PlayBackNote: 1\. 在第一次调用PlayBackNote时,count= 1: * Sound1.Source被设置为在notes中的第1项,即1.wav; * 调用Sound1.Play,播放1.wav; * 由于count值(1)小于notes的长度(3),因此count递增为2,并再次调用PlayBackNote; 2\. 第二次调用PlayBackNote时,count=2: * Sound1.Source被设置为notes中的第2项,即3.wav; * 调用Sound1.Play,播放3.wav; * 由于count(2)小于notes的长度(3),因此count递增为3,并再次调用PlayBackNote; 3\. 第三次调用PlayBackNote时,count=3: * Sound1.Source被设置为notes中的第3项,即6.wav; * 调用Sound1.Play,播放6.wav; * 由于count(3)不小于notes的长度(3),因此跳出if块,回放结束。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbf16f77e.png) 提示:虽然递归功能强大,但运用起来存在危险。来做一个思想实验:问问自己,如果程序员忘了在PlayBackNote块中插入count递增的块,会发生什么事情。 这里的递归是正确的,但这个例子中还有另一个问题:在两次调用Sound1.Play之间几乎没有时间间隔(程序运行的速度非常快),因此每个音符都被下一个音符截断,除了最后一个。所有音符(除了最后一个)都等不到播放完,Sound1的source属性就已经被改写为下一个音符,并由Sound1.Play播放出来。为了获得正确的行为,需要在两次调用PlayBackNote之间添加延迟功能。 ### **播放适当延迟的音符** 延迟的设定与两个音符之间的时间差有关,我们用clock来为这个时间差计时。例如,如果时间差为3,000毫秒(3秒),则将Clock1.TimerInterval设置为3000,并启动计时器;在计时结束时再调用PlayBackNote。对PlayBackNote的if块做出修改,如图9-14所示。创建Clock1.Timer事件并编写事件处理程序,来说明计时结束时将发生的事情。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fbf979843.png) **图 9-14 在音符之间加入延迟** #### **块的功能** 现在假设两个列表中记录了以下内容: * notes:1.wav,3.wav,6.wav * times:12:00:00,12:00:01,12:00:04 如图9-14所示,在PlayButton.Click中设置count为1,并调用PlayBackNote。 1\. 第一次调用PlayBackNote时,count= 1: * Sound1.Source被设置为notes中的第1项,即“1.wav”; * 调用Sound1.Play播放1.wav; * 因为count(1)小于notes的长度(3),于是Clock1.TimerInterval被设置为times列表中的第1项(12:00:00)与第2项(12:00:01)之间的时间差:1秒。Count递增到2,启用Clock1.Timer并开始计时; Clock1.Timer开始计时,间隔1秒之后,计时结束,定时器暂时禁用,并调用PlayBackNote。 2\. 第二次调用PlayBackNote时,count= 2 : * Sound1.Source被设置为notes中的第2项,即“3.wav”; * 调用Sound1.Play播放3.wav; * 因为count(2)小于notes的长度(3),于是Clock1.TimerInterval被设置为times列表中的第2项(12:00:01)与第3项(12:00:04)之间的时间差:3秒。Count递增到3,启用Clock1.Timer并开始计时; Clock1.Timer计时开始,间隔3秒之后,定时器暂时禁用,并调用PlayBackNote。 3\. 第三次调用PlayBackNote时,count= 3 : * Sound1.Source被设置为notes中的第3项,即“6.wav”; * 调用Sound1.Play来播放6.wav; * 由于count(3)不小于notes的长度(3),跳出if块,回放完成。 ## **改进** 下面是一些可供探讨的备选方案: * 目前,在回放过程中,没有对用户点击ResetButton做任何限制,这将导致程序的崩溃(错误提示:select list item: Attempt to get item number 4 of a list of lengh 0。)(你知道原因吗?)修改PlayButton.Click,让ResetButton在回放期间禁用,回放完成后再重新启用。将PlayBackNote中的if块改为ifelse块,并在“else”中重新启用ResetButton。 * 类似问题也发生在PlayButton上,用户可以在回放过程中再次点击该按钮。(想象一下会发生什么。) 在PlayButton.Click中禁用PlayButton,并将其Text属性改为“播放中...... ”,并像ResetButton一样,在PlayBackNote的ifelse块中重新启用该按钮,并重置Text属性。 * 添加一个按钮来显示一首歌曲的名字,如“致爱丽丝”。当用户单击时,向notes及times列表中填写相应的值,将count设定为1,并调用PlayBackNote。有一个非常有用的块Clock1.MakeInstantFromMillis(用毫秒设置时间间隔),可以用来设定音符之间的延迟。 * 如果用户按下一个音符,然后去做别的事情了,几小时后回来,又按下另一个音符,尽管音符可能属于同一首歌,但这绝不是用户的意图。有两种方法可以改进程序:(1)在一个合理的时间间隔后,停止记录音符,如1分钟;(2)通过对Clock1.TimerInterval使用Math抽屉中的max块,来限制音符播放的时长。 * 通过改变按钮的外观,如Text、BackgroundColor或ForegroundColor属性,来形象地提示当前正在播放的音符。 ## **小结** 以下是本章涵盖的概念: * 通过修改Sound组件的Source属性,可以用一个而非八个Sound组件来播放不同音频文件。记住要在应用初始化时加载声音文件,以免运行时加载所引起的问题。(见图9-6); * 列表(Lists)可以为程序提供存储功能,可以在列表中保存用户的操作记录,并在以后对存储内容进行提取和再处理。我们使用这个功能来录制及播放歌曲; * Clock组件可以用来确定当前时间,两个时间值只差为我们提供了两个事件之间的时间间隔; * Clock组件的TimerInterval属性可以在程序中设置,就像我们设置两个音符之间的时间间隔一样; * 编写一个能自我调用的过程不仅是可能的,有时也是必要的。这种强大的技术称为递归。在编写递归过程时,一定要确保为程序的退出设定一个基本条件,它的重要性远大于为自我调用设定条件,否则程序将陷入无限循环。 ## **资源下载** [1.wav](http://www.17coding.net/download/9/1.wav) [2.wav](http://www.17coding.net/download/9/2.wav) [3.wav](http://www.17coding.net/download/9/3.wav) [4.wav](http://www.17coding.net/download/9/4.wav) [5.wav](http://www.17coding.net/download/9/5.wav) [6.wav](http://www.17coding.net/download/9/6.wav) [7.wav](http://www.17coding.net/download/9/7.wav) [8.wav](http://www.17coding.net/download/9/8.wav)
';

第 8 章 总统测验

最后更新于:2022-04-01 02:53:38

“总统测验”是一个关于美国前总统的问答游戏。虽然测验的内容与总统有关,但你可以把它当作模板,来实现对任何题目的测验。 在前几章中,你已经了解了一些编程的基本概念。现在,准备好面对更大的挑战吧。你会发现,无论是编程技巧,还是抽象思维,这一章都要求你有一个概念性的飞跃。特别需要强调的是,本章将使用两个列表变量来存储数据——应用中的问题和答案,使用索引变量来跟踪用户正在回答的题目。在本章结束时,对于创建测验类应用和其他需要使用列表的应用,你已经掌握了必要的知识。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fad42ac34.png) 本章假设你已经熟悉了App Inventor的基础知识:使用组件设计器构建用户界面,用块编辑器来定义事件处理程序并为组件添加行为。如果你还不熟悉,在继续学习之前,请复习前面几章。 在测验中,用户通过单击“下一题”按钮,连续地回答问题,并收到回答是否正确的反馈。 ## **学习要点** 如图8-1所示,本章覆盖以下内容: * 定义列表变量:用来存储问题和答案; * 使用索引遍历列表,用户每次点击“下一题”按钮时,显示下一个问题; * 使用条件语句(if)控制行为:只有在特定条件下才能执行某些操作。在用户测验到最后一题时,将使用if块来处理程序; * 每一道题对应一张不同的图片,要实现图片的切换。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fad499d30.png) **图 8-1 “总统测验”在手机中** ## **准备开始** 登陆App Inventor网站,创建新项目“PresidentsQuiz”,并设置屏幕的标题为“总统测验”,连接测试设备。从appinventor网站下载测验中用到的图片:roosChurch.gif,nixon.gif,carterChina.gif和atomic.gif。在下一节中将这些图片加载到项目中。 ## **设计组件** “总统测验”应用的界面很简单:显示问题并允许用户来回答。图8-2显示了应用在组件设计器中的截图,按图来创建组件。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fad524feb.png) **图 8-2 组件设计器中的“总统测验”** 首先将下载的图片加载到项目中:单击Media区域的Upload File按钮,选择一个文件(如roosChurch.gif),其他图片也是如此。然后添加表8-1中列出的组件。 **表8-1 “总统测验”应用所需组件** | 组件类型 | 面板中分组 | 命名 | 作用 | | --- | --- | --- | --- | | Image | User Interface | Image1 | 与问题一同显示的图片 | | Label | User Interface | QuestionLabel |显示正在回答的问 | | HorizontalArrangement| Layout| HorizontalArrangement1 |放置答案输入框及“提交”按钮 | | TextBox| User Interface | AnswerText | 用户在此输入答案 | | Button| User Interface | AnswerButton |用户点击之后提交答案 | | Label | User Interface | RightWrongLabel |显示“正确”或“不正确”的反馈 | | Button| User Interface| NextButton | 用户点击进入下一题 | 按照下面提示设置组件属性: * Image1:Picture为roosChurch.gif(最先出现);Width为“Fill parent”,Height为200; * QuestionLabel:Text为“问题…”(在块编辑器中输入第一个问题); * AnswerText:Hint为“输入回答”,Text为空,放置到HorizontalArrangement1中; * AnswerButton:Text为“提交”,放置到HorizontalArrangement1中; * NextButton:Text为“下一步”; * RightWrongLabel:Text为空。 ## **为组件添加行为** 编程来实现以下行为: * 应用启动时,显示第一个问题以及相应的图片; * 点击“下一题”按钮时,显示第二题,再次点击,显示第三题,以此类推; * 当显示最后一题时,点击“下一题”按钮将回到第一题; * 在用户回答问题之后,反馈回答是否正确; 首先按照表8-2的提示,定义两个列表变量:QuestionList用来保存问题,AnswerList用来保存答案。图8-3显示在块编辑器中创建的两个列表。 **表8-2 用于保存问题和答案的列表变量** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | Initialize global QuestionList to | Variables | 保存问题的列表(更名为QuestionList) | | Initialize global AnswerList to | Variables | 保存答案的列表(更名为AnswerList) | | make a list | Lists | 为QuestionList插入列表项 | | 问题内容(三个) | Text | 问题 | | make a list | Lists | 为AnswerList插入列表项 | | 答案内容(三个) | Text | 答案 | ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fad5ba987.png) **图 8-3 问题及答案列表** ### **定义索引变量** 在整个测试过程中,每次用户点击“下一题”按钮,都要跟踪用户正在回答的问题。定义变量currentQuestionIndex作为QuestionList和AnswerList的索引值。表8-3列出了所需的块,图8-4显示了变量的定义。 **表8-3 创建索引** | 块的类型 | 在抽屉 | 作用 | | --- | --- | --- | | Initialize global currentQuestionIndex to | Variables | 保存当前问题(与答案)的索引(位置) | | 数字1 | Math | 将currentQuestionIndex的初始值设为1(第一题) | ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fad62105f.png) **图 8-4 索引变量的初始值为1** ### **显示第一个问题** 有了这些变量,就可以为应用设定交互行为。无论是何种应用,渐进式的开发是非常重要的,而且每一步只定义一个行为。我们首先考虑与问题相关的行为,具体而言,在应用启动时显示列表中的第一道题,稍后再来处理图片的事情。 代码块的设定应该与列表中的具体问题无关,这样,如果需要更换问题或创建新的测验类应用时,只需改变列表中的具体问题,而不必修改事件处理程序。 鉴于上述考虑,对于第一道题,不要直接引用“哪位总统在大萧条时期实施了‘新政’?”这样的题目内容,而是引用“QuestionList的第一个插槽”这样抽象的形式(与具体问题无关)。这样,即使第一个插槽中的问题改变了,这些程序块仍然有效。 select list item块用来选择列表中的项,使用中要求指定list(列表)及index(索引)(列表中的位置)。如果列表中有三个项,可以输入1、2或3作为索引。 第一个行为是,在应用启动时选择QuestionList中的第一道题,将其写入QuestionLabel;还记得“Android,我的车在哪儿?”的应用吧,如果想让某件事发生在应用启动时,可以将有关指令放在Screen1.Initialize事件处理程序中,表8-4中列出所需的块。 **表8-4 应用启动时加载第一个问题所需的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | Screen1.Initialize | Screen1 | 应用启动时触发该事件 | | set QuestionLabel.Text to | QuestionLabel | 将第一道题内容写入QuestionLabel | | select list Item | Lists | 从QuestionList中选择第一道题 | | get Global QuestionList | Variables | 从其中选择问题的列表 | | 数字1 | Math | 用索引值1来选择第一道题 | #### **块的作用** 应用启动时触发Screen1.Initialize事件。如图8-5所示,变量QuestionList中的第一项被选中,并被写入QuestionLabel.Text。因此,应用启动时,用户会看到第一道题。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadb69d51.png) **图 8-5 应用启动时选择并显示第一道题** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadbb84a4.png) 测试:连接装有AI伴侣的设备,或点击“connectEmulator”打开Android模拟器。当应用启动后,你是否看到QuestionList中的第一道题:“哪位总统在大萧条时期实施了'新政'?” ### **遍历所有问题** 现在为“下一题”按钮的行为编程。之前定义的currentQuestionIndex用来记住用户正在回答的问题,现在设定当用户单击“下一题”时,为currentQuestionIndex加1(即,从1变为2,或从2变为3,依此类推),并根据currentQuestionIndex的值来选择并显示新的问题。挑战一下你自己,看看是否可以自己搭建这些块。完成之后,与图8-6进行对照。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadc1311e.png) **图 8-6 显示下一题** #### **块的作用** 第一行的块让变量currentQuestionIndex递增。如果当前值为1则加到2;如果是2则加到3,以此类推。一旦currentQuestionIndex值改变,应用将以此来选择新的问题并显示。首次单击“下一题”时,currentQuestionIndex从1变为2,应用将选择并显示QuestionList中的第二道题:“哪位总统在1979年实现中美建交?”;第二次单击“下一题”时,currentQuestionIndex从2变为3,应用将选择并显示QuestionList中的第三道题:“哪位总统因水门事件而辞职?” > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadc80fbf.png) 提示:花一分钟的时间来比较一下NextButton.Click与Screen.Initialize两个事件处理程序的差别。在Screen.Initialize中,用具体数字1来选择列表项;而在NextButton.Click中,用索引变量currentQuestionindex来选择列表项,即选择第currentQuestionindex项,而非第一或第二第三项,因而点击“下一题”将选中不同的项。这是索引最常见的用法——增加索引值来找到并显示列表项。 问题是,索引的每次递增,都会转到下一题,那么当测验到最后一题时,怎么办呢?即:当currentQuestionIndex=3时点击“下一题”,currentQuestionIndex将从3变为4,应用将从问题列表中选择第currentQuestionIndex项,即第4项,而列表QuestionList中只有3项,此时Android设备将不知所措并强行退出应用。那么应用如何知道已经测验到最后一题了呢? > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadbb84a4.png) 测试:测试“下一题”按钮,看看应用运行是否正常。在手机上按“下一题”按钮,是否显示第二题“哪位总统在1979年实现中美建交?”?应该是的;再按“下一题”,应该出现第三题。但如果再次点击,就会看到错误提示:“Attempting to get item 4 of a list of length 3.(试图从只有3个项的列表中获取第4项。)”这就是程序的bug!知道原因吗?在继续阅读之前试试看自己解决它。 当点击“下一题”按钮时,应用要问一个问题,并根据问题的答案执行不同的操作。既然已知QuestionList中包含三个问题,问题可以这样来问:“currentQuestionIndex是否>3?”如果是,将currentQuestionIndex设回1,这样就回到了第一道题。表8-5中列出了所需的块。 **表8-5 检查索引值是否到了列表的结尾所需的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | if | Control | 判断用户是否正在做最后一题 | | = | Math | 检查currentQuestionIndex的值是否为3 | | get global currentQuestionIndex | Variables | 放入“=”左边的插槽 | | 数字3 | Math | 放入“=”右边的插槽 | | set global currentQuestionIndex to | Variables | 设为1来转回到第一道题 | | 数字1 | Math | 设置索引值为1 | > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadbb84a4.png) 测试:单击手机上的“下一题”按钮,会照常出现第二题“哪位总统在1979年实现中美建交?”,继续点击“下一题”,将显示第三题。下面是你真正想测的:如果再次点击,将出现第一题(“哪位总统在大萧条时期实施了‘新政’?”)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fade76cc4.png) **图 8-7 检查索引值递增** 单击“下一题”时,索引照旧会递增。但程序会检查是否currentQuestionIndex>3(问题的数量)。如果大于3,则将currentQuestionIndex重新设置为1,并显示第一题;如果≤3,则不执行if块内的程序,并照常显示当前问题。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadecaf07.png) **图 8-8 检查测验是否到了最后一题(第三题)** ### **让测验易于修改** 如果NextButton.Click中的块能够正常运行,恭喜你,你正在成为一名合格的程序员!但是,如果想在测验中添加新题目(及答案),该怎么办?这些块还能正常运行吗?为了验证这一点,先在QuestionList中添加第四道题,并在AnswerList中添加第四个答案,如图8-9。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fae4bccef.png) **图 8-9 向两个列表中分别添加一项** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadbb84a4.png) 测试:多次单击“下一题”按钮,你发现无论点击多少次,第四题始终不出现。知道问题所在吗?在继续阅读之前,尝试做些修改,以便让第四题出现。 问题出在“最后一题”的判断条件太具体:currentQuestionIndex>3。如果把3改为4,程序正常了,但问题是,每次增减问题和答案时,都要记着修改判断条件。计算机程序中的这种强相关性最容易导致错误,特别是当程序变得复杂时。好的对策是让程序的设计与列表中的问题数量无关。这种通用性,对于程序员来说,当你想创建其他专题的定制测验时,可以让程序的移植更加容易。尤其是在处理动态列表时,这样做是必须的,例如,测验中允许用户添加新问题(见第10章)。一个通用性好的程序不该与3这样的具体数字相关联,因为这只对那些有三个问题的测验有效。对currentQuestionIndex的判断条件应该是QuestionList列表的长度(项数),而不是具体数字。当条件更具通用性时,即使是添加或删除QuestionList中的项,程序也能正常运行。现在修改NextButton.Click事件处理程序,替换掉具体数字3。表8-6中列出了所需要的块。 **表8-6 检查列表长度所需的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | length of list| Lists | 询问列表QuestionList中有多少个列表项 | | get global QuestionList | Variables | 插入length of list块的list插槽中 | #### **块的作用** If块中将currentQuestionIndex值与QuestionList的列表长度进行比较,如图8-10所示。如果currentQuestionIndex为5,而QuestionList的长度为4,则currentQuestionIndex将被重新设置为1。值得注意的是:由于程序块不再与3或任何具体数字相关联,因此无论列表中有多少项,程序都将正常运行。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fae55b6bc.png) **图 8-10 采取更加通用的方式检查列表的结尾** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadbb84a4.png) 测试:当单击“下一题”按钮时,程序是否在四个问题间循环?在第四题后是否又回到第一题? ### **为每道题切换图片** 现在程序已经可以遍历所有的问题(而且代码更加聪明灵活,也更抽象),下面来设置图片。眼下无论显示什么问题,图片都是同一个,我们希望当用户单击“下一题”时,图片与问题相匹配。此前在Media中载入了四张图片,现在用图片的文件名来创建第三个列表PictureList。然后修改NextButton.Click事件处理程序,同时切换问题与图片。(想到currentQuestionIndex就说明你已经开窍了!)首先创建列表PictureList,用图片文件名初始化列表,要保证列表中的文件名与先前加载的图片文件名完全相同。图8-11显示了PictureList块的样子。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3faeb34734.png) **图 8-11 PictureList中用图片文件名来充当列表项** 下面来修改NextButton.Click事件处理程序,以便图片可以随问题索引的改变而改变。Image组件的Picture属性用于指定要显示的图片。表8-7中列出了修改NextButton.Click所需的块。 **表8-7 显示与问题相匹配的图片所需的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | set Image1.Picture to | Image1 | 改变图片 | | select list Item | Lists | 选择一个与当前问题相匹配的图片 | | global PictureList | Variables | 从列表中选择一个文件名 | | get global currentQuestionIndex | Variables | 选择第currentQuestionIndex项| #### **块的作用** rrentQuestionIndex同时充当QuestionList和PictureList两个列表的索引,这要求正确设置各个列表,如,第一题对应第一个答案及第一张图,第二题对应第二个答案及第二张图,依此类推,这样一个索引值可用于三个列表,如图8-12所示。举例说明:第一张图roosChurch.gif是罗斯福总统的图(与英国首相丘吉尔在一起),而“罗斯福”是第一个问题的答案。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3faeb9f974.png) **图 8-12 每次选择与问题匹配的第currentQuestionIndex张图片** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadbb84a4.png) 测试:多次点击“下一题”,每次点击是否出现不同的图片? ### **检查用户答案** 现在应用已经可以遍历所有的试题及答案(及匹配答案的图片),这是列表应用的极好案例。但真实的测验要对用户的回答判断正误。下面添加一些块来告诉用户他的回答是否正确。用户在AnswerText中输入答案,并点击AnswerButton提交答案;程序用Ifelse块将用户输入与标准答案作比较,并用RightWrongLabel显示比较结果。表8-8列出了程序中用到的块。 **表8-8 用于显示答案是否正确的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | AnswerButton.Click | AnswerButton | 点击AnswerButton按钮时触发该事件 | | ifelse | Control | 如果回答正确,做一件事,否则做另一件事 | | = | Math | 判断回答是否正确 | | AnswerText.Text | AnswerText | 包含了用户的回答 | | select list Item | Lists | 从AnswerList列表中选择当前问题的答案 | | get global AnswerList | Variables | 答案的列表 | | get global currantQuestionIndex | Variables | 当前用户正在回答的问题的索引值 | | set RightWrongLabel.Text to | RightWrongLabel | 显示回答是否正确 | | “正确” | Text | 回答正确时显示 | | set RightWrongLabel.Text to | RightWrongLabel | 显示回答是否正确 | | “不正确” | Text | 回答错误时显示 | #### **块的作用** 在图8-13中,Ifelse块用来检验用户的输入(AnswerText.Text)是否等于AnswerList中的第currentQuestionIndex项。如果currentQuestionIndex=1,程序将用户的回答与AnswerList中的第一项“罗斯福”作对比,同样,如果currentQuestionIndex=2,则与AnswerList中的第二项“卡特”作对比,等等。如果对比结果相同,则执行then块,即RightWrongLabel显示“正确!”;如果对比结果不同,执行else块,即RightWrongLabel显示“不正确!”。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3faf163e54.png) **图 8-13 检查用户的回答,并告诉用户答案是否正确** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadbb84a4.png) 测试:尝试回答一道题,程序会显示你的回答是否正确。分别试验正确和错误的回答。你会注意到,回答正确,意味着你的输入必须与AnswerList中的答案完全匹配(包括大小写、标点或空格)。继续测试后面的问题,并确认运行正常。 应用运行正常,但你会看到,当单击“下一题”时,虽然图片和问题都切换到下一题,但“正确!”或“不正确!”的文本以及前一题中输入的回答仍然显示在屏幕上,如图8-14所示。尽管这一点无伤大雅,但用户肯定会发现这类的界面问题。将RightWrongLabel及AnswerText清空,需要在NextButton.Click事件处理程序中添加几个块,表8-9列出了所需的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3faf714649.png) **图 8-14 用户界面上的小问题** **表8-9 清除RightWrongLabel及AnswerText的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | set RightWrongLabel.Text to | RightWrongLabel | 需要清空内容的label | | “” | Text | 当用户点击“下一题”时,删除对上一题回答的反馈 | | set AnswerText.Text to | AnswerText | 用户对上一题的回答 | | “” | Text | 当用户点击“下一题”时,删除对上一题的回答 | #### **块的作用** 用户单击“下一题”时,图8-15中的前两行用于清空RightWrongLabel和AnswerText。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3faf789288.png) **图 8-15 当转入下一题时,清空上一题的答案及对答案的反馈** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fadbb84a4.png) 测试:回答一个问题,然后点击“提交”,再单击Next按钮,上一题的答案及反馈是否消失了? ## **完整的应用:总统知识测验** 图8-16与8-17显示了“总统测验”应用中块的最终配置。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3faf836d1b.png) **图 8-16 “总统测验”应用中块的最终配置(之一)** ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3faf896f22.png) **图 8-17 “总统测验”应用中块的最终配置(之二)** ## **改进** 一旦测验应用开始正常运行,你也许会乐于做一些改进,例如: * 现在应用中只显示与问题有关的图片,也可以尝试播放录音或视频片段。在使用声音上,你甚至可以发展出一款“辩声识曲(Name That Tune)”的应用; * 本测验对正确答案的要求过于严格,有几种改进方法:一是使用text.contains块,来检查是用户的输入中是否包含了真正的答案;另一种方法是给每道题提供多个答案,通过遍历(foreach)来检查是否与标准答案相匹配;你还可以想办法处理掉那些用户输入的多余空格,或者不做大小写区分,等等; * 将测验改为多选题,这需要用另一个列表来保存每个问题的可选答案。答案也可能是一个二级列表,第二级列表中保存着特定问题的可选答案。使用ListPicker组件,让用户来选择答案。更多关于lists的内容请参见第19章。 ## **小结** 下面是本教程中所涉及到的概念: * 将应用程序划分为数据(通常保存在列表中)及事件处理程序两个部分;使用Ifelse块来做条件判断,有关条件语句的更多信息,请参见第18章; * 在事件处理程序中,程序块只能引用抽象的名称来指代列表项及列表长度,以便当列表数据发生变化时,程序还可以正常运行; * 索引变量可以跟踪当前项在列表中的位置,当索引递增时,要小心列表的末尾,使用if块来处理应用中的行为。
';

第 7 章 安卓,我的车在哪?

最后更新于:2022-04-01 02:53:36

你把车停得尽量靠近体育馆,但演唱会一结束,你却忘了车停在哪儿,你的同伴也很茫然。幸运的是,你的Android手机还在,它从来不忘事,你新装了一款热门应用“Android,我的车在哪儿?”有了这个应用,在停车时点一下按钮,Android的位置传感器会“记住”车的GPS坐标和地址。当稍后重新打开应用时,它会指给你从现在位置到停车位置的方向,问题解决了! ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa4089ec8.png) ## **学习要点** 本章涵盖如下概念: * LocationSensor组件:确定Android设备的位置; * TinyDB组件:直接在设备数据库中记录数据; * ActivityStarter组件:在应用中打开谷歌地图,并显示从一个位置到另一个位置的方向。 ## **准备开始** 登陆App Inventor网站,开始一个新项目“AndroidWhere”(项目名称不能有空格),将屏幕标题设置为“Android,我的车在哪儿?”,连接测试手机。 ## **设计组件** 应用包含下列可视组件: * 多个Label组件:显示当前位置和“记住”的位置信息,有些Label显示静态文本,如GPSLabel显示“GPS:”;其他Label,如CurrentLatLabel显示来自位置传感器的数据。给这些Label设定一个默认值(0,0),当GPS取得位置信息时,这个值将随之改变; * 两个Button组件:记录位置和指示该位置的方向; 以及三个非可视组件: * LocationSensor组件:获取当前位置信息; * TinyDB组件:永久保存位置信息; * ActivityStarter组件:用于打开谷歌地图,以获得当前位置和记住位置之间的路线。 按照图7-1所示的组件设计器截图来创建组件。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa418ef60.png) **图 7-1 组件设计器中应用的用户界面** 跟随表7-1,逐个拖出组件,并做相应设置,创建如图7-1所示的用户界面。 **表7-1 应用中的所有组件** | 组件类型 | 面板中分组 | 命名 | 作用 | | --- | --- | --- | --- | | Label | User Interface | CurrentHeaderLabel | 显示标题“当前位置”| | HorizontalArrangement | Screen Arrangement | CurrentAddrArrangement | 放置地址信息 | | Label | User Interface | CurrentAddressLabel | 显示“地址:” | | Label | User Interface | CurrentAddressDataLabel | 显示动态数据:当前地址 | | HorizontalArrangement | Screen Arrangement | CurrentGPSArrangement | 安置GPS信息 | | Label | User Interface | GPSLabel | 显示“GPS:” | | Label | User Interface | CurrentLatLabel | 显示动态数据:当前纬度| | Label | User Interface | CommaLabel | 显示“,” | | Label | ser Interface | CurrentLongLabel | 显示动态数据:当前经度| | Button | User Interface | RememberButton | 点击记录当前位置 | | Label | User Interface | RememberedAddressTitleLabel | 显示“已记录的地点” | | HorizontalArrangement | Screen Arrangement |RememberAddrArrangement | 安置已保存的GPS信息 | | Label | User Interface | RememberedAddressLabel | 显示“地址:” | | Label | User Interface| RememberedAddressDataLabel | 显示动态数据:已记录的地址 | | HorizontalArrangement | Screen Arrangement | RememberGPSArrangement | 安置已记录的GPS信息 | | Label | User Interface | RememberedGPSlabel | 显示“GPS:” | | Label | User Interface | RememberedLatLabel | 显示动态数据:已记录的纬度 | | Label | User Interface | Comma2Label | 显示“,” | | Label | User Interface | RememberedLongLabel | 显示动态数据:已记录的经度 | | Button | User Interface | DirectionsButton | 点击来显示地图 | | LocationSensor | Sensors | LocationSensor1 | 感知GPS信息 | | TinyDB | Storage | TinyDB1| 永久保存已记录的位置信息 | | ActivityStarter | Connectivity | ActivityStarter1 | 打开地图 | 用以下方式设置组件属性: * 设置显示静态文本的Label的Text属性为固定文本,参照表7-1; * 设置显示动态GPS数据的Label的Text属性为“0.0”; * 设置显示动态地址的Label的Text属性为“未知”; * 取消勾选RememberButton和DirectionsButton的Enabled属性(设置为不可用); * 设置ActivityStarter属性(表7-2),以便ActivityStarter.startActivity可以打开谷歌地图。(图7-1中ActivityStarter的属性显示不完整。)表7-2中未列出的属性可以留空。 **表7-2 打开谷歌地图所要设定的ActivityStarter属性 | 属性 | 值 | | --- | --- | | Action | android.intent.action.VIEW | | ActivityClass | com.google.android.maps.MapsActivity | | ActivityPackage | com.google.android.apps.maps | > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa4207fd1.png) 提示:ActivityStarter组件可在应用中打开安装在设备上的任何其他Android应用。要打开地图,表7-2中的属性必须一字不差地输入;要打开其他应用,请参阅[http://appinventor.googlelabs.com/learn/reference/other/activitystarter.html](http://appinventor.googlelabs.com/learn/reference/other/activitystarter.html) 中的App Inventor文档。 ## **为组件添加行为** 需要为应用设定如下行为: * 当LocationSensor读取到位置信息时,将数据填写到相应的Label中,表示传感器已经读取到当前位置信息,用户这时可以选择保存此位置信息; * 当用户点击RememberButton时,当前位置信息被复制到“已记录的地点”名下的Label中。这些信息要保存到设备数据库中,以便用户关闭并再次打开应用时,数据不会消失; * 当用户点击DirectionsButton时,打开谷歌地图,并显示“已记录”位置的方向; * 当应用重新启动时,从数据库中加载“已记录”的位置信息。 ### **显示当前位置** 两种情况会触发LocationSensor.LocationChanged事件,(1)传感器首次读取位置信息时;(2)设备的位置变化,传感器读数更新时。首次读数有时仅需几秒钟,但如果GPS卫星信号受到屏蔽,会一直没有读数(也与设备的设置有关)。有关GPS和LocationSensor的更多信息,请参见第23章。 在读取到位置信息时,程序要将数据写到相应的Label中。表7-3列出了所有相关的块。 **表7-3 读取到位置信息时,用户界面显示这些信息所需要的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | LocationSensor1.LocationChanged | LocationSensor | 当手机收到新的GPS读数时,触发该事件 | | set CurrentAddressDataLabel.Text to | CurrentAddressDataLabel | 将当前地址的新数据写入label | | LocationSensor1.CurrentAddress | LocationSensor | 该属性保存了街道地址信息| | set CurrentLatLabel.Text to | CurrentLatLabel | 将纬度信息写入相应的label | | get latitude | Variables | 插入set CurrentLatLabel.Text to块的插槽 | | set CurrentLongLabel.Text to | CurrentLongLabel | 将经度信息写入相应的label| | get longitude | Variables | 插入set CurrentLongLabel.Text to块的插槽 | | set RememberButton.Enabled to | RememberButton | 设置“记住我现在的位置”按钮属性 | | true | Logic | 插入set RememberButton.Enabled to插槽 | #### **块的作用** 如图7-2所示,latitude(经度)和longitude(纬度)是LocationChanged事件的参数,因此可以从Variables抽屉中抓取;但CurrentAddress则不是参数,而是LocationSensor的属性,因此要从LocationSensor抽屉里抓取。LocationSensor除了获取GPS位置信息之外,还通过调用谷歌地图,获得了与位置信息相对应的街道地址信息。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa42548db.png) **图 7-2 使用LocationSensor读取当前位置信息** 事件处理程序还启用了RememberButton,该按钮的初始设置为禁用(未选中),因为在传感器获得读数之前,用户不需要“记住”什么,而现在我们可以为“记住”行为编写程序了。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa47aab9f.png) 测试:用手机(wifi与电脑连接)实时测试位置感知应用是无效的。将程序打包并下载到手机上:选择“buildApp(provide QR code for .apk)”,按照提示在手机上打开应用。GPS及地址信息显示在屏幕上,同时RememberButton变为可用。 如果没有获得读数,检查一下Android设备的位置及安全性设置,并尝试走到户外。要了解更多信息,请参见第23章。 ### **记录当前位置** 当用户点击RememberButton时,当前位置信息被写入“已记录的地点”下方的label中。表7-4显示了实现这一功能所需要的块。 **表7-4 记录并显示当前位置所需要的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | RememberButton.Click | RememberButton | 用户点击按钮时触发该事件 | | set RememberedAddressDataLabel.Text to | RememberedAddressDataLabel | 将传感器获得的地址信息写入“已记录”label中 | | LocationSensor1.CurrentAddress | LocationSensor | 该属性保存了街道地址信息| | set RememberedLatLabel.Text to | RememberedLatLabel | 将纬度信息写入“已记录”label中 | | LocationSensor1.Latitude | LocationSensor | 该属性保存了纬度信息 | | set RememberedLongLabel.Text to | RememberedLongLabel | 将经度信息写入“已记录”label中 | | LocationSensor1.Longitude | LocationSensor | 该属性保存了经度信息 | | set DirectionsButton.Enabled to | DirectionsButton|设置DirectionsButton的Enabled属性 | | true | Logic | 设置DirectionsButton的Enabled属性为真 | #### **块的作用** 当用户点击RememberButton时,当前位置信息将写入“已记录”label中,如图7-3所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa481f8e0.png) **图 7-3 将当前位置信息写入“已记录”label中** 注意到DirectionsButton已可用,这会有点儿小麻烦,因为如果用户立即点击DirectionsButton,记住的位置也是当前位置,因而地图中不会提供方向有关的信息。但是,人们似乎不会这么做,当用户移动位置时(例如步行到演唱会),则当前位置将偏离已记录的位置。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa47aab9f.png) 测试:将应用的新版本下载到手机,并再次测试。当单击RememberButton时,当前位置信息是否被写入到“已记录”的label中? ### **显示“已记录”位置的方向** 当用户点击DirectionsButton 时,应用将打开谷歌地图,地图中显示从用户当前位置到“已记录”位置(即停车的位置)的方向。 ActivityStarter组件可以打开任何Android应用,也包括谷歌地图,但必须做一些相应的设置。不过像打开浏览器或地图这样的应用,设置起来相当简单。 打开地图的关键是设置ActivityStarter.DataUri属性,该属性无异于你在浏览器中直接输入的网址。要想搞清楚这一点,只需在浏览器中打开http://maps.google.com,并询问,比如旧金山与奥克兰之间的方向。当结果出来时,点击地图的左上部的链接按钮,并检查显示的URL。这正是你在应用中所需要的URL。 所不同的是,带有方向的地图涉及到两个位置,即起点和终点,它们分别用一组特定的GPS坐标来表示(而非城市之间)。该URL必须采用以下形式: http://maps.google.com/maps?saddr=37.82557,-122.47898&daddr=37.81079,-122.47710 在浏览器中输入网址,说说看,它指引你跨越了那个著名的地标性建筑? 这里需要为URL设定动态参数:起点地址(saddr)和终点地址(daddr)。在前几章中,你已经学会用join块将文本连接起来,这里也是如此。将当前位置和已记录位置的GPS数据插入到URL中,设置ActivityStarter.DataUri属性为URL,然后调用ActivityStarter.StartActivity。表7-5列出了此项功能所需要的块。 #### **块的作用** 用户点击DirectionsButton时,事件处理程序生成一个地图URL,然后调用ActivityStarter打开地图应用并加载地图,如图7-4所示,用join创建的URL发送给地图应用。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa48bb212.png) **图 7-4 生成一个URL,用来打开地图并指示方向** 最终的URL包含了地图域名([http://maps.google.com/maps](http://maps.google.com/maps))以及两个URL参数:saddr与daddr,用来指定方向的起点位置及终点位置。在本应用中,saddr被设定为当前位置的纬度和经度,而daddr被设定为已记录的停车位置的纬度和经度。 **表7-5 打开一张带有方向指示的地图所需要的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | DirectionsButton.Click | DirectionsButton | 用户点击”指示方向”按钮触发该事件 | | set ActivityStarter1.DataUri to | ActivityStarter1 | 设置要打开地图的URL | | join | Text | 将URL的各组成部分连接起来 | | “http://maps.google.com/maps?saddr=” | Text | URL中固定的部分,后面接起点经纬度 | | CurrentLatLabel.Text | CurrentLatLabel | 当前位置的纬度值 | | “,” | Text | 放在经纬度值之间的逗号 | | CurrentLongLabel.Text | CurrentLongLabel | 当前位置的经度值 | | “&daddr=” | Text | URL中的第二个参数,后面接终点经纬度 | | RememberedLatLabel.Text | RememberedLatLabel | 已记录位置的纬度 | | “,” | Text| 放在经纬度值之间的逗号 | | RememberedLongLabel.Text | RememberedLongLabel | 已记录位置的经度 | | ActivityStarter1.StartActivity | ActivityStarter1 | 打开地图 | > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa47aab9f.png) 测试:用手机下载新的版本并再次测试,一旦取得读数,单击RememberButton然后走开。当单击DirectionsButton时,地图是否提示您如何追溯你的脚步?点击几次后退按钮。你是否有回到了你的应用? ### **永久保存已记录的位置信息** 现在已经具备了一个全功能的应用:记住起点位置,并从当前用户所在的位置绘制一张回到起点的地图。虽然用户“记住”了位置,但假如应用被关闭,然后再重新打开,“记住”的信息也将消失。实际上你希望用户能够记录下车的位置,关闭应用,走到别处,然后重新启动应用,并获取已记录的车辆所在位置的方向。 如果你能想起“开车不发短信”应用(第4章),说明你的思路是正确的,我们需要使用TinyDB数据库来永久保存这些数据,采取的方案也与之前的应用类似: 1\. 当用户点击RememberButton时,位置信息存储到数据库中; 2\. 当应用启动时,从数据库中加载位置信息并保存到一个变量或属性中。 从修改RememberButton.Click事件处理程序开始,来存储这些要被“记住”的信息。存储纬度、经度和地址三组信息,需要三次调用TinyDB.StoreValue。表7-6列出了所要补充的块。 **表7-6 永久保存位置信息所需要的块** | 块的类型 | 所在抽屉 | 作用 | | --- | --- | --- | | TinyDB1.StoreValue(3) | TinyDB1 | 将数据保存在设备数据库中 | | “address” | Text | 插入TinyDB1.StoreValue的tag插槽中 | | LocationSensor1.CurrentAddress | LocationSensor1 | 插入TinyDB1.StoreValue的value插槽中,永久保存地址信息 | | “lat” | Text | 插入第二个TinyDB1.StoreValue的tag插槽中 | | LocationSensor.CurrentLatitude | LocationSensor | 插入第二个TinyDB1.StoreValue的value插槽中,永久保存纬度信息 | | “long” | Text | 插入第三个TinyDB1.StoreValue的tag插槽中 | | LocationSensor.CurrentLongitude | LocationSensor | 插入第三个TinyDB1.StoreValue的value插槽中,永久保存经度信息 | #### **块的作用** 如图7-5所示,TinyDB1.StoreValue将LocationSensor属性中的位置信息保存到数据库中。你该记得在“开车不发短信”中,StoreValue函数有两个参数,tag与value,tag充当已存储数据的标识,value是你实际想保存的数据,即本例中的LocationSensor数据。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa4e7dea2.png) **图 7-5 在数据库中存储被“记住”的位置信息** ### **启动应用时读取“记住”的位置信息** 将数据保存在数据库中,是为了以后可以调用它。在本应用中,如果用户在保存了位置信息之后退出应用,那么当应用重新打开时,你希望从数据库中读出信息并显示给用户。 在前几章中讨论过,应用的启动会触发Screen.Initialize事件,而在启动时从数据库中读取数据是一种惯例,我们也不例外。 使用TinyDB.GetValue函数来读取存储的GPS数据。要读取的存储数据包括地址、纬度及经度,因此要调用GetValue函数三次。像在“开车不发短信”中一样,要事先检查数据库中否保存了数据(如,第一次启动应用时,TinyDB.GetValue将返回一个空文本)。 挑战一下自己,看看是否可以独立创建这些块,然后再与图7-6进行比较。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa59050bf.png) **图 7-6 在应用启动时,从数据库中读取数据,如果数据不为空则显示数据** #### **块的作用** 理解这些块的方法是设想用户的使用过程:用户首次打开应用,先保存位置信息,稍后再次打开应用。首次打开应用,数据库中没有信息可加载,也不必填写“已记录”label或启用DirectionsButton。在后续的使用中,如果确有数据存储,就要从数据库中加载这些位置信息。 首先用“address”为tag(标签)调用TinyDB1.GetValue函数,之前在存储位置信息时使用过这个tag。读取的值保存在变量tempAddress中,并检查其是否为空。 if块将检查从数据库中读出的数据。如果TinyDB对指定的tag没有返回值,则返回空文本。首次启动应用时没有数据可读,直到用户点击了RememberButton。由于变量tempAddress中保存了数据库的返回值,因此if块将检查tempAddress的长度,如果长度>0,则TinyDB有地址信息返回,也表明经纬度.GetValue读出经纬度信息。当设置完所有信息,最后启用DirectionsButton。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa47aab9f.png) 测试:将新版本应用下载到手机,并再次测试。点击RememberButton,并确保“记住”读数。关闭应用并再次打开。那些数据是否还在? ## **完整的应用:Android,我的车在哪儿?** 图7-7显示了完整的“Android,我的车在哪儿?”应用中所用到的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3fa59e2e32.png) **图 7-7 “Android,我的车在哪儿”应用中所有的块** ## **改进** 可以尝试如下改进: * 创建一个“Android,他们在哪儿?”的应用,让一群人可以了解彼此行踪。无论你在徒步旅行还是在公园散步,这个应用都有助于你节省时间,甚至可能挽救生命。应用中的数据是共享的,因此要使用web数据库,用TinyWebDB组件来替代TinyDB。更多信息请参见第22章。 * 创建一个“行踪”应用,用列表来记录自己的位置改变,即行踪。当记录数达到一定数量时,或超过一定时间时,开始一个新的“行踪”,因为即使是轻微的位移也会产生一个新的位置读数。这类应用需要使用列表来存储位置记录,需要帮助时请参见第19章。 ## **小结** 下面是本章涉及到的概念: * LocationSensor组件:可以报告用户的纬度、经度及当前的街区地址。当传感器首次获得数据或数据发生变化(设备移动)时,将触发LocationChanged事件。有关LocationSensor的更多信息,请参见第23章; * ActivityStarter组件:可以在一个应用中打开其他应用,包括谷歌地图。对于地图,需要将ActivityStarter的DataUri属性设置为想要打开的地图的URL地址。如果你想显示两个GPS坐标之间的方向,URL应该写成下面的格式,你可以用实际位置的GPS坐标来替换下面的示例数据:http://maps.google.com/maps/?saddr=0.1,0.1&daddr=0.2,0.2; * join用来将文本片段拼凑(连击)成单一的文本对象,也可以让静态文本与动态数据相连接。对于地图URL来说,GPS坐标就是动态数据; * TinyDB让数据永久地保存在在手机的数据库中。保存在变量或属性中数据,会随着应用的关闭而丢失,但存储在数据库中的数据,可以在每次启动应用时被载入。有关TinyDB和数据库的详细信息,请参见第22章。 ### **资源下载** [AndroidWhere.aia](http://www.17coding.net/download/7/AndroidWhere.aia) [AndroidWhere.apk](http://www.17coding.net/download/7/AndroidWhere.apk)
';

第 6 章 巴黎地图旅游

最后更新于:2022-04-01 02:53:34

本章将创建一个“向导”应用,带给你一次巴黎的梦幻之旅。而你的朋友,虽然不能与你同行,也能借此做一次虚拟的巴黎之旅。创建一个完整的地图应用看似复杂,不过App Inventor提供了ActivityStarter组件,可以为每个选定的虚拟位置打开对应的谷歌地图。创建过程分为两步,首先通过点选菜单打开埃菲尔铁塔、卢浮宫以及巴黎圣母院的地图;然后修改有关参数,使应用同时适用于卫星视图及普通地图视图。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c17a142.png) ## **学习要点** 本章介绍以下App Inventor组件及概念: * ActivityStarter组件:可以在当前应用中打开其他Android应用。本章用它来打开带有多个参数的谷歌地图; * ListPicker组件:用户可以从地点列表中进行选择。 ## **设计组件** 首先创建一个名为“ParisMapTour”的新项目,界面中包含: * Image组件——显示一张巴黎的图片; * Label组件——显示文字; * ListPicker组件——用一个关联按钮来打开列表; * ActivityStarter组件:非可视组件。 组件设计如图6-1所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c1f2eef.png) **图 6-1 设计器中的巴黎地图旅游** 表6-1中列出了创建用户界面所用的组件。从Palette中拖出组件,并修改为指定的名称。 **表6-1 巴黎地图旅游中用到的组件** | 组件类型 | 面板中分组 | 命名 | 作用 | |----|---|---|---| | Image | User Interface | Image1 | 在屏幕上显示巴黎的静态图片 | | Label | User Interface | Label1 | 显示文本:用Android发现巴黎 | | ListPicker | User Interface | ListPicker1 | 显示备选目的地列表 | | ActivityStarter | Connectivity | ActivityStarter1 | 选择目的地之后打开地图应用 | ### **设置ActivityStarter组件的属性** ActivityStarter组件用于在当前应用中,打开其他任何Android应用,如浏览器、谷歌地图,甚至你自己的应用。当用户从你的应用中打开另一个应用时,可以通过单击“后退”按钮返回到你的应用。在ParisMapTour应用中,将根据用户选择打开地图应用,来显示指定的地图。用户可以点击“后退”按钮返回到你的应用中,并继续选择不同的目的地。 ActivityStarter是一个相当底层的组件,需要为它设置一些属性信息,这些信息对于Java Android SDK程序员来说非常熟悉,但对世界上其他99.999%的人来说都很陌生。在本应用中,输入如表6-2中指定的属性,并要小心地使用大小写字母,这非常重要。 **表6-2 用ActivityStarter打开谷歌地图必须设置的属性** | 属性 | 值 | | --- | --- | | Action | android.intent.action.VIEW | | ActivityClass | com.google.android.maps.MapsActivity | | ActivityPackage | com.google.android.apps.maps | 还有一个DataUri属性必须在块编辑器中设置,用于在谷歌地图中打开指定地图。这个属性是动态的,即根据用户对目的地的选择而改变:埃菲尔铁塔、卢浮宫或巴黎圣母院。 现在切换到块编辑器,在编程添加组件行为之前,还有两个细节需要特别关照一下: 1\. 下载文件metro.jpg并加载到项目中,将其设置为Image1的Picture属性; 2\. ListPicker组件自带一个按钮,当用户点击时将列出备选项。将ListPicker1的Text属性设置为“选择巴黎的目的地”。 ## **为组件添加行为** 在块编辑器中,需要定义一个目的地列表,并设定两种行为: * 当应用开始时,为ListPicker组件加载目的地列表,以供用户选择; * 当用户从ListPicker中选择了一个目的地,地图应用将打开,并显示目的地地图。在应用的第一个版本中,只需打开地图,并搜索选中的地点。 ### **创建目的地列表** 打开块编辑器,用表6-3中列出的块创建一个destinations列表变量。 **表6-3 创建destinations列表变量所需的块** | 块的类型 | 所在抽屉 | 作用 | |--|--|--| | Initialize global destinations to | Variables | 创建一个目的地列表 | | make a list | Lists | 添加列表项 | | “埃菲尔铁塔” | Text | 第一个目的地 | | “卢浮宫” | Text | 第二个目的地 | | “巴黎圣母院” | Text | 第三个目的地 | 如图6-2所示,变量destinations将调用make a list函数,其中插入了三个旅游目的地。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c26b313.png) **图 6-2 在App Inventor中创建列表非常容易** ### **让用户选择一个目的地** ListPicker组件用来显示列表项供用户选择。通过将ListPicker的Elements属性设置为某个list,可以预先将备选项目加载到ListPicker中。这里将ListPicker的Elements属性设置为刚刚创建的destinations列表。因为想在应用启动时显示此列表,因此加载列表行为必须在事件Screen1.Initialize中定义。表6-4中列出了所需的块。 **表6-4 在应用启动时加载ListPicker备选项所需的块** | 块的类型 | 所在抽屉 | 作用 | |--|--|--| | Screen1.Initialize | Screen1 | 应用启动时触发该事件 | | set ListPicker1.Elements to | ListPicker1 | 将Elements属性设置为需要显示的列表 | | get global destinations | Variables | 读取目的地列表 | #### **块的作用** 应用启动时触发Screen1.Initialize,如图6-3所示,事件处理程序通过设置ListPicker的Elements属性来显示三个备选目的地。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c2b65bd.png) **图 6-3 在应用启动时需要执行的某些操作必须放在Screen1.Initialize事件处理程序中** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c304304.png) 测试:首先点击”connect”重新连接测试设备;然后在手机上点击 “选择巴黎的目的地”按钮,出现有三个选项的列表。 ### **使用搜索打开地图** 下面编写程序,当用户选中目的地时,ActivityStarter将打开谷歌地图并搜索选定的位置。 当用户选中ListPicker中的项目时,将触发ListPicker.AfterPicking事件;在AfterPicking事件的处理程序中,需要设置ActivityStarter组件的DataUri属性,让它知道要打开哪里的地图,然后用ActivityStarter.StartActivity打开谷歌地图。表6-5列出了此功能所需的块。 **表6-5 用ActivityStarter打开谷歌地图所需要的块** | 块的类型 | 所在抽屉 | 作用 | |--|---|---| | ListPicker1.AfterPicking | ListPicker1 | 当用户选择ListPicker中的某项时触发该事件 | | set ActivityStarter1.DataUri to | ActivityStarter1 | DataUri告诉Maps在启动时打开哪里的地图 | | join | Text | 将两段文本连接成DataUri | | “http://maps.google.com/?q=” | Text | Maps所需的DataUri信息中的第一部分 | | ListPicker1.Selection | ListPicker1 | 用户选中的项(DataUri信息中的第二部分) | | ActivityStarter1.StartActivity | ActivityStarter1 | 打开地图 | #### **块的作用** 用户在ListPicker中选定的项存储在ListPicker.Selection中,选择触发了AfterPicking事件。如图6-4,DataUri属性是“http://maps.google.com/?q=”与选中项组合。如果用户选中了第一项“艾菲尔铁塔”,则DataUri将被设置为http://maps.google.com/?q=埃菲尔铁塔。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c35465c.png) **图 6-4 设置DataUri打开选中的地图** 为打开地图设定了ActivityStarter的其他属性,因此ActivityStarter1.StartActivity块将打开地图应用,并根据DataUri所限定的条件进行搜索。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c304304.png) 测试:重新连接测试设备,点击“选择巴黎的目的地”按钮。当选中了某个地点,是否出现了该地点的地图?谷歌地图提供的“后退”按钮,可以返回到你的应用中并做再次选择,怎么样,有用吗?(可能需要多次单击后退按钮) ## **设立虚拟旅游** 现在我们给应用增添一点趣味,让应用打开一些超级放大的地图,并欣赏巴黎的街景,以便在你旅游的同时,你的朋友在家也能跟随你游览的脚步。要做到这一点,你首先要在谷歌地图中找到那些特定地图的URL地址。继续使用相同的巴黎地标性建筑为目的地,但是当用户选中目的地时,使用选中项的索引值(在列表中的位置),来选择并打开一个指定放大倍数的地图或街景图。 在进行下一步之前,您可能想把到目前为止创建的简单地图应用保存起来(Save Project As),以便接下来所做的事情万一让应用出了问题,你还可以随时找回现有版本,并再试一次。 ### **为特定地图寻找DataUri** 首先在电脑上打开谷歌地图,搜索某个目的地的具体地图: 1\. 在计算机上浏览[http://maps.google.com](http://maps.google.com/); 2\. 搜索地标(例如,艾菲尔铁塔); 3\. 放大到你预期的级别; 4\. 选择你想要的视图类型(如:地址、卫星或街道视图); 5\. 点击地图窗口左上部的Link(链接)按钮,(如图6-5)复制地图的URL地址。这个网址(或部分网址)将用于打开你应用中的地图。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c3e8931.png) **图 6-5 从浏览器中获取特定地图的URL地址** 表6-6显示了即将使用的URL地址。 **表6-6 在谷歌地图上完成虚拟之旅的URL地址** | 地标 | 地图URL | | --- | --- | | 埃菲尔铁塔 | https://maps.google.com/maps?q=%E5%9F%83%E8%8F%B2%E5%B0%94%E9%93%81%E5%A1%94&hl=zh-CN&ie=UTF8&**ll=48.858369,2.294482**&spn=0.001578,0.004128&sll=37.0625,-95.677068&sspn=61.153041,135.263672&**t=h&z=19**&iwloc=A。短网址:http://goo.gl/maps/e3oBk | | 卢浮宫 | https://maps.google.com/maps?q=%E5%8D%A2%E6%B5%AE%E5%AE%AB&hl=zh-CN&ie=UTF8&**ll=48.86061,2.337642**&spn=0.003155,0.008256&sll=48.85826,2.294582&sspn=0.001578,0.004128&**t=h&z=18** | | 巴黎 圣母 院(街景) | https://maps.google.com/maps?q=%E5%B7%B4%E9%BB%8E%E5%9C%A3%E6%AF%8D%E9%99%A2&hl=zh-CN&ie=UTF8&**ll=48.852545,2.348389**&spn=0.006311,0.016512&sll=48.861595,2.33522&sspn=0.001585,0.004128&**t=h&z=17**&layer=c&cbll=48.852545,2.348389&panoid=3yzCn-eLwNIAAAQJORRDXw&cbp=12,30.77,,0,-16.32 | 将表6-6中的网址粘贴到浏览器中,你会看到前两个是放大的卫星视图,而第三个是街景图。 可以用这些URL直接打开地图,也可以用[http://mapki.com](http://mapki.com/)中列出的谷歌地图协议。例如,仅凭GPS坐标来显示埃菲尔铁塔地图。从表6-6的长地址中找出这些坐标及地图geo:协议: geo: 48.858369,2.294482&t=h&z=19 使用这样的DataUri打开的地图,与包含这些GPS坐标的长地址打开的地图基本相同。t = h特指既可以显示卫星视图,又可以显示地址视图的混合模式,而z = 19特指缩放级别。如果你有兴趣详细了解不同类型地图的参数设置,请查看[http://mapki.com](http://mapki.com/)中的文档。 两种类型的URL随你选择用,我们对前两个DataUri使用geo格式,第三个使用长地址格式。 ### **定义DataURIs列表** 创建列表变量DataURIs,其中包含了每个地图的DataURI,如图6-6所示,其中的选项分别与destinations列表中的选项相对应(即第一个dataURI对应第一个目的地:埃菲尔铁塔)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c9566f0.png) **图 6-6 虚拟之旅的地图地址列表** 前两项显示了埃菲尔铁塔和卢浮宫的DataURIs,使用geo:协议。第三个DataURI是在太长,无法完整显示。从表6-6的最后一行复制此网址,并粘贴在text块中。 ### **修改ListPicker.AfterPicking行为** 在第一个版本的ListPicker.AfterPicking中,将ActivityStarter1的DataUri属性设置为字符串“http://maps.google.com/?q=”与用户所选的目的地(如“埃菲尔铁塔”)的串联(或组合);在第二个版本中,AfterPicking程序则更为复杂,用户从一个列表(目的地)中选择,而DataUri要从另一列表(dataURIs)中读取。具体来说,当用户从ListPicker中选中一项,此时需要知道选中项的索引,以便用这个索引从dataURIs列表中选择正确的DataUri。稍后我们会详细解释索引的含义,但为了更好地描述这一概念,要先把这些块创建出来,便于我们理解。这项功能需要许多块,均在表6-7中列出。 **表6-7 根据用户在第一个列表中的选择,来确定在第二个列表中的选择,需要如下的块** | 块的类型 | 所在抽屉 | 作用| | --- | --- | --- | | Initialize global index to | Variables | 变量用来保存用户所选项的索引 | | 数字1 | Math | 变量index的初始值设为1 | | ListPicker1.AfterPicking | ListPicker1 | 当用户选中一项时,触发该事件 | | set global index to | Variables | 将变量值设为所选项在列表中的排列位置 | | index in list | Lists | 获得所选项的位置(索引) | | ListPicker1.Selection | ListPicker1 | 所选项如"埃菲尔铁塔",将其插入index in list的thing插槽 | | get global destinations | Variables | 将其插入index in list的list插槽 | | set ActivityStarter1.DataUri | ActivityStarter1 | 在启动Activity打开地图之前,设置该属性 | | select list item | Lists | 从dataURIs列表中选择一项 | | get global DataURIs | Variables | 读取DataURIs列表 | #### **块的功能** 当用户选中ListPicker1中的某项时触发AfterPicking事件,如图6-7所示。选中的项,如“埃菲尔铁塔”,成为ListPicker.Selection。AfterPicking事件的处理程序借此找到所选项在列表destinations中的位置,即索引值。索引值对应于所选目的地在列表中的位置。所以,如果“艾菲尔铁塔”被选中,则index为1;如果“卢浮宫”被选中,index为2;同样,如果“巴黎圣母院”被选中,则index为3。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c9afa25.png) **图 6-7 根据用户在一个列表中的选择,在另一个列表中做对应的选择** 使用索引在另一个列表(本例中DataURIs)中做选择,选中项被设置为组件ActivityStarter的DataUri属性。一旦设置完成, ActivityStarter.StartActivity就可以打开地图了。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3c304304.png) 测试:在手机上点击“选择巴黎的目的地”按钮,将出现有三个选项的列表。选择其中一个,看看会出现哪张地图。 ## **改进** 下面是一些可以尝试的改进建议: * 创建一个虚拟旅游,目的地富有异国情调,也可以是你的工作场所或学校; * 创建一个可定制的虚拟旅游应用,让用户自己输入旅游地点,并地图URL连接,来生成一个旅游向导。相关数据需要存储到TinyWebDB数据库中,并且让应用可以调用这些已经输入的数据。有关创建TinyWebDB数据库的例子,请参见出题及测验应用。 ## **小结** 下面是本章涉及到的概念: * 列表变量(List Variable):可用于保存像旅行目的地以及地图URL这样的多条目数据; * ListPicker组件:允许用户从项目列表中选择。ListPicker的Elements属性用来保存列表内容,Selection属性用来保存选中的项,而用户选中后将触发AfterPicking事件; * ActivityStarter组件:用于打开另一个应用。本章展示了如何调用地图应用,你也可以打开浏览器或任何其他的Android应用,甚至是你自己创建的应用。更多信息请参见[http://appinventor.googlelabs.com/learn/reference/other/activitystarter.html](http://appinventor.googlelabs.com/learn/reference/other/activitystarter.html)(无法访问); * 想要打开谷歌地图中的详细地图,可以设置ActivityStarter的DataUri属性。如何确定DataUri的值呢?可以在浏览器中打开详细地图,然后点击链接按钮,获得并使用URI: 1\. 直接将URI设置为ActivityStarter的DataUri属性; 2\. 或者使用在[http://mapki.com](http://mapki.com/)中定义的协议,创建你自己的URI。 * Index(索引):根据某一项在整个列表中的位置来定义索引。在ListPicker块中,可以根据用户选中项在列表中的位置来确定索引。获取索引值是至关重要的,例如在本章中,需要用索引值在第二个相关的列表中进行选择。关于List变量及ListPicker组件的更多信息,请参见第19章。 ### **背景知识** **Activity** 中文译为“活动”(名词),是开发人员使用的术语。在Android应用中,一个“活动”通常代表一个单独的屏幕,即App Inventor中的Screen,用来容纳各种用户界面组件、侦听用户事件并做出响应。它有四种基本状态: 1\. 活跃:用户可见并可与之交互; 2\. 暂停:用户部分可见但不能与之交互; 3\. 停止:用户完全不可见,但仍然停留在内存中; 4\. 清除:从内存中将“活动”删除。 **HTTP** 超文本传输协议(HyperText Transfer Protocol)。 * hyper: 亢奋的 * text: 正文,文本 * transfer: 传输 * protocol: 协议 Web浏览器、服务器以及相关的web应用程序都是通过HTTP相互通信的。HTTP是现代全球因特网中使用的公共语言。--摘自《HTTP权威指南》 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3e3ca5ac0f.png) **2012年9月出版的《HTTP权威指南》** **HyperText** 有趣的是,hyper在正式的英汉词典中被译为“亢奋、精力旺盛的”,然而在一个流行的网络词典Urban dictionary(城市词典)中,对它的解释则更为生动形象,这里摘录两段:“当你无法集中注意力,感觉想要跳起来撞墙...通常是一种自然状态下的兴奋...由糖引起的。” 另一则:“当你吃了大量的糖,而且跳来跳去高声喊叫并勒令老师闭嘴。”这两种解释中共同的部分是“糖与跳跃”,用这个词来形容一段文字,首先文字是引人入胜的(糖),其次文字让人跳跃(链接)。而这正是互联网信息给人们的阅读方式带来的改变。 HyperText通常被译为“超文本”,其本意为“有链接的文本”。我们所见到的网页背后就是一个超文本文件,其中包含的链接体现为两个方面,一是可以链接到其他的网页(另一个超文本文件),二是指向某些网络资源(也是文件),如图片、声音、视频等。 **Transfer** 译为“传输”,指的是一个资源从一个位置被转移到另一个位置。比如我们打开一个网页,实际上是一个文件从服务器端被转移到了个人电脑上,并在浏览器中呈现出来。 **Protocol** 译为“协议”。中文“协议”这个词很容易被理解为“合同”,一种在双方或多方之间建立起来的某种书面约定。英文“protocol”的含义之一是“礼仪、礼节”,还有一种解释是“外交官及国家元首必须遵从的仪式和礼节”。想象一下我们的国家主席在天安门广场欢迎外国元首时的一系列安排:是先鸣礼炮呢,还是先奏国歌呢?他们会说什么呢?先说什么,后说什么?这些可是一点都不能含糊的。俗话说“外交无小事”。同样,当两台计算机之间进行交流时,也必须遵守某种约定。如果访问者发出的请求无法让被访问者清楚明白,那么被访问者将不予回应,交流就无法达成;再或者被访问者听懂了访问者的请求,但是给出的回应让访问者无法理解,这样的交流也是无效的。再举个例子,假如你去法国旅游,你用中文跟当地人交流,那会是怎样的情景呢?此时,需要为交流设定一种语言(符号体系),假如你会说英文,你就可以问人家是否也会说英文,如果对方会说英文,这样交流就可以建立起来了。这就是protocol的本质。计算机之间的通信协议保证了全世界计算机之间的互联互通,它规定了通信的规则,包括了三个方面的要素,一是建立通信的请求及应答方法,二是交流过程中要传递的数据的格式,三是规定了通信的顺序(时序)。http协议是众多通信协议中的一种,它规定了互联网上浏览器与服务器之间的通信规则。 **URL** 统一资源定位符(Uniform Resource Locator),由一组字符串构成,用来说明如何访问web服务器上的某项资源。如“http://www.17coding.net/index.html”,它代表了本网站的主页地址(你可能在浏览器地址栏中输入的是www.17coding.net,浏览器替你把输入的内容解释为http://www.17coding.net/index.html),它由三部分组成:①协议声明“http://”; ②服务器的地址(www.17coding.net);③资源在服务器上的具体位置(文件夹)及名称(文件index.html在服务器的根目录下)。 **URI** 统一资源标识符(Uniform Resource Identifier),由一组字符串组成,是web服务器上某项资源在全世界范围内的独一无二的标识。资源从本质上讲就是文件(文件的内容可能是网页、音频、视频、图片等)。现在几乎所有的URI都是URL。 **DataURI** DataURI可以将小型的资源文件(如小图片)通过编码后直接嵌入到网页内,随页面加载直接加以呈现。与普通的图片加载相比,可以减少一次对服务器的http请求。不过DataURI的使用仅限于小型的资源文件,而且可能会影响到页面的加载速度。本章中的ActivityStarter的DataUri属性采用了两种方式,一种是普通的URI,另一种是谷歌地图的内部协议geo(未公开)。[DataURI标准文档](http://tools.ietf.org/html/rfc2397) ### **英汉对照** activity: 活动 starter: 启动装置 list: 列表 picker: 选择器 palette: 调控面板 metro: 地铁,大都市 destination: 目的地 make: 制造 element: 元素 selection: 选择[名词] ### **资源下载** [meteo.jpg](http://vdisk.weibo.com/s/vCICwOOxNOuf)
';

第 5 章 瓢虫快跑

最后更新于:2022-04-01 02:53:31

游戏是移动应用中最令人兴奋的部分,无论是玩游戏,还是做游戏。最近红极一时“愤怒的小鸟”,根据开发者Rovio公司称,第一年下载量达50万次,同时每天运行的人时数超过一百万小时。(甚至有人说要把它拍成故事片!)我们可无法保证电影的成功,但可以让您用App Inventor创建自己的游戏“瓢虫快跑”,里面的瓢虫要吃蚜虫,同时要避免被青蛙吃掉。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d82d67cd6.png) ## **应用描述** 如图5-1所示的“瓢虫快跑”应用,用户可以: * 通过倾斜设备来控制瓢虫移动; * 查看屏幕上的能量指示条,能量会随时间减少,并引起瓢虫的饥饿; * 让瓢虫追逐并吃掉蚜虫来获得能量,抵御饥饿; * 帮助瓢虫躲避青蛙,因为青蛙吃瓢虫。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d832d306d.png) **图 5-1 瓢虫快跑游戏手机截屏** ## **学习要点** 在开始探索本章之前,我们假设你已经完成了第3章MoleMash的学习,并熟悉了过程创建、随机数生成、Ifelse块以及ImageSprite、Canvas、Sound和Clock组件。 本章在复习MoleMash以及前几章内容的基础上,主要介绍以下内容: * 使用多个ImageSprite组件,并检测它们之间的碰撞; * 使用OrientationSensor(方向传感器)组件检测设备的倾斜,并用它来控制ImageSprite; * 改变ImageSprite的显示图片; * 在Canvas组件上画线; * 用Clock组件控制多个事件; * 用变量来记录数值(瓢虫的能量水平); * 创建和使用带参数的过程; * 使用and块。 ## **设计组件** 在应用中,使用一个Canvas组件作为三个ImageSprite组件的活动场地,三个ImageSprite组件分别代表瓢虫、蚜虫和青蛙,此外,还要为青蛙配一个声音组件。OrientationSensor(方向传感器)通过测量设备的倾斜来移动瓢虫,Clock组件用来改变蚜虫的运动方向。另有一个显示瓢虫能量水平的Canvas组件;一个重新启动按钮,当瓢虫饿死或被吃掉时,用来重新启动游戏。表5-1提供了本应用中使用的全部组件列表。 **表5-1 瓢虫快跑游戏中的所有组件** | 组件类型 | 面板中分组 | 命名 | 作用 | | --- | --- | --- | --- | | Canvas | Drawing and Amination | FieldCanvas | 运动场地 | | ImageSprite | Drawing and Amination | Ladybug | 用户控制的角色 | | OrientationSensor | Sensor | OrientationSensor1 | 测试手机的倾斜,控制瓢虫移动 | | Clock | User Interface | Clock1 | 决定何时改变Imagesprite的方向 | | ImageSprite | Drawing and Amination | Aphid | 蚜虫:瓢虫的捕食对象 | | ImageSprite | Drawing and Amination | Frog | 青蛙:瓢虫的捕食者 | | Canvas | Drawing and Amination | EnergyCanvas | 显示瓢虫的能量水平 | | Button | User Interface | RestartButton | 重启游戏 | | Sound | Media | Sound1 | 青蛙吃瓢虫时发出的声音 | ## **准备开始** 下载瓢虫、蚜虫、死瓢虫及青蛙的图像,此外还有青蛙的声音文件。 登陆App Inventor网站,建一个名为“LadybugChase”新项目,屏幕标题设置为“瓢虫快跑”。打开块编辑器并连接到测试设备,将下载的图片及声音文件上载(Upload file)到媒体面板。 如果使用设备而不是模拟器,你需要禁用“屏幕自动旋转”功能,否则当设备旋转时,会改变设备的显示方向。在大多数设备上,可以点击设置->显示,然后取消选中的“屏幕自动旋转”复选框即可。 ## **活动的瓢虫** 在这个“第一人称”的游戏中,瓢虫代表玩家,玩家通过倾斜手机来控制瓢虫的运动。与MoleMash不同,这里玩家被带入游戏,而不是在设备以外用手触碰。 ### **添加组件** 在前几章,我们一次性地创建了所有的组件,但这不是开发人员的习惯做法。相反,通常每次只创建一部分组件,编写相应的程序,并进行测试,然后在进入到下一部分。在本节中,我们先来创建瓢虫并控制它的运动。 * 在组件设计器中创建一个Canvas,命名为FieldCanvas,并设置其宽度为“Fill parent”,高度为300像素; * 在FieldCanvas上放置一个ImageSprite,重命为Ladybug,并设置其Picture属性为活的瓢虫图片。不必在意它的x、y属性,这取决于ImageSprite被放在画布上的位置。 也许你已经注意到,ImageSprites还有Interval、Heading以及speed属性,而这些都是在本程序中要用到的: * Interval属性:在本游戏中可以设置为10(毫秒),来设定ImageSprite自身的移动频率(而不是像MoleMash中那样,运动被MoveTo过程所控制); * Heading属性:指示ImageSprite将要移动的方向。例如:0表示向右,90表示向上,180表示向左,等等。现在就让它取默认值——向右,我们将在块编辑器中改变它; * Speed属性:指定ImageSprite在每个时间间隔内移动的距离(单位为像素)。我们将在块编辑器中设置Speed属性。 瓢虫的运动由OrientationSensor通过检测设备的倾斜程度来进行控制;Clock组件用来每隔10毫秒(每秒100次)检测一次设备的方向,并相应地改变瓢虫的Heading(方向)属性 。我们将在块编辑器中做如下设置: 1\. 添加OrientationSensor组件,它将出现在“不可见组件”区域; 2\. 添加Clock组件,它也将出现在“不可见组件”区域,并设置其TimerInterval属性为10毫秒。对照图5-2检查添加的组件。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d83359946.png) **图 5-2 在组件设计器中为动画瓢虫设置用户界面** ### **添加行为** 切换到块编辑器,创建名为UpdateLadybug的过程(procedure)及Clock1.Timer块,如图5-3所示。尝试不使用抽屉,直接输入块的名字(如“when Clock1.Timer”)来生成块。(请注意,对数字100的乘法操作使用的是星号(*),但图中看不到。)虽然可以单击右键选择添加注释,但这不是必须的。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d8347f8a9.png) **图 5-3 每隔10毫秒改变一次瓢虫的方向及速度** 在UpdateLadybug过程里用到了两个OrientationSensor最有用的属性: * Angle(角度):表示设备倾斜的方向; * Magnitude(幅度):表示设备的倾斜程度,范围从0 (不倾斜)至1(最大倾斜)。 Magnitude乘以100是告诉瓢虫,在每个时间间隔(TimerInterval)内,在某个特定的方向,移动的距离在0到100像素之间。时间间隔为之前在组件设计器中设定的10毫秒。 虽然在连接设备上可以测试瓢虫的移动,但与打包下载到设备上的运行效果相比,瓢虫的速度要么太慢,要么太快。对于安装运行的应用,如果太慢,可以增加速度;相反,则减小速度。 ## **显示能量水平** 在第二个Canvas组件上用一个红色线条来显示瓢虫的能量水平。线条高度为1个像素,宽度为瓢虫的能量值,取值范围从200(健康)到0(死)。 ### **添加组件** 在组件设计器中,在FieldCanvas下方创建一个新的Canvas组件,命名为EnergyCanvas;设置Width属性为“Fill parent”,Height属性为1个像素。 ### **创建变量:Energy** 在块编辑器中,创建一个初始值为200的变量来记录瓢虫的能量水平。(还记得吧,在第2章PaintPot中,第一次使用变量dotSize)以下是具体步骤: 1\. 在块编辑器中,拖出一个initialize global name to块,将name改为energy; 2\. 如果energy块的右侧插槽内有其他块,删掉它:选中并按Delete键或直接拖到垃圾桶; 3\. 创建一个数组块200(直接输入数字200或拖动Math抽屉中的0块),然后插入initialize global energy to块,如图5-4所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d83a666b2.png) **图 5-4 将变量energy初始化为200** 图5-5中显示了当鼠标悬浮在初始化变量块的“energy”文本上时,呼出了全局变量energy的“get”及“set”块; ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d8404c194.png) **图 5-5 从初始化变量块中获得set及get块** ### **画出能量条** 我们要在变量energy与红色线条之间建立通信,使线条长度(像素)与能量值相等。为此创建如下两个类似的组块: 1\. 在EnergyCanvas上从(0, 0)点到(energy, 0)点画一条红线,以显示当前的能量水平; 2\. 在EnergyCanvas上从(0, 0)点到(EnergyCanvas.Width, 0)点画一条白线,在画新能量水平线之前,清除当前的能量水平线。(记得前面设置EnergyCanvas.Width为“Fill parent”。) 然而,最好能创建一个过程,能用任何颜色在EnergyCanvas上画任意长度的线。为此,需要定义两个参数:length(长度)和color(颜色),当程序被调用时,我们只需要指定参数值,就像在MoleMash一章中调用random integer内置过程一样。下面是创建DrawEnergyLine过程的步骤,如图5-6所示。 1\. 进入Procedures抽屉,拖出一个to procedure块; 2\. 点击过程名(可能是“procedure” ),改为“DrawEnergyLine”; 3\. 点击过程块左上角的蓝色方块,呼出两个块:input及input x; 4\. 将input x块插入到input块内,将x修改为color; 5\. 重复步骤4:插入第二块input x并命名为“length”; 6\. 按照图5-6所示,为该过程添加的其余的块:将鼠标悬停在to DrawEnergyLine块的参数color及length文本上,获得get color及get length块;或者从Variables抽屉中直接拖出get块,插入到to DrawEnergyLine内部的块中,点击下拉菜单选择color或length。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d8409f0fd.png) **图 5-5a 为DrawEnergyLine过程添加输入(参数)** ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d8415180f.png) **图 5-6 定义过程DrawEnergyLine** 现在,你已经掌握了创建过程的窍门,让我们再写一个DisplayEnergy的过程,两次调用DrawEnergyLine过程:第一次用来擦除旧线(覆盖整个EnergyCanvas的白线),第二次用来显示新的能量线,如图5-7所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d841afcfa.png) **图 5-7 定义过程DisplayEnergy** DisplayEnergy过程由以下四行命令组成: 1\. 设定画笔颜色为白色; 2\. 画一条贯穿EnergyCanvas的横线(1个像素高); 3\. 设定画笔颜色为红色; 4\. 画一条长度等于energy值的线。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d842276ee.png) 提示:将若干行代码规整到一个过程中,通过调用这个过程来取代逐行地执行这些代码,这个过程被称作重构,这种强大的技术使得程序更易于维护,也更可靠。在这种情况下,如果我们想改变能量线的高度或位置,我们只需对DrawEnergyLine过程做一次修改,而不必分两次来完成这一修改。 ### **饥饿而死** 不同于前几章的应用,本游戏设定了结束环节:如果瓢虫吃不到足够的蚜虫,或者被青蛙吃掉,则游戏结束。此时我们希望瓢虫不再移动(设置Ladybug.Enabled为false),并将活瓢虫图片换成死瓢虫(将Ladybug.Picture设置为已上传的图片文件名)。GameOver过程的创建如图5-8所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d84289466.png) **图 5-8 定义GameOver过程** 再按图5-9所示向UpdateLadybug(由Clock.Timer每10毫秒调用一次)添加红框内的代码: * 减少瓢虫的能量(energy = energy - 1); * 显示新的能量水平(call DisplayEnergy); * 如果energy值为0则游戏结束。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d842dc357.png) 测试:你可以在设备上测试这段代码,并验证能量水平随时间的减少,并最终导致瓢虫死亡。重启应用可以点击“Reset Connection->AI Companion”。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d84338d36.png) **图 5-9 UpdateLadybug过程的第二个版本** ## **添加蚜虫** 下面来添加蚜虫,即让蚜虫在FieldCanvas上浮动。如果瓢虫撞上蚜虫(视同“吃”掉它),则瓢虫的能量水平升高,而蚜虫消失,且稍后会再次出现。(在用户看来,这完全是另一只蚜虫,但实际上是同一个ImageSprite组件。) ### **添加一个ImageSprite** 添加蚜虫首先要回到组件设计器,创建另一个ImageSprite,要确保它不落在瓢虫上,命名为Aphid,其属性设置如下: 1\. Picture属性:设置为已上传的蚜虫图像文件; 2\. Interval属性:设置为10,即:像瓢虫一样,每10毫秒移动一次; 3\. Speed属性:设置为2,因此蚜虫移动不会太快,以便让瓢虫能抓住它。 不必在意它的x、y属性(只要不是在瓢虫上)或title属性,这些可以在块编辑器中设置。 ### **控制蚜虫** 实验发现,蚜虫每隔50毫秒(Clock1跳动5次)改变一次方向的效果最好。可以通过创建第二个Clock组件,并设定其TimerInterval属性为50毫秒来实现这一效果。但是,我们希望能够尝试不同的技术:使用random fraction(随机分数)块,每次调用,它都将返回一个≥0但<1的随机数。创建UpdateAphid过程,并用Clock1.Timer来调用它,如图5-10所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d843ce221.png) **图 5-10 添加UpdateAphid过程** #### **块的作用** 定时器每次跳动(每秒100次)都将调用UpdateLadybug及UpdateAphid过程。UpdateAphid过程首先生成一个介于0到1之间的随机数,例如0.15,如果该数<0.20(在20%的时间里),蚜虫将改变方向,改变的角度为0到360之间的随机数;如果该数≥0.20(在其余80%的时间里),蚜虫方向保持不变。 ### **瓢虫吃掉蚜虫** 下一步,当他们碰撞时,让瓢虫“吃掉”蚜虫。幸运的是,App Inventor提供了ImageSprite组件之间的碰撞检测。问题是:当瓢虫与蚜虫碰撞时,会发生哪些事情?在继续阅读之前,请你停下来想想这个问题。 为了处理瓢虫与蚜虫的碰撞,创建EatAphid过程,其具体步骤如下: * 瓢虫的能量水平上升50,来模拟享受美食; * 让蚜虫消失(设置其Visible属性为false); * 让蚜虫停止移动(设置其Enabled属性为false); * 让蚜虫移动到屏幕上任意位置(这与MoleMash中移动地鼠遵循了相同的编码方式)。 请对照图5-11检查您的块。如果你还能想到发生其他事情,比如音效,可以自行添加。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d84447eb2.png) **图 5-11 创建EatAphid过程** #### **块的作用** 每次调用EatAphid,变量energy增加50,缓解了瓢虫的饥饿。然后,蚜虫的Visible及Enabled属性都被设置为false,看上去像是消失了。最后,产生随机的x、y坐标,并调用Aphid.MoveTo,这样,蚜虫会在一个新位置再次出现(否则,它一出现便会被立即吃掉)。 ### **瓢虫与蚜虫之间的碰撞检测** 图5-12显示了在瓢虫与蚜虫之间做碰撞检测的代码。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d844aa265.png) **图 5-12 检测并处理瓢虫与蚜虫之间的碰撞** #### **块的作用** 当瓢虫与另一个ImageSprite碰撞时,将调用Ladybug.CollidedWith,参数“other”指向任何与瓢虫发生相撞的ImageSprite。此时,只有蚜虫可以碰撞,但稍后会有青蛙加入进来。我们采用防御性编程方式,即在调用EatAphid之前,要确认碰撞的对象就是蚜虫;此外还要确认蚜虫可见,否则,蚜虫在被吃掉之后而重新出现之前,还会与瓢虫再次碰撞。如果缺少这项确认,隐形的蚜虫会被再次吃掉,并引起能量水平的再次增加,这会让用户感到费解。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d842276ee.png) 提示:防御性编程是一种避免错误的编程方式,当程序被修改时,仍然可以正常工作。在图5-12中,对other=Aphid的检查并不是绝对必要的,因为此时瓢虫可碰撞的唯一对象就是蚜虫,但检查可以防止后续程序的错误:当添加另一个ImageSprite(青蛙)时,如果忘记了修改Ladybug.CollidedWith,程序就会出错。通常来说,程序员修复bug的时间要多余写新代码的时间,所以多花一点时间尝试防御型编程是非常值得的。 ### **蚜虫的回归** 最终蚜虫要重新出现,按图5-13所示修改UpdateAphid:仅当蚜虫可见时,令其改变方向(改变一个不可见的蚜虫岂不是浪费时间。);若蚜虫不可见(如刚刚被吃掉),将有1/20(5%)的机会重新出现,或者说会被再吃掉。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d854435a4.png) **图 5-13 修改UpdateAphid使隐形蚜虫起死回生** #### **块的功能** UpdateAphid变得有些复杂,让我们仔细推敲一下: * 如果蚜虫可见(这应该是常态,除非刚刚被吃掉),UpdateAphid的行为没有变化,即有20%的几率改变方向; * 如果蚜虫不可见(刚被吃掉),则执行“else”部分。首先生成一个随机分数,如果它<0.05(在5%的时间里),蚜虫再次变得可见并且可用,即,有资格被再次吃掉。 因为Clock1.Timer每隔10毫秒调用一次UpdateAphid,而当蚜虫隐形后,只有1/20(5%)的机会恢复可见,因此蚜虫平均重现的时间是200毫秒(1/5秒)。 ## **添加重新启动按钮** 在测试蚜虫被吃的新功能时,你可能已经注意到,游戏的确需要一个重新启动按钮。(在创建应用过程中,将应用分解成小的功能模块,完成一块就测试一块,这种开发模式大有裨益。在测试过程中,经常会发现一些被忽略了事情,一边做一边测一边改,比起整个应用完成之后再回来做修改,要容易得多。)在组件设计器中,将Button组件添加在EnergyCanvas下方,改名为“ResetButton”,并设置其Text属性为“重新启动”。 在块编辑器中,创建当RestartButton被点击时的代码,如图5-14所示: 1\. 能量水平设置回200; 2\. 重新使蚜虫可见并且可用; 3\. 重新使瓢虫可用,并将其图片改为活的瓢虫(除非你想要僵尸瓢虫!)。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d854d4041.png) **图 5-14 按下“重新启动”按钮让游戏重新开始** ## **添加青蛙** 到目前为止,让瓢虫活着并不难,因此我们需要一个捕食者。就是说我们要添加一个奔向瓢虫的青蛙,如果发生碰撞,瓢虫被吃掉了,游戏结束。 ### **让青蛙追捕瓢虫** 首先回到组件设计器,在FieldCanvas上添加第三个ImageSprite组件Frog,设置其Picture属性为相应的图片,interval属性为10,Speed为1,让它的移动速度慢于其他生物。 图5-15显示了Clock1.Timer中调用了新创建的过程。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d8555b470.png) **图 5-15 让青蛙向着瓢虫移动** #### **块的功能** 现在你应该可以熟练地使用随机分数来控制事件的发生几率了,这里,青蛙有10%的机会直接直奔向瓢虫。这里用到了三角函数,但别害怕,你不必明白其中的道理!App Inventor提供了大量的数学函数,也包括三角函数。本例中使用的ATAN2(反正切)块,返回值是一个角度,该角度由一组给定的x、y值相对应。(如果你熟悉三角函数,会发现求解ATAN2时所用的y值与你所期望的y值符号正好相反,即y的减法顺序是反的,这是因为在Android的画布上,向下是y坐标的增加方向,这与标准的x-y坐标系正相反。) ### **让青蛙吃掉瓢虫** 现在需要修改碰撞代码,如果瓢虫与青蛙碰撞,能量水平以及能量线都将变为0,且游戏结束,如图5-16所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d855e9297.png) **图 5-16 让青蛙吃掉瓢虫** #### **块的功能** 在第一个if(瓢虫与蚜虫的碰撞检测)块的基础上,添加了第二个if块,来检测瓢虫与青蛙的碰撞。如果瓢虫和青蛙碰撞,将发生三件事情: 1\. 变量energy降为0,瓢虫失去生命力; 2\. DisplayEnergy被调用,以清除此前的能量线(并绘制新的空白线); 3\. 调用前面写过的GameOver过程,以便让瓢虫停止移动,并将图片改为死瓢虫。 ### **瓢虫回归** RestartButton.Click已经用程序将死瓢虫图片替换成了活的图片。现在,需要添加代码将瓢虫移动到任意位置。(想想看,在新游戏开始时,如果没有移动瓢虫,会发生什么?瓢虫与青蛙的位置关系如何?)图5-17显示了游戏重新启动时用来移动瓢虫的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d85657219.png) **图 5-17 ResetButton.Click的最终版本** #### **块的功能** 两个版本的RestartButton.Click之间,唯一的差别就是Ladybug.MoveTo块及其参数。调用了两次内置的随机整数函数,分别用于生成合法的x、y坐标。虽然没有任何设置来防止瓢虫与蚜虫、瓢虫与青蛙的位置上的重叠,但几率起到了决定性作用。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d842dc357.png) 测试:重新启动游戏,并确信瓢虫出现在一个新的任意位置。 ## **添加音效** 测试游戏时,你可能注意到:当蚜虫或瓢虫被吃掉时,缺少良好的反馈。要添加音效及触觉反馈,请执行以下操作: 1\. 在组件设计器中添加一个Sound组件。设置其Source属性为已上传的声音文件; 2\. 进入块编辑器,进行如下操作: * 在EatAphid过程中添加Sound1.Vibrate块,参数为100毫秒,以便在蚜虫被吃掉时,设备产生振动; * 在Ladybug.CollidedWith中调Sound1.Play,位置在调用GameOver之前,以便当青蛙吃掉瓢虫时发出叫声。 ## **改进** 下面这些想法目的是改进游戏,或者让游戏更个性化: * 目前,当游戏结束时,青蛙和蚜虫还在移动,这与其Enabled属性有关:在GameOver中将其设置为false,并在RestartButton.Click中重新设置为true; * 设置并显示一个分数,来表示瓢虫的存活时间。你可以用Label来显示一个数值,该数值在Clock1.Timer内不断递增; * 将EnergyCanvas的Height属性增加为2,以便使能量条更加明显,并在DrawEnergyLine内画两条线,一个在另一个之上。(使用一个过程,而不是复制代码先擦除再重绘能量线,这样做的另一个好处是:如果你需要修改线的粗细、颜色或位置时,只需要修改一处的代码。) * 添加背景图和更多音效来渲染气氛,比如用真声或预警声来提示瓢虫能量水平的降低; * 让游戏随时间推移而变得越来越难,如增加青蛙的速度,或降低Interval属性值; * 从技术上来说,被青蛙吃掉的瓢虫应该消失。改变游戏规则:如果瓢虫被吃,则隐形;如果是饿死,则显示死瓢虫图; * 将瓢虫、蚜虫和青蛙的图片换成更适合你个人口味的图片,如霍比特人、半兽人以及邪恶的巫师;或者是反叛星际战斗机、能源舱及帝国战机。 ## **小结** 已经有两个游戏被你收入囊中(假设你学习了MoleMash),现在你该知道如何创建自己的游戏了,这是许多新程序员或有志者的目标!具体来说,您学习了: * 可以创建多个ImageSprite组件(瓢虫,蚜虫和青蛙),并在它们之间做碰撞检测; * 用OrientationSensor可以检测设备的倾斜,而测得的值可用于控制sprite(或你能想到的任何其他对象)的移动; * 一个Clock组件可以控制多个发生频率相同(改变瓢虫和青蛙的方向),或通过使用random fraction块来控制频率不同的事件。例如,如果你想在一个周期中,有大约1/4(25%)的时间里会发生某事件,只要将它放在if块中,并设定条件为random fraction的结果<0.25即可; * 一个应用中可以使用多个Canvas组件,我们的例子中有两个,一个用于游戏场地,另一个用于变量的图形化显示(而不是用Label显示); * 用户自定义过程可以使用参数(如在DrawEnergyLine 中使用的“color”及“length”)来控制其行为,这极大地扩展了过程抽象的能力。 另一个游戏中常用的组件是Ball,与ImageSprite唯一不同的是,它的外观是一个被填充的圆形,而不是一张任意的图片。 ### **资源下载** [frog.png](http://www.17coding.net/download/5/frog.png) [ladybug.png](http://www.17coding.net/download/5/ladybug.png) [deadladybug.png](http://www.17coding.net/download/5/deadladybug.png) [aphid.png](http://www.17coding.net/download/5/aphid.png) [frog.wav](http://www.17coding.net/download/5/frog.wav) ### **英汉对照** orientation: 方向 field: 场地 ladybug: 瓢虫 aphid: 蚜虫 frog: 青蛙 energy: 能量 restart: 重新开始 chase: 追逐,奔跑 upload: 上载,上传 file: 文件 interval: 间隔 heading: 前进方向 speed: 速度 timer: 计时器 updat: 更新 angle: 角度 magnitude: 幅度 delete: 删除 procedure: 过程 input: 输入 display: 显示 enable: 使有效 reset: 重置 visible: 可见的 eat: 吃 collide: 碰撞 with: 与... else: 否则 fraction: 分数
';