我们写这本书的目的是向每一个开发者打开移动和设备开发的奇妙世界。您不再需要学习定制的移动编程语言或成为移动应用设计专家来编写好看、专业的业务应用。我们相信,在未来,手机和平板电脑将只是应用开发人员的另一个部署目标,借助移动闪存和Fle×技术,这一未来就在眼前。
这本书以对Flash工具链和底层技术的温和介绍开始,并通过例子教授编程概念。如果你有另一种基于C语言的经验,如Java、JavaScript或Objective-C,这本书的进度将允许你在学习Flash和Fle×mobile概念和API的同时学习ActionScript和MXML。
Flash和Fle×平台利用了Android的所有强大功能,同时使程序员不必处理AndroidAPIs和编程模型的复杂性。这意味着,只要终端用户对Android有简单的了解,你就可以成为一名应用开发人员,向AndroidMarket发布你自己的基于Flash的应用。
让我们面对现实吧——你拿起这本书不仅仅是为了成为另一个移动开发者。你想要拓展平台的极限,挖掘超越平均水平的特性和能力,并构建出酷毙了的应用。
我们与你同在,这就是为什么我们在开发这本书时将技术推向极限。在本书后面的章节中,您将学习如何利用原生Android功能,配置和调整您的应用以获得最佳性能,以及部署到除简单手机之外的各种不同设备。
我们不是那些以写书为生的普通无名作者。我们是应用开发人员和技术极客,就像您一样。我们投资于我们讨论的技术、移动开发的未来,以及最重要的,您作为未来Flash平台开发人员的成功。
所有作者都在网上有大量的社区参与,包括领先的Adobe用户组和技术宣传。我们对这项技术感到兴奋,并乐于接受问题、询问和对话。我们不仅仅是另一个作者团队,而是您自己的个人Flash开发团队。
通过阅读这本书和编写练习,你会学到很多东西,但不要止步于此。开始与其他读者和Flash开发者对话。加入专门研究Flash和Fle×技术的技术用户组。带着问题、想法、概念和猜想联系我们,作者。
最重要的是,让技术成为你自己的。
这本书,ProAndroidFlash,是使用无处不在的Flash平台在移动设备上构建丰富、普及的用户体验的权威指南。我们将向您展示如何利用构成Flash平台的强大而成熟的技术、框架和工具来构建高度定制的应用,这些应用充分利用了用户要求其设备具备的所有移动功能。在阅读本书时,您将获得针对移动Android设备的基本知识,包括设备密度、硬件输入、本机集成和性能优化。
有许多不同的移动平台可供选择,也有大量的移动和平板设备可供消费者选择。与桌面市场不同,桌面市场已经有了大量的整合和巩固,移动市场也在不断发展,不断推出新的设备和功能。
显而易见的问题是,你的目标平台是哪个?我们的答案是从安卓开始;然后,通过利用闪存技术,您可以避免受限于任何特定的平台。
这本书着重于在运行Android操作系统的设备上创建应用。这是因为Android正在迅速成为世界上最受欢迎的移动操作系统,对不同硬件平台和多种外形的支持最好。
根据尼尔森公司的数据,Android是2010年下半年购买智能手机的人的首选。黑莓RIM和苹果iOS在统计上处于第二位,如图图1–1所示。
图1–1。美国移动OS流量份额1
这可能是由于许多不同的因素,包括平台是开源的这一事实,这吸引了设备制造商,AndroidMarket、谷歌的设备内应用店面或谷歌体验提供的相对自由,谷歌体验为最终用户提供了Gmail、谷歌地图、Gtalk、YouTube和谷歌搜索的无缝集成。不管Android流行的原因是什么,很可能你的大部分客户已经拥有Android设备,或者正在考虑在不久的将来购买一台。
与此同时,您正在构建一个具有巨大横向增长潜力的平台。Android只是Flash平台的开始,它受益于一个抽象的虚拟机和API,这些API旨在跨多个不同的操作系统和设备工作。您可以利用Flash为所有移动应用带来的跨平台透明性。
Adobe启动了OpenScreenProject,2,这是一项全行业的计划,旨在将Flash驱动的应用的优势带到您生活中的所有屏幕上。Adobe已经宣布了支持iOS、黑莓、Windows7和webOS的计划,将你从平台锁定中解放出来。
黑莓支持最初针对其平板电脑操作系统,第一个可用的设备是黑莓PlayBook。预计这种支持将来会扩展到包括它的其他移动设备。
苹果仍然限制在浏览器中运行Flash,但它已经开放了应用商店,允许第三方框架。这意味着对于iOS设备,您可以在任何iOS设备上部署Flash作为AIR应用,包括iPodtouch、iPhone和iPad。
您还可以在浏览器中支持Flash的任何设备上部署Flashweb应用。这包括谷歌电视、webOS和Windows7。未来,我们有望看到更多支持闪存技术的平台。
Android是一个完整的移动堆栈,包括操作系统、服务和基础设施,以及一组核心应用。虽然您不需要成为Android方面的专家来有效地编写Flash应用并将其部署到Android设备上,但是熟悉Android的工作方式确实会有所帮助。
Android的核心是基于Linux操作系统。它使用Linux内核的修改版本,该版本具有额外的驱动程序并支持移动硬件设备。
在此之上,有一组库和核心服务组成了基本的Android功能。你很少会直接与这些库进行交互,但是每当你播放一个媒体文件,浏览一个网页,甚至在屏幕上绘图,你都在经历一个核心的Android库。
原生Android应用是使用编译成Dalvik字节码的Java编程语言编写的。Dalvik是Android特殊虚拟机的名称,它抽象了硬件并支持垃圾收集等高级功能。您运行的所有Android应用(包括AdobeAIR应用)都在Dalvik虚拟机中执行。
完整的Android系统架构,按Linux内核、库和运行时、应用框架和应用细分,如图1–2所示。
图1–2。安卓系统架构3
除了拥有非常坚实的技术基础,Android还在不断发展,以适应新的硬件进步。Android平台的一些当前功能包括:
这些功能使得Android平台成为构建移动应用的一个非常强大的基础。此外,AdobeFlash和AIR构建在这些基础功能之上,使Flash成为开发Android应用的绝佳平台。
AdobeFlashPlatform是一个完整的系统,集成了运行在不同操作系统、浏览器和设备上的工具、框架、服务器、服务和客户端。许多行业的公司都使用FlashPlatform来消除设备和平台碎片,并开发出一致且富于表现力的交互式用户体验,不受设备限制。让我们来看看Flash平台的运行时和工具。
创建Flash应用时,您可以选择两个不同的部署目标。第一个是AdobeFlashPlayer,这是一个嵌入式浏览器插件,第二个是AdobeAIR,这是一个独立的客户端运行时。这两个选项都可以在桌面和移动设备上使用,并为您提供了很大的灵活性来定制您的应用部署,以满足最终用户的需求。
据Adobe称,FlashPlayer安装在98%的联网电脑和超过4.5亿台设备上,4为运行在客户端的应用提供了最广泛的应用。2011年,Adobe预计将有超过1.32亿部智能手机支持FlashPlayer,并且已经有超过2000万部智能手机预装了FlashPlayer。预计2011年还会有另外50款新的平板设备支持FlashPlayer。
AdobeFlashPlayer在浏览器中的安全容器中运行。这允许您将Flash内容与用HTML和JavaScript编写的其他web内容混合在一起。您还可以获得免安装操作的优势。
目前为FlashPlayer发布内容的设计人员和开发人员也可以重新利用相同的内容来为AdobeAIR运行时制作应用。在撰写本文时,有8400万部智能手机和平板电脑可以运行AdobeAIR应用,Adobe预计到2011年底将有超过2亿部智能手机和平板电脑支持AdobeAIR应用。
AdobeAIR将Flash扩展到浏览器之外,允许您的内容从AndroidMarket下载并作为一流的应用安装。此外,AdobeAIR应用可以请求用户的许可,以访问受限的硬件,如照相机、麦克风和文件系统。
Table1–1总结了在FlashPlayer中部署或作为AdobeAIRmobile应用部署的优势。由于AIR是FlashAPIs的适当超集,因此也可以创建部署在两者下的单个应用。
Adobe非常积极地向Flex框架添加移动功能,如视图、触摸支持和移动优化皮肤。在本书中,我们将利用AdobeFlex技术来演示移动API。同时,我们将演示纯ActionScriptAPIs的使用,如果您正在构建一个不包含FlexSDK的应用,则可以使用这些API。
自从CreativeSuite5.5(CS5.5)发布以来,所有用于Flash和Flex开发的Adobe工具也支持移动开发。
Table1–2列出了Adobe提供的工具,您可以使用这些工具通过Flash和Flex开发移动应用。它们之间的互操作非常紧密,这使得利用每种工具的优势变得很容易。这扩展到Adobe设计工具,如InDesign、Photoshop、Illustrator和Fireworks,它们可用于为您的应用开发内容,这些内容将直接插入到您的Flash和Flex应用中。
AdobeFlashBuilder软件旨在帮助开发人员快速开发适用于Flash平台的跨平台富互联网应用和游戏。用户可以通过编写ActionScript代码来创建游戏,就像使用FlashProfessional一样。借助FlashBuilder,您还可以使用Flex框架编写应用,Flex框架是一个用于开发和部署富互联网应用(RIA)的免费、高效的开源框架。
如果您正在开发一个具有复杂UI和复杂算法或业务逻辑的大型应用,您肯定会希望添加FlashBuilder4.5。它基于全功能的EclipseIDE,提供了您期望从专业开发环境中获得的一切,包括代码导航、键盘加速器和完整的GUI生成器。
DeviceCentral是FlashProfessional附带的补充应用,允许您在桌面上模拟不同的移动设备,包括对倾斜、多点触摸和加速度计的支持。它还让您可以访问一个巨大的信息库,其中列出了Flash平台支持的所有可用的移动和嵌入式设备,包括完整的规格和自定义仿真器。
注意:截至本文撰写时,DeviceCentral尚未更新到AIR2.6以支持Android设备。
FlashCatalyst是Adobe的快速应用开发平台。它允许您将Photoshop、Illustrator或Flash中制作的艺术资源转化为一流的UI控件。Catalyst的移动工作流是创建或修改包含您的组件和素材的FXP文件,然后在FlashBuilder中打开它,添加业务逻辑并在移动平台上运行。
所有这些应用都可以免费试用;但是,如果您想使用纯开源堆栈进行开发,您可以使用FlexSDK直接从命令行进行Flex和ActionScript开发。作为FlashBuilder和Catalyst基础的所有组件都是FlexSDK的一部分,可以通过编程方式访问。如果您正在配置一个自动构建来编译和测试您的Flex应用,这也是您想要使用的。
除了已经列出的工具,Adobe还有一个强大的工作流程,允许设计人员使用AdobeInDesign、AdobePhotoshop、AdobeIllustrator和AdobeFireworks等程序将图形移动到FlashProfessional或FlashBuilder中进行进一步开发,如图1–3所示。这意味着在处理图形时很少出现转换问题,也不存在将图形从设计转移到开发的漫长过程。
图1–3。从设计到开发再到发布到多个平台/设备的Flash工作流程
我们将在第九章中更详细地讨论设计人员/开发人员的工作流程,给出如何在不同工具之间简化工作流程的真实例子。
开始编写Flash应用的最简单方法是使用AdobeFlashProfessional。它为构建简单的电影提供了一个可视化的环境,也为构建更复杂的逻辑提供了良好的ActionScript编辑功能。
图1–4。FlashProfessional新建模板对话框
这将创建一个新项目,画布的大小完全适合纵向模式下的移动项目,并且允许您在FlashProfessional中或通过USB在设备上测试您的应用。有关设备部署的更多信息,请参阅第五章“应用部署和发布”。
为了演示设备功能,我们将创建一个名为FlashCapabilityReporter的简单应用。它将会有一个简单的滚动列表,列举出你正在运行的模拟器或设备的所有功能。
对于ActionScript代码,我们将使用来自Capabilities和Multitouch类的静态常量。其中大多数返回true或false,但有些会返回string或integer值。通过使用字符串连接操作符,我们可以很容易地对它们进行显示格式化,如清单1–1所示。
清单1–1。Flash能力校验码
`importflash.system.Capabilities;importflash.ui.Multitouch;
虽然这在功能上已经完成,但我们在完成的图书样本中添加了一些额外的图形细节,包括以下内容:
图1–5。桌面上ADL中运行的FlashCapabilityReporter应用
您可以在自己的开发过程中使用该示例来比较桌面和移动设备的功能。您可以随意添加功能列表,并尝试在不同设备上运行。
您会注意到,即使我们在ADL的移动模式下运行,返回的值也与您在设备上运行时得到的值不一致。在本章的后面,我们将向你展示如何在Android模拟器中或者通过USB在设备上运行你的应用。
新版FlashBuilder为移动设备构建Flash和Flex应用以及直接从IDE运行和调试这些应用提供了强大的支持。在本节中,我们将向您展示如何从头开始创建一个新的移动项目,演示Flex移动开发的基础知识,包括视图、控件和多点触摸手势识别。
我们将创建的应用称为手势检查。它允许您分析您的设备,以直观地发现支持哪些手势,并测试它们是否被成功识别。在创建此示例的过程中,您将全面了解FlashBuilder的移动功能,包括如何创建新的Flex移动项目、使用FlashBuilder调试器调试应用以及通过USB部署在设备上运行应用。
图1–6。Flex移动项目创建向导
将项目命名为GestureCheck,并选择一个文件夹来存储项目。
提示:如果您创建的项目名称中没有空格,Flex将创建与您选择的名称相匹配的项目文件。如果您的名称包含空格、破折号或其他在ActionScript标识符中无效的字符,它将使用通用名称“Main”来代替。
完成后,点击下一步进入向导的移动设置页面,如图Figure1–7所示。
图1–7。【移动设置】选项卡用于选择应用模板和设置
FlashBuilder附带了几个用于开发移动项目的内置模板,可用于快速启动新项目。其中包括一个简单的空白应用、一个从主页开始的基于视图的应用,以及一个允许您在不同命名视图之间切换的选项卡式应用。您可以在第三章中找到更多关于视图和选项卡导航的信息。
在本练习中,选择默认的基于视图的基本应用模板。您还可以选择重定向、全屏模式和密度缩放。确保禁用自动重定向,使应用停留在纵向模式。我们将在第二章中更深入地讨论纵向/横向切换。
在移动设置页面上完成后,单击完成创建您的移动应用。
首先,Flex模板为您提供了以下项目结构(标有internal的文件您永远不要直接修改):
清单1–2。第一次手势显示的UI元素
图1–8。Flash移动运行配置对话框
首先,我们将使用桌面上的AIRDebugLauncher(ADL)运行应用。为此,选择桌面启动方法,并选择一个合适的设备进行模拟(对于本例,您将需要选择一个具有高密度显示屏的设备,如DroidX)。
单击Run按钮将在ADL中执行应用,向您显示您之前添加的UI元素,如图Figure1–9所示。
图1–9。手势检查用户界面
这构建了基本的UI模式,但是没有连接任何应用逻辑来设置CheckBoxes的状态。为了实现这一点,我们将使用一个initialize函数,该函数遍历由Multitouch类报告的所有supportedGestures。这显示在清单1–3中。
清单1–3。检测手势支持和用法的附加代码以粗体突出显示
privatefunctioninit():void{foreach(vargesture:StringinMultitouch.supportedGestures){this[gesture+"Enabled"].selected=true;addEventListener(gesture,function(e:GestureEvent):void{e.currentTarget[e.type+"Tested"].selected=true;});}}]]>
注意,我们已经向CheckBoxes添加了一些id,以便从initialize函数中引用它们。命名约定是手势名称附加“启用”或“已测试”字样。在设置selected状态的代码中使用了相同的命名约定。
当创建视图时,init函数被调用一次,并遍历所有的supportedGestures。它将启用的CheckBox的状态设置为true,并添加一个事件监听器,当在应用中使用该手势时,该监听器会将测试的CheckBox的状态设置为true。如果你想了解更多关于手势和事件监听器的功能,我们会在第二章中详细介绍。
如果您运行更新后的示例,您将获得相同的UI,但也会触发一个错误。ActionScript错误对话框显示在Figure1–10中,虽然您可能很清楚程序中的问题是什么,但我们将利用这个机会演示FlashBuilder调试器是如何工作的。
图1–10。更新后的应用执行时ActionScript出错
注意:只有在启用了手势支持的电脑上运行,比如带触摸板的Macintosh,才会出现前述错误。相反,您可以在带有触摸屏的移动设备上运行,以重现相同的错误。
在上一节中,我们在运行应用时遇到了一个错误,但是错误窗口在识别发生了什么或者让我们检查当前状态方面并没有特别大的帮助。事实上,如果我们在移动设备上运行应用,它会继续执行,甚至不会让我们知道发生了错误。虽然这种行为对于生产应用来说是理想的,如果执行可以安全地继续,您不希望小错误困扰您的最终用户,但是这使得调试应用具有挑战性。
当您这样做时,与正常应用运行的唯一显著区别是,您现在将在控制台面板中获得跟踪输出和错误。当试图诊断应用行为时,这本身就非常有用;但是,如果在执行过程中遇到错误,系统会询问您是否要切换到FlashDebug透视图,如图Figure1–11所示。
图1–11。Flash调试透视图突出显示手势检查应用中的错误
FlashDebug透视图使您能够在应用执行时查看其内部,这是非常强大的。在左上角的调试窗格中,您可以启动和停止您的应用,以及导航堆栈框架,例如我们遇到的错误情况。
由于我们已经确定了问题,您可以停止应用,并通过使用FlashBuilder窗口右上角的透视图选择器切换回代码透视图。
正如您将在第二章中了解到的,Android上的Flash支持五种不同的手势事件。这些措施如下:
清单1–4展示了完整的应用,它将让您尝试这些手势。
清单1–4。手势检查示例应用的完整代码清单
如果您从ADL桌面模拟器测试此应用,您将会根据您的桌面手势支持获得不同的结果。对于不支持多点触摸的机器,不会启用任何手势;然而,如果你足够幸运,拥有一台带有支持多点触摸的触摸板的台式机或笔记本电脑,你或许可以对该应用进行一些有限的测试,如图Figure1–12所示。
图1–12。在配有触控板的MacBookPro上运行时,手势支持有限
虽然它报告五个手势中有四个是启用的,但在我们用来执行这个示例的计算机上,实际上只可能执行平移、旋转和缩放。正如我们将在下一节中看到的,在完全支持所有多点触摸手势的设备上运行它会有趣得多。
FlashBuilder使在移动设备上执行应用变得非常容易。只需点击一下,它就可以部署应用,在设备上启动它,甚至还可以连接一个远程调试器。
图1–13。安卓开发设置屏幕
如你所见,在一个真实的设备上,锻炼所有的手势事件是可能的。当测试不同的设备以查看它们支持什么手势以及它们如何响应这些手势时,这个应用应该会派上用场。
图1–14。完成了在Android移动设备上运行的手势检查应用
如果您的Android手机无法连接到电脑,以下是您可以遵循的一些故障诊断步骤:
如果您仍然遇到问题,您应该验证您的手机是否在您正在使用的FlashBuilder版本的支持设备列表中,并与您的制造商联系以确保您拥有正确的驱动程序和设置。
除了从FlashProfessional和FlashBuilder中运行之外,您还可以使用AIRDebugLauncher(ADL)从命令行启动应用。如果您在没有工具支持的情况下直接使用Flex,这也是您测试应用的方式。
要使用ADL,您必须下载免费的Flex4.5SDK,或者导航到FlashBuilder安装的sdks/4.5.0文件夹。确保FlexSDK的bin文件夹在您的路径中,以便您可以调用ADL命令行工具。
ADL工具的语法如下:
adl(-runtime
ADL支持许多可选参数,其中大部分是可选的。以下是对所有论点的简要描述,对移动开发重要的论点以粗体突出显示:
要运行您之前开发的手势检查应用,请导航到根项目文件夹并执行以下命令:
adl-profilemobileDevice-screensizeDroidbin-debug/GestureCheck-app.xml
这将使用摩托罗拉Droid的移动配置文件和屏幕大小在AIRDebugLauncher中执行手势检查应用。由于手势检查应用在其应用描述符中没有将全屏设置为true,因此ADL使用的窗口大小将为480×816。
执行后,您应该会得到与图1–12中的所示相同的结果,与您在FlashBuilder中执行的早期运行相匹配。
这是一个开始移动开发的激动人心的时刻。智能手机的采用,尤其是基于Android的设备,正在呈指数级增长,您最终可以使用具有完整创作工具支持的现代开发框架,如Flash和Flex。
这只是FlashAndroid移动开发的冰山一角。在接下来的章节中,我们将向您展示如何构建引人入胜、身临其境的Flash应用,充分利用所有移动功能。然后,我们演示如何将您的应用部署和发布到AndroidMarket。最后,我们将讨论一些高级主题,如原生Android集成、性能调优以及将您的应用扩展到平板电脑、电视等。*
与台式机相比,移动设备的资源非常有限。移动处理器正迅速赶上昨日台式机的速度,但内存和存储仍处于溢价状态。与此同时,用户希望移动应用能够瞬间启动,并对任何时候的硬崩溃或软崩溃具有完全的容错能力。
虽然许多相同的概念适用于桌面应用开发,例如使用的工具和编程语言、可用的服务器通信协议以及可用于UI开发的控件和外观,但移动设备有一些独特的特征会影响UI和应用设计,例如屏幕大小、输入法和部署。
Android是操作系统和软件栈,不是硬件平台。Google提供了一个开源平台,包括一个修改过的Linux内核和基于Java的应用,可以在各种硬件平台上运行。然而,他们并不能控制运行Android的最终设备的确切特征。这意味着设备的确切配置变化很大,屏幕尺寸是分辨率、物理尺寸和像素密度有很大差异的一个方面。Table2–1显示了终端用户可能在其上运行您的应用的各种常见Android设备的屏幕特征。
在表2–1中,分辨率是水平和垂直方向的物理像素数,尺寸是屏幕的对角线尺寸,密度是每英寸的像素数(ppi)。Type为屏幕分辨率提供了一个通用名称,它是下列之一:
你的应用的可用区域也会因为Android状态栏的高度而减少。中密度显示器(如HTCHero)的条形高度为25像素,高密度显示器(如NexusOne)的条形高度为38像素,超高密度显示器的条形高度为50像素。当显示器从纵向模式切换到横向模式时,这也会发生变化。比如NexusOne在人像模式下的可用面积是480×762,而在风景模式下变成了442×800。
您可能只有一两个设备需要测试,但这并不意味着您的应用不能支持所有的设备。Flash可以自动缩放应用以适应屏幕大小,并且很容易获得屏幕分辨率来以编程方式修改界面。清单2–1展示了如何从ActionScript代码中检索屏幕分辨率和密度。
清单2–1。程序化屏幕分辨率和密度捕捉
varresY=Capabilities.screenResolutionX;varresX=Capabilities.screenResolutionY;vardpi=Capabilities.screenDPI;trace("ScreenResolutionis"+resX+"x"+resY+"at"+dpi+"ppi");
注意:术语每英寸点数(dpi)和每英寸像素(ppi)是等价的度量。这些在整个ActionScriptAPIs中可以互换使用。
虽然XperiaX10mini的屏幕分辨率与NexusOne相比微不足道,但屏幕的物理尺寸仅小30%。这意味着用户界面中的所有图形都需要大幅缩小以适合屏幕。另一方面,在为XperiaX10mini构建时,由于像素太大,即使很小的目标也可以被用户轻松操纵。对于NexusOne,你需要将目标做得更大。
在2006年完成的一项研究中,奥卢大学和马里兰大学的研究人员发现,用拇指操纵触摸屏的最小目标尺寸在9.2毫米到9.6毫米之间。
图2–1。几种安卓设备的物理尺寸和分辨率
例如,为了实现触摸交互,你需要在XperiaX10mini上将目标尺寸调整为57像素宽,或者在NexusOne上调整为92像素宽。通过调整用户界面的大小以考虑密度,您可以确保用户界面仍然可用,同时最大化活动设备的屏幕空间。
Android有一个与设备无关的像素的概念,可以用来做布局,即使显示器的物理大小不同,也会出现相似的布局。它基于160dpi屏幕的参考平台,相当于每英寸大约一个13×13像素的正方形。如果你指定了一个Android布局,使用与设备无关的像素,平台会根据你的应用运行的设备自动调整。
Flash没有设备无关像素的概念,但是用代码模拟非常容易。基本公式是dips=像素*(160/密度)。清单2–2演示了如何在ActionScript中进行计算。
清单2–2。ActionScript函数计算设备无关像素(dips)
functionpixelsToDips(pixels:int){returnpixels*(160/Capabilities.screenDPI);}trace("100pixels="+pixelsToDips(100)+"dips");
使用模拟的设备无关像素,您可以在Flash应用中重现与原生Android应用相似的布局行为。
如果您计划根据当前设备密度缩放应用图形,请确保您的应用没有设置为自动调整大小以填充屏幕或在旋转时居中显示内容。有关如何操作的更多信息,请参见本章后面的“Flash中的自动方向翻转”一节。
Flex有内置的支持来缩放应用的用户界面,包括图形、字体和控件。它支持三种常见显示密度的离散比例因子,而不是任意缩放。Table2–2列出了所有不同的显示密度,以及用于为当前设备选择密度的映射DPI范围。
为了利用Flexdensity支持,将您的Application对象上的applicationDPI属性设置为应用最初设计的比例。在运行时,您的应用将根据设备屏幕的密度自动缩放。一个240dpi的应用描述符的例子包含在清单2–3中。
清单2–3。应用描述符设置applicationDPI
applicationDPI的唯一有效值是文本字符串“160”、“240”和“320”,对应于三种支持的密度。只能通过MXML设置applicationDPI属性。
根据作者密度与设备密度的比率,使用矢量图形和文本构建的应用部分会根据需要平滑地放大或缩小。就字体而言,调整字体大小,确保文本在任何显示器上都易于阅读。
位图图形也将被缩放,但放大时可能看起来模糊,缩小时可能会丢失细节。为了确保您的位图大小适合不同的密度,您可以通过使用MultiDPIBitmapSource类提供基于显示密度自动换入的替代图像。
为了更好地了解密度如何影响您的Flex应用,我们将指导您创建密度浏览器应用。该应用允许您输入应用dpi和设备dpi作为参数,并计算将在不同设备上使用的灵活调整的设备密度和比例因子。
首先,使用移动应用模板创建一个名为“密度浏览器”的新Flex移动项目。这将自动生成一个标准项目模板,其中包括一个AdobeAIR应用描述符(DensityExplorer-app.xml)、一个ViewNavigatorApplication(DensityExplorer.mxml)和一个初始视图(DensityExplorerHomeView.mxml)。
第一步是打开DensityExplorerHomeView.mxml并添加一些控件,让你设置作者密度和设备DPI。我们将在第五章中更详细地介绍Flex控件,但是对于这个应用来说,几个标签、单选按钮和一个水平滑块就足够了。
清单2–4显示了允许使用RadioButton和HSlider类输入作者密度和设备dpi的基本代码。
清单2–4。密度浏览器控件用于applicationDPI和deviceDPI条目
首先,引入一些可绑定的脚本变量来保存applicationDPI和deviceDPI。这些并不是显示基本UI所必需的,但是它们将使以后连接输出部分变得更加容易。主控件在VGroup中垂直组织,而RadioButtons使用HGroup水平组织。
使用一个简单的click处理器将RadioButtons连接到applicationDPI。当滑块改变时,双向数据绑定表达式(前缀为@操作符)用于更新dpi的值。为了完成UI的这一部分,设备dpi文本包含一个对DPI的绑定引用,以便您可以在滑块的值发生变化时看到它。
运行这个程序会给你一个简单的Flex应用,如图2–2所示。您可以通过移动滑块来验证功能,这将更新deviceDPI设置。
图2–2。密度探索者第一部分:基本控件
此应用的目标是计算Flex将使用的调整后的设备密度和比例因子。幸运的是,有一个新的Flex4.5API可以通过ActionScript公开这些信息。我们需要调用的类叫做DensityUtil,可以在mx.utils包中找到。DensityUtil有两个带有以下签名的静态方法:
除了这些函数,我们还需要知道当前的applicationDPI和设备dpi值,这样我们就可以填充UI控件的初始值。这些可以通过以下API进行查询:
利用这些API,我们可以扩充之前的代码,添加初始化逻辑以及密度和规模的读数。清单2–5用粗体显示了更新后的代码。
清单2–5。更新了密度探测器代码,并进行初始化和输出
在由View.initialize调用的方法内部执行初始化,以确保所有值都可用。首先从parentApplication对象更新applicationDPI,并通过对返回的字符串执行ID查找来选择正确的RadioButton。接下来从Capabilities对象设置dpi。为了确保初始值赋值和滑块后续更新对dpi的所有更新都将重新计算deviceDensity,bindsetter被配置为触发对dpi的所有更新。
为了显示deviceDensity的当前值和计算出的刻度,在View的末端增加了一个带有几个Labels的BorderContainer。通过使用一个BorderContainer作为环绕组,很容易改变样式,使输出在视觉上不同于输入。
最后一步是添加一个额外的群组,它将随着dpi设置的更新而淡入设备图片。为了确保图像针对不同密度的显示器进行优化缩放,我们使用了一个MultiDPIBimapSource,它指的是不同的预缩放伪影。该代码如清单2–6所示。
清单2–6。*【MXML】用于显示代表性设备图像的代码使用了MultiDPIBitmapSource*
所有选择的图片都是手机标准新闻图片的缩放版本。为了在接近dpi值时慢慢淡入设备,将一个简单的数学公式应用于alpha:
1-Math.abs(deviceDPI-{physicalDPI})/{threshold}
完成的密度浏览器应用如图2–3所示。这是试验应用dpi和设备dpi的不同值的好机会,以查看它们对您部署的应用的影响。
图2–3。完成了密度探测器的应用
为了进行对比,Figure2–4展示了在物理设备上以160、240和320dpi运行的DensityExplorer的屏幕截图。请注意,尽管屏幕的物理尺寸差异很大,但应用的布局和图形的质量都保持不变。通过将作者密度设置为240,可以保证您的应用在任何密度的设备上都具有相同的外观和感觉,而无需修改代码。
图2–4。密度浏览器在分类为160dpi(左)、240dpi(中)和320dpi(右)的设备上运行时的并排比较
Flex支持两种类型的选择器。第一种类型允许您根据设备类型选择样式。清单2–7中的代码演示了如何根据运行设备的类型来改变字体颜色。
清单2–7。演示设备媒体选择器的代码示例
`@namespaces"library://ns.adobe.com/flex/spark";
@media(os-platform:"IOS"){s|Label{color:red;}}
@media(os-platform:"Android"){s|Label{color:blue;}}`
将该样式表添加到您的应用中会将所有Labels的颜色变为蓝色或红色,这取决于您运行的移动平台。但是,当作为桌面应用运行时,这不会有任何影响。
第二种选择器允许您根据应用dpi改变样式。与之匹配的有效值是标准弯曲密度160、240和320。使用dpi选择器,您可以微调布局和字体,甚至用不同密度的显示器替换图像。
重要提示:为了使用CSS媒体选择器,您需要确保没有在您的移动应用类上设置applicationDPI属性。
为了演示dpi选择器的使用,我们将更新DensityExplorer示例,使用样式表来替换图像,而不是将其嵌入到带有MultiDPIBitmapSource的代码中。应用图像的简化应用代码如清单2–8所示。
清单2–8。更新了DensityExplorer代码用于整合CSS媒体查询
注意,我们在父对象上使用了getStyle方法来分配图像源。如果您使用的是图标或按钮状态这样的样式,这通常是不需要的,但是image类上的source是一个普通的属性。使用这种技术绑定到一个命名的样式使得Image源可以通过CSS访问。
为了完成这个例子,我们还需要创建一个样式表,利用dpi媒体选择器来替换一个适当缩放的图像。这类似于设备选择器,如清单2–9所示。
清单2–9。CSS基于应用dpi进行图像切换
`@media(application-dpi:160){#phones{xperiaX10Mini:Embed("/assets/xperia-x10-mini160.jpg");htcHero:Embed("/assets/htc-hero160.jpg");htcEvo4g:Embed("/assets/htc-evo-4g160.jpg");nexusOne:Embed("/assets/nexus-one160.jpg");atrix:Embed("/assets/atrix160.jpg");}}
@media(application-dpi:240){#phones{xperiaX10Mini:Embed("/assets/xperia-x10-mini240.jpg");htcHero:Embed("/assets/htc-hero240.jpg");htcEvo4g:Embed("/assets/htc-evo-4g240.jpg");nexusOne:Embed("/assets/nexus-one240.jpg");atrix:Embed("/assets/atrix240.jpg");}}
@media(application-dpi:320){#phones{xperiaX10Mini:Embed("/assets/xperia-x10-mini320.jpg");htcHero:Embed("/assets/htc-hero320.jpg");htcEvo4g:Embed("/assets/htc-evo-4g320.jpg");nexusOne:Embed("/assets/nexus-one320.jpg");atrix:Embed("/assets/atrix320.jpg");}}`
最后一步是确保我们在ViewNavigatorApplication中引用了样式表。您还需要移除applicationDPI设置,否则样式表选择器将总是将dpi报告为常量值,如清单2–10所示。
清单2–10。完成DensityExplorer应用类对整合媒体查询的支持
在不同设备上运行该程序的输出几乎与之前在图2–4中的结果相同,只是间距略有不同。原因是Flexcontrol团队也在他们的控件中放入了dpi提示,这样它们就可以根据目标设备自动调整大小,即使没有将applicationDPI固定为一个常量值。
既然你已经学习了CSS媒体选择器,你就有了一个强大的工具来从你的代码中提取样式,甚至是对密度敏感的应用。
移动设备的一个独特之处是可以在手中旋转。在桌面世界中,这相当于将显示器翻转过来。虽然旋转桌面显示器有一些创造性的用途,如图2–5所示,但这肯定不是一个常见的用例。
图2–5。独特的利用监视器旋转来制造光弧3
在移动设备中,旋转是一个重要的UI范例,它让您可以充分利用有限的屏幕空间。一个表现良好的移动应用应该在旋转时调整用户界面的大小,让用户停留在他或她喜欢的方向,通常显示一个完全不同的视图,这是为那个方向定制的。
要在Flex项目中打开自动定向,有两种方法。最方便的方法是在从标准模板创建新的Flex移动应用时,只需选中“自动重定向”复选框。图2–6显示了选中“自动重定向”选项的项目创建向导的屏幕截图。
图2–6。选中了“自动重定向”选项的Flexbuilder项目向导
如果您有一个现有的项目或者想要手动更改自动定向,您需要在应用描述符中设置autoOrients属性。应用描述符位于根项目目录中名为*-app.xml的文件中,其中的autoOrients属性应该被创建为initialWindow元素的子元素,并被设置为true,如清单2–11所示。
清单2–11。应用描述符更改,允许载物台自动定位
这会旋转舞台并调整其大小,还会引发事件,您可以通过监听这些事件来更改应用布局。
然而,简单地打开自动定向通常会产生不理想的结果。例如,如果您在DensityExplorer应用上启用自动定向,用户界面的底部会被裁剪掉,如Figure2–7所示。
图2–7。
对于DensityExplorer应用的横向方向,理想的布局是将手机图片放在控件的左侧。在Flex应用中有两种方法可以实现这一点。第一个是为动态改变布局的旋转事件添加一个事件处理程序。由于这是一种纯粹的ActionScript方法,它在FlashProfessional中也同样适用。第二个是利用新的肖像和风景状态,这只能从MXML访问。在下面的小节中,我们将演示这两种方法。
每次旋转Flash移动设备时,都会触发方向事件来通知任何侦听器。方向事件处理程序通过标准的addEventListener方法添加到Stage对象上。定向事件的事件类为StageOrientationEvent,事件类型为StageOrientationEvent.ORIENTATION_CHANGE。
注意:在StageOrientationEvent类上也有一个ORIENTATION_CHANGING事件类型,但这不适用于Android设备。
StageOrientationEvent有两个变量对处理方向变化特别有用:
将所有这些放在一起,您可以修改密度浏览器,以根据手机的方向改变布局。
清单2–12。MXML新增支持舞台布局变更
**
下一步是实现stageInit函数来连接一个方向改变事件监听器。除了连接侦听器之外,用当前方向触发一个初始事件通常也很有帮助。这将确保即使您的应用以横向模式启动,它也将遵循相同的代码路径,就像它以纵向模式打开,然后被用户旋转一样。这个动作脚本显示在清单2–13中。
清单2–13。实现stageInit和orientationChange功能
**protectedfunction**orientationChange(e:StageOrientationEvent):**void**{**switch**(e.afterOrientation){**case**StageOrientation.DEFAULT:**case**StageOrientation.UPSIDE_DOWN:inner.addElementAt(phones,0);**break**;**case**StageOrientation.ROTATED_RIGHT:**case**StageOrientation.ROTATED_LEFT:outer.addElementAt(phones,0);**break**;}}**protectedfunction**stageInit():**void**{stage.addEventListener(StageOrientationEvent.ORIENTATION_CHANGE,orientationChange);orientationChange(**new**StageOrientationEvent(StageOrientationEvent.ORIENTATION_CHANGE,**false**,**false**,**null**,stage.orientation));}
在这种情况下,向右和向左旋转的行为是相同的,尽管如果你想变得有创意,你可以根据手机旋转的方式将设备显示屏放在屏幕的不同侧。
运行修改后的密度浏览器应用的结果如图2–8所示。正如您所看到的,设备显示屏以可用的大小显示,其余的控件不再在很宽的显示屏上展开。最令人印象深刻的是,当你旋转手机时,布局会动态更新,以优化纵向和横向。
图2–8。改进了景观布局的密度浏览器应用
这些states将在设备改变方向时自动触发。要修改布局,您可以利用includedIn属性和Reparent标签来移动手机图像的位置。你需要做的代码修改如清单2–15所示。
清单2–15。当状态改变时,用户界面改变以恢复手机图像
**
最终结果是,用8行MXML代码就可以完成用事件方法需要21行代码才能完成的事情。运行该应用的结果与在图2–8中获得的结果相同。
Flash应用也可以配置为在设备旋转时自动翻转舞台的方向。要在Flash项目中启用自动方向切换,需要勾选Android发布设置对话框的空中自动方向复选框,如图Figure2–9所示。
图2–9。【闪光CS5.5自动定向设置】带圈
设置此选项将导致应用的纵横比在用户旋转设备时自动从横向翻转到纵向。方向改变时,载物台将旋转,使其垂直定向,旋转后调整大小以填充新尺寸,然后在显示器内居中。
如果您想要更改内容的布局以填充屏幕并完全控制舞台的大小,您需要禁用自动缩放和定位。这可以在ActionScript中通过改变Stage对象的scaleMode和align属性来完成,如清单2–16所示。
清单2–16。从ActionScript中禁用舞台缩放和对齐
stage.scaleMode=StageScaleMode.NO_SCALE;//turnoffscalingstage.align=StageAlign.TOP_LEFT;//aligncontenttothetop-leftofthestage
这可以添加到应用启动时执行的任何关键帧中,并保持舞台左上角对齐,不调整内容的大小。然后,您可以在方向改变时添加事件侦听器,以便修改您的应用来适应屏幕大小。
为了演示如何在FlashCS5.5中快速创建调整方向的内容,您将创建一个小的示例应用,该应用在方向改变时将一张快乐的笑脸图片变形为一张魔鬼图片。
首先,您需要创建一个新的AIRforAndroid项目,大小为480×480像素。选择与较小设备尺寸大小相等的正方形画布的原因是为了确保旋转时不会发生额外的缩放。
图2–10显示了快乐笑脸图片的开始状态,画出的方框表示横向和纵向模式的范围。两个框的交叉点是480×480的画布,附加图形水平和垂直溢出。当方向改变时,这些会被裁剪掉,使笑脸很好地居中。
图2–10。快乐笑脸图片起始状态,用方框显示横向和纵向范围
你可以随心所欲地创作自己的图形,但要将元素放在单独的图层中,这样便于以后制作动画和变形。
下一步是创建大约一秒钟的魔鬼笑脸关键帧。这应该包括从快乐的笑脸平滑过渡的运动或形状补间。图2–11显示了一些延时帧,这些帧显示了笑脸以及一些背景场景元素的动画。
图2–11。从快乐到恶魔般的笑脸的动画
同时,也创建反向动画,在大约两秒钟后回到快乐的笑脸。虽然有一些在Flash中反转动画的ActionScript技术,但是它们不直观,并且通常会带来性能损失。
清单2–17。响应方向改变事件的ActionScript代码
`importflash.events.StageOrientationEvent;
stop();
stage.addEventListener(StageOrientationEvent.ORIENTATION_CHANGE,onChanged);
functiononChanged(event:StageOrientationEvent):void{play();}`
所需代码的最后一点是在这个邪恶的笑脸框上添加一个stop()调用,这样它就会在一个旋转事件之后停止。
另外,如果用户的设备不支持定向事件,例如用户在桌面或电视上运行,您可以向用户添加警告。给出反馈的最简单方法是创建一个隐藏层,在检查方向支持的基础上使其可见,如清单2–18所示。
清单2–18。定位校验码隐藏/显示错误页面
if(Stage.supportsOrientationChange){orientationNotSupported.visible=false;}
在设备上运行的完整旋转笑脸应用如图2–12所示。虽然这个例子相当简单,但它展示了向应用添加方向支持是多么容易。
图2–12。完成纵向(左)和横向(右)旋转笑脸示例
用户界面长期以来一直受到桌面鼠标的限制。第一只鼠标是道格拉斯·恩格尔巴特在1963年发明的。它有两个垂直的轮子用于跟踪运动,还有一条长绳子,类似于啮齿动物或老鼠的尾巴。在这之后,出现了内置球、光学跟踪和多个按钮的鼠标,如80年代初生产的Dépraz鼠标。这两种早期装置都显示在图2–13中。
图2–13。恩格尔巴特鼠(右下)和德普拉兹鼠(左上)的照片
现代鼠标包括诸如滚轮、激光跟踪和无线操作等功能。然而,所有的鼠标都有一个共同的限制,那就是它们一次只能在屏幕上支持一个光标。
移动界面最初也有指针驱动的单点触摸交互的限制。然而,它们已经进化到利用人类的体质。我们有两只手,总共十个手指,每个手指都能够单独与设备上的触摸点进行交互和操作。
大多数Android设备支持至少两个同步触摸点,这是处理所有移动手势所需的最低要求。这也是移动设备由手指支撑并用两个拇指操作的最常见的使用场景。然而,支持几乎无限数量的接触点的新设备正在被引入。
您可以通过查询Multitouch对象来检索您的设备支持的触摸点数量,如清单2–19所示。
清单2–19。通过ActionScript检索触摸点数
trace("MaxTouchPoints:"+Multitouch.maxTouchPoints);
在本节中,您将学习如何利用多点触摸和用户手势,改善用户体验和Flash应用的可用性。
使用多点触控最简单的方法是利用Flash支持的预定义手势。对于任何至少有两个触摸点的Android设备,您将能够使用表2–3中的手势。
在不支持手势的情况下,在应用中提供一些其他机制来完成相同的行为通常是一个好主意;然而,移动用户已经开始期待手势提供的便利和速度,因此在应用设计中适当地映射它们是很重要的。
要发现运行您的应用的设备是否支持手势,并动态查询手势功能,您可以在Multitouch类上调用以下静态方法:
在使用手势事件之前,需要将touchMode设置为手势输入模式,如清单2–20所示。
清单2–20。启用手势识别支持的动作脚本代码
Multitouch.inputMode=MultitouchInputMode.GESTURE;
在您期望接收手势事件之前,应该在您的程序中调用它。
为了演示如何处理手势事件,我们将通过一个示例程序来构建一个响应多点触摸事件的图片剪贴簿。为简单起见,我们将图像作为资源加载(参见第七章了解更多关于从相机胶卷中检索图像的信息)。
以下是我们将支持的一些多点触摸事件:
此外,虽然它不是多点触摸手势,但我们将挂接拖动监听器,以便您可以通过用单个手指拖动图像来定位它们。
在Flash应用中有两种不同的方法来连接多点触摸监听器。第一种是通过纯ActionScript,在基于Flash或Flex的应用中同样适用。第二种是通过使用InteractiveObject类上的事件挂钩,如果您正在使用Flex组件,这是最方便的选择。我们将在这一部分展示两者的例子。
Flash剪贴簿示例的核心将是一个MultitouchImage组件,它扩展了sparkImage类以添加大小调整和旋转功能。对于这个类,我们将使用addEventListener机制来连接缩放和旋转手势的多点触摸监听器。代码如清单2–21所示。
清单2–21。MultitouchImage类添加旋转和调整大小支持
`packagecom.proandroidflash{importflash.events.TransformGestureEvent;importflash.geom.Point;importflash.ui.Multitouch;importflash.ui.MultitouchInputMode;importmx.events.ResizeEvent;importspark.components.Image;
publicclassMultitouchImageextendsImage{publicfunctionMultitouchImage(){addEventListener(ResizeEvent.RESIZE,resizeListener);addEventListener(TransformGestureEvent.GESTURE_ROTATE,rotateListener);addEventListener(TransformGestureEvent.GESTURE_ZOOM,zoomListener);Multitouch.inputMode=MultitouchInputMode.GESTURE;}protectedfunctionresizeListener(e:ResizeEvent):void{transformX=width/2;transformY=height/2;}
protectedfunctionrotateListener(e:TransformGestureEvent):void{rotation+=e.rotation;}
protectedfunctionzoomListener(e:TransformGestureEvent):void{scaleX*=e.scaleX;scaleY*=e.scaleY;}}}`
在构造函数中,我们通过调用addEventListener方法并传入GESTURE_ROTATE和GESTURE_ZOOM常量来添加旋转和缩放监听器。旋转回调简单地获取TransformGestureEvent的旋转参数,并将其添加到节点的当前旋转中,将值保存回来。在这两种情况下,旋转都以度数表示为数值。缩放监听器类似地获取TransformGestureEvent的scaleX和scaleY参数,并将它们乘以节点的scaleX和scaleY以获得新值。在这两种情况下,手势事件为您提供了自上次调用侦听器以来的增量,因此您可以简单地增量调整节点值。
为了确保旋转和缩放发生在图像节点的中心,我们将transformX和transformY设置在resizeListener中节点的中点。这也是通过构造函数中的addEventListener连接的,因此每次节点大小改变时都会触发。
我们做的最后一件事是将inputMode设置为MultitouchInputMode.GESTURE,这样事件监听器将被触发。根据我们的需要频繁地设置这个值是安全的,所以我们在每次调用构造函数时都这样做。
对于其余的手势事件,我们将利用InteractiveObject事件挂钩,通过MXML轻松连接;然而,你也可以通过遵循表2–4中的类和常量信息,使用addEventListener机制连接所有其他手势。
我们将使用的另一个助手类是DraggableGroup类。这实现了一个标准的指向和拖动隐喻,作为spark组的扩展,如清单2–22所示。除了有利于封装之外,从手势事件中提取鼠标事件允许您同时处理多个事件。
清单2–22。DraggableGroup实现点和拖动力学的类
`packagecom.proandroidflash{importflash.events.MouseEvent;importmx.core.UIComponent;importspark.components.Form;importspark.components.Group;
publicclassDraggableGroupextendsGroup{publicfunctionDraggableGroup(){mouseEnabledWhereTransparent=false;addEventListener(MouseEvent.MOUSE_DOWN,mouseDownListener);addEventListener(MouseEvent.MOUSE_UP,mouseUpListener);}
protectedfunctionmouseDownListener(e:MouseEvent):void{(parentasGroup).setElementIndex(this,parent.numChildren-1);startDrag();}
protectedfunctionmouseUpListener(e:MouseEvent):void{stopDrag();//fixforbuginFlexwherechildelementsdon'tgetinvalidatedfor(vari:int=0;i DraggableGroup的代码是一个相当简单的Flex组件实现,使用与手势代码相同的addEventListener/回调范例。虽然您可以使用触摸事件实现相同的代码,但是坚持使用鼠标抬起和鼠标放下事件的好处是,即使在没有触摸支持的情况下,UI的这一部分也可以工作。 代码中的一些微妙之处值得指出。 为了显示图像,我们将使用一个简单的视图,将各个图像页面的呈现委托给一个ItemRenderer。首先,我们将看看组成剪贴簿示例主屏幕的View类。完整的代码如清单2–23所示。 清单2–23。Flash剪贴簿主视图代码,滑动事件处理程序以粗体突出显示 虽然有相当多的代码来创建视图,但是实际的视图定义本身只包含五行来创建标题的VGroup和显示图像的DataGroup。剩下的主要是一大块代码,用于简单地将嵌入的图像加载到一个ArrayList中,以及一些嵌入在脚本标签中的辅助函数。 挂钩swipe事件处理程序所需的代码以粗体突出显示。通过利用InteractiveObject上的gesture*事件属性,您可以像这样快速地将事件监听器连接到您的应用。每当用户滑动标题VGroup时,将调用protectedswipe方法,其中包含滑动方向的信息。与旋转和缩放事件在手势持续期间不断调用不同,swipe在手势结束时只被调用一次。您可以通过检查offsetX和offsetY属性来解读滑动的方向。 值得注意的是,刷卡将总是在水平或垂直方向。不支持对角线滑动,不会被识别为手势。此外,您需要确保您将滑动监听器钩到的组件足够宽或足够高,以提供足够的移动距离来识别手势。一旦用户的手指离开组件,手势识别就会结束。 现在我们有了一个带有DataGroup的主页,我们需要实现引用的ItemRenderer来构建剪贴簿页面。这也将是MultitouchImage,DraggableGroup和我们之前定义的主视图之间的链接。 剪贴簿页面实现的完整代码如清单2–24所示。 清单2–24。FlashScrapbookPage项目渲染器代码,平移和双指点击事件处理程序以粗体突出显示 最后一步是实现在pushView方法调用中引用的ImageView类。因为Flex为我们处理了所有的视图导航逻辑,所以实现非常简单。我们增加的唯一额外功能是另一个双指轻击手势,这样你就可以导航回主视图,而不用点击Android的后退按钮。 ImageView类的代码如清单2–25所示。 清单2–25。ImageView代码,用粗体字突出显示的双指点击事件处理程序 简单表达式可以内联在事件处理程序中,就像我们在这里所做的那样。这避免了创建Script标签的需要,使得代码清单非常简洁。 这也完成了Flash剪贴簿应用的最后一个文件,所以你现在可以给它一个测试驱动器。启动应用后,您应该会看到类似于图2–14所示的屏幕。 在应用的此页面上,尝试执行以下操作: 图2–14。首页查看页面完成Flash剪贴簿申请 完成最后一步后,您将进入应用的ImageView页面,如图图2–15所示。要返回主视图,你可以用两个手指再次点击图像,或者使用Android的后退按钮。 图2–15。完成了图片浏览器页面上的Flash剪贴簿示例 通过完成这个简单的示例,您已经成功探索了FlashAndroid上所有可用的手势事件。尝试在自己的应用中以创新的方式使用这些手势! 处理多点触摸输入的另一种方法是使用触摸点API来直接访问设备上生成的事件。这使您可以根据自己的应用需求进行定制的多点触摸处理。要确定您的设备是否支持触摸事件处理,您可以查询Multitouch对象,如清单2–26所示。 清单2–26。打印出是否支持触摸事件的代码片段 trace("Supportstouchevents:"+Multitouch.supportsTouchEvents); 由于处理触摸事件与手势识别直接冲突,您需要更改应用的输入模式来开始接收触摸点事件。这可以通过将Multitouch.inputMode变量设置为TOUCH_POINT来实现,如清单2–27所示。 清单2–27。启用触摸点事件的代码片段 Multitouch.inputMode=MultitouchInputMode.TOUCH_POINT; 注意:将inputMode设置为TOUCH_POINT将禁止识别任何手势,如缩放、旋转和平移。 在被调度的事件的数量和类型方面,接触点API是相当低级的。您可以注册并监听表2–5中列出的任何事件,只要目标对象扩展InteractiveObject。 大多数触摸事件都是不言自明的,但是touchOver,touchOut,touchRollOver,和touchRollOut可能会有点混乱。例如,以三个嵌套的矩形为例,分别标记为A(外部)、B(中间)和C(内部)。当手指从A滚动到C时,您会收到以下滚动事件: **touchRollOver(A)**->touchRollOver(B)->touchRollOver(C) 同时,您还会收到以下溢出/溢出事件: **touchOver(A)->touchOut(A)/touchOver(B)->touchOut(B)/touchOver(C)** 矩形A将直接接收的事件以粗体突出显示。滚动事件不会传播,所以您只收到一个touchRollOver事件。然而,touchOver/Out事件确实会传播到父节点,所以除了两个额外的touchOut事件之外,您还会收到三个touchOver事件。 作为触摸点API的一个简单示例,我们将指导您如何创建一个支持多点触摸的Flash应用,当您在屏幕上拖动手指时,该应用会生成毛虫。 首先,我们将创建一些可用于构建示例的艺术素材: 应用逻辑的机制非常简单。当用户在屏幕上拖动他或她的手指时,我们将在当前的一个或多个触摸位置为毛虫的身体不断创建新的球对象。一旦用户的手指离开屏幕,我们将绘制毛毛虫的头部。此外,如果用户点击其中一个卡特彼勒头,我们将播放电影来改变所显示的面孔。 卡特彼勒发电机应用的完整代码列表如列表2–28所示。你可能想把它放在一个叫做Actions的单独层的第一帧,以区别于程序中的图形元素。 清单2–28。卡特彼勒发电机应用的代码列表 `importflash.ui.Multitouch;importflash.ui.MultitouchInputMode;importflash.events.TouchEvent;importflash.events.KeyboardEvent;importflash.ui.Keyboard; Multitouch.inputMode=MultitouchInputMode.TOUCH_POINT;stage.addEventListener(TouchEvent.TOUCH_BEGIN,beginListener);stage.addEventListener(TouchEvent.TOUCH_MOVE,moveListener);stage.addEventListener(TouchEvent.TOUCH_END,endListener);stage.addEventListener(KeyboardEvent.KEY_DOWN,keyListener); varlastScale:Number;varstartX:Number;varstartY:Number; functionbeginListener(event:TouchEvent):void{lastScale=0;} functionmoveListener(event:TouchEvent):void{varball;if(event.isPrimaryTouchPoint){ball=newGreenBall();}else{ball=newBlueBall();}ball.x=event.stageX;ball.y=event.stageY;lastScale=Math.max(lastScale,event.pressure*7);ball.scaleX=lastScale;ball.scaleY=lastScale;addChild(ball);} functionendListener(event:TouchEvent):void{varball=newRedBall();ball.x=event.stageX;ball.y=event.stageY;ball.scaleX=lastScale;ball.scaleY=lastScale;ball.addEventListener(TouchEvent.TOUCH_MOVE,ballMoveListener);ball.addEventListener(TouchEvent.TOUCH_TAP,changeFace);addChild(ball);} functionballMoveListener(event:TouchEvent):void{event.stopImmediatePropagation();} functionchangeFace(event:TouchEvent):void{event.target.play();} functionkeyListener(event:KeyboardEvent):void{if(event.keyCode=Keyboard.MENU){clearAll();}} functionclearAll():void{for(vari:int=numChildren-1;i>=0;i--){if(getChildAt(i).name!="background"){removeChildAt(i);}}}` 注意,我们将事件监听器添加到了Stage中,而不是背景中。这样做的原因是,当在手指下添加额外的节点以组成卡特彼勒身体时,它们会遮挡背景,从而防止触发额外的移动事件。但是,stage会接收所有事件,而不管它们发生在哪个对象中。 向Stage添加事件侦听器是一把双刃剑,因为这也意味着接收任何点击事件极其困难。为了防止caterpillar脸上的移动事件蔓延到舞台,我们称之为event.stopImmediatePropogation().,这允许在不受舞台事件监听器干扰的情况下处理点击手势。 我们使用的另一个技术是通过使用Math.max函数来确保每个后续添加的球都比前一个大。这确保了即使当用户将他或她的手指从屏幕上移开时压力减小,卡特彼勒视角也保持不变。 在设备上运行时,最终应用看起来应该类似于图2–16。 图2–16。【毛虫发电机】应用显示杂草中的几条毛虫 这个应用也可以作为一个粗略的性能测试,因为它不断地生成Sprites并将其添加到stage中。您可以通过按下手机上的菜单按钮随时清除场景和重置应用,该按钮已连接到clearAll功能。 尝试为pressure使用不同的乘数来调整应用,以在您的设备上获得最佳性能。 在本章中,您学习了如何设计和构建充分利用移动平台的应用。您将能够在未来的移动项目中应用的一些要点包括: 我们将在整本书中继续使用这些概念来构建更加复杂和强大的应用,但是您应该已经有了一个设计自己的移动用户界面的良好开端。 第一章和第二章介绍了如何使用Flash和Flex作为创建移动应用的平台。现在,您已经了解了选择Flash平台的原因,以及为使用触摸手势作为主要用户输入形式的各种屏幕设备编写应用时需要考虑的一些事项。下一步是着手编写自己的应用。在本章结束时,您将知道如何在各种类型的Flex应用之间做出选择,如何编写自己的View以及如何使用FlexSDK中的移动就绪控件为这些View提供丰富的内容。 简而言之,是时候向您展示将您的应用想法变为现实所需的一切了! 由于其便利性和开发人员生产力特性,MXML是定义Flex移动应用主用户界面的首选方式。然而,MXML的便利性是以性能为代价的。因此,有些任务,比如List项目渲染器,最好在纯ActionScript中完成。我们将在第十章中更深入地讨论这个特殊的话题。 图3–1显示了纵向和横向手机的基本构造ViewNavigatorApplication。源代码可以在位于本书示例代码的chapter-03目录中的ViewAppAnatomy示例项目中找到。 图3–1。屈曲移动的基本解剖ViewNavigatorApplication 在图3–1中,应用的ActionBar在屏幕顶部伸展。由三个区域组成:导航区域、标题区域和动作区域。图3–1中的ActionBar在ActionBar导航区域包含一个标记为Nav的按钮,而标题区域显示的是“ActionBar字符串。ActionBar的操作区包含两个标记为A1和A2的按钮。View的内容区域由ActionBar下方的屏幕其余部分组成。请记住,尽管View使用ActionBar来显示它的标题和控件,但这两者在组件层次结构中是兄弟。除非ViewNavigator的overlayControls属性设置为true,否则View的width和height不包括ActionBar所占的面积。如果overlayControls设置为true,那么ActionBar以及TabbedViewNavigatorApplication的标签栏将部分透明,这样它们下面的任何View内容都是可见的。 作为这种基于View的应用结构的替代方案,您也可以从一个普通的ApplicationMXML文件开始创建一个完全定制的界面,就像您对基于web或基于桌面的应用所做的那样。 ViewNavigatorApplication创建一个单一的ViewNavigator来管理整个移动应用的View之间的转换。应用容器还有一个firstView属性,该属性决定应用启动时将显示哪个View组件。清单3–1显示了一个非常基本的ViewNavigatorApplication的代码。这段代码来自本书示例代码的examples/chapter-03目录中的HelloView示例项目。 清单3–1。一个简单的开始:你的第一个Flex移动ViewNavigatorApplication ` 太神奇了!在大约20行代码中,我们有了一个全功能的移动应用,它有多个View和它们之间的动画转换。这就是为什么Flex是移动开发的一个令人信服的选择。Adobe的团队让您可以轻松快速地开始开发Android移动应用。 在清单3–1中的应用容器的正下方显示了FirstView的源代码。文件中的根组件是一个SparkView组件。 注意,Button的click事件处理程序调用了navigator对象的pushView函数,这是View对应用的ViewNavigator实例的引用。这个方法的第一个参数是应该显示的View的类名。在这种情况下,我们告诉navigator接下来显示SecondView。而SecondView则只是简单的指示用户使用Android内置的“返回”按钮返回到FirstView。在SecondView的代码中没有对navigator对象进行显式调用。这是可能的,因为ViewNavigator自动添加一个监听器到Android的后退按钮。由于ViewNavigator还维护一个已经显示的View的堆栈,它可以从堆栈中弹出最近的View并返回到前一个View以响应“后退”按钮的按下,而无需应用开发人员编写任何额外的代码。第一个HelloWorld应用在图3–2中运行。 图3–2。一个简单的ViewNavigatorApplicationHelloWorld节目 事件是任何Flex和Flash应用的生命线。它们不仅允许您对应用中正在发生的事情做出反应,而且知道事件到达的顺序也很重要,这样您就可以选择适当的处理程序来放置程序逻辑。图3–3显示了在三个应用阶段接收一些更重要事件的顺序:启动、关闭和从一个View到另一个View的转换。图中代表应用容器接收到的事件的方框是深色的,而Views接收到的事件是浅色的。 图3–3。应用及其View接收重要事件的顺序 在创建第一个View之前,应用接收到了initialize事件。因此,我们知道如果您需要以编程方式而不是作为XML属性中的简单字符串来设置ViewNavigatorApplication的firstView和firstViewData属性,那么initialize处理程序是一个很好的地方。一个方便的例子是,当您在关机期间保存数据,并希望在下次启动时读回数据并恢复应用的View状态。 在应用接收到initialize事件后,第一个View将接收到它的initialize、creationComplete和viewActivate事件。当设置你的Views时,记住这个顺序是很重要的。如果你需要在你的控件上编程设置一些初始状态,如果可能的话,最好在initialize处理程序中完成。如果您一直等到调用creationComplete处理程序,那么控件实际上被初始化了两次。这可能不会造成明显的延迟,但在为移动平台开发时,意识到性能问题总是值得的。同样,viewActivate事件将是你在View启动序列中发表意见的最后机会。 一旦第一个View完成其初始启动序列,应用将接收其creationComplete和activate事件。那么第一个View也会收到最后一个activate事件。只有应用的第一个View会收到activate事件。只有当应用运行时第一个创建的是View时,该事件处理程序才是您想要运行的代码的合适位置。 在一个View转换序列中,新的View将在旧的View接收其viewDeactivate事件之前接收其initialize和creationComplete事件。尽管旧的View仍然有效,但是您应该避免View之间的相互依赖。任何需要从一个View传递到下一个View的数据都可以使用新的View的data参数来传递。我们将在本章的后面向您展示如何做到这一点。View转换序列的最后一步是新的View接收其viewActivate事件。关于这个序列需要记住的重要事情是,在ViewNavigator播放到新View的动画过渡之前,新View将接收到initialize和creationComplete事件。viewActivate事件将在过渡播放后接收。如果你想让新的View控件在转场播放时处于某个特定状态——并且它们对用户可见——你需要使用initialize或creationComplete事件。同样,initialize是首选,这样控件就不会被初始化两次。另一方面,在过渡播放之前做大量的处理将导致用户输入和View过渡开始之间的明显滞后,这将导致您的界面感觉迟钝。因此,尽可能将处理延迟到viewActivate事件是一个好主意。 当应用关闭时,应用容器将接收到deactivate事件,随后是View堆栈中的每个View。如果一个View被实例化多次,它将接收多个deactivate事件,每个实例一个。在移动环境中,关闭并不总是意味着应用从内存中删除。例如,在Android中,当一个应用正在运行时按下“home”按钮将导致该应用接收其停用事件。但是应用还没有退出;它只是在后台运行。如果您真的希望您的应用在停用时退出,您可以从应用容器的deactivate处理程序中调用NativeApplication类中的exit函数,如清单3–2所示。 清单3–2。使移动应用在停用时完全退出 清单3–3。宣告一个TabbedViewNavigatorApplication ` 清单3–4。我们的TabbedViewNavigatorApplication中Hello页签的两个视图 privatefunctiononChange(event:IndexChangeEvent):void{data.selectedIndex=event.newIndex;navigator.pushView(LanguageView,listData.getItemAt(event.newIndex));} /***Initializesthedataobjectifitdoesnotexist.Ifitdoes,*thenrestoretheselectedlistindexthatwaspersisted.*/privatefunctiononInitialize():void{if(data==null){data={selectedIndex:-1};} helloList.selectedIndex=data.selectedIndex;}]]> fx:Script HelloView用两个属性定义了对象的静态ArrayCollection:一个hello属性包含用某种特定语言写的单词“Hello”,另一个lang属性指定该语言是什么。这个ArrayCollection然后被用作View中显示的火花List的dataProvider。关于这个View要注意的第一件事是,当显示其他View时,它使用它的data属性为自己保存数据。这在onInitialize功能中完成。如果View的data对象为空,换句话说,如果这是第一次初始化View,那么使用ActionScript的对象文字符号创建一个新的data对象。否则,现有的data对象——在其他View显示时保持不变——用于检索之前在List中选择的项目的索引,并在View重新激活时重新选择它。 HelloView源代码还演示了如何将数据传递给另一个View,正如在List的onChange处理程序中所做的那样。当用户在HelloView的List中选择一个项目时,onChange处理程序会首先将IndexChangeEvent的newIndex属性保存在自己的data对象中,这样在下次激活View时可以恢复List选择。然后,处理函数使用同一个newIndex属性从ArrayCollection中获取对所选对象的引用。它将对象作为ViewNavigator的pushView函数的第二个参数传递给LanguageView的data属性。在清单3–4的底部,您可以看到LanguageView的代码用两个Label组件向用户显示了data对象的hello和lang属性,这两个组件的text属性被绑定到data对象的属性。 图3–4显示了在HelloTabbedView示例应用中运行的Hello选项卡的这两个View。这个项目的源代码可以在本书示例代码的examples/chapter-03目录中找到。 图3–4。HelloWorldTabbedViewNavigatorApplication的Hello标签下的View 那关于世界选项卡呢?世界标签包含一个View,它的名字很有创意,叫做WorldView。不直观地说,它不包含地球的图片。相反,它展示了基于GUI的HelloWorld程序的另一个主要部分:问候消息。图3–5展示了这种View的作用。 图3–5。HelloWorldTabbedViewNavigatorApplication的World标签下的View 这个特定实现的独特之处在于,它演示了ActionBar可以包含任何种类的Spark控件,而不仅仅是按钮。清单3–5展示了这是如何实现的。 清单3–5。一个问候消息的简单实现 privatefunctiononChange(event:TextOperationEvent):void{viewLabel.text="Hello,"+textInput.text;}]]> 通常显示View标题字符串的View的titleContent,已经被一个SparkTextInput控件取代。TextInput的change处理程序只是复制已经输入到Label的text属性中的字符。这是一个很好的例子,说明了定制ActionBar的灵活性。 创建新的Flexmobile项目的第三个选项是从空白应用开始。如果您正在处理一个独特的移动应用,它的界面不使用多“视图”的典型模式,那么您可以选择这个选项。如果您正在处理一个只有一个View的应用,因此您不需要ViewNavigator和它带来的所有东西,那么您也可以利用这个选项。当您选择从一个空白的应用开始时,您将得到的正是这个,如清单3–6所示。 清单3–6。一个空白的手机应用 然而,您从一个空白应用开始并不意味着您不能利用Flex4.5中包含的特定于移动设备的控件。举个例子,你正在开发一个只有一个屏幕的应用。为一个屏幕设置一个完整的基于View的系统是不值得的。但是这并不意味着你不能使用一个ActionBar来让你的应用看起来更像一个传统的移动应用!清单3–7展示了一个应用,它最初是一个空白的应用,现在看起来像任何其他Flex移动应用。 清单3–7。一款没有ViewNavigator的Flex移动应用 ` 不过,要明确的是,ActionBars属于屏幕的顶部;不要在家里尝试,我们是专业人士!在前面的例子中,我们已经多次提到了ViewNavigator、View和ActionBar。在下一节中,我们将更深入地探讨移动Flex应用的这些主要部分。 图3–6。一个带有ActionBar组件和一个可见弹出容器的应用 ViewNavigator还显示一个ActionBar,它显示由激活的View定义的上下文信息。每当显示新的View时,ViewNavigator自动更新ActionBar。ViewNavigator类中主要感兴趣的方法如下: ViewNavigator将自动处理Android后退按钮的按压,并代表应用调用popView。ActionBar、View和ViewNavigator类都合作为移动开发者提供了许多这样的特性。本节的其余部分将探讨这些类如何协同工作,为您提供一个高效、灵活、健壮的框架来开发您的移动应用。 在移动应用中,ActionBar是View标题和控件的传统位置。ActionBar有三个不同的区域:导航区域、标题区域和动作区域。回头参考图3–1中显示这些区域的示例。这三个区域都可以包含任意控件,但是默认情况下,标题区域将显示一个标题字符串。尽管标题区域也可以显示任意的控件,但是如果给了它可显示的替代内容,它就不会显示标题字符串。 每个ViewNavigator都有一个由导航器实例化的View共享的ActionBar控件。因此,一个ViewNavigatorApplication对于整个应用将只有一个ActionBar,而一个TabbedViewNavigatorApplication对于应用中的每个ViewNavigator将有一个单独的ActionBar。ActionBar有七个属性决定了它的内容和布局。 我们已经展示了在导航和操作区域显示控件的示例应用,以及用TextField替换标题内容的应用,所以在本节中我们不再重复这些示例。您可能会发现有用的另外一条信息是影响ActionBar外观的两种特殊样式的使用。titleAlign风格允许您将标题字符串的对齐方式设置为left、right或center。defaultButtonAppearance风格可以设置为normal或beveled。在Android上,这些默认为左对齐的标题和正常的按钮外观。您可以根据应用的需要更改它们,或者如果您计划将应用移植到iOS平台,也可能需要更改它们。在这种情况下,iOS上的通常会有倾斜的按钮和居中的标题。图3–7显示了这种情况。 图3–7。一个机器人ActionBar穿着iPhone去约会 将斜面样式应用到defaultButtonAppearance甚至为导航内容中的按钮添加了传统的iOS箭头形状。微小的触动会让一切变得不同。清单3–8显示了创建图3–7的ActionBar外观的代码。 清单3–8。一个iOS风格的ActionBar,有格调! 我们已经利用了在MXML文件中直接为ViewNavigatorApplication定义navigationContent的能力。由于这种布局,后退按钮会出现在整个应用的每个View上。除了titleAlign和defaultButtonAppearance样式,我们还为ActionBar定义了自定义颜色。ActionBar将使用chromeColor样式作为填充ActionBar背景的渐变的基础。为ActionBar定义一个定制chromeColor是定制一个移动应用以实现品牌或独特性的常见方式。 View过渡控制一个View替换另一个View时播放的动画。当按下一个新的View时,默认的过渡是向左一个SlideViewTransition,而弹出一个View会向右一个SlideViewTransition。这两种默认转换都使用其push模式。然而,您可以用无数不同的方式定制View过渡动画。Table3–1显示了转换及其模式和方向。 当你在组合中加入不同的easers时,你真的有大量的组合可以玩。例如,uncover模式中的SlideViewTransition和向下方向看起来像当前的View正在向下滑动离开屏幕,以显示位于其下方的新的View。再放一个Bounce画架,顶部的View会滑下来,碰到屏幕底部会反弹。您甚至可以通过扩展ViewTransitionBase类来编写自己的自定义过渡,通过实现IEaser接口来编写自己的easers。你真的只是被你的想象力所限制! 您可以通过将您的转换作为第四个参数传递给pushView或replaceView函数来指定ViewNavigator将使用的转换。ViewNavigator的popView、popAll和popToFirstView也采用单个可选参数,指定在更改Views时播放哪个过渡。啊,但是如果用户按下Android的后退按钮呢?在这种情况下,我们不能显式地调用pop函数,所以相反,您必须将ViewNavigator的defaultPopTransition属性设置为您希望它默认播放的过渡。如果您没有为pop函数指定过渡参数或设置defaultPopTransition属性,则ViewNavigator将在弹出View时播放其默认的幻灯片过渡。即使您使用自定义过渡来推动View,当View弹出时,ViewNavigator也不会尝试反向播放推动过渡。还需要注意的是ViewNavigator有对应的defaultPushTransition。您可以使用这两个属性为特定的ViewNavigator播放的所有过渡设置默认值。 现在唯一合理且有趣的事情是编写一个应用来尝试这些转换组合,对吗?没错。清单3–9显示了ViewTransitions示例程序的TransitionListView的代码。这个View显示了所有内置View过渡的List,每个过渡都显示了一些不同的模式、方向和画师的组合。ViewTransitions项目可以在本书第三章的示例代码中找到。 清单3–9。展示了几个不同品种的每一个View跃迁 fx:Script privatefunctiononChange(event:IndexChangeEvent):void{varselectedItem:Object=transitionList.selectedItem;vartransition:ViewTransitionBase=selectedItem.transition;vardata:Object={name:selectedItem.name}; navigator.defaultPopTransition=transition;navigator.pushView(TransitionedView,data,null,transition);} ]]> 当从List中选择一个过渡时,onChange处理函数检索所选对象,并将过渡的名称传递给下一个View的data.name属性。包含在List中的对象也保留了对所需过渡的引用。所以这个转换属性作为第四个参数传递给导航器的pushView方法。但是请注意,这个转换也用于在调用pushView之前设置导航器的defaultPopTransition属性。这将确保如果在推送过程中播放了一个FlipViewTransition,当返回到TransitionListView时将播放相同的过渡。这有一点欺骗,因为当你通常想要在特定的一对View的推和弹出上播放相同类型的过渡时,你通常会颠倒弹出过渡的方向。这在常规应用中很容易实现,但在这种情况下,不值得在示例代码中为每个当前转换对象定义一个相反方向的转换。图3–8显示了在立方体模式下FlipViewTransition期间捕获的ViewTransitions示例程序。 图3–8。使用立方体翻转过渡从一个View到下一个 图3-9。在立方体翻转过渡上用View过渡ActionBar 清单3–10。宣告一个ViewMenu 一个ViewMenuItem实际上只是另一种按钮。它甚至扩展了SparkButtonBase类。因此,就像任何其他按钮一样,您可以定义标签和图标属性以及一个click事件处理程序。在这个例子中,每个ViewMenuItem都有相同的onClick处理程序,它使用ViewMenuItem的标签向用户显示选择。使ViewMenu容器和ViewMenuItem按钮与普通Spark不同的是,它们的布局设计模仿了原生Android菜单。 提示:记住Windows上的Ctrl+N和Mac上的Cmd+Non会在桌面模拟器中显示Viewmenu。 图3–10显示了在Android设备上运行时产生的ViewMenu的样子。 图3-10。一个ViewMenu带图标 那么,通过data对象的通信似乎是单向的:一个View可以将一个data对象传递给它正在推入堆栈的View,但是当它弹出堆栈时,View无法将数据返回给原始的View。那么,如果需要的话,新的View如何将数据返回给原来的View?答案是新的View将简单地覆盖View的createReturnObject功能。该函数返回一个保存在ViewNavigator的poppedViewReturnedObject属性中的对象,其类型为ViewReturnObject。所以为了访问新的View返回的对象,原来的View将访问navigator.poppedViewReturnedObject.object。 您也可以通过使用context对象将数据传递给新的View。你可以传递一个context对象作为ViewNavigator的pushView函数的第三个参数。context的行为很像data对象;通过访问navigator.context属性,新的View可以随时访问它。当顶部的View从ViewNavigatorView栈中弹出时,先前的View的context也被恢复。弹出的View的context对象也存在于navigator的poppedViewReturnedObject.context属性中。 data和context对象的使用在某种程度上是可以互换的,在这种情况下,你应该更喜欢使用data对象。context对象对于那些有一个View的情况很有用,根据用户导航到View的方式,这个【】的显示会略有不同。例如,您可能有一个显示一个人的联系信息的细节View。有时,根据用户如何导航到“View”——无论是通过点击“查看”按钮还是“编辑”按钮——View应该相应地调整其显示。这是一个使用context对象来区分包含在data对象中的联系信息是应该简单地呈现给用户还是应该可编辑的好地方。 我们已经在清单3–4中看到,并且在上一节中简要讨论过,当View被ViewNavigator弹出函数之一重新激活时,View的data对象被恢复。因此,View数据的一个持久性策略是在调用pushView之前将值存储在它的data对象中,或者存储在viewDeactivate事件的处理程序中。如果新的View调用其中一个弹出函数,那么先前由原来的View存储的数据将可以通过它的data对象再次访问。这种策略只对正在运行的应用中的View之间的持久化数据有效。如果用户触发调用NativeApplication的exit功能的动作或Android操作系统关闭应用,那么所有Viewdata对象都将丢失。 PersistenceManager类用于在应用运行之间保存数据。ViewNavigatorApplication和TabbedViewNavigatorApplication容器引用了一个persistenceManager实例,该实例可用于在应用启动或关闭时保存和加载持久化数据。清单3–11展示了一个使用persistenceManager保存应用启动次数的简单例子。这段代码是名为简单持久性的第三章示例项目的一部分。 清单3–11。在应用运行之间保存数据 privatefunctiononDeactivate():void{varrc:Number=Number(persistenceManager.getProperty(RUN_COUNT));persistenceManager.setProperty(RUN_COUNT,++rc);NativeApplication.nativeApplication.exit(0);}]]>` 当应用被放到后台或被Android操作系统关闭时,应用的onDeactivate处理程序被调用。所以在这个处理程序中,我们增加了运行计数,并调用persistenceManager的setProperty函数来保存它。然后我们调用NativeApplication的exit函数来确保应用在两次运行之间被关闭。这确保了我们的数据真正被持久化和恢复。 当应用的onInitialize处理程序被触发时,使用getProperty函数从persistenceManager中检索应用的运行计数。getProperty函数接受一个参数,该参数是要检索的属性的键String。该运行计数被转换为一个Number并传递给应用的第一个View,在此显示如图3–11所示。 图3–11。显示应用的持续运行次数 对于移动应用,性能和外观对于确保用户界面的可用性都非常重要。出于这个原因,强烈建议远离Flex中的旧MX包,并专注于具有移动皮肤的Spark控件。 Spark控件的完整列表显示在表3–2中,以及它们对移动应用的适用性。 许多控件目前没有移动优化外观,因此目前不应在移动设备上使用。例如,ComboBox、NumericStepper和DropDownList如果在移动设备上使用它们的桌面皮肤,就不会有一致的外观和交互。如果您需要一个具有这些功能之一的控件,您可以创建自己的自定义外观,以匹配您的移动应用的样式。 一些可用的组件也没有针对移动设备上的性能进行优化。我们在第十章的中更详细地讨论了这个话题,但是如果你遵循前面的指导方针,使用TextArea和TextInput而不是RichText和RichEditableText,你应该没问题。对于Image类也是如此,重复使用时可以是重量级的,比如在ListItemRenderers中。 该列表是截至Flex4.5的最新版本,但是Adobe正在为其余控件添加额外的移动外观,因此请参考API文档以了解有关移动控件兼容性的最新信息。 在本章的其余部分,我们将详细介绍如何使用每个支持移动设备的控件的全部功能。 三个为移动设备优化并能给你带来最佳性能应用的控件是Label、TextArea和TextInput。每个控件都可以通过CSS样式高度定制。 Label让您能够以统一的格式显示单行或多行文本。它在幕后使用Flash文本引擎(FTE),这使它快速而轻量,但不如使用全文布局框架(TLF)的RichText控件灵活。Labels应该用在任何你想在屏幕上显示不可修改的文本的地方,比如控件标签或者章节标题。 TextInput和TextArea分别提供单行和多行使用的文本输入。当在移动设备上使用时,它们在幕后使用StyleableTextField类,这使它们具有极高的性能,但功能有限。在桌面上,这些控件由TLF支持,为您提供国际语言支持、改进的排版和嵌入的CFF字体。如果你在移动设备上需要这些功能,你将不得不使用RichEditableText控件,这会带来很大的性能损失。 三个推荐文本组件的移动样式如表3–3所示。虽然在桌面配置文件上运行时还有其他样式属性可用,如kerning、lineBreak和renderingMode,但由于移动主题中使用的轻量级文本引擎,这些属性在移动设备上不受支持。 正如您所看到的,这些文本组件支持的样式几乎是相同的,只是为内容区域、边框以及TextInput和TextArea的焦点添加了一些样式。 使用不同文本组件的最大区别在于使用不同的属性进行文本编辑。这些属性中有许多在TextInput和TextArea上可用,但对于Label并不需要,因为它仅用于文本渲染。Table3–4列出了三个文本组件的所有可用属性。 为了展示文本组件的不同风格和功能,我们构建了一个小的示例应用,它呈现了美国独立宣言的前几段以及一个可编辑的签名列表。该应用的代码如清单3–12所示。 清单3–12。显示独立宣言的文本组件示例代码 运行这个例子会得到如图Figure3–12所示的输出。 图3–12。独立宣言测试样本 作为测试文本组件的一个练习,尝试对应用进行以下更改: 提示:使用一个禁用了可编辑性且样式类似于Label的TextInput比直接使用一个Label组件性能更高,这是因为使用了幕后的StyleableTextField实现。 当使用文本组件时,Android软键盘会像你所期望的那样在焦点上自动触发。然而,有时您需要更精细地控制软键盘何时被触发,以及当它被激活时会发生什么。 Flex中的软键盘由应用焦点控制。当一个将needsSoftKeyboard属性设置为true的组件获得焦点时,软键盘将出现在前面,舞台将滚动,以便选定的组件可见。当该组件失去焦点时,软键盘将消失,舞台将返回其正常位置。 有了对焦点的理解,你可以通过做以下事情来控制软键盘: 这对于通常不触发软键盘的组件来说工作良好;但是,对于自动升高键盘的组件,将needsSoftKeyboard设置为false没有任何作用。防止键盘在这些组件上弹出的一个解决方法是侦听激活事件,并用如下代码抑制它: 这段代码捕获了TextArea组件上的softKeyboardActivating事件,并取消了提升软键盘的默认动作。 除了在激活时获取事件,您还可以捕捉softKeyboardActivate和softKeyboardDeactivate事件,以便根据软键盘状态执行操作。 清单3–13展示了一个软键盘示例应用,它展示了所有这些技术一起使用来完全控制软键盘。 清单3–13。软键盘交互示例代码 [Bindable]privatevartype:String; privatefunctionhandleActivating(event:SoftKeyboardEvent):void{state="Activating...";type=event.triggerType;} privatefunctionhandleActivate(event:SoftKeyboardEvent):void{state="Active";type=event.triggerType;} privatefunctionhandleDeactivate(event:SoftKeyboardEvent):void{state="Deactive";type=event.triggerType;} privatefunctionpreventActivate(event:SoftKeyboardEvent):void{event.preventDefault();}]]> 这段代码创建了几个控件,并为它们附加了动作,这样你就可以随意隐藏和显示软键盘,还可以看到当前软键盘的状态,就像涓流事件所报告的那样。运行该应用后,您将会看到如图图3–13所示的UI。 图3–13。演示如何控制软键盘的示例应用 请注意,通常触发软键盘的TextArea控件不再弹出软键盘,而高亮按钮在获得焦点时会立即弹出软键盘。底部显示和隐藏键盘的两个按钮仅仅是玩焦点把戏,让Flash随意显示和隐藏键盘。 你可以在你的应用中使用同样的技术来完全控制软键盘。 也许任何用户界面最基本的元素之一就是按钮。自从1973年施乐Alto上出现第一个图形用户界面以来,它就一直存在。Figure3–14展示了一张桌子大小的Alto的图片,以及其文件管理器中使用的按钮样式。自那时以来,我们已经走过了漫长的道路,但基本概念并没有多大变化。 图3–14。施乐Alto的图像(左)和其GUI中使用的按钮样式的复制品(右) Flex有几个内置的按钮控件,具有移动优化的样式,可以在设备上以可用的大小呈现它们。其中包括以下按钮类型: 所有这些控件都有相似的样式,可以用来自定义它们。由于手机皮肤的不同,并不是所有的桌面风格都可用,比如cffHinting``direction``renderingMode。Table3–5列出了按钮类上支持移动的样式。 所有按钮类型都支持其中的大多数样式,包括嵌入在类型为ButtonBarButton的ButtonBar中的按钮。有一些例外,样式被明确地从子类中排除,比如textAlign和icon,它们在CheckBoxes和Radiobuttons中都不被支持。对于在ButtonBarButtons上设置样式,通常只需在ButtonBar上设置样式,并让CSS继承将它应用到创建的子按钮上。 由于功能的不同,操作按钮的可用属性也略有不同。Table3–6列出了可用的公共属性,包括它们适用于哪个按钮类。 除了这些属性之外,button类上还有一些事件和方法有助于交互性。其中最重要的是clickHandler函数,每当用户在按钮上按下并释放鼠标时,该函数就会被调用。此外,您可以监听一个buttonDown事件,并覆盖buttonReleased和mouseEventHandler函数来进行更高级的交互。可切换按钮子类(CheckBox、RadioButton和ButtonBarButton)上的另一个可用事件是change事件,每当selected属性改变时就会触发该事件。 为了演示不同按钮控件的使用,我们制作了一个小按钮示例,模仿了现代微波炉上的复杂控件集。本例的代码如清单3–14所示。 清单3–14。现代微波的代号举例 当在移动设备上运行时,该应用将类似于图3–15中所示。 图3–15。运转现代微波输出的例子 要使用您所学的一些新样式和属性进行练习,请尝试以下方法: s可能是移动应用中最重要的控件之一。由于有限的屏幕空间,它们取代了数据网格,通常用于通过菜单或层次结构进行向下导航。 FlexList控件已经针对移动应用进行了彻底的改进,其行为类似于你对移动设备的期望。这包括带有图标和装饰的大图形,以及当你通过一个List的开始或结束时的滚动“反弹”。 最简单的方法是,你可以创建并显示一个FlexList,只需给它一个要渲染的对象集合,如清单3–15所示。 清单3–15。代码从一个ArrayCollection创建一个List 上述代码将默认的dataProvider属性设置为字符串的ArrayCollection。默认情况下,它将使用LabelItemRenderer,它只是在一个StyleableTextField中显示List的每个条目。执行该程序将产生一个基本的List,如图3–16中的所示。 图3–16。基本List例题使用LabelItemRenderer 要创建一个更复杂的List,你可以改变itemRenderer来使用一个更复杂的渲染器。Flex4.5附带了第二个名为IconItemRenderer的内置渲染器,它具有显示以下项目组件的附加功能: 为了演示IconItemRenderers的用法,我们制作了一个示例,让您浏览所有50个州的格言和纪念币列表。该示例的代码如清单3–16所示。 清单3–16。*IconItemRenderer*样本应用代码 对于IconItemRenderer,我们使用Component标签创建了一个内联实例,并将其直接分配给itemRenderer属性。还要注意,我们选择使用*Field版本来选择标签、消息和图标。出于性能原因,这是更可取的,因为这意味着IconItemRenderer知道值是静态的,可以进行更多的缓存来提高性能。 Figure3–17显示了在移动设备上运行的状态信息示例。 图3–17。国家信息应用展示纪念币和格言 通过利用IconItemRenderer类的样式属性,您可以进一步定制列表。这里有一些关于你可以尝试的改变的建议: 在创建移动应用时,您会发现其他几个控件也很有用。这些控件包括HSlider、Scroller和BusyIndicator控件。 HSlider是一个标准的水平滑块控件,允许您指定用户可以选择的值范围。HSlider的一些特性包括: 除此之外,您可以使用几个属性来控制数据提示的显示,包括将Numeric值转换为字符串的dataTipFormatFunction、dataTipPrecision来选择小数位数,以及让您完全禁用数据提示的showDataTip。数据提示文本使用与上一节中提到的按钮组件相同的文本样式属性。对于移动使用,支持以下文本样式:fontFamily、fontStyle、leading、letterSpacing、locale、textDecoration。 为了控制滑块的范围,有几个属性可用,包括minimum、maximum、stepSize和snapInterval。这些都是Numeric值,让您控制滑块的范围以及步进和捕捉行为。您可以使用value属性设置滑块的初始值。当用户与滑块交互时,该属性也会动态更新。 最后,在使用滑块时,您可以跟踪几个事件来进行交互行为。这包括change、changing、thumbDrag、thumbPress和thumbRelease的事件。 Scroller是一个支持移动的控件,允许用户在大于可视区域的内容周围翻页。移动皮肤是一个完整的重新设计,它使用触摸事件而不是静态滚动条来平移视口。这使得在触摸屏显示器上操作更加容易,同时提供了等效的功能。 Scroller的子节点必须实现IViewport接口,该接口包括Spark库中的Group、DataGroup和RicheditableText组件。下面的示例代码展示了如何创建一个新的Scroller实例来浏览静态图像: 在这个例子中,viewport的默认属性被设置为一个VGroup,它包含一个大的BitmapImage。Group只是用来包装BitmapImage,确保外部组件是IViewport类型,否则用户看不到。在运行这个示例时,用户将能够在屏幕上拖动鼠标来浏览图像。 除此之外,由于简化的移动用户界面,真的没有什么可定制的。文本或滚动条样式都不适用,包括字体、颜色以及隐藏或显示滚动条。 最后一个组件显示一个简单的繁忙指示器小部件,带有一组圆形的旋转叶片。当图形显示在屏幕上时,它会不断地使图形产生动画效果,表明诸如加载之类的活动正在后台进行。 BusyIndicator的直径计算为高度和宽度的最小值,四舍五入为2的倍数。另外两个专用于BusyIndicator组件的刻度盘是一个用于控制动画速度(以毫秒为单位)的rotationInterval和一个用于改变等待图形颜色的symbolColor。 为了演示这三个控件的用法,我们制作了一个快速演示,它使用所有控件来提供图像的缩放和平移。HSlider控件用于改变图像的缩放级别,Scroller组件在图像被放大时提供平移,而BusyIndicator在这些动作发生时显示活动。 这个例子的完整代码显示在清单3–17中。 清单3–17。示例代码演示了HSlider、Scroller和BusyIndicator控件的使用 在运行这个示例时,您将看到类似于Figure3–18的输出,这是在Scroller拖动操作的中途捕获的。 图3-18。*Scroller*平移操作期间捕获的示例 尝试操作控件来缩放和平移图像,并随意用您选择的图像替换此示例中的图像。 在阅读了前面几节关于所有可用控件的内容并对示例进行了实验之后,您现在已经对Flextoolkit的UI功能有了很好的理解。 本章详细分析了mobileFlex应用。有了这些知识,您现在应该能够开始编写自己的移动应用了。以下主题现在是您开发工具的一部分: 您现在知道如何创建移动应用。在下一章中,我们将向你展示如何用图形、动画和图表来增加一些活力! 本章的第一部分向你展示了如何渲染各种二维形状,如矩形、椭圆、贝塞尔曲线和路径。本章的第二部分包含一个使用线性渐变和径向渐变渲染几何对象的代码示例。本章的第三部分提供了一个代码示例,说明如何使用滤镜效果,包括Blur、DropShadow和Glow。 本节中的移动代码示例演示了如何呈现各种2D形状,如矩形、椭圆形、贝塞尔曲线、多边形和路径。此外,一些代码示例包含采用各种阴影技术的多个图形图像,这将使您能够对图形图像的代码进行并排比较。 让我们从渲染两个矩形和一个椭圆开始,这是两个大家都很熟悉的2D形状。使用移动应用模板创建一个名为RectEllipse1的新Flex移动项目,并添加如清单4–1所示的代码。 清单4–1。渲染两个矩形和一个椭圆 fx:Declarations` 清单4–1以一个XMLRect元素开始,该元素指定了属性id、x、y、width和height的值。注意,XMLRect元素包含一个XMLfill元素和一个XMLstroke元素,而不是一个fill属性和一个stroke属性,这与SVG不同,SVG通过属性指定fill和stroke值。但是,XMLstroke元素包含一个XMLSolidColorStroke子元素,它将color和weight指定为属性,而不是XML元素的值。注意,SVG使用了一个stroke和一个stroke-width属性,而不是一个color属性和一个weight属性。 清单4–1还包含一个XMLEllipse元素,它定义了一个椭圆,具有与XMLRect元素几乎相同的属性和值,但是生成的输出是一个椭圆而不是矩形。 第二个XMLRect元素类似于第一个Rect元素,但是颜色不同,在屏幕上的位置也不同。 图4–1显示了两个矩形和一个椭圆。 图4–1。两个矩形和一个椭圆 Flex移动应用支持线性渐变和径向渐变。顾名思义,线性渐变以线性方式计算起始色和结束色之间的中间色。例如,如果线性渐变从黑色变化到红色,那么初始颜色是黑色,最终颜色是红色,颜色的阴影线性“过渡”在黑色和红色之间。 径向梯度不同于线性梯度,因为过渡以径向方式发生。想象一颗扔进池塘的鹅卵石,观察半径增加的圆圈的波纹效果,这让你对径向渐变是如何渲染的有所了解。 作为一个示例,下面的移动代码呈现一个具有线性渐变的矩形和一个具有径向渐变的椭圆。使用移动应用模板创建一个名为LinearRadial1的新Flex移动项目,并添加如清单4–2所示的代码。 清单4–2。使用线性渐变和径向渐变 清单4–2包含一个XMLPanel元素,该元素包含一个XMLGroup元素,其属性指定面板的布局。XMLGroup元素包含两个XML子元素:一个XMLRect元素和一个XMLEllipse元素。XMLRect元素定义了一个带有线性渐变的矩形,如下所示: ` 前面的XMLRect元素指定了属性id、x、y、width和height的值。接下来,XMLRect元素包含一个XMLfill元素(正如您在前面的示例中看到的),该元素又包含一个XMLLinearGradient元素,该元素指定了三个XMLGradientEntry元素,每个元素都为ratio和alpha属性指定了一个十进制值(在0和1之间)。XMLRect元素的最后一部分包含一个XMLstroke元素,该元素包含一个XMLSolidColorStroke元素,该元素指定属性color和weight的值。 清单4–2还包含一个XMLEllipse元素,它定义了一个带有径向渐变的椭圆。这段代码包含与XMLRect元素几乎相同的属性和值,除了它表示一个椭圆而不是矩形。 图4–2显示了一个带有线性渐变的矩形和一个带有径向渐变的椭圆。 图4–2。【线性渐变的矩形】和径向渐变的椭圆 Flex支持三次贝塞尔曲线(有两个端点和两个控制点)和二次贝塞尔曲线(有两个端点和一个控制点)。您可以轻松识别三次贝塞尔曲线,因为它以字母“C”(或“C”)开头,二次贝塞尔曲线以字母“Q”(或“Q”)开头。大写字母“C”和“Q”指定“绝对”位置,而小写字母“C”和“Q”指定相对于XMLPath元素中前面一点的位置。 三次或二次贝塞尔曲线的点中列出的第一个点是第一个控制点,在三次贝塞尔曲线的情况下,后面是另一个控制点,然后是第二个端点。二次和三次贝塞尔曲线中的第一个端点是XMLPath元素中指定的前一个点;如果未指定点,则将原点(0,0)用作第一个端点。 您也可以使用字母“S”(对于三次贝塞尔曲线)或字母“T”(对于二次贝塞尔曲线)来指定贝塞尔曲线序列。 使用移动应用模板创建一个名为BezierCurves1的新Flex移动项目,并添加如清单4–3所示的代码,该代码显示了四条贝塞尔曲线的代码:一条三次贝塞尔曲线、一条二次贝塞尔曲线、两条组合的三次贝塞尔曲线以及一条组合的三次和二次贝塞尔曲线。 清单4–3。渲染三次和二次贝塞尔曲线 清单4–3包含一个XMLPanel元素,该元素又包含四个XMLPath元素,这些元素指定带有各种阴影的贝塞尔曲线。第一个XMLPath元素指定了一条三次贝塞尔曲线,如下所示: 此三次贝塞尔曲线的第一个端点是(0,0),因为没有指定点;控制点为(100,300)和(200,20);并且目的地端点是(300,100)。 这个XMLPath元素包含一个XMLLinearGradient元素,该元素从白色到红色变化,不透明度为0.5,后跟宽度为4的蓝色笔划,如下所示: 第二个XMLPath元素指定了一条二次贝塞尔曲线,该曲线的第一个端点是(0,0),因为没有指定点;这条二次贝塞尔曲线的单个控制点是(250,200);而目的地端点是(100,300)。这个XMLPath元素包含一个XMLLinearGradient元素,从黑色到蓝色变化,不透明度为0.8。 第三个XMLPath元素指定了一条与第二条三次贝塞尔曲线“连接”的三次贝塞尔曲线,如下所示: 这条三次贝塞尔曲线的两个控制点是(100,300)和(20,300),目的端点是(300,100)。这个XMLPath元素的第二部分指定了一条二次贝塞尔曲线,它的控制点是(250,200),目标端点是(300,250)。 这个XMLPath元素包含一个指定从黄色到红色的线性渐变的XMLLinearGradient元素,后面是一个指定黑色和宽度为4单位的XMLstroke元素。 最后一个XMLPath元素指定了一条三次贝塞尔曲线,后跟第二条三次贝塞尔曲线,如下所示: 这条三次贝塞尔曲线的控制点是(250,300)和(200,150),目的端点是(350,100)。这个XMLPath元素的第二部分指定了一条二次贝塞尔曲线,它的控制点是(250,250),目标端点是(400,280)。 这个XMLPath元素包含一个XMLLinearGradient元素,它指定从黄色到红色的线性渐变,不透明度为0.5,后面是一个XMLstroke元素,它指定黑色和线宽为4单位。 图4–3显示了三次、二次和组合贝塞尔曲线。 图4–3。三次、二次和组合贝塞尔曲线 在前面的例子中,您看到了如何使用Path元素来呈现一组贝塞尔曲线。元素还可以让你组合其他的2D形状,比如线段和带有线性渐变和径向渐变的贝塞尔曲线。使用移动应用模板创建一个名为Path1的新Flex移动项目,并添加如清单4–4所示的代码。 清单4–4。结合线段和贝塞尔曲线 清单4–4中的XMLPanel元素包含一个XMLPath元素,它使用线段来呈现一个梯形,后跟一对三次贝塞尔曲线。XMLPath元素的data属性如下所示: data属性的第一部分(以字母M开始)指定一个梯形;data属性的第二部分(以字母C开始)呈现一条三次贝塞尔曲线;data属性的第三部分(以字母T开始)指定了另一条三次贝塞尔曲线。 图4–4显示了一条梯形和两条三次贝塞尔曲线。 图4–4。基于路径的梯形和贝塞尔曲线 Flex滤镜效果对于在基于Flex的应用中创建丰富的视觉效果非常有用,这些效果可以真正增强应用的吸引力。Spark原语支持多种滤镜,包括Blur滤镜,一个DropShadow滤镜,一个Glow滤镜,都属于spark.filters包。 使用移动应用模板创建一个名为RectLGradFilters3的新Flex移动项目,并添加如清单4–5所示的代码。 清单4–5。用火花滤镜画矩形 清单4–5包含一个XMLRect元素,它定义了一个用线性渐变渲染的矩形。ratio属性是一个介于0和1之间的十进制数,它指定了颜色过渡从起点到终点的距离的分数。在清单4–5中,第一个GradientEntry元素有一个ratio属性,其值为0,这意味着矩形用颜色0xFF0000(红色的十六进制值)呈现。第二个GradientEntry元素有一个ratio属性,它的值是0.33,这意味着矩形是用颜色0xFFFF00(黄色的十六进制值)从初始位置到目的地的33%的位置呈现的。第三个GradientEntry元素有一个值为0.66的ratio属性,因此矩形从初始位置到目的位置的66%处用颜色0x0000FF(蓝色的十六进制值)呈现。 alpha属性是不透明度,是介于0(不可见)和1(完全可见)之间的十进制数。清单4–5中的三个GradientEntry元素有一个0.5的alpha属性,所以矩形是部分可见的。尝试比率属性和alpha属性的不同值,以便可以找到创建令人愉悦的视觉效果的组合。 XMLRect元素的最后一部分包含一个XMLstroke元素,该元素指定红色和描边宽度2,后跟三个火花过滤器,如下所示: 本例中的三个Spark过滤器具有直观的名称,表明当您将它们包含在代码中时可以创建的效果。第一个Spark过滤器是一个向XMLRect元素添加“投影”的DropShadowFilter。第二个火花过滤器是一个BlurFilter,它增加了模糊效果。第三个也是最后一个火花过滤器是一个GlowFilter,它创建了一个辉光过滤器效果。 Figure4–5显示了一个带有线性渐变和三个火花过滤器的矩形。 图4–5。一个带有线性渐变和三个火花滤镜的矩形 本章的这一节介绍了如何将变换应用于几何对象,包括本章上一部分讨论的对象。Spark原语支持以下效果和变换: 这些Spark原语在spark.effects包中,它们可以应用于Spark组件,也可以应用于MX组件;mx.effects包(包含在Flex4SDK中)包含可以应用于MX组件的相应功能。 以下小节包含一个Flex代码示例,它说明了如何在Flex中创建缩放效果。 缩放效果(即,扩展或收缩形状)对于面向游戏的应用非常有用,并且在基于Flex的应用中非常容易创建。使用移动应用模板创建一个名为ScaleEffect1的新Flex移动项目,并添加如清单4–6所示的代码。 清单4–6。用线性渐变创建缩放效果 fx:Library 清单4–6包含一个XMLDefinition元素,它指定一个带有矩形定义的XMLRect元素,以及另一个XMLDefinition元素,它指定一个带有椭圆定义的XMLEllipse元素。XMLGroup元素包含两个对矩形的引用和两个对椭圆的引用,如下所示: 第一个XML元素通过为属性scaleX和scaleY指定值6和3来缩放先前定义的矩形。第二个XML元素通过为属性scaleX和scaleY指定值3和8来缩放先前定义的矩形。 图4–6显示了两个缩放的矩形和两个缩放的椭圆。 图4–6。两个缩放的矩形和椭圆 本节包含的移动代码展示了如何将动画效果应用到几何对象上,包括本章上一部分讨论的那些对象。动画效果的火花原语如下: 以下部分提供了移动代码示例,说明如何使用XMLAnimate元素以及如何并行和顺序定义动画效果。 对于面向游戏的应用来说,动画效果显然非常受欢迎,而且它们也可以有效地用于其他类型的应用。同时,请记住,在以业务为中心的应用中谨慎使用动画效果可能是个好主意。 使用移动应用模板创建一个名为AnimPropertyWidth的新Flex移动项目,并添加如清单4–7所示的代码。 清单4–7。动画显示矩形的宽度 fx:Declarations 清单4–7包含一个XMLDeclarations元素,该元素又包含一个定义动画特定细节的XMLAnimate元素。XMLAnimate元素有一个值为MyAnimate1的id属性,该属性在本节稍后描述的点击处理事件中被引用。 清单4–7包含一个XMLVGroup元素,该元素又包含一个XMLRect元素,其内容类似于您在本章中已经看到的例子。 清单4–7包含一个XMLButton元素,使您能够开始动画效果。每当用户单击或点击这个按钮时,代码将执行事件处理程序,其id属性为MyAnimate1,这在前面的代码示例中已定义。动画效果很简单:矩形宽度在两秒钟(2000毫秒)内从200个单位增加到400个单位。 图4–7和图4–8显示了当用户点击按钮时,一个矩形在屏幕上水平移动的两个快照。 图4–7。一个带有动画的矩形(初始位置) 图4–8。一个带动画的矩形(最终位置) 清单4–8。创造连续动画效果 fx:Declarations 清单4–8包含一个XMLDeclarations元素,该元素又包含两个XMLSequence元素,这两个元素指定了三种转换效果。动画效果从XMLMove元素开始(提供翻译效果),然后是XMLRotate元素(提供旋转效果),最后是XMLScale元素(提供缩放效果)。当用户点击第一个XMLButton元素时,这将调用XMLSequence元素中定义的动画效果,该元素的id属性的值为transformer1。 类似的注释也适用于第二个XMLSequence元素和第二个按钮,只是动画效果包含两次完整的旋转,而不是一次旋转。 请注意,通过用XMLParallel元素替换XMLSequence元素,您可以轻松地将动画效果从顺序改为并行,如下所示: 图4–9和图4–10显示了两个按顺序经历动画效果的按钮。由于截图只捕获了初始和最终的动画效果,因此在移动设备上启动这个移动应用,这样您还可以看到滑动效果和旋转效果。 图4–9。一个带有连续动画的按钮(初始) 图4–10。一个带有连续动画的按钮(后) Flex支持多种3D效果,包括移动、旋转和缩放JPG文件。3D“移动”效果包括移动JPG图像以及减小图像的尺寸,而3D缩放效果包括将JPG图像的宽度和高度从起始值(通常为1)增加(或减小)到最终值(可以大于或小于1)。3D“旋转”效果包括旋转JPG图像,使其看起来像在三维空间中旋转。 清单4.9中的以下代码示例向您展示了如何在基于移动设备的应用中创建移动、旋转和缩放JPG文件的3D效果。 Figure4–11显示了卡桑德拉·陈(斯蒂芬·陈的女儿)的JPG形象Cassandra4.jpg,该形象用于说明这三种3D动画效果的代码示例中。 图4–11。3D特效的JPG 清单4–9。制作3D动画效果 fx:Declarations ` 清单4–9包含一个XMLDeclarations元素,该元素包含三个3D效果元素,以及三个XMLButton元素,用户可以单击这些元素来创建3D效果。XMLMove3D元素通过属性xBy和zBy指定目标位置,还有一个值为2的repeatCount(执行动画效果两次),以及一个值为reverse(每次都返回到原始位置)的repeatBehavior。相应的XMLButton元素包含一个值为Move的label属性和一个值为moveEffect.play()的click属性,后者调用在XMLDeclarations元素中定义的XMLMoveEffect元素中指定的移动动画效果。 旋转效果通过XMLRotate3D元素处理,其属性angleYFrom和angleYTo分别指定0和360的开始和结束角度(即一次完整的旋转)。这种旋转效果会出现四次。XMLButton元素包含一个值为Rotate的label属性和一个值为rotateEffect.play()的click属性,该属性调用在XMLDeclarations元素中定义的XMLRotate3D元素中指定的缩放动画效果。 缩放效果(这是第三个也是最后一个效果)是通过XMLScale3D元素处理的,该元素包含几个属性,这些属性的值指定了同一个JPG图像的动画行为的细节。id属性的值为atScale,用于在代码的其他地方引用这个元素。属性target引用了XML元素,其id的值为targetImg,引用了JPG图像。scaleXBy属性的值为-0.25,它将JPG图像缩小25%。repeatCount属性的值为4,repeatBehavior属性的值为reverse,表示动画效果出现四次,从左到右来回交替。另外两个属性是effectStart和effectEnd,它们指定动画开始和结束时的行为,在本例中是禁用然后启用playButton。 注意,XMLImage元素指定了Cassandra4.jpg,的位置,它位于这个移动项目的顶层目录的images子文件夹中。出于布局目的,XMLImage元素在XMLVGroup元素中指定,该元素还包含一个XMLHGroup元素,该元素包含三个XMLButton元素。 Figure4–12显示了经过3D“移动”效果后的JPG。 图4–12。一个3D移动后的JPG效果 图4–13显示了经过3D“旋转”效果后的JPG。 图4–13。一个3D旋转后的JPG效果 图4–14显示了经过3D“缩放”效果后的JPG。 图4–14。一个JPG经过3D缩放后的效果 当您希望在移动应用的某些方面创建更丰富的视觉效果时,自定义外观非常有用。例如,您可以创建多个自定义外观,将图形效果(包括您在本章前面学习的那些)应用于按钮。我们将要讨论的代码示例清楚地展示了创建Spark自定义皮肤效果的过程。 清单4–10到4–12分别显示CustomSkinHomeView.mxml、ButtonSkin1.mxml和ButtonSkin2.mxml中的代码内容。 在本节讨论MXML文件之前,让我们看一下下面的将文件ButtonSkin1.mxml(在skins包中)添加到项目中的步骤列表。 对定制皮肤ButtonSkin2.mxml重复前面的一组步骤,并对您想要添加到这个项目中的任何额外的定制皮肤重复这些步骤。现在让我们看看CustomSkin.mxml的内容,显示在清单4–10中 清单4–10。创建自定义火花皮肤 清单4–10包含一个XMLVGroup元素,该元素包含十个“成对的”XML元素,用于呈现一个标准XMLLabel元素和一个标准XMLButton元素,其中第一个是一个普通的按钮,如下所示: 前面的XML元素很简单:第一个是标签(“ThisisaNormalButton”),第二个呈现按钮。 第一对包含皮肤按钮的XML元素显示标签“FirstSkinnedButton:”,第二个元素基于包skins中Flex皮肤ButtonSkin1的内容呈现一个XMLButton元素。类似地,下一对包含皮肤按钮的XML元素显示标签“SecondSkinnedButton:”,这一对中的第二个元素基于包skins中Flex皮肤ButtonSkin2的内容呈现一个XMLButton元素。类似的注释也适用于其他两个自定义按钮。 现在让我们看看清单4–11中ButtonSkin1.mxml的内容,它包含了渲染第二个按钮(这是第一个皮肤按钮)的数据。 清单4–11。创建带有图形的按钮皮肤 fx:Metadata[HostComponent("spark.components.Button")] 清单4–11包含一个XMLSkin根节点,其中有三个XML子元素定义了定制皮肤的行为。第一个子元素是XMLMetadata元素,如下所示: 前面的XML元素指定了Button类的包名,这也是您在项目中添加自定义皮肤ButtonSkin1.mxml时指定的名称。 第二个子元素是XMLstates元素,如下所示: 清单4–12。创建第二个按钮皮肤 fx:Metadata[HostComponent("spark.components.Button")] 注意,清单4–12和清单4–11的唯一区别是XMLPath元素而不是XMLRect元素。 XMLPath元素很简单:它包含一个数据属性,该属性的值是一组指定矩形的线段,矩形的颜色是#FF0000(红色),边框是#0000FF(蓝色),宽度是4。 如你所见,Flex使得定义自定义皮肤变得非常容易。然而,更复杂(也更有趣)的自定义皮肤通常指定鼠标事件(如鼠标按下、鼠标抬起等)的行为以及相应的状态变化方面的触摸事件。您可以“绑定”在这些事件期间执行的ActionScript函数(由您编写),以便更改应用各个方面的可视显示。 Figure4–15显示了一个标准的Flex按钮和四个使用自定义皮肤的按钮。 图4–15。一个标准按钮和四个带有自定义火花皮肤的按钮 Flex4为以下2D图表和图形提供了良好的支持: 在下面的示例中,您将学习如何编写用于呈现2D条形图和2D饼图的移动代码示例,您还将看到具有动画效果并可以处理鼠标事件和触摸事件的代码示例。请注意,Flex使用术语“条形图”表示水平条形图(即每个条形元素从左到右水平呈现),术语“柱形图”指垂直条形图。 现在,使用移动应用模板创建一个名为BarChart1的新Flex移动项目,添加一个名为chartdata的新顶级文件夹,然后在这个名为ChartData.xml的文件夹中添加一个新的XML文档,其中包含了清单4–13中显示的数据。 清单4–13。定义基于XML的图表数据 现在让我们看一下清单4–14,它包含使用清单4–13中基于XML的数据呈现条形图的代码。 清单4–14。创建条形图 fx:Declarations fx:Style@namespaces"library://ns.adobe.com/flex/spark";@namespacemx"library://ns.adobe.com/flex/mx";mx|ColumnChart{font-size:12;font-weight:bold;} 清单4–14定义了XML文档ChartData.xml在XMLModel元素中的位置,以及由基于XML的数据组成的ArrayCollection和一个简单的数据格式化程序。清单4–14包含一个XMLStyle元素,该元素为两个CSS属性font-size和font-weight指定值,分别为12和bold,用于在饼图中呈现文本。 XMLColumnChart元素指定了一个柱形图,以及属性dataProvider、height和weight的适当值,它们的值分别是chartData、75%和80%。注意,chartData是一个在XMLDeclarations元素中定义的ArrayCollection变量,而chartData是用XML文档ChartData.xml中指定的数据值填充的。 height和weight属性的值被指定为呈现饼图的屏幕尺寸的百分比;根据您希望条形图占据屏幕的百分比来调整这些属性的值(50%表示半宽或半高,25%表示四分之一宽或四分之一高,依此类推)。 XMLColumnChart元素包含两个重要的元素。首先,有一个XMLhorizontalAxis元素指定了水平轴的month值(在ChartData.xml中指定)。其次,有一个XMLseries元素,引用条形图水平轴的month值和垂直轴的revenue值。 图4–16显示了一个基于XML文件ChartData.xml中数据的条形图,显示在清单4–13中。 图4–16。2D条形图 饼图也很受欢迎,因为它以一种更容易理解数据元素之间关系的方式显示数据。我们将创建一个饼图,它使用了清单4–13中的XML文档ChartData.xml中的数据,这些数据与上一个示例中用于呈现条形图的数据相同。使用移动应用模板创建一个名为PieChart1的新Flex移动项目,并添加如清单4–15所示的代码。 清单4–15。创建饼状图 fx:Style@namespaces"library://ns.adobe.com/flex/spark";@namespacemx"library://ns.adobe.com/flex/mx";mx|PieChart{font-size:12;font-weight:bold;} 清单4–15包含一个XMLDeclarations元素和一个XMLStyle元素,它们与清单4–14相同。定义私有函数getWedgeLabel的XMLScript元素返回由每个饼图楔形区的name:value对组成的字符串,如下所示: XMLPieChart元素指定了一个饼图,以及其值指定如何呈现饼图的属性。例如,height和width属性都有值80%,这意味着图表的高度和宽度是屏幕尺寸的80%。根据您希望饼图占据屏幕的百分比来调整这些属性的值(就像您对条形图所做的那样)。 XMLPieChart元素还包含一个XMLPieSeries元素,该元素又包含四个属性,使您能够指定如何呈现饼图数据和饼图扇区。field属性的值为revenue,这意味着XMLrevenue元素的数据值呈现在饼图中。 labelFunction属性的值为getWedgeLabel,这是一个ActionScript函数(在前面的fx:Script元素中定义),它指定了饼图中每个饼图“楔形”的标签。 labelPosition属性的值为callout,这意味着每个饼图扇区的标签都呈现在饼图扇区之外,从饼图扇区到其标签之间有一条“断开的”线段。注意,labelPosition属性可以有另外三个值:inside、outside或insideWithCallout。尝试这些值,看看它们如何改变饼图的呈现。 最后,explodeRadius属性的值为0.05,它呈现的饼图在相邻的饼图扇区之间留有空间,产生了一种“爆炸”效果。 图4–17显示了一个2D饼图。 图4–17。【2D】饼状图 第三章包含了对FXG的简要介绍,这一节包含了一个代码示例,演示了如何将清单4–1(包含了渲染矩形和椭圆的代码)转换成使用FXG的Flex项目。 使用移动应用模板创建一个名为FXG1的新Flex移动项目,创建一个名为components的顶层文件夹,然后在这个名为RectEllipse1.fxg的文件夹中创建一个文件,其内容如清单4–16所示。 清单4–16。使用FXG定义图形元素 XMLGraphic元素包含两个XML元素,它们的数据值与清单4–1中的XMLRect元素和XMLEllipse元素相同,除此之外还有以下区别: 清单4–17展示了如何引用清单4–16中定义的元素。 清单4–17。引用FXG组件 清单4–17包含一个引用FXG文件RectEllipse1.fxg的名称空间,该文件位于components子目录中。XMLVGroup元素在comps名称空间中包含一个XMLRectEllipse1元素,该元素引用一个XML元素,该元素的id属性的值为rect1,该值在FXG文件RectEllipse1.fxg中定义,如清单4–16所示。 图4–18显示一个椭圆和两个矩形,与图4–1中的相同。 图4–18。【一个长方形和一个椭圆】 从这个例子可以推测,FXG使您能够模块化Flex项目中的代码。此外,以下Adobe产品使您能够将项目导出为FXG文件,然后您可以将这些文件导入Flex项目: 你可以在第九章中看到FXG文件更复杂的例子。 使用MobileFlex应用模板创建一个名为Sketch1的新Flex移动项目,并添加显示在清单4–18中的代码。出于讨论的目的,代码以较小的代码块呈现,请记住完整的代码可以从本书的网站下载。 清单4–18。渲染并保存草图 importmx.graphics.ImageSnapshot;importmx.graphics.SolidColor;importmx.graphics.codec.JPEGEncoder; privatevarcolors:Array=[0xFF0000,0x00FF00,0xFFfF00,0x0000FF];privatevarsingleTapCount:int=0;privatevartouchMoveCount:int=0;privatevarwidthFactor:int=0;privatevarheightFactor:int=0;privatevarcurrentColor:int=0;privatevarrectWidth:int=20;privatevarrectHeight:int=20; if(event.isPrimaryTouchPoint){currentColor=colors[touchMoveCount%colors.length];}else{currentColor=colors[(touchMoveCount+2)%colors.length];} varmyRect:Rect=newRect();myRect.x=event.localX;myRect.y=event.localY;myRect.width=rectWidth;myRect.height=rectHeight;myRect.fill=newSolidColor(currentColor); varmyGroup1:Group=event.targetasGroup;myGroup1.addElement(myRect);}` 清单4–18以一个XMLScript元素开始,该元素包含各种导入语句和在一些ActionScript3方法中使用的恰当命名的变量的定义(例如,用于跟踪触摸事件)。 函数touchMove包含处理移动事件的代码。该函数首先递增变量touchMoveCount,然后使用该变量作为数组colors的索引,从而呈现一组矩形,其颜色遍历该数组。该函数中的其余代码在触摸事件的位置创建一个小矩形。这实际上是图形渲染代码的“心脏”,但是其他函数处理其他事件。 下一个代码块包含函数touchEnd()的代码,它实际上是可选的,但是它向您展示了一个在这个事件处理程序中可以做什么的例子。 `functiontouchEnd(event:TouchEvent):void{++touchMoveCount; if(event.isPrimaryTouchPoint){currentColor=colors[touchMoveCount%colors.length];}else{currentColor=colors[0];} widthFactor=(touchMoveCount%3)+1;heightFactor=(touchMoveCount%3)+2; varmyRect:Rect=newRect();myRect.x=event.localX;myRect.y=event.localY;myRect.width=rectWidthwidthFactor;myRect.height=rectHeightheightFactor;myRect.fill=newSolidColor(currentColor); 函数touchEnd中处理“touchup”事件的代码递增变量touchMoveCount,然后使用该变量作为数组colors的索引,但是在这种情况下,执行一些简单的算法来呈现不同尺寸的矩形。 `functiontouchSingleTap(event:TouchEvent):void{varmyRect:Rect=newRect();myRect.x=event.localX;myRect.y=event.localY; ++singleTapCount;if(event.isPrimaryTouchPoint){currentColor=colors[singleTapCount%colors.length];myRect.width=rectWidth3;myRect.height=rectHeight2;}else{currentColor=colors[(singleTapCount+1)%colors.length];myRect.width=rectWidth2;myRect.height=rectHeight3;} myRect.fill=newSolidColor(currentColor); 处理单击事件的逻辑在函数touchSingleTap中。该函数递增变量touchSingleTapCount,然后应用一些简单的逻辑来确定在单击事件位置呈现的矩形的尺寸。 `functiontouchMoveHandlerImage(event:TouchEvent):void{touchMove(event);} functiontouchTapHandlerImage(event:TouchEvent):void{touchSingleTap(event);} privatefunctionsaveImageToFileSystem():void{varjPEGEncoder:JPEGEncoder=newJPEGEncoder(500);varimageSnapshot:ImageSnapshot=ImageSnapshot.captureImage(imgPanel,0,jPEGEncoder); varfileReference:FileReference=newFileReference();fileReference.save(imageSnapshot.data,"fingersketch.jpg");}]]>` 两个函数touchMoveHandlerImage和touchTapHandlerImage(顾名思义)处理JPG文件fingersketch.jpg的移动事件和单击事件,该文件存储在Flex应用的images子目录中。这两个函数包含一行调用相应函数touchMove和touchTapHandler的代码,这在本节前面已经讨论过了。 每当您单击SaveSketch按钮时,就会调用函数saveImageToFileSystem,它包含将当前草图保存到移动设备的文件系统的代码。将出现一个弹出对话框,其中包含JPG文件的默认位置和名称,在保存当前草图之前,您可以更改这两个位置和名称。 ` XMLGroup元素还包含各种图形对象的定义,包括椭圆、矩形和贝塞尔曲线,这在本章前面已经学过。这些图形对象显然是可选的,它们只是为了让你知道如何制作一个对用户有吸引力和视觉吸引力的草图程序。 图4–19显示了在移动设备中启动草图程序后的草图样本。 图4–19。一张样图 在本章中,您学习了如何使用Spark组件在面向移动设备的图形应用中呈现各种2D图形形状。如果您已经熟悉渲染基于Flex的图形,那么您可以快速轻松地利用现有知识来创建使用图形的移动应用。 您使用的图形图像和图形效果取决于应用特定的要求,您可以在自己的移动项目中使用的一些效果包括: 使用条形图和饼图执行有效的数据可视化* 至此,我们已经向您展示了如何在Flash平台上构建引人入胜的应用,这些应用利用了FlashProfessional和FlashBuilder中的移动功能。然而,为了展示您新开发的应用,您需要知道如何准备您的应用进行部署,将它们安装在开发设备上,并将您的应用部署到AndroidMarket,最终用户可以在那里下载它们。 在本章中,我们将首先向您展示如何安装和配置AndroidSDK,并在Android模拟器中运行。这是在一系列不同设备类型和操作系统组合上试验您的应用的好方法,这通常需要专门的设备测试实验室。 接下来,我们将向您展示如何从FlashProfessional和FlashBuilder部署您的应用,并使用您在前面章节中开发的一些应用作为示例。这是对高级主题的补充,如证书创建、命令行部署和Android模拟器的打包。 最后,我们向您展示如何将您的应用发布到AndroidMarket和AmazonAppstore。一旦您成功发布了一个应用,它将像商店中的任何其他本机应用一样出现,因为它是在Flash平台上构建的,这一事实对您的最终用户是完全透明的。 如果您没有现成的Android设备,或者正在寻找一种方法在新的或不同的硬件上部署和测试您的代码,AndroidEmulator是一个很好的选择。SDK附带的Android模拟器尽可能接近于运行真实的东西,包括运行完整的Android操作系统堆栈,并支持类似的开发人员与USB连接设备的交互。 Table5–1比较了在设备上运行、在仿真器中运行和在AIRDebugLauncher(ADL)中运行的体验。 正如你所看到的,ADL是一种在开发过程中测试Flash应用的方便方法,但它不是一个完整的Android环境。相比之下,Android模拟器在虚拟设备上运行完整版本的Android操作系统,因此您可以测试您的应用在不同的操作系统版本和屏幕组合上的表现。 在桌面模拟器中运行时有一些限制。最值得注意的是,你没有多点触摸支持。此外,一些Android按钮和功能只能通过命令行选项或按键绑定来使用,这将在下一节“模拟器按键绑定”中详细介绍 尽管有这些限制,Android模拟器是在多种不同设备和Android操作系统版本上测试您的应用的一种非常经济有效的方式,也是一种您不想失去的工具。 在设备上安装和运行Flash的先决条件是安装AndroidSDK。为了运行模拟器,您需要下载并安装JavaSDK和AndroidSDK。您可以在此下载适用于您的平台的最新版本的Java: 注:Java预装在MacOSX上 AndroidSDK可免费用于个人和商业用途,并可通过以下网址从Google下载: 初始下载相对较小,可以解压缩到您选择的目录中。要完成安装,您必须在主目录中运行SDK安装程序。这将提示您一个软件包安装对话框,如图Figure5–1所示。 图5–1。AIRforAndroid软件包安装对话框 您可以选择想要的软件包,方法是分别选择它们并单击“接受”按钮,或者只需单击“全部接受”。一旦您点击安装按钮,您接受的软件包将被下载和安装。 一个可选的步骤是安装Eclipse和Eclipse的Android开发工具(ADT)插件。正如下一章所讨论的,如果你想做任何原生Android开发,这是很有帮助的。ADTEclipse插件可以从以下URL下载: 也可以使用您选择的IDE来开发原生Android应用。您只需使用AndroidSDK附带的命令行工具来编译和打包您的项目。 Android模拟器的核心是Android虚拟设备(AVDs)。每个AVD指定该设备特有的设置,包括API版本、屏幕大小和硬件属性。使用AVDs,您可以拥有自己的私有虚拟设备实验室,针对要测试应用的每个目标设备进行不同的配置。 首先,您需要创建第一个AVD来运行Flash平台应用。这是在AndroidSDK和AVD管理器中完成的,在第一次安装SDK时运行。 您可以通过导航到sdk/tools目录并启动android可执行文件,从命令行重新启动AndroidSDK和AVD管理器。 片刻之后,Android将启动SDK管理器。在这里,您可以通过执行以下步骤来创建一个新的AVD。 图5–2。对话框创建新的Android虚拟设备 步骤3中AVD的名称只是一个建议,所以您可以用另一个名称替换这个字符串。 要启动新创建的AVD,请在列表中选择它,然后单击Start…按钮。它会显示标准的Android启动屏幕,然后是一个锁定屏幕。拖动锁形符号解锁仿真器后,您将看到熟悉的主屏幕,如图图5–3所示。 图5–3。【Android2.3.3】运行在桌面上的仿真器 模拟器的Android标准皮肤在左边显示你的设备屏幕,在右边显示完整的Android按钮和按键。有些键,比如拨号和挂断键,可能并不是在每个Android设备上都可以找到,但是模拟器仍然可以让你测试你的应用在这些键被按下时的行为。 几乎所有你能在普通Android设备上做的事情都可以在模拟器上实现,所以在继续安装你自己的应用之前,先熟悉一下用户界面。您可以启动预装的应用,如浏览器、联系人或电子邮件,或者来自AndroidMarket的新应用。默认情况下,模拟器映像带有所有启用的开发选项,如USB调试、“保持清醒”和模拟位置,但熟悉设置应用也是值得的。 当您通过USB调试在物理设备上运行时,如果您的AIRSDK包含较新版本,FlashBuilder将自动提示您升级已安装的AIR版本。您还可以选择直接从AndroidMarket下载并安装AIR的发布版本,这正是在您运行没有安装AIR的FlashAndroid应用时会发生的情况。 然而,在模拟器的情况下,你不能直接在AndroidMarket之外使用AIR的版本,因为它们不兼容。此外,由于FlashBuilder不直接与Android模拟器集成,因此您也不能使用自动更新机制来安装AIR。 解决方法是从SDK手动安装AIR运行时。AIRSDK可以在FlashBuilder安装的sdks/ runtimes/air/android/emulator/Runtime.apk 注意:设备和模拟器有单独的AIR运行时,因此请确保为此选择模拟器运行时。 可以从命令行使用AndroidDebugBridge(ADB)程序安装该文件。ADB是AndroidSDK附带的工具之一,可以在platform-tools文件夹中找到。清单5–1展示了在MacOSX的默认安装位置安装模拟器APK的命令 ***Listing5–1.**InstallationCommandfortheAIREmulatorRuntime* adbinstall"/Applications/AdobeFlashBuilder4.5/sdks/4.5.0/runtimes/air/android/emulator/Runtime.apk" 在Windows上,该命令非常相似,只是FlashBuilder安装的路径位置不同。 提示:您还可以使用AIR调试工具(ADT)来安装AIR运行时。配置ADT将在本章后面的“设置ADT”一节中介绍。使用ADT安装AIR运行时的命令如下: adt-installRuntime-platformandroid 在Android模拟器中运行时,您可以选择使用普通的桌面键盘作为输入。除了Android设备上有几个特殊的键没有到桌面键盘的正常映射之外,这种方式工作得相当好。例如电源按钮、音量和相机按钮。 为了便于在Android设备上按下这些按钮,默认的设备皮肤在物理仿真器面板上包含了这些按钮,因此您可以用鼠标单击它们。然而,不能保证将来你自己安装的Android皮肤或自定义皮肤会有完整的按钮。一个这样的皮肤,如图图5–4所示,给你一个接近照片质量的NexusS设备外观,1但是缺少一些按钮。 图5–4。NexusS安卓模拟器皮肤 为了克服这个限制,Android模拟器提供了完整的按键绑定。Table5–2列出了从普通Android键到桌面键盘修饰键的映射,当使用任何仿真器皮肤时,您都可以键入这些修饰键。 除了重新映射Android按钮之外,模拟器还有一些隐藏的功能,只能通过按键绑定来访问。Table5–3显示了一些特殊的键绑定,在仿真器中测试应用时,您会发现它们很有用。 为了充分利用前面的键绑定,您需要知道如何从命令行启动模拟器并传入参数。从命令行启动Android模拟器是对emulator可执行文件的直接调用,该文件可以在sdk/tools目录中找到: emulator-avd 您替换的虚拟设备名称与Android工具中定义的名称完全相同,如前一节所示。然后,您可以添加任何想要使用的附加选项,例如-trace或-onion。 如果您一直使用设备通过USB测试您的应用,那么您已经在开发过程中进行了有限形式的部署。然而,您可能使用的是调试版本,当您的最终用户获得一个完全打包的应用时,不必担心对他们来说很重要的许多事情,比如权限、适当的证书和图标。 在这一节中,我们将更详细地研究应用描述符,演示如何微调您的应用部署,以改善用户体验并为您的公司形象树立品牌。 虽然可以通过FlashProfessional和FlashBuilder完成整个发布工作流,但对于自动化和脚本编写而言,能够从命令行完成相同的活动非常有用。AdobeAIRSDK提供了一个名为AIRDeveloperTool(ADT)的命令行,它允许您从脚本或构建文件执行任何操作。 要从命令行使用ADT,必须预先设置以下内容: 设置完成后,您可以使用ADT从命令行完成许多不同的打包和部署活动。其中包括以下内容: 在本章中,我们将利用ADT来展示Flash工作流的自动化潜力。 用户对您的应用的第一印象将是它在安装时请求的不同权限的列表。因此,您应该确保您所请求的权限对您的应用有意义,并且是您可以交付功能的最小集合。 请求太大的权限可能会让用户暂停安装应用。例如,Twitter客户端没有理由需要写入外部存储,因此请求该权限可能会阻止精明的用户出于安全考虑安装您的应用。 提示:您可能已经默认启用的权限之一是INTERNET权限。应用的USB调试需要该权限,因此在开发过程中启用该权限非常重要。大多数应用还需要访问Internet运行时,因此很可能您发布的应用版本也需要此权限;但如果没有,记得禁用这个。 FlashProfessional有一个专用的用户界面来管理所有部署选项,包括权限。若要打开“设置”面板,请从“文件”菜单中选择“AirforAndroid设置…”。然后点击Permissions选项卡,您将得到一个对话框,其中每个权限都有复选框,如图Figure5–5所示。 图5–5。【Flash职业权限】选项卡中的空中安卓设置对话框 您还可以通过选择顶部的复选框来手动设置应用描述符文件中的权限。如果您想这样做,请参阅“手动更改应用描述符中的权限”一节。 FlashBuilder允许您在首次创建项目时设置权限。为此,点击新建移动项目向导第二页中的权限选项卡,如图Figure5–6所示。 图5–6。新建项目向导中的FlashBuilder权限选项卡 请注意,当您进入对话框时,INTERNET权限是预先选定的。这是USB设备调试工作所必需的。如果您需要任何额外的权限,可以在开始项目之前设置它们。 一旦创建了项目,就不能再通过项目设置对话框更改权限。相反,您可以按照下一节中的说明直接编辑为您创建的应用描述符文件。 如果您选择手动管理权限(对于FlashProfessional)或在项目创建后修改权限(对于FlashBuilder),则您需要知道如何修改应用描述符文件来更改权限。 清单5–2。示例AIR应用描述符的权限部分 对于您想要启用的每个权限,您可以复制uses-permission标记并用适当的权限名称替换PERMISSION_NAME占位符。清单5–3以适当的格式显示了所有可用的Android权限的例子,直接包含在应用描述符中。 清单5–3。全套可用安卓权限 在这些权限中,有一些可以作为一个组来启用和禁用,例如: 因此,如果您计划使用这两种API中的任何一种,请确保同时启用这两种权限。 每次用户打开你的应用,他们都会看到你选择的Android启动图标,所以这是很专业的,并且代表你的应用,这一点很重要。 从Android2.0开始,他们标准化了面向前的图标设计,建议选择应用的一个方面,并用全尺寸的描述来强调这一点。图5–7突出显示了一些启动器图标,它们是推荐的Android外观和感觉的模型示例。 图5–7。示例Android应用启动器图标2 为了更容易地构建符合这些标准的应用图标,Android团队提供了一个包,其中包含不同大小图标的样本材料和模板。您可以从以下网址下载Android图标模板包: 该软件包包括Photoshop模板,您可以使用这些模板在边框内精确排列图形,以及配置为应用适当效果(如图标投影)的滤镜。 图5–8显示了本书示例中使用的ProAndroidFlash图标的Photoshop文件。它采用“网络水果”图形作为封面艺术的中心,并使用这一单一元素作为书籍的代表性图标。 图5–8。AdobePhotoshop中的安卓图标模板 因为我们使用的形状是圆形的,所以它可以接触到外部的蓝色边界(1/6的间隔)。方形图标不应超出橙色边界线(2/9英寸的距离)。我们也使用推荐的高密度投影设置,2像素距离,5像素大小,90度角。Table5–4列出了不同密度图标的图标尺寸、边框大小和阴影设置。 要完成图标的准备工作,请隐藏用于创建图标的任何参考图层,并将其保存为透明的可移植网络图形(PNG)文件。如果您使用的是Photoshop,最好的方法是使用“文件”菜单中的“存储为Web和设备所用格式”命令。这可以确保您的图像文件尽可能小,删除任何不必要的头信息。 一旦你创建了你的图形,你可以将它们包含在你的应用描述符中,这样它们将与你的应用捆绑在一起,并显示在你部署的应用的启动器和菜单中。FlashProfessional有一个配置页面,允许您选择图标并将它们链接到您的应用,如图5–9所示。 图5–9。Flash专业设置对话框的图标选择选项卡 您选择的每个图形文件都将被移动到名为AppIconsForPublish的文件夹中,该文件夹位于您的项目文件位置。部署后,这些图标将被复制到生成的.apk文件中,并作为相应的密度素材进行链接。 如果您使用FlashBuilder或手动管理应用描述符,则必须手动编辑XML。在文本编辑器中打开应用描述符后,添加一个icon部分,列出应用支持的不同密度图标的绝对或相对路径。清单5–4展示了应用描述符的icon部分应该是什么样子。 清单5–4。空中应用描述符的示例icon部分 icon标签应该直接位于文件的外部application标签之下。在本例中,所有图标资源都与应用描述符文件位于同一个文件夹中,因此路径是一个简单的文件名。您可以将文件命名为任何名称,只要它们与描述符文件中的内容相匹配。 Android要求所有部署的应用都要签名。为了将应用部署到AndroidMarket,您不必从证书颁发机构购买昂贵的代码签名证书。所需的只是一个简单的自签名证书,因为谷歌负责检查在其市场上销售应用的各方的身份。 FlashProfessional和FlashBuilder都提供用户界面来快速轻松地创建证书。您也可以使用AIRDeveloperTool(ADT)从命令行创建证书。所有这些机制创建的证书都是相同的,可以在工具之间互换使用。 若要在FlashProfessional中创建证书,请从“文件”菜单中打开AirforAndroid设置…。在此对话框的“部署”选项卡上,您可以单击“创建…”按钮,通过弹出窗口生成新的证书,如图5–10所示。 图5–10。Flash职业证书创建对话框 我们将在前面更详细地讨论创建证书的字段,但是如果您是为了开发目的而创建证书,那么您可以输入您想要的任何内容。FlashProfessional要求您在继续之前填写所有字段。 图5–11。FlashBuilder证书创建对话框 FlashBuilder对话框与FlashProfessional对话框几乎相同,只是省略了有效期。这将自动默认为25年。 若要从命令行创建证书,可以使用AIR开发工具(ADT)。有关设置ADT的更多信息,请参阅本章前面的“设置ADT”一节。 要通过命令行创建代码签名证书,您需要键入以下命令: adt-certificate-cn 括号中的参数是可选的,可以省略。您可以自己选择的值用尖括号括起来。所有参数的描述和有效值在表5–5中列出。 注意:AndroidMarket要求证书的有效期必须超过2033年10月22日。因此,建议有效期至少为25年。 例如,以下命令将创建一个有效的Android代码签名证书: adt-certificate-cnProAndroidFlash-validityPeriod251024-RSAproandroidflash.p12superSecretPassword 然后,您可以通过运行checkstore命令来验证证书是否有效: adt-checkstore-storetypepkcs12-keystoreproandroidflash.p12-storepasssuperSecretPassword 如果证书创建成功,该命令将返回"validpassword"。 一旦您设置了适当的权限、图标和证书,从FlashProfessional发布应用就像按一个按钮一样简单。事实上,您可以选择几个按钮或菜单项: 这三个位置在图5–12中描述。除了在错误输入或信息不完整的情况下(例如证书密码丢失),您将被重定向到AIRforAndroid设置对话框,它们的工作方式完全相同。 图5–12。AIRforAndroid部署设置对话框 在AIRforAndroid设置对话框的部署选项卡上有几个部署选项,我们还没有谈到,但它们在发布时很重要。首先是选择一个Device、Emulator或Debug版本。如果您正在创建一个供最终用户使用的Android包,请确保选择一个Device版本。 如果您计划在Android模拟器中测试您的应用,那么Emulator版本是必需的,所以如果这是您计划测试您的应用的方式,请确保选择此选项。然而,记住切换回一个Device版本以分发给最终用户。 一个Debug版本是当你通过USB调试测试你的应用时通常会构建的项目类型。与Device版本相比,这种版本的性能较慢,并且在错误条件下的行为略有不同,因此不建议将其用于分发目的。 您还可以选择应用下载AIR运行时的位置。目前支持的两个应用商店是谷歌AndroidMarket和亚马逊Appstore。如果您计划将应用部署到这两个应用商店,则应该使用不同的AIR运行时设置为每个商店创建单独的版本。 最后一组选项将自动在第一个连接的USB设备上安装和启动您的应用包。这些对于以发布形式测试新构建的应用非常方便。如果您正在运行模拟器,FlashProfessional会将其视为连接的设备,并且还可以自动部署到该设备。 点击发布,一个Android包文件(APK)将为你的应用创建。APK文件是一个自包含的安装包,您可以将其部署到设备或通过应用商店发布。APK文件的位置和名称在“发布设置”对话框的“输出文件”栏中设定。 FlashBuilder还能够为您的应用打包APK文件。从项目菜单中选择导出发布版本…开始导出过程,之后您将看到一个向导对话框,如图5–13所示。 图5–13。【FlashBuilder导出发布构建向导】 向导的第一页允许您选择平台、文件名和签名选项。对于移动应用,您通常希望选择第一个选项来为每个目标平台签署包,在我们的例子中只有一个。要进入第二页,请单击“下一步”按钮。 第二页包含几个选项卡,其中包含数字签名、包内容和部署的选项。如果您已经按照本章前面的讨论设置了您的签名,除了可能输入密码之外,您不必在第一个选项卡上进行任何更改。PackageContents选项卡显示了所有资源的列表,将包含在APK文件中。除非您想明确排除任何文件,如未使用的图形,否则您不需要在此进行任何更改。最后,最后一个选项卡有一个选项,可以在连接的移动设备上自动部署和运行(如果可用),这是默认选择的。 单击“完成”按钮后,FlashBuilder将打包一个用于发布的APK文件,并可能在已安装的设备上部署和启动该文件。 与FlashProfessional发布过程相比,您可能会注意到在FlashBuilder对话框中没有提到Android模拟器。此外,如果您尝试在Android模拟器上安装由FlashBuilder创建的APK文件,安装将会失败并出现错误。 但是,您可以从命令行使用AIRDeveloperTool(ADT)手动创建同一应用的模拟器友好版本。有关设置ADT的更多信息,请参阅本章前面的“设置ADT”一节。 作为一个例子,清单5–5向您展示了如何为第一章中构建的手势检查项目构建一个仿真器友好的APK。请确保在FlashBuilder已经构建了SWF文件后,从bin-debug文件夹执行此命令。 ***Listing5–5.**CommandtoBuildanAPKFilefortheGestureCheckProject* adt-package-targetapk-emulator-storetypepkcs12-keystore 这将构建一个与Android模拟器兼容的APK文件。在模拟器运行时,您可以使用ADT工具通过执行以下命令来安装它: adt-installApp-platformandroid-packageGestureCheck.apk 提示:您也可以使用Android调试桥(ADB)安装APK文件,如下所示: adbinstallGestureCheck.apk 您的应用现在将出现在模拟器的应用菜单中。您可以通过菜单手动启动应用,或者使用以下命令以编程方式启动应用: adt-launchApp-platformandroid-appidcom.proandroidflash.GestureCheck.debug 请注意,appid与您的应用描述符中的idused相同,只是多了一个“.”。debug”追加到末尾。 提示:您也可以使用ADB启动AIR应用,如下所示: adbshellamstart-aandroid.intent.action.MAIN-nair.com.proandroidflash.GestureCheck.debug/.AppEntry Figure5–14展示了一个在普通Android模拟器上运行的手势检查应用的真实例子。 图5–14。运行在桌面模拟器上的Android2.3.3 如前所述,Android模拟器不支持多点触摸事件,这一点通过查看手势检查应用在模拟器中运行时的输出可以明显看出。 虽然FlashProfessional和FlashBuilder使从工具内部部署应用变得非常方便,但是能够从命令行执行相同的部署通常也很有用。这允许您创建一个可重复的、自动化的过程,该过程是为您的开发工作流的确切需求而定制的。 我们将使用的命令行工具称为AIRDeveloperTool(ADT),它可以自动执行多种不同的任务,从证书创建到应用打包再到设备部署。有关设置ADT的更多信息,请参见前面的“设置ADT”一节。 ADT中用于包装空气应用的主要标志是-package。这表明您将为桌面、移动或其他部署打包AIR应用。以下是用ADT打包Android应用的全部论据: adt-package-target(apk|apk-debug|apk-emulator)(-connect Table5–6讨论了这些参数以及有效值。 虽然打包选项的排列看起来令人望而生畏,但是您只需要所需的参数就可以完成大多数任务。例如,下面将使用应用描述符中的信息打包一个简单的应用: adt-package-targetapk-storetypepkcs12-keystorecert.p12Sample-app.xmlSample.swf 这是从描述符和SWF文件打包应用的最小参数集。要构建调试版本,您应该执行以下操作: adt-package-targetapk-debug-listen-storetypepkcs12-keystorecert.p12Sample-app.xmlSample.swf 这将在启动时监听端口7936上的USB调试接口。 如果您知道要为同一个项目创建多个部署,AIRIntermediate(AIRI)文件会非常方便。您可以从FlashBuilder导出一个AIRI文件,也可以在命令行上使用以下语法的prepare命令创建一个文件: adt-prepareSample.airiSample-app.xmlSample.swf 然后,您可以使用package命令的input-package变体部署到多个不同的目标: 这将创建两个不同的APK文件,一个准备部署到AndroidMarket,另一个使用不同的AIR下载URL从AmazonAppstore获取运行时。 命令行部署练习 本练习将指导您逐步完成打包、签名、安装和启动FlashCapabilityReporter示例的过程。 以下是练习的先决条件: 首先打开命令提示符或终端。您应该能够键入不带选项的命令adt,并获得命令参数的帮助。如果它找不到adt命令或者抱怨java不在路径中,验证你已经正确地更新了你的path环境变量。 创建代码签名证书 要创建代码签名证书,您可以发出以下命令,其中尖括号中的值应该替换为您的名称和密码: adt-certificate-cn 如果命令成功完成,它将返回退出代码0。 打包应用 要打包该应用,请确保您已经在FlashProfessional中运行了该应用一次,以便可以创建电影(*.swf)和应用描述符(*-app.xml)文件。然后使用以下命令将其打包成一个APK文件: adt-package-targetapk-storetypepkcs12-keystoreexercise.p12FlashCapabilityReporter.apkFlashCapabilityReporter-app.xmlFlashCapabilityReporter.swfAppIconsForPublish/ 这将创建一个包含应用可部署版本的APK文件。 安装并启动应用 另外,您可以使用以下命令将应用安装并启动到通过USB连接的设备上: adt-installApp-platformandroid-packageFlashCapabilityReporter.apkadt-launchApp-platformandroid-appidcom.proandroidflash.FlashCapabilityReporter 如果成功,FlashCapabilityReporter应用将安装并运行在您的Android手机上。 AndroidMarket是谷歌为Android设备创建和运营的应用商店。与苹果应用商店或亚马逊应用商店等其他应用商店相比,安卓市场非常开放。它没有一个限制性的筛选过程,它允许最终用户试用一个应用一天,如果他们不喜欢它,可以选择全额退款。 谷歌向开发者收取25美元的费用,以便创建一个可以用来提交无限数量的应用的帐户。据谷歌称,这项费用旨在通过防止应用垃圾来提高市场质量。 本节概述了将AdobeAIR应用发布到Androidmarketplace的三步流程。 要创建AndroidMarket开发者帐户,请访问以下网站: 为了上传到Android商店,你需要一个被打包成APK文件的签名应用。您可以从FlashProfessional、FlashBuilder或命令行执行此操作,如前几章所述。 提交申请时,你需要记住以下几点: 一旦你建立了APK文件,你就可以开始发布你的应用到AndroidMarket了。 AndroidMarket应用提交过程是完全自动化的,包括每个步骤的详细说明。Figure5–15展示了一个例子,展示了如果您使用第一章中的手势检查示例应用,提交过程会是什么样子。 图5–15。安卓市场提交流程 大部分的申请提交过程只需上传你的APK文件。这包括选择图标、设置权限和选择支持的平台。 除了APK文件之外,你还需要提交至少两张你的申请截图以及一个大尺寸图标(512像素见方)。要截屏你的应用,你可以在桌面上的模拟器中运行它,然后把图片裁剪到合适的大小,或者使用截屏工具直接在你的Android设备上拍照。 在填写必填字段后,您可以提交您的应用,它将立即在AndroidMarket中可用。图5–16显示了一个成功的应用部署到AndroidMarket的成功结果。 图5–16。成功部署安卓市场应用 亚马逊应用商店是第二个购买Android设备应用的市场。它与亚马逊的店面紧密集成,允许你从一个界面购买Android应用以及书籍、CD和其他产品。此外,它使用亚马逊的专利一键式购买系统来简化在移动设备上购买应用的过程。 通过亚马逊应用商店发布应用的费用要高得多,每年订阅费用为99美元。幸运的是,亚马逊已经免除了同时注册亚马逊开发者计划的开发者第一年的费用。 发布到亚马逊Appstore的要求和流程与AndroidMarket非常相似。设置您的帐户、打包您的应用并将其上传到商店的三个步骤仍然适用。 提交到亚马逊Appstore时,一定要设置AIR运行时URL指向亚马逊Appstore进行下载。这可以通过FlashProfessionalUI中的部署设置来完成,也可以通过命令行将ADT的-airDownloadURLproperty设置为以下内容来完成: Figure5–17展示了AmazonAppstore应用提交的一个例子。 图5–17。亚马逊应用商店提交流程 作为开发人员,在向AmazonAppstore提交应用时,您会注意到以下一些主要差异: 尽管存在这些差异,但大多数应用提交过程都非常相似,这使得将您的AIRAndroid应用部署到这两个应用商店非常容易。 本章结束了Flash平台的端到端移动应用开发过程。现在,您已经知道如何将应用从初始阶段发展到最终用户可以从市场上下载的完全发布的Android应用。 在本章中,您学习了如何执行以下操作: 在接下来的几章中,我们将进一步深入探讨与Android的原生集成、针对移动设备的性能调整以及与设计师的合作。 您已经学习了如何创建有趣的基于Flex的移动应用,在本章中,您将了解AdobeAIR中可用的其他有用功能,以及如何将Android特定的功能合并到AdobeAIR移动应用中。 首先,您将学习如何执行AdobeAIR中提供的两项操作:如何在AIR应用中启动本机浏览器,以及如何在SQLite数据库中存储特定于应用的数据。本章的下一部分深入探讨了Android的基础知识,你需要理解本章后面讨论的代码示例。本节将向您展示如何创建一个简单的原生Android应用,并讨论Android应用中的主要文件。您还将了解重要的Android特定概念,如活动、Intents和Services。 本章的第三部分包含一个AdobeAIRmobile应用的示例,该应用调用外部API来提供用户已向外部服务注册的网站的状态信息。我们的AdobeAIR应用将每个网站的状态存储在SQLite数据库中,然后在数据网格中显示状态详细信息。这个移动应用还允许用户点击一个按钮,向原生Android代码发送更新,然后在Android通知栏中显示更新。本章的最后一部分包含将AdobeAIRmobile应用与本机Android代码集成所需的步骤。 关于本章的内容,有几点需要记住。首先,Android内容旨在帮助您了解如何将原生Android功能集成到AdobeAIR应用中。因此,只涵盖了Android主题的一个子集,这不足以成为一名熟练的Android应用开发人员。其次,AdobeAIR是一个不断发展的产品,因此AdobeAIR目前不可用的一些Android功能可能会在未来的版本中可用。第三,AdobeAIR应用与原生Android功能的集成不受Adobe官方支持;因此,如果您在整合过程中遇到困难,没有正式的支持机制来帮助您解决这些困难。 另一个需要考虑的问题与AdobeAIRmobile应用的目标设备所支持的Android版本有关。例如,支持Android2.2的移动设备的数量目前远远大于支持Android2.3.x或Android3.0的移动设备,这两种移动设备目前都仅限于几款平板电脑(如三星GalaxyTab10.1和摩托罗拉Xoom)和一款智能手机(三星GalaxySII)。 另一方面,如果AdobeAIR支持您创建移动应用所需的所有功能和特性,那么您就不需要本章中说明如何将AdobeAIR应用与Android应用合并的任何代码示例。如果是这种情况,你可以跳过这些材料而不失去连贯性。 使用移动应用模板创建一个名为URIHandlers的新Flex移动项目,并添加如清单6–1所示的代码。 清单6–1。调用URI处理程序 privatevargeo:Geolocation; privatefunctiononTel():void{navigateToURL(newURLRequest("tel:"+tel));} privatefunctiononSMS():void{navigateToURL(newURLRequest("sms:"+sms));} privatefunctiononMailto():void{navigateToURL(newURLRequest("mailto:"+mailto+"subject=Hello%20AIR"));} privatefunctiononSearch():void{navigateToURL(newURLRequest("market://searchq=iReverse"));} privatefunctiononGeo():void{this.geo=newGeolocation();this.geo.addEventListener(GeolocationEvent.UPDATE,onLocationUpdate);} AdobeAIR使您能够启动定制的HTML页面(如前面所示),还可以导航到任意HTML页面(如清单6–2所示)。 使用ActionScript移动应用模板创建一个名为StageWebViewHTML1的新Flex移动项目,并添加如清单6–2中所示的代码。 清单6–2。启动一个硬编码的HTML页面 `package{importflash.display.Sprite;importflash.display.StageAlign;importflash.display.StageScaleMode;importflash.geom.Rectangle;importflash.media.StageWebView; publicclassStageWebViewHTML1extendsSprite{publicfunctionStageWebViewHTML1(){super(); //supportautoOrientsstage.align=StageAlign.TOP_LEFT;stage.scaleMode=StageScaleMode.NO_SCALE;varwebView:StageWebView=newStageWebView();webView.stage=this.stage;webView.viewPort=newRectangle(0,0,stage.stageWidth,stage.stageHeight);//createanHTMLpagevarhtmlStr:String=""+""+""+" HellofromtheAuthorTeam: StephenChin DeanIverson OswaldCampesato PaulTrani Thisisthekeylineofcode: webView.loadString(htmlStr,'text/html';); 'htmlStr'containstheHTMLcontents //launchtheHTMLpagewebView.loadString(htmlStr,"text/html");}}}` 清单6–2包含几个import语句和自动生成的代码,变量htmlStr是一个字符串,其中包含一行代码启动的HTML页面的内容: webView.loadString(htmlStr,"text/html"); 如果您计划在移动应用中调用硬编码的HTML页面,请尝试使用不同的HTML5标签来创建HTML页面所需的样式效果。 图6–2显示了来自清单6–2的输出,它呈现了一个带有硬编码内容的HTML页面。 图6–2。启动一个硬编码的HTML页面 在前面的示例中,您学习了如何启动硬编码的HTML页面,在本节中,您将学习如何导航到任何HTML页面,然后在AdobeAIRmobile应用中启动该HTML页面。 使用ActionScript移动应用模板创建一个名为StageWebViewLaunch2的新Flex移动项目,并添加如清单6–3所示的代码。 清单6–3。启动用户指定的URL 清单6–3包含一个允许用户输入URL的输入字段,以及一个通过ActionScript3文件StageWebViewLaunch.as中定义的方法StageWebViewExample()启动指定URL的按钮。 现在创建文件StageWebViewLaunch.as并插入清单6–4中的代码。 清单6–4。启动URL的ActionScript3代码 `importflash.media.StageWebView;importflash.geom.Rectangle; importflash.events.ErrorEvent;importflash.events.Event;importflash.events.LocationChangeEvent; privatevarwebView:StageWebView=newStageWebView(); publicfunctionStageWebViewExample(){webView.stage=this.stage;webView.viewPort=newRectangle(0,0,stage.stageWidth,stage.stageHeight); webView.addEventListener(Event.COMPLETE,completeHandler);webView.addEventListener(ErrorEvent.ERROR,errorHandler);webView.addEventListener(LocationChangeEvent.LOCATION_CHANGING,locationChangingHandler);webView.addEventListener(LocationChangeEvent.LOCATION_CHANGE,locationChangeHandler);//launchtheuser-specifiedURLwebView.loadURL(url);} //DispatchedafterthepageorwebcontenthasbeenfullyloadedprotectedfunctioncompleteHandler(event:Event):void{dispatchEvent(event);} //DispatchedwhenthelocationisabouttochangeprotectedfunctionlocationChangingHandler(event:Event):void{dispatchEvent(event);} //DispatchedafterthelocationhaschangedprotectedfunctionlocationChangeHandler(event:Event):void{dispatchEvent(event);} //DispatchedwhenanerroroccursprotectedfunctionerrorHandler(event:ErrorEvent):void{dispatchEvent(event);}` 清单6–4定义了引用用户指定的URL的Bindable变量url,后面是包含特定于URL的功能的变量webView。方法StageWebViewExample()定义了各种事件处理程序,所有这些程序都调用内置方法dispatchEvent(),然后用户指定的URL通过这行代码启动: webView.loadURL(url); 图6–3显示了谷歌主页,这是清单6–4中的默认URL。 图6–3。启动用户指定的网址 AdobeAIR支持访问存储在移动设备上的SQLite数据库中的数据。您也可以直接从原生Android代码访问SQLite数据库,但AdobeAIR提供了更高的抽象级别(代码也更简单)。 使用移动应用模板创建一个名为SQLite1的新Flex移动项目,并添加如清单6–5所示的代码。 清单6–5。查看SQLite数据库中的数据 清单6–5包含一个XML脚本元素,该元素引用一个ActionScript3文件,该文件包含访问SQLite数据库的方法,其内容将显示在清单6–6中。标签和文本输入字段使用户能够输入将被添加到我们的数据库中的每个新人的名字和姓氏。 有一个XMLButton元素用于通过addPerson()方法添加一个新人员,还有一个XMLButton用于通过removePerson()方法从我们的数据库中删除一个现有人员。两种方法都在SQLiteAccess.as中定义。变量dp是一个Bindable变量,包含数据网格中显示的数据,由于用户可以添加和删除数据行,dp是一个Bindable变量。 因为我们要访问存储在SQLite数据库中的数据,所以我们需要在ActionScript3中定义几个方法来管理数据库内容的创建、访问和更新。在与文件SQLite1HomeView.mxml相同的目录下创建一个名为SQLiteAccess.as的新ActionScript3文件,并添加如清单6–6所示的代码。 清单6–6。在ActionScript3中定义数据库访问方法 `importflash.data.SQLStatement;importflash.errors.SQLError;importflash.events.Event;importflash.events.SQLErrorEvent;importflash.events.SQLEvent;importflash.events.TimerEvent;importflash.filesystem.File;importflash.utils.Timer; importmx.collections.ArrayCollection;importmx.utils.ObjectUtil; importorg.osmf.events.TimeEvent;` `//sqlconnholdsthedatabaseconnectionpublicvarsqlconn:SQLConnection=newSQLConnection(); //sqlstmtholdsSQLcommandspublicvarsqlstmt:SQLStatement=newSQLStatement(); //abindableArrayCollectionandthedataproviderforthedatagrid[Bindable]publicvardp:ArrayCollection=newArrayCollection(); //invokedaftertheapplicationhasloadedprivatefunctionstart():void{//set'people.db'asthefileforourdatabase(createdafterit'sopened)vardb:File=File.applicationStorageDirectory.resolvePath("people.db"); //openthedatabaseinasynchronousmodesqlconn.openAsync(db); //eventlistenersforhandlingsqlerrorsand'result'are//invokedwheneverdataisretrievedfromthedatabasesqlconn.addEventListener(SQLEvent.OPEN,db_opened);sqlconn.addEventListener(SQLErrorEvent.ERROR,error);sqlstmt.addEventListener(SQLErrorEvent.ERROR,error);sqlstmt.addEventListener(SQLEvent.RESULT,result);}` `privatefunctiondb_opened(e:SQLEvent):void{//specifytheconnectionfortheSQLstatementsqlstmt.sqlConnection=sqlconn; //Table"person_table"containsthreecolumns://1)id(anautoincrementinginteger)//2)first_name(thefirstnameofeachperson)//3)last_name(thelastnameofeachperson)sqlstmt.text="CREATETABLEIFNOTEXISTSperson_table(idINTEGERPRIMARYKEYAUTOINCREMENT,first_nameTEXT,last_nameTEXT);"; //executethesqlstmttoupdatethedatabasesqlstmt.execute(); //refreshthedatagridtodisplayalldatarowsrefreshDataGrid();} //functiontoappendanewrowtoperson_table//eachnewrowcontainsfirst_nameandlast_nameprivatefunctionaddPerson():void{sqlstmt.text="INSERTINTOperson_table(first_name,last_name)VALUES('"+first_name.text+"','"+last_name.text+"');";sqlstmt.execute(); refreshDataGrid();}` `//functiontorefreshthedataindatagridprivatefunctionrefreshDataGrid(e:TimerEvent=null):void{//timerobjectpausesandthenattemptstoexecuteagainvartimer:Timer=newTimer(100,1);timer.addEventListener(TimerEvent.TIMER,refreshDataGrid); if(!sqlstmt.executing){sqlstmt.text="SELECT*FROMperson_table"sqlstmt.execute();}else{timer.start();}} //invokedwhenwereceivedatafromasqlcommandprivatefunctionresult(e:SQLEvent):void{vardata:Array=sqlstmt.getResult().data; //fillthedatagriddp=newArrayCollection(data);} //removearowfromthetableprivatefunctionremovePerson():void{sqlstmt.text="DELETEFROMperson_tableWHEREid="+dp[dg.selectedIndex].id;sqlstmt.execute();refreshDataGrid();} //errorhandlingmethodprivatefunctionerror(e:SQLErrorEvent):void{//Alert.show(e.toString());}` 方法refreshDataGrid()首先检查SQL语句当前是否正在执行;如果是,那么它暂停指定的毫秒数(在本例中是100),然后从person_table中检索所有行(这将包括新添加的人员)。方法result()用刷新的数据集填充变量dp。removePerson()方法删除用户在数据网格中点击选中的行。 Figure6–4显示了一组存储在移动设备上的SQLite数据库中的行。 图6–4。SQLite数据库中的一组记录 Android是一个用于开发Android移动应用的开源工具包,在撰写本文时,Android3.0(“蜂巢”)是最新的主要版本;Android的最新版本是3.1。注意,AdobeAIR2.5.1formobileapplications至少需要Android2.2,您可以在支持Android2.2或更高版本的移动设备上安装AdobeAIR应用。 以下部分提供了一些关于Android3.0的主要特性、从哪里下载Android以及Android中的关键概念的信息。当你读完这一节,你将理解如何创建原生Android应用。 GoogleAndroid3.0(2011年初发布)为Android2.3(2010年12月发布)中的功能提供了向后兼容的支持。Android3.0提供了比2.3版本更好的功能,以及以下新功能: 如果您有一台Windows机器,您需要将环境变量JAVA_HOME设置为解压缩Java发行版的目录。 对于Windows平台,Android发行版是一个具有以下类型名称的文件(这在本书出版时可能略有不同):android-sdk_r06-windows.zip。 完成Java和Eclipse安装后,按照Android安装步骤在您的机器上安装Android。 你还需要创建一个AVD(Android虚拟设备),具体操作步骤在第五章中。 尽管Android应用是用Java编写的,但Java代码被编译成Dalvik可执行文件,该文件(以及其他素材)是部署到Android设备的.apk应用文件的一部分。 除了支持标准Java语言特性之外,Android应用通常还包含以下特定于Android的概念: 在您掌握了前面列表中的概念之后,您可以了解IntentFilter和ContentProvider(对这两个主题的全面讨论超出了本章的范围)以及如何使用它们来提供更细粒度的基于Intent的功能以及跨Android应用共享数据的能力。 一个Android应用可以包含多个AndroidActivities,每个AndroidActivity可以包含多个Intent和IntentFilter,此外,一个Android应用可以包含一个AndroidService和一个AndroidBroadcastreceiver,它们都被定义为AndroidManifest.xml中Androidactivity元素的兄弟元素。 清单6–7提供了您可能在AndroidManifest.xml项目文件中找到的内容的概要。在这种情况下,项目文件包含两个Android活动、两个AndroidService和两个AndroidBroadcast接收者的“存根”。XML元素的属性已经被省略,这样您就可以看到Android应用的整体结构,在本章的后面,您将看到一个完整的AndroidManifest.xml内容的例子。 清单6–7。一个轮廓AndroidManifest.xml 清单6–7包含两个Androidactivity元素、两个Androidservice元素和两个Androidreceiver元素。在这三对中的每一对中,都有一个包含Androidintent-filter元素的元素,但是请记住,AndroidManifest.xml的内容可能有许多变化。项目文件的确切内容取决于您的Android应用的功能。 Android还支持Java本地接口(JNI),允许Java代码调用C/C++函数。但是,您还需要下载并安装Android原生开发工具包(NDK),它包含一组工具,用于创建包含可从Java代码调用的函数的库。如果你需要比单独使用Java更好的性能(尤其是图形性能),你可以使用JNI,但是这个主题的细节已经超出了本章的范围。 AndroidActivity对应于应用的一个屏幕或一个视图,Android应用的主要入口点是一个AndroidActivity,它包含一个onCreate()方法(覆盖其超类中的相同方法),每当您启动Android应用时都会调用该方法。当你启动你的Android应用时,它的AndroidActivity会自动启动。例如,清单6–8显示了HelloWorld.java的内容,这是在本章稍后创建基于Eclipse的“HelloWorld”Android应用时自动生成的。 清单6–8。HelloWorld.java的内容 `packagecom.apress.hello; importandroid.app.Activity;importandroid.os.Bundle; publicclassHelloWorldextendsActivity{/**Calledwhentheactivityisfirstcreated.*/@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);}}` 在Android应用的项目创建步骤中,您指定包名和类名;其余生成的代码对于每个Android项目都是一样的。 注意,HelloWorld扩展了android.app.Activity,并且它也覆盖了onCreate()方法。正如你可能猜到的,AndroidActivity是一个包含一组方法(如onCreate())的Android类,你可以在你的Android应用中覆盖这些方法。一个Activity包含一个或多个属于Android应用的View。 AndroidView是用户在屏幕上看到的东西,包括Android应用的UI小部件。HelloWorldAndroid应用包含一个Android类,它扩展了AndroidActivity类,并用您的自定义代码覆盖了onCreate()方法。注意,Android应用也可以扩展其他Android类(比如Service类),它们也可以创建线程。 一个Android应用可以包含不止一个AndroidActivity,正如您已经知道的,每个Activity都必须在XML文档AndroidManifest.xml中定义,XML文档是每个Android应用的一部分。 HelloWorldAndroid项目包含XML文档AndroidManifest.xml,其中Android类HelloWorld注册在XMLactivity元素中,如清单6–9所示。 清单6–9。AndroidManifest.xml的内容 请注意句号(“.”)位于清单6–9中的AndroidActivityHelloWorld之前。这个句点是强制的,因为字符串.HelloWorld被附加到包名com.apress.hello(也在清单6–9中指定),所以这个Android项目中HelloWorld.java的完全限定名是com.apress.hello.HelloWorld。 AndroidUI(用户界面)由Intent和View组成。抽象地说,AndroidIntent表示在Android应用中要执行的动作(通常由动词描述)的细节。 一个Intent本质上是Android活动(或Services)之间的通知。一个Intent使一个AndroidActivity能够向其他Android活动发送数据,也能够从其他Android活动接收数据。 AndroidIntent类似于事件处理器,但是Android提供了处理多个Intent的额外功能,以及使用现有Intents与启动新Intent的选项。AndroidIntents可以启动一个新的AndroidActivity,它们还可以广播消息(由AndroidBroadcast接收器处理)。下面的代码片段说明了如何通过一个Intent来启动一个新的Activity: Intentintent=newIntent(action,data);startActivity(intent); AndroidActivities和Intents提供了一组松散耦合的资源,让人想起SOA(面向服务的架构)。AndroidActivity的对等物是一个web服务,AndroidActivity可以处理的Intent类似于web服务提供给世界的“操作”或方法。其他Android应用可以显式地调用这些方法中的一个,或者它们可以发出一个“通用”请求,在这种情况下,“框架”决定哪些web服务将处理该通用请求。 你也可以广播Intent以便在组件之间发送消息。下面的代码片段说明了如何广播一个Intent: Intentintent=newIntent(a-broadcast-receiver-class);sendBroadcast(intent); 这种类型的功能为Android应用提供了更大的灵活性和“开放性”。 Android有几种类型,每一种提供的功能都略有不同。定向的Intent是具有一个接收者的Intent,而广播的Intent可以被任何进程接收。一个显式指定了需要调用的Java类。一个隐式Intent是一个不指定Java类的Intent,这意味着Android系统将决定哪个应用将处理隐式Intent。如果有几个应用可以响应隐式的Intent,Android系统会让用户选择其中一个应用。** **Android也有IntentFilters的概念,用于Intent分辨率。一个IntentFilter表示一个机器人Activity(或机器人Service)可以“消费”的Intent,细节在XMLintent-filter元素中指定。注意,如果应用不提供IntentFilter,那么它只能被显式的Intent调用(而不能被隐式的Intent)。 在文件AndroidManifest.xml的一个AndroidActivity中指定了一个IntentFilter,如清单6–10所示。 清单6–10。安卓中的一个IntentFilter的例子 清单6–10显示了AndroidManifest.xml内容的一个片段,如清单6–9所示。清单6–10中的XMLaction元素指定默认值android.intent.action.MAIN,XMLcategory元素指定android.intent.category.LAUNCHER(也是默认值),这意味着父Activity将显示在应用启动器中。 一个IntentFilter必须包含一个XMLaction元素,并且可以选择包含一个XMLcategory元素或者一个XMLdata元素。如您所见,清单6–10包含指定默认动作的强制XMLaction元素,以及可选的XMLcategory元素,但不包含可选的XMLdata元素。 一个IntentFilter是定义特定动作的一组信息;XMLdata元素指定要操作的数据,XMLcategory元素指定执行动作的组件。 在一个IntentFilter中可以指定这三个XML元素的各种组合,因为其中两个元素是可选的,Android使用一种基于优先级的算法来确定将为您在AndroidManifest.xml中定义的每个IntentFilter执行什么。如果您需要更详细地了解IntentFilters,请查阅Android文档以获取更多信息。 OpenIntents还提供了一个可以被Android活动调用的公开可用的AndroidIntent的注册表,以及它们的类文件和它们的服务描述。有关更多信息,请访问OpenIntents主页。 AndroidService可用于处理后台任务和其他不涉及视觉界面的任务。由于AndroidService运行在主进程的主线程中,AndroidService通常会在需要执行工作时启动一个新线程,而不会阻塞Android应用的UI(在主线程中处理)。因此,Android应用可以通过该服务公开的一组API“绑定”到一个Service。 AndroidService是通过AndroidManifest.xml中的XMLservice元素定义的,如下所示: 清单6–11显示了ServiceName.java的内容,它为自定义AndroidService类的定义提供了“框架”代码。 清单6–11。SimpleService.java的内容 `publicclassSimpleServiceextendsService{@OverridepublicIBinderonBind(Intentintent){returnnull;} @OverrideprotectedvoidonCreate(){super.onCreate();startservice();//definedelsewhere} @OverrideprotectedvoidonCreate(){//insertyourcodehere} @OverrideprotectedvoidonStart(){//insertyourcodehere}}` 例如,如果您需要定期执行某件事情,那么您可以包含一个Timer类的实例,该实例根据您的应用的需要来调度和执行一个TimerTask。 AndroidBroadcast接收器的目的是“听”AndroidIntents.清单6–12在本章稍后讨论的基于小部件的Android应用的AndroidManifest.xml文件中显示了AndroidBroadcast接收器的定义。 清单6–12。为AndroidManifest.xml中的接收人录入样本 清单6–12包含一个XMLreceiver元素,该元素将MyHelloWidget指定为这个小部件的Java类。这个Androidreceiver包含一个XMLintent-filter元素和一个XMLaction元素,当需要更新AppWidgetMyHelloWidget时,这个XMLaction元素会导致一个动作发生,如下所示: 在本章的前面,您看到了HelloWorld.java的内容,它包含了覆盖超类中相同方法的onCreate()方法。事实上,onCreate()是组成Android应用生命周期的七种Android方法之一。 谷歌Android应用包含以下方法,这是Android应用生命周期中调用方法的顺序1: 创建Activity时会调用onCreate()方法,其作用类似于其他语言中的init()方法。当从内存中移除一个Activity时,就会调用onDestroy()方法,它的作用实质上是C++中的一个析构函数方法。当Activity必须暂停时(比如回收资源),调用onPause()方法。当重新启动Activity时,调用onRestart()方法。当Activity与用户交互时,调用onResume()方法。当Activity在屏幕上可见时,调用onStart()方法。最后,调用onStop()方法来停止Activity。 方法onRestart()、onStart()和onStop()处于可见阶段;方法onResume()和onPause()处于前台阶段。Android应用在执行期间可以暂停和恢复多次;细节是特定于应用的功能的(也可能是用户交互的类型)。 这一节描述了如何在Eclipse中创建Android应用,随后的一节向您展示了Android应用的目录结构,随后讨论了在每个Android应用中创建的主要文件。 启动Eclipse并执行以下步骤,以创建一个名为HelloWorld的新Android应用: 图6–5显示了从Eclipse启动的Android模拟器中HelloWorld应用的输出。 图6–5。helloworld安卓应用 导航到您在上一节中创建的HelloWorld项目,右键单击项目名称以显示展开的目录结构。接下来的几个小节讨论了每个Android应用的目录结构和主文件的内容。 清单6–13显示了Android项目HelloWorld的目录结构。 清单6–13。一个安卓项目的架构 `+HelloWorldsrc/com/apress/hello/HelloWorld.java gen/com/apress/hello/R.javaAndroid2.3/android.jarassets/res/drawable-hdpi/icon.pngdrawable-ldpi/icon.pngdrawable-mdpi/icon.pnglayout/main.xmlvalues/strings.xmlAndroidManifest.xmldefault.propertiesproguard.cfg` 这个Android应用包含两个Java文件(HelloWorld.java和R.java)、一个JAR文件(android.jar)、一个图像文件(icon.png)、三个XML文件(main.xml、strings.xml和AndroidManifest.xml)以及一个文本文件default.properties。 这里列出了我们将在本节中讨论的HelloWorldAndroid应用中的文件(所有文件都是相对于项目根目录列出的): 清单6–14显示了HelloWorld.java的内容,其中包含了这个Android应用所需的所有定制Java代码。 清单6–14。HelloWorld.java的内容 importandroid.app.Activity;importandroid.os.Bundle;publicclassHelloWorldextendsActivity{/**Calledwhentheactivityisfirstcreated.*/@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);}}` 在本章的前面,您已经看到了HelloWorld.java的内容。您可以将这段代码视为“样板”代码,它是在项目创建过程中根据用户提供的包名和类名的值自动生成的。 您的自定义代码包含在该语句之后: setContentView(R.layout.main); 除了从文件main.xml中设置View之外,从另一个XML文件或者从Android项目中其他地方定义的自定义类中设置View也很常见。 现在让我们看一下清单6–15,它显示了资源文件R.java的内容,该文件是在您创建Android应用时自动生成的。 清单6–15。R.java的内容 `/*AUTO-GENERATEDFILE.DONOTMODIFY.**Thisclasswasautomaticallygeneratedbythe*aapttoolfromtheresourcedataitfound.It*shouldnotbemodifiedbyhand.*/ packagecom.apress.hello; publicfinalclassR{publicstaticfinalclassattr{}publicstaticfinalclassdrawable{publicstaticfinalinticon=0x7f020000;}publicstaticfinalclasslayout{publicstaticfinalintmain=0x7f030000;}publicstaticfinalclassstring{publicstaticfinalintapp_name=0x7f040001;publicstaticfinalinthello=0x7f040000;}}` 清单6–15中的整数值本质上是对应于Android应用素材的引用。例如,变量icon是对位于res目录的子目录中的icon.png文件的引用。变量main是对位于res/layout子目录中的XML文件main.xml(将在本节稍后介绍)的引用。变量app_name和hello是对XML文件strings.xml(本节前面已经介绍过)中的XMLapp_name元素和XMLhello元素的引用,该文件位于res/values子目录中。 既然我们已经研究了基于Java的项目文件的内容,让我们把注意力转向Android项目中基于XML的文件。清单6–16显示了AndroidManifest.xml的全部内容。 清单6–16。AndroidManifest.xml的内容 清单6–16中的XMLapplication元素包含一个值为@drawable/icon的android:icon属性,该属性引用位于res子目录中的一个图像文件icon.png。Android支持三种类型的图像文件:高密度、中密度和低密度。对应的目录有drawable-hdpi、drawable-mdpi、drawable-ldpi,都是每个安卓应用根目录下res目录的子目录。 清单6–16中的XMLapplication元素还包含一个值为@string/app_name的android:label属性,该属性引用了位于res/values子目录中的文件strings.xml中的一个XML元素。 清单6–16包含一个XMLintent-filter元素,这在本章前面已经简要讨论过了。清单6–10的最后一部分指定了该应用所需的最低Android版本号,如下所示: 在我们当前的例子中,最低版本是9,这也是我们在这个Android应用的创建步骤中指定的数字。 现在让我们看看清单6–17,它显示了XML文件strings.xml的内容。 清单6–17。strings.xml的内容 清单6–17很简单:它包含一个XMLresources元素和两个XML子元素,用于显示字符串“HelloWorld,HelloWorld!”当你启动这个安卓应用。注意,XML文档AndroidManifest.xml中的XMLapplication元素还引用了第二个XMLstring元素,其name属性的值为app_name,如下所示: 清单6–18。main.xml的内容 清单6–18包含一个XMLLinearLayout元素,这是Android应用的默认布局。Android支持其他布局类型,包括AbsoluteLayout、FrameLayout、RelativeLayout和TableLayout(本章不讨论)。 XMLLinearLayout元素包含一个fill_parent属性,该属性指示当前元素将与父元素一样大(减去填充)。属性layout_width和layout_height指定了View的宽度和高度的基本值。 XMLTextView元素包含属性layout_width和layout_height,它们的值分别是fill_parent和wrap_content。wrap_content属性指定View的大小刚好足够包含它的内容(加上填充)。属性text是指在strings.xml文件(位于res/values子目录中)中指定的XMLhello元素,其定义如下所示: 字符串“HelloWorld,HelloWorld!”是在部署此Android应用后,在Android模拟器或Android设备中启动“HelloWorld”Android应用时显示的文本。 詹姆斯·沃德(他为这本书写了前言)在这一部分贡献了基于套接字的代码,EladElrom提供了将AdobeAIRmobile应用与原生Android应用合并的分步说明。 这一节很长,因为有一个初始设置序列(涉及六个步骤),两个AdobeAIR源文件,以及这个移动应用的Android源文件。第一部分描述了设置序列;本节的第二部分讨论了带有AdobeAIR代码的两个源文件;第三部分讨论了包含原生Android代码的两个源文件。 现在我们已经完成了初始设置步骤,让我们使用移动应用模板创建一个名为Foo的新Flex移动项目,并添加如清单6–19所示的代码。 清单6–19。接收数据并将数据发送给Android上的通知 清单6–19包含一个XML按钮,它调用MontasticAPIs来检索在Montastic中注册的网站的状态。当用户单击此按钮时,网站的状态存储在SQLite数据库中,并且数据网格用新的一组行刷新。 第二个XML按钮使用户能够删除SQLite表中的所有行,这很方便,因为该表的大小可以快速增加。如果您想维护这个表的所有行,您可能应该为datagrid提供滚动功能。 当用户单击第三个XML按钮元素时,这将在端口12345上启动一个基于客户端套接字的连接,以便将网站的最新状态发送到运行在本地Android应用中的服务器端套接字。Android应用读取客户端发送的信息,然后在Android通知栏中显示状态。 清单6–20中的ActionScript3代码类似于清单6–8,因此您将能够快速阅读其内容,尽管对代码进行了各种特定于应用的更改。 清单6–20。接收数据并将数据发送给Android上的通知 `Importflash.data.SQLConnection;importflash.data.SQLStatement;importflash.events.Event;importflash.events.IOErrorEvent;importflash.errors.SQLErrorEvent;importflash.events.SQLEvent;importflash.events.TimerEvent;importflash.filesystem.File;importflash.net.URLLoader;importflash.net.URLRequest;importflash.net.URLRequestHeader;importflash.net.URLRequestMethod;importflash.utils.Timer; importmx.collections.ArrayCollection;importmx.utils.Base64Encoder; //sqlconnholdsthedatabaseconnectionpublicvarsqlconn:SQLConnection=newSQLConnection();//sqlstmtisaSQLStatementthatholdsSQLcommandspublicvarsqlstmt:SQLStatement=newSQLStatement(); //abindableArrayCollectionandthedataproviderforthedatagrid[Bindable]publicvardp:ArrayCollection=newArrayCollection();[Bindable]publicvarallStatuses:String="1:UP#2:UP#3:UP"; privatevarurlList:Array=newArray();privatevarstatusList:Array=newArray();` 清单6–20包含各种导入语句,后面是用于打开数据库连接和执行SQL语句的变量。Bindable变量提供对数据库表内容的访问,以及在Montastic注册的网站的URL和状态。 变量checkpointsXMLList包含您已经向Montastic注册的网站的“实时”数据。 `//invokedaftertheapplicationhasloadedprivatefunctionstart():void{//set'montastic.db'asthefileforourdatabase(createdafterit'sopened)vardb:File=File.applicationStorageDirectory.resolvePath("montastic.db"); //eventlistenersforhandlingsqlerrorsand'result'are//invokedwheneverdataisretrievedfromthedatabasesqlconn.addEventListener(SQLEvent.OPEN,db_opened);sqlconn.addEventListener(SQLErrorEvent.ERROR,error);sqlstmt.addEventListener(SQLErrorEvent.ERROR,error);sqlstmt.addEventListener(SQLEvent.RESULT,result);} privatefunctiondb_opened(e:SQLEvent):void{//specifytheconnectionfortheSQLstatementsqlstmt.sqlConnection=sqlconn; //Table"montastic_table"containsthreecolumns://1)id(anautoincrementinginteger)//2)url(theurlofeachwebsite)//3)status(thestatusofeachwebsite)sqlstmt.text="CREATETABLEIFNOTEXISTSmontastic_table(idINTEGERPRIMARYKEYAUTOINCREMENT,urlTEXT,statusTEXT);"; //refreshthedatagridtodisplayalldatarowsrefreshDataGrid();}` `//functiontoappendnewrowstomontastictable//useabegin/commitblocktoinsertmultiplerowsprivatefunctionaddWebsiteInfo():void{allStatuses="";sqlconn.begin(); for(vari:uint=0;i stmt.text="INSERTINTOmontastic_table(url,status)VALUES(:url,:status);";stmt.parameters[":url"]=urlList[i];stmt.parameters[":status"]=statusList[i]; stmt.execute();} //inserttherowsintothedatabasetablesqlconn.commit(); refreshDataGrid();} //refreshtheMontasticdatainthedatagridprivatefunctionrefreshDataGrid(e:TimerEvent=null):void{//timerobjectpausesandthenattemptstoexecuteagainvartimer:Timer=newTimer(100,1);timer.addEventListener(TimerEvent.TIMER,refreshDataGrid); if(!sqlstmt.executing){sqlstmt.text="SELECT*FROMmontastic_table"sqlstmt.execute();}else{timer.start();}} //invokedwhenwereceivedatafromasqlcommand//thismethodisalsocalledforsqlstatementstoinsertitems//andtocreateourtablebutinthiscasesqlstmt.getResult().data//isnullprivatefunctionresult(e:SQLEvent):void{vardata:Array=sqlstmt.getResult().data; //fillthedatagridwiththelatestdatadp=newArrayCollection(data);} //removeallrowsfromthetableprivatefunctionremoveAll():void{sqlstmt.text="DELETEFROMmontastic_table";sqlstmt.execute(); 方法addWebsiteInfo()是addPerson()方法的“对应物”,在begin/end块中执行数据库插入,以便在一个SQL语句中执行多行插入。这种技术使我们能够使用与两种方法refreshDataGrid()和result()相同的逻辑,从数据库中检索最新的数据,而不会出现争用错误。 请注意,我们现在有一个从数据库表中删除所有行的方法removeAll(),而不是从数据网格中删除选定行的方法remove()。 `//functionsforMontasticpublicfunctioninvokeMontastic():void{varloader:URLLoader=newURLLoader();loader.addEventListener(Event.COMPLETE,completeHandler);loader.addEventListener(IOErrorEvent.IO_ERROR,ioErrorHandler); varrequest:URLRequest=newURLRequest(montasticURL);request.method=URLRequestMethod.GET; varencoder:Base64Encoder=newBase64Encoder();encoder.encode("yourname@yahoo.com:insert-your-password-here");request.requestHeaders.push(newURLRequestHeader("Authorization","Basic"+encoder.toString()));request.requestHeaders.push(newURLRequestHeader("pragma","no-cache"));request.requestHeaders.push(newURLRequestHeader("Accept","application/xml"));request.requestHeaders.push(newURLRequestHeader("Content-Type","application/xml"));loader.load(request);}privatefunctioncompleteHandler(event:Event):void{varloader:URLLoader=URLLoader(event.target);checkpointsXMLList=newXML(loader.data); urlList=newArray();statusList=newArray(); foreach(varcheckpoint:XMLincheckpointsXMLList.checkpoint){statusList.push(checkpoint.status.toString());urlList.push(checkpoint.url.toString()); } allStatuses="1="+statusList[0]+"#2="+statusList[1]; addWebsiteInfo();}` 注意,方法completeHandler()是在对Montastic网站的异步请求返回了基于XML的数据之后调用的。 变量allStatuses被适当地更新(我们需要将这个字符串发送到服务器套接字),然后方法addWebsiteInfo()被执行,它用我们从Montastic接收的数据更新数据库表montastic_table。 privatefunctionioErrorHandler(event:IOErrorEvent):void{trace("IOError"+event);}privatefunctionsqlError(event:SQLErrorEvent):void{trace("SQLError"+event);} 正如您之前看到的,我们使用了一个硬编码的XML字符串,其中包含您在Montastic注册的网站的信息样本。目前,您可以通过从命令行调用“curl”程序来检索基于XML的网站状态信息,如下所示(作为单行调用): 既然我们已经讨论了特定于AIR的代码,那么让我们把重点放在处理来自客户端的信息的基于套接字的原生Android代码上。套接字代码是我们命名为FooAndroid的Android应用的一部分。 在我们讨论这个应用的Java代码之前,让我们看一下清单6–21,它包含了我们的Android应用的文件AndroidManifest.xml。注意清单6–21显示了这个配置文件的最终版本,而不是在Android项目FooAndroid的创建步骤中生成的内容。 清单6–21。AndroidManifest.xml为原生安卓应用 清单6–21指定MainApp.java作为我们的Android应用的AndroidActivity。正如你将看到的,Java类MainApp.java(包含一些定制的Java代码)扩展了AndroidActivityAppEntry.java,它是AndroidActivity类的子类。请注意,清单6–21指定了一个名为TestService.java的AndroidService类,它包含基于套接字的定制代码,用于处理从AdobeAIR客户端接收的信息。 现在在Eclipse中创建一个名为FooAndroid的原生Android应用,它带有一个Java类MainApp,该类扩展了类AppEntry。Java类AppEntry.java是一个简单的预建JavaActivity,它是AndroidActivity类和我们定制的Java类MainApp之间的中间类。清单6–22显示了Java类MainApp的内容。 清单6–22。主安卓Activity级 `packagecom.proandroidflash; importandroid.app.Activity;importandroid.content.Intent;importandroid.os.Bundle; publicclassMainAppextendsAppEntry{/**Calledwhentheactivityisfirstcreated.*/ @OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); try{Intentsrv=newIntent(this,TestService.class);startService(srv);}catch(Exceptione){//servicecouldnotbestarted}}}` Java类MainApp(它是AndroidActivity类的间接子类)在我们的Android应用启动时执行;MainApp中的onCreate()方法启动我们的定制Java类TestService.java(稍后讨论),该类启动服务器端套接字,以便处理来自AdobeAIR客户端的数据请求。 如您所见,onCreate()方法调用了安卓Activity类中的startService()方法,以启动TestServiceService。这个功能是可能的,因为MainApp是AndroidActivity类的子类。 现在在com.proandroidflash包中创建第二个Java类TestServiceApp.java,并将代码插入清单6–23中。 清单6–23。一个AndroidService类,处理来自AIR客户端的数据 importjava.io.BufferedInputStream;importjava.io.DataInputStream;importjava.io.IOException;importjava.net.ServerSocket;importjava.net.Socket; importandroid.app.Notification;importandroid.app.NotificationManager;importandroid.app.PendingIntent;importandroid.app.Service;importandroid.content.Context;importandroid.content.Intent;importandroid.os.IBinder;importandroid.os.Looper;importandroid.util.Log; publicclassTestServiceextendsService{privatebooleanstopped=false;privateThreadserverThread;privateServerSocketss; @OverridepublicIBinderonBind(Intentintent){returnnull;}@OverridepublicvoidonCreate(){super.onCreate(); Log.d(getClass().getSimpleName(),"onCreate"); serverThread=newThread(newRunnable(){publicvoidrun(){try{Looper.prepare();ss=newServerSocket(12345);ss.setReuseAddress(true);ss.setPerformancePreferences(100,100,1); while(!stopped){Socketaccept=ss.accept();accept.setPerformancePreferences(10,100,1);accept.setKeepAlive(true); DataInputStream_in=null; try{_in=newDataInputStream(newBufferedInputStream(accept.getInputStream(),1024));}catch(IOExceptione2){e2.printStackTrace();} intmethod=_in.readInt(); switch(method){//sendanotificationcase1:doNotification(_in);break;}}}catch(Throwablee){e.printStackTrace();Log.e(getClass().getSimpleName(),"**ErrorinListener**",e);} try{ss.close();}catch(IOExceptione){Log.e(getClass().getSimpleName(),"Couldnotcloseserversocket");}}},"Serverthread"); serverThread.start();}` 清单6–23的下一部分包含一个冗长的onCreate()方法,它启动一个服务器端套接字来处理AdobeAIR客户端请求。 onCreate()方法启动一个JavaThread,其run()方法在端口12345(与客户端套接字是同一个端口)上启动一个服务器端套接字。onCreate()方法包含一个while循环,它等待客户端请求,然后在try/catch块中处理它们。 如果客户端请求中的第一个字符是数字“1”,那么我们知道客户端请求来自我们的AIR应用,并且try/catch块中的代码调用方法doNotification()。如果有必要,您可以增强onCreate()(即处理其他数字、文本字符串等的出现),以便服务器端代码可以处理其他客户端请求。 `privatevoiddoNotification(DataInputStreamin)throwsIOException{Stringid=in.readUTF();displayNotification(id);}publicvoiddisplayNotification(StringnotificationString){inticon=R.drawable.mp_warning_32x32_n; CharSequencetickerText=notificationString;longwhen=System.currentTimeMillis();Contextcontext=getApplicationContext();CharSequencecontentTitle=notificationString;CharSequencecontentText="HelloWorld!"; IntentnotificationIntent=newIntent(this,MainApp.class);PendingIntentcontentIntent=PendingIntent.getActivity(this,0,notificationIntent,0);Notificationnotification=newNotification(icon,tickerText,when);notification.vibrate=newlong[]{0,100,200,300}; notification.setLatestEventInfo(context,contentTitle,contentText,contentIntent); Stringns=Context.NOTIFICATION_SERVICE;NotificationManagermNotificationManager=(NotificationManager)getSystemService(ns); mNotificationManager.notify(1,notification);}@OverridepublicvoidonDestroy(){stopped=true;try{ss.close();}catch(IOExceptione){} serverThread.interrupt(); try{serverThread.join();}catch(InterruptedExceptione){}}}` doNotification()方法只是读取输入流中的下一个字符串(由客户端发送),然后调用方法displayNotification()。在我们的例子中,这个字符串是一个串联的字符串,包含每个注册网站的状态(“UP”或“DOWN”)。 您可能已经猜到了,displayNotification()方法包含了在Android通知栏中显示通知的Android代码。需要注意的关键点是,这个方法用Java类MainApp.java创建了一个AndroidIntent。新的Intent使我们能够创建一个AndroidPendingIntent,这反过来又允许我们创建一个AndroidNotification实例。displayNotification()中的最后一行代码启动我们的通知,它在Android通知栏中显示注册网站的状态。 代码的最后一部分是onDestroy()方法,它停止在onCreate()方法上启动的服务器端套接字。 现在我们已经完成了所有的AdobeAIR代码和原生Android代码,我们准备将文件合并到一个移动应用中,这样做的步骤将在本章的下一节中显示。 您可以在Figure6–6中看到本节中调用示例应用的示例,该示例显示了一组由URL及其状态组成的记录,这些记录存储在移动设备上的SQLite数据库中。 图6–6。一组注册网站的状态记录 本节包含将原生Android功能(如本章中的示例)集成到AdobeAIRmobile应用的过程。请注意,Adobe不支持完成此操作的过程,实际步骤可能会在本书出版时发生变化。该信息由EladElrom提供。 清单6–24中的命令使用实用程序adt、apktool和adb将AdobeAIR应用MyAIRApp的内容与原生Android应用AndroidNative.apk的内容合并,以创建AdobeAIR移动应用MergedAIRApp.apk。 清单6–24显示了您需要调用的实际命令,以便创建一个新的.apk文件,该文件包含来自AdobeAIR移动应用和原生Android移动应用的代码。确保您更新了变量APP_HOME的值,以便它反映您的环境的正确值。 清单6–24。使用AdobeAIR和原生Android代码创建合并的应用 APP_HOME="/users/ocampesato/AdobeFlashBuilder/MyAIRApp"cd$APP_HOME/bin-debugadt-package-targetapk-storetypepkcs12-keystorecertificate.p12-storepassNyc1982out.apkMyAIRApp-app.xmlMyAIRApp.swfapktoold-rout.apkair_apkapktoold-rAndroidNative.apknative_apkmkdirnative_apk/assetscp-rair_apk/assets/*native_apk/assetscpair_apk/smali/app/AIRApp/AppEntry*.smalinative_apk/smali/app/AIRAppapktoolbnative_apkcdnative_apk/distjarsigner-verbose-keystore~/.android/debug.keystore-storepassandroidout.apkandroiddebugkeyzipalign-v4out.apkout-new.apkcd../../cpnative_apk/dist/out-new.apkMergedAIRApp.apkrm-rnative_apkrm-rair_apkrmout.apkadbuninstallapp.AIRAppadbinstall-rMergedAIRApp.apk 如果您熟悉Linux或Unix,清单6–24中的命令很简单。如果您更喜欢在Windows环境中工作,您可以通过进行以下更改,将清单6–24中的命令转换为一组相应的DOS命令: 要记住一个重要的细节:您必须获得正确的自签名证书(在清单6–24中称为certificate.p12),以使前面的合并过程正确工作。您可以为基于Flex的应用生成证书,如下所示: 生成自签名证书后,将该证书复制到您执行清单6–24中显示的shell脚本命令的目录中,如果您做的一切都正确,您将生成一个可以部署到基于Android的移动设备的合并应用。 本节总结了创建AdobeAIR应用并将其与原生Android应用合并的多步过程。正如您所看到的,这个集成过程不是微不足道的,也可以说是非直观的,因此在这个过程中您一定会遇到困难(当您遇到困难时不要气馁)。需要记住的是,为AdobeAIR应用添加原生Android支持可能会使您的应用有别于市场上的类似应用。 在本章中,您学习了如何启动本机浏览器、访问数据库以及将AIRmobile应用与本机Android代码相结合。更具体地说,您了解了以下内容: 将AdobeAIR应用与原生Android应用合并的特定步骤序列。 1** 在前一章中,您已经了解了如何将您的AndroidFlash应用与Android操作系统提供的本地软件服务相集成。在本章中,您将学习如何利用Android驱动设备中包含的硬件传感器。本章结束时,你将能够捕捉声音、图像和视频;接入地理定位服务以读取设备的位置;读取加速度计数据以确定设备的方向,所有这些都在您的Flash应用中完成。 现代移动设备有一系列令人惊叹的硬件传感器,从加速度计到摄像头再到GPS接收器。有效的移动应用应该能够在需要时利用这些特性。AIR运行时提供了允许您访问这些本机硬件资源的类。这些类中的一些,比如Microphone和Camera,对于有经验的Flash开发者来说可能很熟悉。其他的,如CameraUI和CameraRoll,是新增加的,允许AIR应用利用Android设备上常见的功能。 如果支持Microphone,那么您可以继续检索一个Microphone实例,设置您的捕获参数,并附加一个事件监听器,使您能够接收来自设备麦克风的声音数据。 清单7–1展示了本书示例代码的examples/chapter-07目录中MicrophoneBasic示例项目的摘录中的这些步骤。 清单7–1。初始化并从麦克风读取样本 `privatevaractivityLevel:uint; privatefunctiononCreationComplete():void{if(Microphone.isSupported){microphone=Microphone.getMicrophone(); microphone.setSilenceLevel(0)microphone.gain=100;microphone.rate=22;microphone.addEventListener(SampleDataEvent.SAMPLE_DATA,onSample); initGraphics();showMessage("Speak,Icanhearyou...");}else{showMessage("flash.media.Microphoneisunsupportedonthisdevice.");}} privatefunctiononSample(event:SampleDataEvent):void{if(microphone.activityLevel>activityLevel){activityLevel=Math.min(50,microphone.activityLevel);}} privatefunctionshowMessage(msg:String):void{messageLabel.text=msg;}` 麦克风初始化代码位于View的creationComplete处理程序中。如果Microphone不被支持,onCreationComplete()函数调用showMessage()函数向用户显示一条消息。showMessage()函数简单地设置位于视图顶部的火花Label的文本属性。然而,如果支持Microphone,那么调用静态函数Microphone.getMicrophone(),它返回一个麦克风对象的实例。然后设置对象的增益和速率属性。设置100是麦克风的最大增益设置,速率22指定最大采样频率为22kHz。这将确保即使是轻柔的声音也能以合理的采样率被捕捉到。你应该注意到Microphone支持高达44.1kHz的采集速率,这与光盘上使用的采样速率相同。然而,记录的质量受限于底层硬件所能支持的。手机麦克风可能会以低得多的速率捕捉音频。虽然Flash会将捕获的音频转换为您要求的采样率,但这并不意味着您最终会获得CD品质的音频。 最后,我们为SampleDataEvent.SAMPLE_DATA事件添加一个监听器。一旦连接了这个监听器,应用将开始接收声音数据。该事件有两个特别有趣的特性: 应用通常会将data字节复制到应用创建的ByteArray中,以保存整个音频剪辑,直到可以播放、存储或发送到服务器。关于采集和回放音频数据的更多细节,请参见第八章。MicrophoneBasic示例应用通过检查activityLevel属性简单地显示来自麦克风的音频数据的视觉反馈,如清单7–1所示。 需要记住的一件重要事情是在应用描述符XML文件中设置android.permission.RECORD_AUDIO设置。没有此权限,您将无法在Android设备上读取麦克风数据。示例项目的清单部分如下面的代码片段所示。 Flash对捕获音频样本的支持实际上相当复杂。您甚至可以使用setSilenceLevel()设置“零”电平,或者使用setUseEchoSuppression()启用回声抑制。我们鼓励您查看Adobe优秀的在线文档1。 Figure7–1展示了MicrophoneBasic应用在实际手机上运行时的样子。 图7–1。Android手机上运行的MicrophoneBasic示例应用 你会发现大多数移动设备上都有一个摄像头(有时是两个)。AndroidFlash应用可以使用相机捕捉静态图像和动态视频。一些设备甚至能够捕捉高清视频。 有两种不同的方式来访问设备的摄像头。flash.media.Camera类将让你访问来自摄像机的原始视频流。这允许您在从设备的主摄像头捕捉图像时对图像进行实时处理。 注意:从AIR2.5.1开始,flash.media.Camera类不支持在Android设备上从多个摄像头进行捕捉的功能。在未来发布的AIRforAndroid中,有望实现在视频拍摄过程中选择相机的功能。 替代方法是使用flash.media.CameraUI来捕捉高质量的图像和视频。CameraUI非常适合只需轻松捕捉图像或视频的应用。它使用原生的Android摄像头接口来处理繁重的工作。这意味着您的应用的用户将能够在给定的设备上访问Android原生支持的所有功能,包括多个摄像头和调整白平衡、地理标记功能、对焦、曝光和闪光灯设置的能力。 Android还提供了一个标准界面,用于浏览设备上拍摄的图像和视频。AIR通过flash.media.CameraRoll类提供对该服务的访问。CameraRoll提供了一种将图像保存到设备的简单方法。它还允许用户浏览以前捕获的图像,如果用户选择了图像或视频文件,它会通知您的应用。和CameraUI一样,CameraRoll是原生Android媒体浏览器界面的包装器。用户喜欢感觉熟悉并且看起来像他们使用的其他本机应用的应用。因此,AIR提供了对相机功能的本地接口的简单访问是一件好事。如果它们满足您的应用需求,应该是您的首选。 在接下来的部分中,我们将更深入地探索这三个类。我们将首先向您介绍基本的Camera类,然后展示一个将一些强大的闪光滤镜效果应用到实时视频流的例子。手机上的实时视频处理!这有多酷?之后,我们将带你参观一下CameraRoll和CameraUI类,并向你展示如何使用它们通过Android的本地界面来捕获、保存和浏览媒体。让乐趣开始吧! 构成Flash和FlexSDKs的API通常设计良好。视频捕捉功能也不例外。这个复杂过程的职责被划分到两个易于使用的类中。flash.media.Camera类负责底层视频捕捉,flash.media.Video类是一个DisplayObject,用于向用户显示视频流。因此,获取摄像头的视频信息是一个简单的三步过程。 清单7–2中的代码演示了这些基本步骤。您可以在FlashBuilder4.5中创建新的Flexmobile项目,并将清单7–2中的代码复制到作为项目一部分创建的View类中。或者,如果您已经下载了本书的示例代码,也可以通过查看examples/chapter-07目录中的CameraBasic项目来继续学习。 View将其动作栏的可见性设置为false,以最大化视频显示的屏幕空间。所有的初始化工作都在creationComplete处理程序中完成。如前面步骤3所述,可以使用一个UIComponent作为视频流的容器,使其在舞台上可见。Camera、Video和UIComponent都被设置为与视图本身相同的大小。 清单7–2。移动中的基本图像捕捉View类 fx:Script privatefunctiononCreationComplete():void{if(Camera.isSupported){varscreenWidth:Number=Screen.mainScreen.bounds.width;varscreenHeight:Number=Screen.mainScreen.bounds.height; camera=Camera.getCamera();camera.setMode(screenWidth,screenHeight,15); varvideo:Video=newVideo(screenWidth,screenHeight);video.attachCamera(camera); videoContainer.addChild(video);}else{notSupportedLabel.visible=true;}}]]> 该视图还在前景中包含一个标签组件,如果出于某种原因不支持该相机,该组件会向用户显示一条消息。使用文本组件(如标签)是在移动设备的小屏幕上向用户显示状态和错误消息的一种简单方法。这里你看到了静态属性isSupported的第二次出现,这次是在Camera类上。检查Camera的isSupported属性以确保该特性在用户的设备上受支持是一个很好的做法。例如,移动浏览器目前不支持Camera。 注意:电视设备的AIR目前也不支持摄像头。然而,Adobe的文档指出,即使getCamera总是返回null,在那个环境中isSupported仍然返回true。为了处理这种情况,您可以将前面示例中的isSupported检查改为if(Camera.isSupported&&(camera=Camera.getCamera())!=null){…}。 仔细查看摄像机初始化代码,可以看到在通过调用静态的getCamera方法获得Camera实例之后,还有一个对setMode方法的调用。如果没有这个调用,相机将默认捕捉160×120像素的视频,当显示在分辨率通常为800×480或更高的现代手机上时,会看起来非常像素化。setMode方法的第一个和第二个参数指定您希望捕获视频的宽度和高度。setMode的第三个参数规定了视频捕捉的帧速率,单位为每秒帧数,也称为FPS。 然而,你要求的不一定是你得到的。摄像机将被置于与您的请求参数最匹配的固有模式。setMode调用的第四个可选参数控制在选择原生相机模式时是优先考虑您的分辨率(宽度和高度)还是FPS请求。默认情况下,相机会尝试满足您的分辨率要求,即使这意味着无法满足您的FPS要求。 因此,我们调用setMode并请求一个与View的分辨率相匹配的视频捕捉分辨率——本质上是使用this.width和this.height。这与设备屏幕的分辨率相同,因为应用是在全屏模式下运行的,我们将在下一节中介绍。我们还要求以每秒15帧的速度捕捉视频。对于视频来说,这是一个合理的速率,同时不会对性能和电池寿命造成太大的消耗。您可能希望在较慢的设备上降低FPS请求。 在屏幕分辨率为800×480的NexusS手机上,该请求导致相机被设置为以720×432捕捉帧。在分辨率为854×480的摩托罗拉Droid上,摄像头以848×477拍摄。在这两种情况下,相机都选择尽可能接近所要求的分辨率的模式,同时保持所要求的宽高比。 清单7–3展示了CameraBasic项目的最终应用描述符。为清晰起见,已从生成的文件中删除了注释和未使用的设置。如前所述,在创建项目时,应用也被指定为全屏应用。这导致fullScreen的initialWindow设置被设置为true,并导致应用在运行时占据整个屏幕,隐藏屏幕顶部的Android指示条。 清单7–3。CameraBasic-app.xml来自camerabasic项目的应用描述符文件 `CameraBasicCameraBasicCameraBasic0.0.1 [ThisvaluewillbeoverwrittenbyFlashBuilder…]falselandscapetruefalse ****]]>` 您需要在APK文件的清单中指定Android摄像头权限,才能访问设备的摄像头。正如您在清单7–3中看到的,应用描述符的Androidmanifest部分包含了android.permission.CAMERA权限。指定这个权限意味着使用了android.hardware.camera和android.hardware.camera.autofocus特性。因此,它们没有被明确地列在清单附件中。 使用Camera而不是CameraUI的优势在于,您可以在视频流被捕获时访问它。您可以对视频流应用几种类型的图像滤镜效果:模糊、发光、渐变、颜色变换、置换贴图和卷积。其中一些相对便宜,而另一些,如ConvolutionFilter,可能是处理器密集型的,因此会降低捕获的视频流的帧速率。简单的模糊、发光和斜面滤镜使用起来非常简单,所以这个例子将使用一些更复杂的滤镜:ColorMatrixFilter、DisplacementMapFilter和ConvolutionFilter。 清单7–4显示了CameraFilter示例项目的默认视图的代码。如果您已经下载了该书附带的源代码,那么可以在examples/chapter-07目录中找到它。 清单7–4。VideoFilterView.mxml文件来自CameraFilter示例项目 fx:Declarations 您可以想象,如果有多个过滤器可以应用于视频流,那么就需要有一种方法让用户选择哪个过滤器应该是活动的。清单7–4中的所示的ViewMenuItem为用户提供了一个简单的方法来完成这个任务。点击一个ViewMenuItem会导致一个对onFilterChange处理器的调用,它将处理设置新选择的滤镜效果。产生的应用如图7–2所示,菜单可见。 图7–2。允许用户选择应用哪种滤镜效果的菜单 现在菜单已经工作了,是时候看看如何创建图像过滤效果并将其附加到视频流中了。 提示:当用户按下“home”按钮时,Android应用的AIR不会被通知,因为这是Android自己使用的。然而,你可以通过检查你的KeyboardEvent监听器中的Keyboard.BACK和Keyboard.SEARCH来监听Android的“返回”和“搜索”按钮。在这两种情况下,调用event.preventDefault()可能是一个好主意,以防止系统响应这些按钮按下而采取任何潜在的默认动作。 第一步是创建过滤器。清单7–5显示了初始化代码。和前面的例子一样,onCreationComplete()方法是视图的creationComplete处理程序。onCreationComplete()做的第一件事是调用initFilters()方法,它封装了所有的过滤器初始化代码。这个例子使用的三个滤镜效果是ColorMatrixFilter、ConvolutionFilter和DisplacementMapFilter。 清单7–5。创建图像过滤器实例 `privatefunctiononCreationComplete():void{varscreenWidth:Number=Screen.mainScreen.bounds.width;varscreenHeight:Number=Screen.mainScreen.bounds.height; initFilters(screenWidth,screenHeight); if(Camera.isSupported){//ThesameCameraandVideoinitializationasbefore…}else{showNotSupportedMsg();}} privatefunctioninitFilters(screenWidth:Number,screenHeight:Number):void{varcolorMat:Array=[.5,0,0,0,0,0,10,0,0,0,0,0,.5,0,0,0,0,0,1,0]; nightVisionFilter=newColorMatrixFilter(colorMat); varsharpMat:Array=[0,-5,0,-5,20,-5,0,-5,0]; ultraSharpFilter=newConvolutionFilter(3,3,sharpMat); varbmpData:BitmapData=newBitmapData(screenWidth,screenHeight,false);varpt:Point=newPoint(0,0); displacementFilter=newDisplacementMapFilter(bmpData,pt,BitmapDataChannel.RED,BitmapDataChannel.RED,40,40);}` AColorMatrixFilter使用4×5矩阵,其值乘以每个像素的颜色通道。例如,矩阵第一行中的条目乘以未过滤像素的红色、绿色、蓝色和alpha分量,将结果与该行中的第五个值相加,并指定为最终过滤像素的红色分量。分别使用矩阵的第二、第三和第四行,类似地计算滤波像素的绿色、蓝色和阿尔法分量。对源图像中的每个像素都这样做,以产生最终的滤波图像。ColorMatrixFilter能够实现许多复杂的颜色处理效果,包括饱和度变化、色调旋转、亮度到alpha变换(源图像中的像素越亮,过滤后的图像越透明)等。正如你所看到的,这个示例程序使用ColorMatrixFilter通过增强绿色通道和减弱红色和蓝色通道来产生一个伪夜视效果。阿尔法通道保持不变。 ConvolutionFilter是图像处理过滤器的主力。它的工作原理是定义一个矩阵,其元素乘以一个像素块。然后将该乘法的结果相加,得到像素的最终值。在本例中,我们使用的是一个3×3矩阵,从它的值可以看出,源图像中每个像素的红色、绿色、蓝色和alpha分量都乘以了20倍,同时,源像素的正北、正南、正东和正西的像素都乘以了-5倍。然后将这些结果相加,得到滤波像素的最终值。矩阵角上的零意味着源像素西北、东北、西南和东南的像素完全从等式中移除。由于负面因素抵消了正面因素,图像的整体亮度保持不变。我们在这里定义的矩阵实现了一个基本的边缘检测算法。并且倍增因子足够大,使得最终的图像将主要是黑色的,边缘是白色的。过滤后的图像看起来有点像是用白色铅笔在黑色页面上绘制的,因此我们将过滤器命名为:铅笔。 DisplacementFilter使用一个位图中的像素值来偏移源图像中的像素。因此,与原始图像相比,所得到的图像将以某种方式扭曲。这会产生各种有趣的效果。initFilters()方法中的代码简单地用一个空位图初始化置换过滤器。置换贴图的选择实际上是在用户选择滑稽脸滤镜或波纹滤镜时设置的。本例使用的位移图如图图7–3所示。为了你自己的理智,不要盯着这些图像太久! 图7–3。示例程序中使用的位移贴图 清单7–6显示了在位移过滤器上设置这些位移图的代码,以响应相应的菜单按钮点击。当选择波纹或滑稽脸效果时,适当的位图(在启动时从嵌入的资源中加载)被绘制到置换过滤器的mapBitmap中。 清单7–6。选择滤镜并设置置换贴图 `[Embed(source="funny_face.png")]privatevarFunnyFaceImage:Class; [Embed(source="ripples.png")]privatevarRippleImage:Class; privatevarrippleBmp:Bitmap=newRippleImage()asBitmap;privatevarfunnyFaceBmp:Bitmap=newFunnyFaceImage()asBitmap; //Thisfunctionistheclickhandlerforallbuttonsinthemenu.videoContainer//isaUIComponentthatisdisplayingthevideostreamfromthecamera.privatefunctiononFilterChange(event:Event):void{varbtn:Button=event.targetasButton;switch(btn.id){case"noFilterBtn":videoContainer.filters=[];break; case"nightVisionBtn":videoContainer.filters=[nightVisionFilter];break; case"sharpBtn":videoContainer.filters=[ultraSharpFilter];break; case"rippleBtn":showDisplacementFilter(true);break; case"funnyFaceBtn":showDisplacementFilter(false);break;} toggleMenu();} privatefunctionshowDisplacementFilter(ripples:Boolean):void{varbmp:Bitmap=ripplesrippleBmp:funnyFaceBmp; varmat:Matrix=newMatrix();mat.scale(width/bmp.width,height/bmp.height); displacementFilter.mapBitmap.draw(bmp,mat); videoContainer.filters=[displacementFilter];}` 图7–4显示了使用这些滤镜拍摄的一些图像。从左上角逆时针方向,你可以看到一个没有滤镜的图像,滑稽脸滤镜,铅笔滤镜,夜视滤镜。 图7–4。NexusS手机拍摄的本例中使用的一些滤镜的输出 应该注意的是,虽然置换贴图和颜色矩阵滤镜在性能方面相对便宜,但卷积滤镜在我们测试的Android设备上是一个真正的性能杀手。这是一个重要的提醒,处理器周期不像桌面系统那样充足。始终在目标硬件上测试您的性能假设! 清单7–7。屏幕上显示一个性能计数器 `privatevartimer:Timer;privatevarfpsString:String; privatefunctiononCreationComplete():void{varscreenWidth:Number=Screen.mainScreen.bounds.width;varscreenHeight:Number=Screen.mainScreen.bounds.height; if(Camera.isSupported){//ThesameCameraandVideoinitializationasbefore… videoContainer.addEventListener(MouseEvent.CLICK,onTouch);fpsString="FPS("+camera.width+"x"+camera.height+")"; timer=newTimer(2000);timer.addEventListener(TimerEvent.TIMER,updateFPS);timer.start();}else{showNotSupportedMsg();}} privatefunctionupdateFPS(event:TimerEvent):void{messageLabel.text=fpsFormatter.format(camera.currentFPS)+fpsString;} privatefunctiononTouch(event:MouseEvent):void{if(messageLabel.visible){timer.stop();messageLabel.visible=false;}else{timer.start();messageLabel.visible=true;}}` 既然应用已经能够从视频流中创建有趣的图像,那么下一个合乎逻辑的步骤就是捕获一帧视频并将其保存在设备上。 我们处理Camera类的一系列示例项目的高潮是CameraFunHouse。这个最终的应用通过整合对从视频流中捕捉图像并将其保存在设备上的支持,结束了这个系列。你必须使用Android新的CameraRoll类的AIR来保存设备上的图像。幸运的是,这是一个简单的过程,将在本节末尾演示。 从视频流中捕捉图像只使用了优秀的老式Flash和Flex功能。首先对View的MXML文件做一些添加,如清单7–8所示。为了方便起见,新增加的内容会突出显示。它们由一个新的UIComponent组成,将显示从视频流中捕获的静态图像的位图。该位图将显示为预览,以便用户可以决定是否应该保存该图像。其他新增加的是按钮,用户可以点击捕捉图像,然后保存或丢弃它。 清单7–8。查看支持图像拍摄和保存的增强功能 fx:Declarations 当应用处于捕获模式时,“捕获图像”按钮将始终可见。该按钮是半透明的,因此用户可以看到按钮后面的视频流。一旦用户点击了捕获按钮,应用将抓取并显示图像,隐藏捕获按钮,并显示保存图像和丢弃图像按钮。这个逻辑由添加到ActionScript文件中的代码以及三个新的点击处理程序控制:onCaptureImage()、onSaveImage()和onDiscardImage()。这些增加的内容显示在清单7–9中。 清单7–9。添加和更改ActionScript代码以支持图像捕捉和保存 initFilters(screenWidth,screenHeight);setCaptureMode(true); //Therestofthemethodisthesameasbefore…} //DetermineswhichcontrolsarevisibleprivatefunctionsetCaptureMode(capture:Boolean):void{videoContainer.visible=capture;bitmapContainer.visible=!capture; captureButton.visible=capture;saveButton.visible=!capture;discardButton.visible=!capture;} privatefunctiononCaptureImage():void{varbmp:BitmapData=newBitmapData(width,height,false,0xffffff);bmp.draw(videoContainer); bitmapContainer.addChild(newBitmap(bmp));setCaptureMode(false);} privatefunctiononDiscardImage():void{bitmapContainer.removeChildAt(0);setCaptureMode(true);} privatefunctiononSaveImage():void{if(CameraRoll.supportsAddBitmapData){varbmp:Bitmap=bitmapContainer.removeChildAt(0)asBitmap;newCameraRoll().addBitmapData(bmp.bitmapData);setCaptureMode(true);}else{showNotSupportedMsg(ROLL_NOT_SUPPORTED);saveButton.visible=false;}}` onCaptureImage()函数只是将videoContainer的内容绘制到一个新的位图中,并在bitmapContainer中显示为预览。在方法结束时对setCaptureMode(false)的调用负责设置所有适当控件的可见性。同样,onDiscardImage()处理程序删除预览位图,并将应用放回捕获模式。 CameraRoll类在用户想要保存图像时发挥作用。如您所见,它遵循了在使用类之前检查支持的常见模式。您应该首先确保设备支持使用CameraRoll.supportsAddBitmapData属性保存图像。假设支持添加图像,onSaveImage()函数创建一个新的CameraRoll实例并调用它的addBitmapData方法,传递一个对保存预览图像的BitmapData对象的引用。CameraRoll当图像已成功保存或出现阻止保存操作的错误时,将发出事件。下一节涉及的照片收集示例将展示使用这些事件的示例。Figure7–5显示了完整的CameraFunHouse应用捕捉一只合作犬的图像。 图7–5。我们的模特欣然同意使用她的肖像作为交换。 CameraRoll类还允许用户浏览和选择保存在设备上的图像以及用户保存在互联网相册中的图像!这个特性将在下一节解释。 用CameraRoll在Android设备上浏览照片几乎和保存照片一样简单。我们将在一个名为PhotoCollage的新示例程序的上下文中说明这个特性。这个程序让你选择已经存储在设备上的图像,并把它们排列成拼贴画。您可以使用多点触控手势来拖动、缩放和旋转图像。当图像按照您的喜好排列后,您可以将新图像存储回“相机胶卷”。这个例子可以在本书的源代码的examples/chapter-07目录中找到。清单7–10显示了应用主视图的MXML文件。 清单7–10。照片收藏应用的首页视图—PhotoCollageHome.mxml 将KeyboardEvent侦听器添加到舞台的代码与前面示例中的代码相同,因此我们在此不再重复。 清单7–11。启动浏览动作并显示选中图像的代码 `privatestaticconstBROWSE_UNSUPPORTED:String="Browsingwith"+"flash.media.CameraRollisunsupportedonthisdevice."; privatevarcameraRoll:CameraRoll=newCameraRoll(); privatefunctiononCreationComplete():void{cameraRoll.addEventListener(MediaEvent.SELECT,onSelect);cameraRoll.addEventListener(Event.CANCEL,onSelectCanceled);cameraRoll.addEventListener(ErrorEvent.ERROR,onCameraRollError);cameraRoll.addEventListener(Event.COMPLETE,onSaveComplete); //…} privatefunctiononBrowse():void{if(CameraRoll.supportsBrowseForImage){cameraRoll.browseForImage();}else{showMessage(BROWSE_UNSUPPORTED);}toggleMenu();} privatefunctiononSelect(event:MediaEvent):void{varloader:Loader=newLoader();loader.contentLoaderInfo.addEventListener(Event.COMPLETE,onLoaded);loader.load(newURLRequest(event.data.file.url));} privatefunctiononLoaded(event:Event):void{varinfo:LoaderInfo=event.targetasLoaderInfo;varbmp:Bitmap=info.contentasBitmap; scaleContainer(bmp.width,bmp.height); varsprite:Sprite=newSprite(); sprite.addEventListener(TransformGestureEvent.GESTURE_ZOOM,onZoom);sprite.addEventListener(TransformGestureEvent.GESTURE_ROTATE,onRotate);sprite.addEventListener(MouseEvent.MOUSE_DOWN,onMouseDown);sprite.addEventListener(MouseEvent.MOUSE_UP,onMouseUp);sprite.addChild(bmp); photoContainer.addChild(sprite);} privatefunctiononSelectCanceled(event:Event):void{showMessage("Selectcanceled");} privatefunctiononCameraRollError(event:ErrorEvent):void{showMessage("Error:"+event.text);} privatefunctiononSaveComplete(event:Event):void{showMessage("CameraRolloperationcomplete");}` 现在应用正在监听所有必要的事件,它准备好处理onBrowse回调。和往常一样,您应该做的第一件事是使用CameraRoll.supportsBrowseForImage属性检查浏览是否受支持。如果这个属性是true,那么可以调用cameraRoll对象上的browseForImage()实例方法。这将触发向用户显示原生Android图像浏览器。如果用户选择一幅图像,应用的onSelect()处理程序将被调用。作为参数传递给该函数的MediaEvent对象包含一个名为data的属性,它是一个MediaPromise对象的实例。MediaPromise对象中的关键信息是文件属性。您可以使用文件的URL来加载选定的图像。因此,如前所示,您真正想要的是event.data.file.url属性。这个属性被传递给一个处理图像数据加载的Loader对象。当加载完成时,它触发onLoaded回调函数,该函数负责获取生成的位图并将其放入Sprite中,以便用户可以对其进行操作。然后将此Sprite添加到photoContainer中,这样它就可以显示在屏幕上。 当然,使用Loader并不是读取数据的唯一方式。如果你只是对显示照片感兴趣,而不是用触摸手势操作,使用火花BitmapImage或光环Image会更容易。在这两种情况下,您只需要将Image或BitmapImage的source属性设置为event.data.file.url,然后将其添加到您的stage中。其他一切都将自动处理。图7–6显示了运行在Android设备上的PhotoCollage应用。 图7–6。NexusS上运行的PhotoCollage程序 FlashBuilder4.5附带的设备上调试器是一个非常棒的工具。但是有时使用好的老式调试输出来了解程序的运行情况会更快。在Flash中添加这种输出的传统方法是使用trace()函数。只有当应用在调试模式下运行时,该函数才会打印消息。FlashBuilder的调试器连接到Flash播放器,并将在调试器的控制台窗口中显示trace消息。此方法在移动设备上调试时也有效。 清单7–12。为Android应用的AIR添加调试输出 `//FromPhotoCollageHome.mxml //FromPhotoCollageHomeScript.asprivatefunctiononCreationComplete():void{//CameraRolleventlistenerinitialization… //Makesurethetextmessagesstaywithintheconfines//ofourview'swidth.messageLabel.maxWidth=Screen.mainScreen.bounds.width;messageLabel.maxHeight=Screen.mainScreen.bounds.height; //Multitouchinitialization…} privatefunctionshowMessage(msg:String):void{if(messageLabel.text&&messageLabel.height 在onCreationComplete()函数中,messageLabel的maxWidth和maxHeight属性被设置为屏幕的宽度和高度。这将防止标签的文本被绘制到屏幕边界之外。最后一部分是一个小的showMessage函数,它将一个消息字符串作为参数。如果messageLabel.text属性当前为空,或者如果messageLabel变得太大,那么文本属性被设置为消息字符串,有效地清除标签。否则,新消息将与换行符一起追加到现有文本中。结果是一个消息缓冲区在屏幕上向下扩展,直到到达底部,此时现有的消息将被删除,新消息将从顶部重新开始。它不是一个全功能的应用日志,并且必须为应用中的每个View重新实现,但是作为一种在屏幕上显示调试消息的简单方式,它是无与伦比的。 您现在应该熟悉使用CameraRoll类在Android设备上浏览和保存图像。您可以在PhotoCollage示例项目的PhotoCollageHomeScript.as文件中找到完整的源代码。ActionScript文件包括之前没有显示的部分,例如多点触摸缩放和旋转手势以及触摸拖动的处理。这段代码应该能让您很好地理解如何处理这类用户输入。如果你需要更多关于这些主题的细节,你也可以参考第二章的“多点触摸和手势”部分。 本章将讨论的Flash相机支持的最后一个方面是通过CameraUI类使用原生Android媒体捕获接口。这将是下一节的主题。 CameraUI类使您能够利用原生Android媒体捕获接口的能力来捕获高质量、高分辨率的图像和视频。这个类的使用包括现在熟悉的三个步骤:确保功能受支持,调用一个方法来调用本机接口,以及注册一个回调以在图像或视频被捕获时得到通知,以便您可以在应用中显示它。清单7–13显示了CameraUIBasic示例项目的捕获视图。这个简短的程序演示了刚刚列出的三个步骤。 清单7–13。基本图像捕捉使用CameraUI fx:Script privatefunctiononCreationComplete():void{if(CameraUI.isSupported){cameraUI=newCameraUI();cameraUI.addEventListener(MediaEvent.COMPLETE,onCaptureComplete);} captureButton.visible=CameraUI.isSupported;notSupportedLabel.visible=!CameraUI.isSupported;} privatefunctiononCaptureImage():void{cameraUI.launch(MediaType.IMAGE);} privatefunctiononCaptureComplete(event:MediaEvent):void{image.source=event.data.file.url;}]]> onCreationComplete()方法检查CameraUI支持,如果存在的话,创建一个新的cameraUI实例。然后向实例对象添加一个事件监听器,以便应用在捕获完成时通过其onCaptureComplete回调函数得到通知。当用户点击捕获图像按钮时,onCaptureImage回调函数调用CameraUI的launch方法来显示Android捕获界面。当捕获完成时,我们附加到CameraUI的回调函数被调用。它接收一个MediaEvent参数,与前面讨论的CameraRoll使用的事件类相同。和以前一样,事件在其data属性中包含一个MediaPromise实例。因此,您可以使用event.data.file.url加载捕获的图像,就像使用CameraRoll的浏览功能选择图像时所做的一样。 CameraUIBasic示例程序显示了一个视图,其中有一个带有“CaptureImage”标签的半透明按钮。当用户点击按钮时,Android的原生相机界面通过CameraUI类启动。捕获图像后,图像数据将返回到原始视图进行显示,如清单7–13所示。图7–7是一系列图像,显示了原生Android摄像头界面捕捉机器人的图像并将其返回给应用进行显示。参加第一届技术挑战机器人竞赛的一组中学生制造了如图图7–7所示的机器人。 注意:用CameraUI拍摄的任何照片也会自动保存到设备的照片库中,因此以后可以使用CameraRoll类检索。 以上就是我们对Android版AIR中摄像头功能的介绍。在本章的剩余部分,你将了解到flash.sensors包的内容:加速度计和地理定位。我们希望你会发现它又快又切题。 图7–7。用原生安卓相机界面和CameraUI拍摄的机器人图像 加速度传感器允许您通过测量重力沿x、y和z轴产生的加速度来检测设备的方向。静止时,地球上的任何物体都会由于重力而经历大约9.8米/秒/秒(米/秒/秒)的加速度。9.8米/秒/秒也被称为1重力或1重力,或者简单地称为1克加速度。因此,10g的加速度是重力的10倍,即98米/秒/秒。这是一个非常大的重力,通常只有战斗机飞行员在极端机动中才会感受到! 由于手机是一个三维物体,它的方向可以通过观察1g的力如何沿其三个轴分布来确定。加速度计还可以告诉你手机是否受到震动或以其他快速方式移动,因为在这种情况下,手机在三个轴上的加速度明显大于或小于1g。您需要知道手机的轴是如何布置的,以便从加速度计值中收集有用的信息。图7–8显示了手机加速轴相对于手机机身的方向。 图7–8。安卓手机的加速度计轴 如果您对齐图7–8中标记的轴之一,使其与重力正对,您将在该轴上读取1g的加速度。如果一个轴垂直于重力,那么它的读数将是0g。例如,如果你把手机放下,让面朝上,放在一个平面上,那么加速度计在z轴上的读数大约为+1g,在x轴和y轴上的读数大约为0g。如果你将手机翻转过来,让它面朝下,加速度计将在z轴上显示-1g。 清单7–14。读取加速度传感器 `privatevaraccelerometer:Accelerometer; privatefunctiononCreationComplete():void{if(Accelerometer.isSupported){showMessage("Accelerometersupported"); accelerometer=newAccelerometer();accelerometer.addEventListener(AccelerometerEvent.UPDATE,onUpdate);accelerometer.addEventListener(StatusEvent.STATUS,onStatus);accelerometer.setRequestedUpdateInterval(100); if(accelerometer.muted){showMessage("Accelerometermuted,accessdenied!");}}else{showMessage(UNSUPPORTED);}}privatefunctiononStatus(event:StatusEvent):void{showMessage("Mutedstatushaschanged,isnow:"+accelerometer.muted);} privatefunctiononUpdate(event:AccelerometerEvent):void{updateAccel(xAxis,event.accelerationX,0);updateAccel(yAxis,event.accelerationY,1);updateAccel(zAxis,event.accelerationZ,2); time.text="EllapsedTime:"+event.timestamp+"ms";} privatefunctionupdateAccel(l:Label,val:Number,idx:int):void{varitem:Object=accelData[idx];item.max=formatter.format(Math.max(item.max,val));item.min=formatter.format(Math.min(item.min,val)); l.text=item.title+"\nCurrentValue:"+formatter.format(val)+"g"+"\nMinimumValue:"+item.min+"g"+"\nMaximumValue:"+item.max+"g";}` 图7–9。运行在安卓设备上的加速度计基础程序 该程序在其操作栏中包括一个按钮,允许用户清除到目前为止已经记录的最小值和最大值。这有助于在规划自己的应用时试验加速度计读数。最小值和最大值被初始化为正负10g,因为手机不太可能经历比这更大的加速度,除非你碰巧是战斗机飞行员。最后要注意的是,为了使用加速度计,您的应用不需要在应用描述符中指定任何特殊的Android权限。 从本节的材料中可以看出,在AIR应用中读取加速度计很容易,并且允许您以新的和创造性的方式接受用户的输入。接下来,我们将看看如何读取移动应用中广泛使用的另一种数据形式:地理位置数据。 近年来,移动设备中地理定位服务的流行导致位置感知应用的数量迅速增加。位置数据可以来自蜂窝塔三角测量,一个已知Wi-Fi接入点的数据库,当然,还有GPS卫星。基于Wi-Fi和手机信号塔的位置数据不如GPS数据准确,但与使用设备的GPS接收器相比,它可以更快地获得并消耗更少的电池电量。由于这种复杂性,获得准确的位置可能比您想象的更复杂。 对于地理定位数据来说,电池使用是一个更大的问题,因为GPS接收机可能会消耗电池寿命。如果设备位于信号较弱的地方,这一点尤其明显。因此,您应该仔细考虑您的应用真正需要位置更新的频率。在位置感知应用中,一分钟(60,000毫秒)或更长的更新间隔并不少见。并且由于方位和速度也与位置数据一起提供,所以可以基于先前的数据进行一定量的推断。这可以显著地平滑您提供给应用用户的位置更新。 您将从类型为GeolocationEvent.UPDATE的GeolocationEvent对象接收位置数据,该数据将被传递给您将向Geolocation实例注册的事件处理程序。GeolocationEvent类包含几个感兴趣的属性: 注意:虽然AIR2.5.1支持GeolocationEvent中的一个heading属性,但是这个属性目前在Android设备上还不支持,会一直返回NaN。 Geolocation类还包含一个名为muted的属性,如果用户禁用了地理定位(或者如果您忘记在应用描述符XML文件的manifest部分指定android.permission.ACCESS_FINE_LOCATION权限),该属性将被设置为true!).当muted属性的值改变时,Geolocation类将发送一个类型为StatusChange.STATUS的StatusChange事件给你的监听器,如果你已经添加了一个的话。清单7–15显示了GeolocationBasic示例项目的源代码。这段代码演示了在应用中接收和显示地理位置数据的步骤。 清单7–15。GeolocationBasicHome视图的源代码 fx:Declarations fx:Script privatestaticconstUNSUPPORTED:String="flash.sensors.Geolocation"+"isnotsupportedonthisdevice."; privatevarloc:Geolocation; privatefunctiononCreationComplete():void{if(Geolocation.isSupported){showMessage("Geolocationsupported"); loc=newGeolocation();if(!loc.muted){loc.addEventListener(GeolocationEvent.UPDATE,onUpdate);loc.addEventListener(StatusEvent.STATUS,onStatus);loc.setRequestedUpdateInterval(1000);}else{showMessage("Geolocationmuted");}}else{showMessage(UNSUPPORTED);}} privatefunctiononStatus(event:StatusEvent):void{showMessage("Geolocationstatuschanged,mutedisnow"+loc.muted);} privatefunctiononUpdate(event:GeolocationEvent):void{geoDataLabel.text="Geolocation"+"\nLatitude:"+f.format(event.latitude)+"\u00B0"+"\nLongitude:"+f.format(event.longitude)+"\u00B0"+"\nHorzAccuracy:"+f.format(event.horizontalAccuracy)+"m"+"\nVertAccuracy:"+f.format(event.verticalAccuracy)+"m"+"\nSpeed:"+f.format(event.speed)+"m/s"+"\nAltitude:"+f.format(event.altitude)+"m"+"\nTimestamp:"+f.format(event.timestamp)+"ms";} privatefunctionshowMessage(msg:String):void{if(messageLabel.text&&messageLabel.height 正如我们顺便提到的,除非您在应用描述符的manifest部分指定了正确的Android权限,否则这段代码将无法运行。该应用的应用描述符如清单7–16所示。 清单7–16。geolocationbasic的应用描述符,显示了精确定位许可的正确使用 [ThisvaluewillbeoverwrittenbyFlashBuilderintheoutputapp.xml]falsefalsefalse Android使用ACCESS_COARSE_LOCATION将地理定位数据限制为仅使用Wi-Fi和手机信号塔获取位置信息。先前使用的ACCESS_FINE_LOCATION权限包括粗略定位权限,还增加了访问GPS接收器以获得更精确读数的能力。图7–10显示了运行中的GeolocationBasic程序的屏幕截图。 图7–10。Android设备上运行的GeolocationBasic程序 本章介绍了各种传感器和硬件功能。从麦克风和摄像头到媒体存储、加速度计和地理定位数据,您现在已经掌握了将您的应用与AIR应用可用的各种硬件服务相集成所需的所有知识。在本章的学习过程中,您已经了解了以下内容: 在下一章中,您将通过探索Flash的媒体播放功能,继续探索AIR和Android探索之路。 前一章向您展示了如何在Android设备上捕捉音频和视频。本章以这些概念为基础,将教您如何使用Flash平台的力量来释放Android移动设备的富媒体潜力。 音效通常是响应各种应用事件(如弹出警告或按下按钮)而播放的简短声音。声音效果的音频数据应该在MP3文件中,可以嵌入到应用的SWF文件中,也可以从互联网上下载。您通过使用Embed元数据标签来标识素材,将MP3素材嵌入到您的应用中,如清单8–1所示。 清单8–1。嵌入带有Embed元数据标签的声音文件 fx:Script [Embed(source="mySound.mp3")]privatevarMySound:Class;privatevarsound:SoundAsset=newMySound();]]> 虽然知道幕后发生的事情很好,但是通常不需要在Flex程序中创建和实例化SoundAsset。您选择的工具通常是SoundEffect类,因为它能够在回放样本时轻松创建有趣的效果。它在回放过程中提供了对循环、平移和音量效果的简单控制。因为它扩展了基本的mx.effect.Effect类,所以它可以在任何可以使用常规效果的地方使用。例如,你可以将一个SoundEffect实例设置为一个Button的mouseDownEffect或者一个Alert对话框的creationCompleteEffect。清单8–2展示了如何做到这一点,以及如何手动弹奏一个SoundEffect。 清单8–2。创建并播放一个循环SoundEffect fx:Declarations** fx:Script privatefunctionplayEffect(event:MouseEvent):void{mySound.end();mySound.play([event.target]);}]]> 然后我们创建两个按钮来说明玩SoundEffect的两种不同方式。第一个按钮只是将SoundEffect的实例id设置为它的mouseDownEffect。每次在按钮上按下鼠标按钮时,都会播放我们的音频样本。每次按下鼠标按钮,都会创建并播放一个新的效果。如果您点按的速度足够快,并且您的声音样本足够长,就有可能听到它们同时播放。 单击第二个按钮将调用playEffect方法,该方法做两件事。首先,它将通过调用end方法来停止当前正在播放的任何效果实例。这确保声音不会与自身的任何其他实例重叠。第二,使用按钮作为目标对象来播放新的声音效果。MouseEvent的target属性提供了一种便捷的方式来引用我们将用作效果目标的按钮。注意,play方法的参数实际上是一个目标数组。这就是为什么我们需要在event.target参数周围加一组方括号。 清单8–3。【声效基础范例计划】之家View fx:Script privatestaticconstCM_URL:URLRequest=newURLRequest(CM_URL_STR); privatefunctionplay(event:MouseEvent,effect:SoundEffect):void{effect.end();effect.play([event.target]);}]]> 这个例子再次演示了播放音效的两种不同方法。在屏幕的底部还有第四个按钮,当点击它时,会启动Android的原生网络浏览器,并通过使用第六章中的方法将你带到“代码猴子”网页。结果应用如图8–1中的所示。 图8–1。运行在Android设备上的代码猴子音效示例 SoundEffect类非常适合播放小的声音效果来响应应用事件。如果您需要对应用中的声音进行更高级的控制,那么是时候深入挖掘Flash平台必须提供的功能了。 对于大多数应用来说,SoundEffect类是一个方便的抽象,这些应用的需求不会超出偶尔提示或通知用户的能力。在一些应用中,声音是主要成分之一。如果你想录制语音备忘录或播放音乐,那么你需要更深入地了解Flash声音API。我们将首先看一看Sound类和它的伙伴:SoundChannel和SoundTransform。所有这三个类都可以在flash.media包中找到。 Sound类充当音频文件的数据容器。它的主要职责是提供将数据加载到其缓冲区的机制,并开始回放该数据。加载到Sound类中的音频数据通常来自MP3文件或应用本身动态生成的数据。不出所料,这个类中需要注意的关键方法是load和play方法。您使用load方法来提供应该加载到Sound中的MP3文件的URL。数据一旦加载到Sound中,就不能更改。如果您稍后想要加载另一个MP3文件,您必须创建一个新的Sound对象。向Sound对象的构造函数传递一个URL相当于调用load方法。Sound类在加载音频数据的过程中调度几个事件,如Table8–1所示。 加载完数据后,调用Sound类的play方法将导致声音开始播放。play方法返回一个SoundChannel对象,该对象可用于跟踪声音播放的进度并提前停止播放。SoundChannel还有一个与之关联的SoundTransform对象,可以用来改变声音播放时的音量和声相。有三个可选参数可以传递给play方法。首先是startTime参数,它将导致声音在样本中指定的毫秒数开始播放。如果您希望声音播放一定的次数,也可以传递循环计数。最后,如果您想在声音开始播放时设置声音的初始转换,也可以提供一个SoundTransform对象作为play方法的参数。您传递的变换将被设置为SoundChannel的SoundTransform。 每次调用Sound.play方法时,都会创建并返回一个新的SoundChannel对象。SoundChannel在声音播放时充当你与声音互动的主要点。它允许你跟踪当前的位置和音量。它包含一个stop方法,该方法中断和终止声音的回放。当一个声音到达其数据的末尾时,SoundChannel类将通过分派类型为flash.events.Event.SOUND_COMPLETE的soundComplete事件来通知您。最后,您还可以使用它的soundTransform属性来操纵声音的音量,并将声音移动到左右扬声器。图8–2说明了这三个协作类之间的关系。 图8–2。Sound``SoundChannel``SoundTransform的关系 诚然,从SoundChannel到说话者的路径并不像图8–2暗示的那样直接。在音频信号到达扬声器之前,存在几个层(包括操作系统驱动程序和数模转换电路)。Flash在flash.media包中还提供了另一个名为SoundMixer的类,它包括几个静态方法,用于在全局级别上操作和收集关于应用正在播放的声音的数据。 这就结束了我们对使用Flash在Android设备上播放声音所需要熟悉的类的概述。在下一节中,我们将看一些使用这些类来播放来自内存缓冲区和存储在设备上的文件的声音的例子。 我们在第七章的MicrophoneBasic示例应用中向您展示了如何从设备的麦克风录制音频数据。扩展该示例将为更深入地探索Flash的音频支持提供一个方便的起点。您可能还记得,我们给Microphone对象附加了一个事件处理程序来处理它的sampleData事件。每次麦克风为我们的应用获取数据时,都会调用处理程序。在那个例子中,我们实际上没有对麦克风数据做任何事情,但是将数据复制到一个ByteArray中用于以后的回放应该是一件简单的事情。问题是:我们如何播放来自ByteArray的声音数据? 如果你在一个没有加载任何东西的Sound对象上调用play()方法,这个对象将被迫寻找声音数据来播放。它通过调度sampleData事件来请求声音样本。事件的类型是SampleDataEvent.SAMPLE_DATA,在flash.events包中找到。这恰好与Microphone类用来通知我们样本可用的事件类型相同。我们之前问题的答案很简单:您只需为Sound的sampleData事件附加一个处理程序,并开始将字节复制到事件的data属性中。 因此,我们增强的应用将为sampleData事件提供两个独立的处理程序。当麦克风处于活动状态时,第一个会将数据复制到一个ByteArray,当我们回放时,第二个会将数据从同一个ByteArray复制到Sound对象。新应用的源代码可以在位于examples/chapter-08目录下的SoundRecorder应用中找到。清单8–4显示了麦克风数据的sampleData事件处理程序。 清单8–4。麦克风数据通知的设置代码和事件处理程序 `privatestaticconstSOUND_RATE:uint=44;privatestaticconstMICROPHONE_RATE:uint=22; //HandlestheView’screationCompleteeventprivatefunctiononCreationComplete():void{if(Microphone.isSupported){microphone=Microphone.getMicrophone();microphone.setSilenceLevel(0)microphone.gain=75;microphone.rate=MICROPHONE_RATE; sound=newSound();recordedBytes=newByteArray();}else{showMessage("microphoneunsupported");}} //ThishandleriscalledwhenthemicrophonehasdatatogiveusprivatefunctiononMicSample(event:SampleDataEvent):void{if(microphone.activityLevel>activityLevel){activityLevel=Math.min(50,microphone.activityLevel);} if(event.data.bytesAvailable){recordedBytes.writeBytes(event.data);}}` onCreationComplete处理程序负责检测麦克风,初始化它,并创建应用用来存储和播放声音的ByteArray和Sound对象。请注意,麦克风的rate设置为22kHz。这对于捕获语音记录来说是足够的质量,并且比以全44kHz记录占用更少的空间。 这个处理程序很简单。与之前一样,Microphone对象的activityLevel属性用于计算一个数字,该数字随后用于确定在显示器上绘制的动画曲线的幅度,以指示声音级别。然后事件的data属性,也就是一个ByteArray,被用来确定是否有麦克风数据可用。如果bytesAvailable属性大于零,那么字节从data数组复制到recordedBytes数组。这对于正常的录音来说效果很好。如果您需要记录数小时的音频数据,那么您应该将数据流式传输到服务器,或者将其写入设备上的文件中。 因为我们处理的是原始音频数据,所以由程序来记录声音的格式。在这种情况下,我们有一个麦克风,为我们提供22kHz单声道(单声道)声音样本。Sound对象期望44kHz立体声(左右声道)声音。这意味着每个麦克风样本必须写入Sound数据两次,以将其从单声道转换为立体声,然后再写入两次,以从22kHz转换为44kHz。因此,每个麦克风样本名义上将被复制到Sound对象的数据数组中四次,以便使用与捕获时相同的速率回放录音。清单8–5显示了执行复制的Sound的sampleData处理程序。 清单8–5。Sound对象的数据请求的事件处理程序 `//ThishandleriscalledwhentheSoundneedsmoredataprivatefunctiononSoundSample(event:SampleDataEvent):void{if(soundChannel){varavgPeak:Number=(soundChannel.leftPeak+soundChannel.rightPeak)/2;activityLevel=avgPeak*50;} //Calculatethenumberofstereosamplestowriteforeachmicrophonesamplevarsample:Number=0;varsampleCount:int=0;varoverSample:Number=SOUND_RATE/MICROPHONE_RATE*freqMultiplier; while(recordedBytes.bytesAvailable&&sampleCount<2048/overSample){sample=recordedBytes.readFloat();for(vari:int=0;i 现在我们到了有趣的部分:将记录的数据传输到声音的数据数组。首先计算overSample值。它解释了捕获频率与回放频率之间的差异。它在内部for循环中用于控制写入多少立体声样本(记住writeFloat被调用两次,因为在回放期间来自麦克风的每个样本都用于左右声道)。通常情况下,overSample变量的值是2(44/22),乘以对writeFloat的两次调用,我们将得到之前计算的每个麦克风样本的四个回放样本。毫无疑问,您已经注意到还包括了一个额外的倍频因子。这个倍增器将让我们能够加快(想想花栗鼠)或减慢播放的频率。freqMultiplier变量的值将被限制在0.5、1.0或2.0,这意味着overSample的值将是1、2或4。与正常值2相比,值1将导致只有一半的样本被写入。这意味着频率会加倍,我们会听到花栗鼠的声音。值为4的overSample将导致慢动作音频回放。 下一个要回答的问题是:每次Sound请求数据时,我们的recordedBytes数组中有多少应该被复制到Sound中?粗略的回答是“在2048到8192个样本之间。”确切的答案是“视情况而定。”你不讨厌吗?但是在这种情况下,宇宙向我们展示了仁慈,因为依赖性是很容易理解的。写入更多样本以获得更好的性能,写入更少样本以获得更好的延迟。因此,如果您的应用只是简单地回放声音,正如它被记录,使用8192。如果你必须生成声音或者动态地改变它,比如说,改变播放频率,那么使用更接近2048的东西来减少用户在屏幕上看到的和他们从扬声器听到的之间的滞后。如果您写入缓冲区的样本少于2048个,那么Sound会将其视为没有更多数据的标志,并且在剩余样本被消耗完之后,回放将会结束。在清单8–5中,while循环确保只要recordedBytes数组中有足够的数据可用,就总是写入2048个样本。 我们现在有能力记录和回放声音样本。应用所缺少的是在两种模式之间转换的方法。 应用有四种状态:stopped、recording、readyToPlay和playing。点击屏幕上的某个地方将使应用从一种状态转换到下一种状态。图8–3说明了这一过程。 图8–3。录音机应用的四种状态 应用在stopped状态下启动。当用户点击屏幕时,应用转换到recording状态,并开始录制他或她的声音。另一次点击停止记录并转换到readyToPlay状态。当用户准备好收听录音时,另一次点击在playing状态下开始回放。然后,用户可以第四次点击以停止播放并返回到stopped状态,准备再次录制。如果播放自行结束,应用也应自动转换到stopped状态。清单8–6显示了这个应用唯一的View的MXML。 清单8–6。【录音笔应用的首页】 现在桌子已经摆好了;定义了我们的用户界面和应用状态。下一步将是查看控制状态更改和UI组件的代码。清单8–7展示了控制从一个状态到下一个状态的转换的ActionScript代码。 清单8–7。控制录音机应用的状态转换顺序 `privatefunctiononTouchTap(event:TouchEvent):void{if(currentState=="playing"&&isDrag){return;} incrementProgramState();} privatefunctiononSoundComplete(event:Event):void{incrementProgramState();} privatefunctionincrementProgramState():void{switch(currentState){case"stopped":transitionToRecordingState();break;case"recording":transitionToReadyToPlayState();break;case"readyToPlay":transitionToPlayingState();break;case"playing":transitionToStoppedState();break;}}` 清单8–8。录音机应用的状态转换功能 `privatefunctiontransitionToRecordingState():void{recordedBytes.clear();microphone.addEventListener(SampleDataEvent.SAMPLE_DATA,onMicSample);currentState="recording";} privatefunctiontransitionToReadyToPlayState():void{microphone.removeEventListener(SampleDataEvent.SAMPLE_DATA,onMicSample);tapLabel.text="TaptoPlay";currentState="readyToPlay";}privatefunctiontransitionToPlayingState():void{freqMultiplier=1;recordedBytes.position=0; canvas.addEventListener(TouchEvent.TOUCH_BEGIN,onTouchBegin);canvas.addEventListener(TouchEvent.TOUCH_MOVE,onTouchMove); sound.addEventListener(SampleDataEvent.SAMPLE_DATA,onSoundSample);soundChannel=sound.play();soundChannel.addEventListener(Event.SOUND_COMPLETE,onSoundComplete); currentState="playing";} privatefunctiontransitionToStoppedState():void{canvas.removeEventListener(TouchEvent.TOUCH_BEGIN,onTouchBegin);canvas.removeEventListener(TouchEvent.TOUCH_MOVE,onTouchMove); soundChannel.stop()soundChannel.removeEventListener(Event.SOUND_COMPLETE,onSoundComplete);sound.removeEventListener(SampleDataEvent.SAMPLE_DATA,onSoundSample); tapLabel.text="TaptoRecord";currentState="stopped";}` transitionToRecordingState函数从recordedBytes数组中清除任何现有的数据,将sampleData监听器添加到麦克风,以便它开始发送数据样本,最后设置currentState变量来触发动画状态转换。类似地,当记录完成时,调用transitionToReadyToPlayState。它负责从麦克风上移除sampleData监听器,将UI中的Label更改为“点击播放”,并再次设置currentState变量来触发动画过渡。 当用户点击屏幕开始回放录制的样本时,会调用transitionToPlayingState功能。它首先将回放频率重置为1,并将recordedBytes数组的读取位置重置为数组的开头。接下来,它将触摸事件监听器添加到画布Group中,以便在回放期间监听控制倍频器的手势。它还为Sound的sampleData事件安装了一个处理程序,这样应用就可以在回放期间为Sound提供数据。然后调用play方法开始播放声音。一旦我们有了对控制回放的soundChannel的引用,我们就可以为soundComplete事件添加一个处理程序,这样我们就可以知道声音是否播放完毕,这样我们就可以自动转换回stopped状态。最后,改变View的currentState变量的值来触发动画状态转换。 最后一个转换是将应用带回到stopped状态。transitionToStoppedState函数负责停止播放(如果声音已经播放完毕,这没有任何作用),并删除所有由transitionToPlayingState函数添加的监听器。它最终重置Label的text属性,并更改currentState变量的值来触发状态转换动画。 剩下的功能是倍频器。清单8–9显示了处理控制这个变量的触摸事件的代码。 清单8–9。用触摸手势控制播放的频率 `privatefunctiononTouchBegin(event:TouchEvent):void{touchAnchor=event.localY;isDrag=false;} privatefunctiononTouchMove(event:TouchEvent):void{vardelta:Number=event.localY-touchAnchor;if(Math.abs(delta)>75){isDrag=true;touchAnchor=event.localY;freqMultiplier*=(delta>02:0.5);freqMultiplier=Math.min(2,Math.max(0.5,freqMultiplier));}}` 当用户第一次发起触摸事件时,调用onTouchBegin处理程序。代码记录下触摸点的初始y位置,并将isDrag标志重置为false。如果接收到触摸拖动事件,onTouchMove处理器检查移动是否大到足以触发拖动事件。如果是这样,isDrag标志被设置为true,因此应用的其余部分知道倍频器调整正在进行中。拖动的方向用于确定倍频器应该减半还是加倍。然后,该值被箝位在0.5和2.0之间。touchAnchor变量也被重置,以便在进一步移动的情况下可以再次运行计算。结果是,在回放期间,用户可以在屏幕上向上或向下拖动手指,以动态地改变回放的频率。 图8–4展示了运行在Android设备上的SoundRecorder示例应用。左边的图像显示了处于recording状态的应用,而右边的图像显示了从readyToPlay状态到playing状态的动画转换。 图8–4。运行在安卓设备上的录音笔应用 我们现在已经向您展示了如何播放和操作存储在ByteArray中的数据。应该注意的是,如果您需要操作存储在Sound对象而不是ByteArray中的数据,这种技术也是可行的。您可以使用Sound类的extract方法来访问原始声音数据,以某种方式操纵它,然后在它的sampleData处理程序中将它写回另一个Sound对象。 声音功能的另一个常见用途是播放音乐,无论是通过互联网还是以MP3文件的形式存储在设备上。如果您认为Flash平台非常适合这种类型的应用,那么您是对的!下一节将向您展示如何用Flash编写移动音乐播放器。 在设备上播放MP3文件的声音并不复杂。然而,音乐播放器不仅仅是播放声音。本节将首先向您展示如何使用Flash的声音API来播放MP3文件。一旦解决了这个问题,我们将看看你在创建移动应用时需要考虑的其他因素。 将MP3文件加载到Sound对象中就像使用以file协议开头的URL一样简单。清单8–10展示了这是如何实现的。 清单8–10。从文件系统加载并播放MP3文件 fx:Script privatefunctiononCreationComplete():void{**varpath:String="file:///absolute/path/to/the/file.mp3";****sound=newSound(newURLRequest(path));****sound.play();**}]]>` 粗体显示的三行是播放MP3文件所需的全部内容。注意file://后面的第三个正斜杠,它用来表示这是MP3文件的绝对路径。在实际应用中,你显然不希望使用这样的常量路径。在本章的后面,当我们讨论制作实际应用的注意事项时,我们将会看到以更优雅的方式处理文件系统路径的策略。 播放音乐文件是一个好的开始;毕竟这是音乐播放器的本质。所有音乐播放器做的另一件事是读取嵌入在文件的ID3tags中的元数据。1这些元数据包括艺术家和专辑的名字、录制年份,甚至歌曲的流派和曲目号。Sound类为读取这些标签提供了内置支持。清单8–11展示了如何将这一功能添加到我们刚刚起步的音乐播放器中。粗体行表示从清单8–10中新增的源代码。 清单8–11。从MP3文件中读取ID3元数据 fx:Script privatefunctiononCreationComplete():void{varpath:String="file:///absolute/path/to/the/file.mp3";sound=newSound(newURLRequest(path));**sound.addEventListener(Event.ID3,onID3);**sound.play()} **privatefunctiononID3(event:Event):void{****metaData.text="Artist:"+sound.id3.artist+"\n"+****"Year:"+sound.id3.year+"\n";****}** 添加了onID3处理程序作为Event.ID3事件的监听器。当从MP3文件中读取元数据并准备好使用时,调用此处理程序。在ID3Info类中有几个预定义的属性,对应于更常用的ID3标签。像专辑名、艺术家名、歌曲名、流派、年份和曲目号都有在类中定义的属性。此外,您还可以访问ID3规范2.3版定义的任何其他文本信息框架。[2例如,要访问包含出版商名称的TPUB帧,您可以使用sound.id3.TPUB。 不支持的一件事是从ID3标签读取图像,如专辑封面。在本章的后面,你将学习如何使用开源的ActionScript库来完成这个任务。 SoundChannel类不直接支持暂停声音数据的回放。然而,通过结合使用类的position属性和它的stop方法,很容易实现暂停特性。清单8–12展示了一种实现播放/暂停切换的可能技术。新添加的代码再次以粗体显示。 2 清单8–12。实现播放/暂停切换 ` fx:Script **[Bindable]privatevarisPlaying:Boolean=false;** privatefunctiononCreationComplete():void{varpath:String="file:///absolute/path/to/the/file.mp3";sound=newSound(newURLRequest(path));sound.addEventListener(Event.ID3,onID3);} privatefunctiononID3(event:Event):void{/*sameasbefore*/} **privatefunctiononClick():void{****if(isPlaying){****pausePosition=channel.position;****channel.stop();****channel.removeEventListener(Event.SOUND_COMPLETE,onSoundComplete);****isPlaying=false;****}else{****channel=sound.play(pausePosition);****channel.addEventListener(Event.SOUND_COMPLETE,onSoundComplete);****isPlaying=true;****}****}** **privatefunctiononSoundComplete(event:Event):void{****isPlaying=false;****pausePosition=0;****}**]]> 在onCreationComplete处理程序中不再调用Sound的play方法。取而代之的是,界面上增加了一个按钮,它的Label根据isPlaying标志的值是“播放”还是“暂停”。点击按钮触发对onClick处理器的调用。如果声音当前正在播放,通道的position保存在pausePosition实例变量中,声音停止,并且soundComplete事件监听器从通道中移除。下次播放声音时,将创建一个新的SoundChannel对象。因此,未能从旧的SoundChannel中移除我们的侦听器将导致内存泄漏。 如果声音当前没有播放,它是通过调用Sound的play方法启动的。将pausePosition作为参数传递给play方法,这样声音将从上次停止的位置开始播放。一个soundComplete事件的监听器被附加到由play方法返回的新的SoundChannel对象上。当声音播放完毕时,将调用此事件的处理程序。当这种情况发生时,处理程序会将isPlaying标志的值重置为false并将pausePosition重置为零。这样,下次点击播放按钮时,歌曲将从头开始播放。 清单8–13。实现音量和声相调整 ` privatefunctiononClick():void{if(isPlaying){/*Sameasbefore/}else{channel=sound.play(pausePosition);channel.addEventListener(Event.SOUND_COMPLETE,onSoundComplete);**onVolumeChange();***onPanChange();**isPlaying=true;}} **privatefunctiononVolumeChange():void{****if(channel){****varxform:SoundTransform=channel.soundTransform;****xform.volume=volume.value/100;****channel.soundTransform=xform;****}****}** **privatefunctiononPanChange():void{****if(channel){****varxform:SoundTransform=channel.soundTransform;****xform.pan=pan.value/100;****channel.soundTransform=xform;****}**]]> 我们添加了两个水平滑块,可以用来调整音量和声音播放时的平移。对于移动设备上的音乐播放器来说,担心声相可能不是一个很好的理由,但是为了完整起见,这里给出了一个例子。也许这个音乐播放器有一天会成长为一个迷你移动混音工作室。如果发生这种情况,您将在这个功能上有一个良好的开端! 当滑块移动时,调用change事件处理程序。注意调整SoundTransform设置所需的模式。您首先获得一个对现有转换的引用,以便从所有当前设置开始。然后更改您感兴趣的设置,并再次在通道上设置变换对象。设置soundTransform属性会触发频道更新其设置。这样,您可以将多个变换更改一起批处理,并且只需支付一次还原通道变换的成本。 SoundTransform的volume属性需要一个介于0.0(静音)和1.0(最大音量)之间的值。类似地,pan属性期望一个介于-1.0(左)和1.0(右)之间的值。change处理程序负责将滑块的值调整到合适的范围。最后要注意的是onVolumeChange和onPanChange在声音开始播放时也会被调用。同样,这是必要的,因为每次调用Sound的play方法都会创建一个新的通道。这个新的通道对象在调用onVolumeChange和onPanChange之前不会有新的设置。 这就结束了我们对基本音乐播放器功能的快速概述。如果这就是你需要知道的全部信息,就没有必要再往下读了,所以你可以直接跳到“播放视频”部分。然而,如果你有兴趣了解把这个简约的音乐播放器变成一个真正的Android应用的所有考虑因素,那么下一节就是为你准备的。 我们已经介绍了在Flash中播放音乐所需的基本技术,但是创建一个真正的音乐播放器应用还需要更多的努力。本节将讨论一些需要完成的事情,包括以下内容: 我们将从一种架构模式开始,这种模式可以帮助您将View的逻辑从它的表示中分离出来,从而创建更具可重用性和可测试性的代码。您可以通过参考在本书源代码的examples/chapter-08目录中找到的MusicPlayer示例应用来跟踪这个讨论。 当我们以前想要将View的逻辑从它的表示中分离出来时,我们依赖于简单地将ActionScript代码移动到一个单独的文件中。然后使用 图8–5。演示模型模式的一种常见实现 总之,使用表示模型模式有两个主要好处: 既然已经了解了应用设计的基本构建模块,那么是时候创建一个新的Flex移动项目了。这个应用将是一个ViewNavigatorApplication,因为我们需要在两个不同的View之间导航:一个View包含歌曲、艺术家或专辑的列表,一个View包含播放歌曲的控件。一旦创建了项目,我们就可以设置应用的包结构。assets、views、viewmodels、models和services各有一个包。这使得按职责组织应用中的各种类变得很容易。这个assets包是应用的所有图形素材,比如图标和闪屏,将被放置在其中。 ViewNavigatorApplication的主要工作是创建和显示第一个View。这通常通过设置 清单8–14。【MXML】主ViewNavigatorApplication fx:Script **privatefunctiononInitialize():void{****varservice:MusicService=newLocalMusicService();****navigator.pushView(SongListView,newSongListViewModel(service));****}**]]>` 这个应用中使用的MusicService接口的具体实现是一个名为LocalMusicService的类,它从设备的本地文件系统中读取文件。这个服务实例然后被用来构建表示模型,在这个例子中是SongListViewModel的一个实例。像这样将服务传递给表示模型比让表示模型在内部构造服务更可取。这使得在测试期间,或者如果程序的功能集被扩展到包括其他类型的音乐服务时,很容易向展示模型提供不同版本的服务。但是我们太超前了。我们将在下一节更详细地讨论这些类。 注意:有些人更喜欢让View类创建自己的表示模型,而不是像我们在这里使用data属性传递它。我们更喜欢将表示模型传递给View,因为在其他条件相同的情况下,您应该总是喜欢类之间的耦合更少。然而,这两种方式在实践中都很有效。 将您的服务类定义为一个interface是一个好主意。那么您的表示模型只依赖于interface类,而不依赖于任何一个具体的服务实现。这使得在您的表示模型中使用不同的服务实现成为可能。例如,您可以创建音乐服务的一个实现,从设备的本地存储中读取音乐文件,而另一个实现可以用于通过互联网传输音乐。 然而,使用服务接口还有一个更好的理由;这使得对你的表示模型进行单元测试变得很容易。假设您通常使用从互联网web服务读取音乐文件的MusicService实现来运行您的应用。如果您的表示模型硬连线使用这个版本,那么您不能孤立地测试表示模型。您需要确保您有一个活动的互联网连接,并且web服务已经启动并且正在运行,否则您的测试将会失败。使表示模型仅依赖于接口使得交换一个模拟服务变得很简单,该模拟服务向表示模型返回一个预定义的MusicEntry对象列表。这使得你的单元测试可靠且可重复。这也使它们运行得更快,因为您不必在每次测试中都从web服务下载数据! 给定一个URL路径,MusicService的工作只是提供一个MusicEntry对象的列表。因此,interface类将包含一个方法,如清单8–15所示。 清单8–15。MusicService界面 `packageservices{importmx.collections.ArrayCollection; publicinterfaceMusicService{/***AMusicServiceimplementationknowshowtousetherootPathtofind*thelistofMusicEntryobjectsthatresideatthatpath.**@returnAnArrayCollectionofMusicEntryobjects.*@seemodels.MusicEntry*/functiongetMusicEntries(rootPath:String=null):ArrayCollection;}}` 一个MusicEntry对象可以代表一首歌曲,也可以代表一个保存一首或多首其他歌曲的容器。这样,我们可以使用多个MusicEntry对象列表来浏览艺术家、专辑和歌曲的分层列表。与大多数数据模型一样,这个类是一个属性集合,几乎没有逻辑。MusicEntry对象如清单8–16所示。 清单8–16。MusicEntry数据模型 packagemodels{importflash.utils.IDataInput;`/***Thisclassrepresentsanobjectthatcanbeeitherasongoracontainer*ofothersongs.*/publicclassMusicEntry{privatevar_name:String;privatevar_url:String;privatevar_streamFunc:Function; publicfunctionMusicEntry(name:String,url:String,streamFunc:Function){_name=name;_url=url;_streamFunc=streamFunc;} publicfunctiongetname():String{return_name;} publicfunctiongeturl():String{return_url;} /***@returnAstreamobjectifthisisavalidsong.Nullotherwise.*/publicfunctiongetstream():IDataInput{return_streamFunc==nullnull:_streamFunc();} publicfunctiongetisSong():Boolean{return_streamFunc!=null;}}}` MusicEntry包含条目name的属性,一个url标识条目的位置,一个stream可用于读取条目(如果是一首歌),一个isSong属性可用于区分代表一首歌的条目和代表一个歌曲容器的条目。由于我们事先不知道阅读歌曲需要什么样的流,所以我们依赖ActionScript的函数式编程功能。这允许一个MusicEntry对象的创建者将一个函数对象传递给该类的构造器,当被调用时,该构造器负责创建适当类型的流。 这个应用将从设备的本地存储中播放音乐文件,所以我们的服务将提供从设备的文件系统中读取的MusicEntry对象。清单8–17展示了LocalMusicService的实现。 清单8–17。从本地文件系统中读取歌曲的MusicService的实现 packageservices{importflash.filesystem.File;importflash.filesystem.FileMode;importflash.filesystem.FileStream;`importflash.utils.IDataInput;importmx.collections.ArrayCollection;importmodels.MusicEntry; publicclassLocalMusicServiceimplementsMusicService{privatestaticconstDEFAULT_DIR:File=File.userDirectory.resolvePath("Music"); /***Findsallofthefilesinthedirectoryindicatedbythepathvariable*andaddsthemtothecollectioniftheyareadirectoryoranMP3file.**@returnAcollectionofMusicEntryobjects.*/publicfunctiongetMusicEntries(rootPath:String=null):ArrayCollection{varrootDir:File=rootPathnewFile(rootPath):DEFAULT_DIR;varsongList:ArrayCollection=newArrayCollection(); if(rootDir.isDirectory){vardirListing:Array=rootDir.getDirectoryListing(); for(vari:int=0;i if(!shouldBeListed(file))continue; songList.addItem(createMusicEntryForFile(file));}} returnsongList;} /***@returnTheappropriatetypeofMusicEntryforthegivenfile.*/privatefunctioncreateMusicEntryForFile(file:File):MusicEntry{varname:String=stripFileExtension(file.name);varurl:String="file://"+file.nativePath;varstream:Function=null; if(!file.isDirectory){stream=function():IDataInput{varstream:FileStream=newFileStream();stream.openAsync(file,FileMode.READ);returnstream;}} returnnewMusicEntry(name,url,stream);} //Otherutilityfunctionsremovedforbrevity…}}` 毫不奇怪,这种类型的服务严重依赖于flash.filesystem包中的类。当使用文件系统路径时,您应该总是尝试使用在File类中定义的路径属性。DEFAULT_DIR常量使用File.userDirectory作为其默认路径的基础,在Android上它指向/mnt/sdcard目录。因此,该服务将默认在/mnt/sdcard/Music目录中查找其文件。这是Android设备上音乐文件的一个相当标准的位置。 注意:File.userDirectory、File.desktopDirectory、File.documentsDirectory都指向安卓设备上的/mnt/sdcard。File.applicationStorageDirectory指向一个特定于您的应用的“本地存储”目录。File.applicationDirectory空。 LocalMusicPlayer中的getMusicEntries实现将提供的rootPath字符串转换为File,或者如果没有提供rootPath则使用默认目录,然后继续遍历位于该路径的文件。它为任何一个目录(其他歌曲的容器)或MP3文件(一首歌曲)的File创建一个MusicEntry对象。如果File是一首歌而不是一个目录,那么createMusicEntryForFile函数创建一个函数闭包,当被调用时,打开一个异步FileStream进行读取。然后,这个函数闭包被传递给播放歌曲时要使用的MusicEntry对象的构造函数。您可能还记得清单8–16中,这个闭包对象的值——不管它是否为空——被用来确定对象所代表的MusicEntry的类型。 清单8–14显示应用创建的第一个View是SongListView。应用的onInitialize处理程序实例化适当类型的MusicService,并使用它为View构建SongListViewModel。然后将SongListViewModel作为navigator.pushView函数的第二个参数传递给View。这将在View的data属性中放置一个对模型实例的引用。 SongListViewModel的工作非常简单。它使用给定的MusicService来检索SongListView要显示的MusicEntry对象列表。清单8–18显示了这个表示模型的源代码。 清单8–18。的演示模式为SongListView `packageviewmodels{importmodels.MusicEntry;importmx.collections.ArrayCollection;importservices.LocalMusicService;importservices.MusicService; [Bindable]publicclassSongListViewModel{privatevar_entries:ArrayCollection=newArrayCollection();privatevar_musicEntry:MusicEntry;privatevar_musicService:MusicService; publicfunctionSongListViewModel(service:MusicService=null,entry:MusicEntry=null){_musicEntry=entry;_musicService=service; if(_musicService){varurl:String=_musicEntry_musicEntry.url:null;entries=_musicService.getMusicEntries(url);}} publicfunctiongetentries():ArrayCollection{return_entries;} publicfunctionsetentries(value:ArrayCollection):void{_entries=value;} publicfunctioncloneModelForEntry(entry:MusicEntry):SongListViewModel{returnnewSongListViewModel(_musicService,entry);} publicfunctioncreateSongViewModel(selectedIndex:int):SongViewModel{returnnewSongViewModel(entries,selectedIndex);}}}` 该类用Bindable进行了注释,因此entries属性可以绑定到View类中的UI组件。 构造函数将存储对传入的MusicService和MusicEntry实例的引用。如果服务引用不为空,则从MusicService中检索条目集合。如果服务为空,那么entries集合将保持为空。 该类中还有两个额外的公共函数。cloneModelForEntry函数将通过传递给它的MusicService引用来创建一个新的SongListViewModel。createSongViewModel将使用这个模型的entries集合和所选条目的索引为SongView创建一个新的表示模型。这是这些函数的逻辑位置,因为这个表示模型引用了创建新表示模型所需的数据。因此,一个表示模型创建另一个表示模型是很常见的。 考虑到这一点,是时候看看View如何使用它的表示模型了。SongListView的源代码如清单8–19所示。 清单8–19。SongListView fx:Script [Bindable]privatevarmodel:SongListViewModel; privatefunctiononInitialize():void{model=dataasSongListViewModel;} privatefunctiononChange(event:IndexChangeEvent):void{varlist:List=List(event.target);varselObj:MusicEntry=list.selectedItemasMusicEntry; if(selObj.isSong){varindex:int=list.selectedIndex;navigator.pushView(SongView,model.createSongViewModel(index));}else{navigator.pushView(SongListView,model.cloneModelForEntry(selObj));}}]]> onInitialize处理程序从data属性初始化View的模型引用。然后model被用来访问作为List的dataProvider的entries。它也用于List的onChange处理程序中。如果选择的MusicEntry是一首歌曲,则用model创建一首新的SongViewModel,用navigator.pushView功能显示一首SongView。否则,创建一个新的SongListViewModel并使用选择的MusicEntry作为新的MusicEntry对象集合的路径显示一个新的。 chevron160.png文件是基本大小,而chevron240.png大50%,chevron320.png大两倍。人字形位图的最佳尺寸将根据运行程序的设备的屏幕属性来选择。图8–6显示了在中低dpi设备上运行的SongListView。请注意,人字形没有因缩放而产生的像素化伪像,如果我们在两个屏幕上使用相同的位图,就会出现这种情况。 图8–6。SongListView运行在不同dpi分类的设备上 这就把我们带到了应用的真正核心:让用户播放音乐的视图!我们希望这个界面具有与大多数其他音乐播放器相同的功能。我们将显示歌名和专辑封面。它应该有控件,允许用户跳到下一首或上一首歌曲,播放和暂停当前歌曲,调整当前歌曲的位置以及音量和平移(只是为了好玩)。产生的界面如图8–7所示。 图8–7。SongView界面运行在两种不同的dpi设置下 从Figure8–7可以看出,这个界面比列表视图稍微复杂一点。它甚至包括一个自定义控件,不仅可以作为播放/暂停按钮,还可以作为当前歌曲播放位置的进度指示器。此外,你可以通过在按钮上来回滑动手指来控制歌曲的位置。编写这个自定义控件只是本节将要讨论的主题之一。 清单8–20。【美国】和SongViewMXML文件的剧本章节 fx:Script [Bindable]privatevarmodel:SongViewModel; privatefunctiononInitialize():void{model=dataasSongViewModel;model.addEventListener(SongViewModel.SONG_ENDED,onSongEnded);} privatefunctiononViewDeactivate():void{model.removeEventListener(SongViewModel.SONG_ENDED,onSongEnded);if(model.isPlaying)model.onPlayPause();} privatefunctiononSongEnded(event:Event):void{progressButton.stop();}]]>` 我们现在将一次一个片段地检查这个View的UI组件。 前面的代码片段创建了作为接口其余部分的容器的Group。再一次,它的width和height被设置为总是充满屏幕。Group在风景模式下使用一个HorizontalLayout,在肖像模式下使用一个VerticalLayout。状态语法确保在设备重定向时使用正确的布局。图8–8显示了横向放置的设备上的SongView界面。 图8–8。横向音乐播放器界面 下一段代码中的Group是专辑封面图像的容器。Group的大小根据方向动态调整,但是宽度和高度总是保持相等——它总是形成一个正方形。 ` 在专辑封面之后,我们到达包含这个View控件的VGroup。这个VGroup实际上是由三个独立的HGroup集装箱组成的。第一个包含上一首歌按钮、自定义的ProgressButton控件和下一首歌按钮。下一个HGroup容器包含水平音量滑块,以及它的FXG图标,以指示滑块两侧的低音量和高音量。最后的HGroup包含水平平移滑块,以及显示左右方向的Label。注意,模型的volume、pan和percentComplete属性通过双向绑定被绑定到接口组件。这意味着绑定的任何一端都可以设置属性的值,而另一端将被更新。 ` 清单8–21。SongViewModel级的宣言 `packageviewmodels{//importstatements… [Event(name="songEnded",type="flash.events.Event")] [Bindable]publicclassSongViewModelextendsEventDispatcher{publicstaticconstSONG_ENDED:String="songEnded"; publicvaralbumCover:BitmapData;publicvaralbumTitle:String="";publicvarsongTitle:String="";publicvarartistName:String="";publicvarisPlaying:Boolean=false; privatevartimer:Timer; publicfunctionSongViewModel(songList:ArrayCollection,index:Number){this.songList=songList;this.currentIndex=index; timer=newTimer(500,0);timer.addEventListener(TimerEvent.TIMER,onTimer); loadCurrentSong();}}}` 该类扩展了EventDispatcher以便它可以在歌曲结束时通知任何可能正在收听的View。当这种情况发生时,模型会调度SONG_ENDED事件。这个模型还用Bindable进行了注释,以确保View可以轻松绑定到属性,如albumCover位图、albumTitle、songTitle、artistName和isPlaying标志。构造函数获取一个集合MusicEntries和该集合中应该播放的歌曲的索引。这些参数被保存到实例变量中以供以后参考,因为当用户想要跳到集合中的上一首或下一首歌曲时会用到它们。构造函数还初始化一个每500毫秒计时一次的计时器。这个定时器读取歌曲的当前位置,并更新类的percentComplete变量。最后,构造函数加载当前歌曲。接下来的两节介绍了关于处理percentComplete更新和loadCurrentSong方法的更多细节。 清单8–22。在展示模型中处理双向绑定 `privatevar_volume:Number=0.5;privatevar_pan:Number=0.0;privatevar_percentComplete:int=0; publicfunctiongetvolume():Number{return_volume;}publicfunctionsetvolume(val:Number):void{_volume=val;updateChannelVolume();} publicfunctiongetpan():Number{return_pan;}publicfunctionsetpan(val:Number):void{_pan=val;updateChannelPan();} publicfunctiongetpercentComplete():int{return_percentComplete;} /***Settingthisvaluecausesthesong'splaypositiontobeupdated.*/publicfunctionsetpercentComplete(value:int):void{_percentComplete=clipToPercentageBounds(value)updateSongPosition();} /***Clipsthevaluetoensureitremainsbetween0and100inclusive.*/privatefunctionclipToPercentageBounds(value:int):int{returnMath.max(0,Math.min(100,value));} /***SetthepositionofthesongbasedonthepercentCompletevalue.*/privatefunctionupdateSongPosition():void{varnewPos:Number=_percentComplete/100.0*song.length;if(isPlaying){pauseSong()playSong(newPos);}else{pausePosition=newPos;}}` 这涵盖了对来自类外的percentComplete更新的处理,但是来自类内的更新呢?回想一下,有一个定时器每半秒钟读取一次歌曲的位置,然后更新percentComplete的值。在这种情况下,我们仍然需要通知绑定的另一方,percentComplete的值已经更改,但是我们不能使用set方法来这样做,因为我们不想每隔半秒钟就停止并重新启动歌曲。我们需要一个替代的更新路径,如清单8–23所示。 清单8–23。在定时器滴答期间更新percentComplete `/**Updatethesong'spercentCompletevalueoneachtimertick.*/privatefunctiononTimer(event:TimerEvent):void{varoldValue:int=_percentComplete; varpercent:Number=channel.position/song.length*100;updatePercentComplete(Math.round(percent));} /***Updatesthevalueof_percentCompletewithoutaffectingtheplayback*ofthecurrentsong(i.e.updateSongPositionisNOTcalled).This*functionwilldispatchapropertychangeeventtoinformanyclients*thatareboundtothepercentCompletepropertyoftheupdate.*/privatefunctionupdatePercentComplete(value:int):void{varoldValue:int=_percentComplete;_percentComplete=clipToPercentageBounds(value); varpce:Event=PropertyChangeEvent.createUpdateEvent(this,"percentComplete",oldValue,_percentComplete);dispatchEvent(pce);}` 这里给出的解决方案是直接更新_percentComplete的值,然后手动调度PropertyChangeEvent通知绑定的另一方值已经改变。 如果能在MP3文件的元数据中嵌入专辑封面的图像,那就太好了。然而,Flash的ID3Info类不支持从声音文件中读取图像元数据。幸运的是,这些年来,围绕Flex和Flash平台已经形成了一个充满活力的开发社区。这个社区已经产生了许多第三方库,帮助填补平台中缺失的功能。一个这样的库是开放源码的Metaphilelibrary。5这个小而强大的ActionScript库提供了从许多流行的文件格式中读取元数据(包括图像)的能力。 使用这个库非常简单,只需从项目网站下载最新的代码,将其编译成一个.swc文件,然后将该文件放在项目的libs目录中。该库提供了一个可以用来读取MP3元数据条目的ID3Reader类,如清单8–24所示。当Sound类使用当前歌曲的MusicEntry实例提供的URL时,Metaphile的ID3Reader类被设置为读取其元数据。当元数据被解析后,会通知一个onMetaData事件处理程序。该类的autoLimit属性设置为-1,因此可以解析的元数据的大小没有限制,并且autoClose属性设置为true,以确保一旦ID3Reader读取完元数据,输入流将被关闭。最后一步是调用ID3Reader的read函数,将通过访问MusicEntry的stream属性创建的输入流作为参数传入。 清单8–24。加载MP3文件并读取其元数据 `/***LoadsthesongdatafortheentryinthesongListindicatedby*thevalueofcurrentSongIndex.*/privatefunctionloadCurrentSong():void{try{varsongFile:MusicEntry=songList[currentIndex]; song=newSound(newURLRequest(songFile.url)); varid3Reader:ID3Reader=newID3Reader();id3Reader.onMetaData=onMetaData;id3Reader.autoLimit=-1;id3Reader.autoClose=true; id3Reader.read(songFile.stream);}catch(err:Error){trace("Errorwhilereadingsongormetadata:"+err.message);}} /***Calledwhenthesong'smetadatahasbeenloadedbytheMetaphile*library.*/privatefunctiononMetaData(metaData:IMetaData):void{varsongFile:MusicEntry=songList[currentIndex];varid3:ID3Data=ID3Data(metaData); artistName=id3.performerid3.performer.text:"Unknown";albumTitle=id3.albumTitleid3.albumTitle.text:"Unknown";songTitle=id3.songTitleid3.songTitle.text:songFile.name; if(id3.image){varloader:Loader=newLoader();loader.contentLoaderInfo.addEventListener(Event.COMPLETE,onLoadComplete)loader.loadBytes(id3.image);}else{albumCover=null;}} /***Calledwhenthealbumimageisfinishedloadingfromthemetadata.*/privatefunctiononLoadComplete(e:Event):void{albumCover=Bitmap(e.target.content).bitmapData}` 向onMetaData处理程序传递一个符合中期库IMetaData接口的参数。由于这个处理程序被附加到一个ID3Reader对象,我们知道将传入的metaData对象强制转换为一个ID3Data对象的实例是安全的。这样做可以让我们轻松访问ID3Data类的属性,比如performer、albumTitle和songTitle。如果在ID3Data类的image属性中存在图像数据,则创建一个新的flash.display.Loader实例,将字节加载到DisplayObject中。当加载图像字节时,onLoadComplete处理程序使用存储在Loader的内容属性中的DisplayObject来初始化albumCoverBitmapData对象。由于View被绑定到了albumCover属性,所以一旦相册封面图像被更新,它就会显示出来。 创建自定义移动组件与在Flex4中创建任何其他自定义Spark组件非常相似。你创建了一个扩展了SkinnableComponent的component类和一个Skin。只要你的图形不是太复杂,你可以使用普通的MXMLSkin。如果您遇到性能问题,您可能需要用ActionScript编写您的Skin。参见第十一章了解有关移动应用性能调整的更多信息。 我们将编写的定制组件是ProgressButton。为了节省用户界面的空间,我们希望将播放/暂停按钮的功能与指示歌曲当前播放位置的进度监视器的功能结合起来。如果需要的话,控制器还将允许用户调整回放位置。因此,如果用户点击控件,我们将把它视为按钮的切换。如果用户触摸控件,然后水平拖动,将被视为位置调整。 因此,该控件将有两个图形元素:一个指示播放/暂停功能状态的图标和一个显示歌曲播放位置的进度条。图8–9显示了各种状态下的控制。 图8–9。自定义ProgressButton控制 当创建自定义Spark控件时,您可以将Skin视为您的View并将SkinnableComponent视为您的模型。清单8–25显示了ProgressButton类,它扩展了SkinnableComponent,因此充当控件的模型。 清单8–25。ProgressButton的申报组成部分 `packageviews{//importsremoved… [SkinState("pause")]publicclassProgressButtonextendsSkinnableComponent{[SkinPart(required="true")]publicvarplayIcon:DisplayObject; [SkinPart(required="true")]publicvarpauseIcon:DisplayObject; [SkinPart(required="true")]publicvarbackground:Group; [Bindable]publicvarpercentComplete:Number=0; privatevarmouseDownTime:Number;privatevarisMouseDown:Boolean; publicfunctionProgressButton(){//Makesurethemousedoesn'tinteractwithanyoftheskinpartsmouseChildren=false; addEventListener(MouseEvent.MOUSE_DOWN,onMouseDown);addEventListener(MouseEvent.MOUSE_MOVE,onMouseMove);addEventListener(MouseEvent.MOUSE_UP,onMouseUp);addEventListener(MouseEvent.CLICK,onMouseClick);} overrideprotectedfunctiongetCurrentSkinState():String{if(isPlaying()){return"play";}else{return"pause";}} overrideprotectedfunctionpartAdded(partName:String,instance:Object):void{super.partAdded(partName,instance); if(instance==pauseIcon){pauseIcon.visible=false;}} overrideprotectedfunctionpartRemoved(partName:String,instance:Object):void{super.partRemoved(partName,instance);} //ConsultListing8–26fortherestofthisclass}}` 大多数组件需要实现三种方法来确保自定义控件的正确行为:getCurrentSkinState、partAdded和partRemoved。当Skin需要更新显示时,它调用getCurrentSkinState函数。ProgressButton组件覆盖这个函数,根据isPlaying标志的当前值返回状态名。当添加和移除Skin部件时,partAdded和partRemoved功能使组件有机会执行初始化和清理任务。在这种情况下,这两个函数都确保在超类中调用它们对应的函数,并且为ProgressButton所做的惟一特殊化是确保pauseIcon在被添加时是不可见的。 清单8–26显示了ProgressButton类中定义的其余函数。它显示了构成该类的公共接口、鼠标事件处理程序和私有实用函数的其他函数。例如,SongView在被通知当前歌曲已经播放完毕时,调用stop函数。 清单8–26。ProgressButton组件类的剩余功能 `/***Ifin"play"state,stopstheprogressandchangesthecontrol's*statefrom"play"to"pause".*/publicfunctionstop():void{if(isPlaying()){togglePlayPause();}} /***@returnTrueifthecontrolisin"play"state.*/publicfunctionisPlaying():Boolean{returnpauseIcon&&pauseIcon.visible;} privatefunctiononMouseDown(event:MouseEvent):void{mouseDownTime=getTimer();isMouseDown=true;} privatefunctiononMouseMove(event:MouseEvent):void{if(isMouseDown&&getTimer()-mouseDownTime>250){percentComplete=event.localX/width*100;}} privatefunctiononMouseUp(event:MouseEvent):void{isMouseDown=false;} privatefunctiononMouseClick(event:MouseEvent):void{if(getTimer()-mouseDownTime<250){togglePlayPause();}else{event.stopImmediatePropagation();}} privatefunctiontogglePlayPause():void{if(playIcon.visible){playIcon.visible=false;pauseIcon.visible=true;}else{playIcon.visible=true;pauseIcon.visible=false;}}` 清单8–27。ProgressButtonSkin宣言 fx:Metadata[HostComponent("views.ProgressButton")] ` 这就结束了我们对在Flash中播放声音和创建一个音乐播放器的研究,通过探索在编写真正的Android应用时必须处理的问题,这个音乐播放器在某种程度上超越了一个简单的示例应用。在本章的其余部分,我们将探索视频回放,这一功能使Flash成为一个家喻户晓的词。 视频编码一半是科学,一半是黑色艺术。有一些很好的资源可以探索这个主题的所有精彩细节。因此,我们将只总结一些最近推荐的最佳实践,同时建议您查看本页脚注中引用的资源,以深入了解该主题。当您为移动设备编码视频时,要记住的主要事情是,您正在处理更有限的硬件,并且您将不得不应对3G、4G和Wi-Fi网络之间波动的带宽。 Adobe建议在对新视频进行编码时,最好使用最大帧速率为24fps(每秒帧数)的H.264格式,并使用44.1kHzAAC编码的立体声音频。如果您必须使用On2VP6格式,那么同样的建议也适用于帧速率和音频采样,只适用于MP3格式而不是AAC格式的音频。如果您正在使用H.264进行编码,并且希望在最大数量的设备上保持良好的性能,那么您应该坚持使用基线配置文件。如果源素材的帧速率高于24,您可能要考虑将其减半,直到低于该目标值。例如,如果您的素材是30fps,那么您将通过以15fps编码它来获得最佳结果,因为编码器不必内插任何视频数据。 表8–2显示了从Adobe最近的出版物以及AdobeMax和360|Flex的会议中收集的编码建议。所有这些数字都假定在基线配置文件中使用H.264编码。请记住,这些只是建议,它们会随着更快的硬件的出现而快速变化,可能不适用于您的特定情况。此外,这些建议针对尽可能多的设备。如果您的应用专门针对运行最新版本Android的高端设备,那么这些数字对于您的需求来说可能有点过于保守。 您还可以在应用中采取几个步骤来确保获得最佳性能。您应该避免使用变换:旋转、透视投影和颜色变换。避免阴影、滤镜效果和像素弯曲效果。您应该尽可能避免透明度和视频对象与其他图形的混合。 我们要看的第一个选项是Flex4中引入的SparkVideoPlayer组件。该组件构建在开源媒体框架(OSMF)之上,这是一个旨在处理全功能视频播放器所需的所有“幕后”任务的库。这个想法是,你写一个很酷的视频播放器GUI,连接到OSMF提供的功能,你就可以开始了。我们将在本章后面更深入地研究OSMF。 因此,SparkVideoPlayer是一个预打包的视频播放器UI,建立在预打包的OSMF库之上。这是最方便的(也是最懒惰的),因为你只需要几行代码就可以给你的应用添加视频播放功能。清单8–28展示了如何在ViewMXML文件中实例化一个VideoPlayer。 清单8–28。在手机应用中使用SparkVideoPlayer privatefunctiononViewDeactivate():void{player.stop();}]]> 如果你使用FlashBuilder或者查阅关于VideoPlayer类的文档,你可能会看到一个不祥的警告,关于VideoPlayer没有“为移动优化”,但是在这种情况下,他们真正的意思是“警告:还没有定义移动皮肤!”你可以直接使用VideoPlayer,但是当你在中等或高dpi的设备上运行你的应用时,视频控件将会非常小(是的,这是一个技术术语),很难使用。解决方案是像我们在这个例子中所做的那样,创建自己的MobileVideoPlayerSkin。 在这种情况下,我们刚刚使用FlashBuilder在原来的VideoPlayerSkin的基础上创建了一个新的Skin,然后对它进行了一点修改。我们去掉了阴影,稍微缩放了控件,并调整了间距。修改后的Skin可以在本书源代码的examples/chapter-08目录下的VideoPlayers示例项目中找到。结果可以在图8–10中看到,我们正在播放视频剪辑中著名的老黄牛:大巴克兔子。这些图片来自NexusS,其中的控件现在已经足够大,可以使用了。 图8–10。NexusS在常规(上图)和全屏(下图)模式下运行的火花VideoPlayer 这只是当前VideoPlayerSkin的一个快速修改,但是当然,由于Flex4中引入的Spark组件的皮肤架构,你可以随心所欲地使用你的新手机Skin。请记住您在移动环境中将面临的一些性能限制。 拥有一个方便的预打包解决方案,比如VideoPlayer是很好的,但是有时候你真的需要一些定制的东西。或者,也许你不想要像OSMF那样“一切都包括在内”的图书馆带来的所有包袱。这就是NetConnection、NetStream和Video类出现的原因。这些类允许你构建一个轻量级的或者全功能的完全定制的视频播放器。 清单8–29。MXML文件为NetStreamVideoView MXML宣言中提到了各种ActionScript函数作为View的initialize和viewDeactivate事件以及Button的click事件的事件处理程序。ActionScript代码已被移到一个单独的文件中,并包含了一个 清单8–30。View事件处理程序为NetStreamVideoView privatevarvideo:Video;privatevarns:NetStream;privatevarisPlaying:Boolean;privatevartimer:Timer;privatevarduration:String=""; privatefunctiononInitialize():void{video=newVideo();videoContainer.addChild(video); varnc:NetConnection=newNetConnection();nc.connect(null); ns=newNetStream(nc);ns.addEventListener(NetStatusEvent.NET_STATUS,onNetStatus);ns.client={onMetaData:onMetaData,onCuePoint:onCuePoint,onPlayStatus:onPlayStatus}; ns.play(SOURCE);video.attachNetStream(ns); timer=newTimer(1000);timer.addEventListener(TimerEvent.TIMER,onTimer);timer.start();} privatefunctiononViewDeactivate():void{if(ns){ns.close();}}` onInitialize处理程序负责所有的设置代码。Video显示对象被创建并添加到它的UIComponent容器中。接下来,创建一个NetConnection,用一个null值调用它的connect方法。这告诉NetConnection它将播放来自本地文件系统或web服务器的MP3或视频文件。如果不同的参数被传递给它的connect方法,那么NetConnection也可以用于FlashRemoting或者连接到Flash媒体服务器。 清单8–31。NetStream事件处理者 `privatefunctiononMetaData(item:Object):void{video.width=item.width;video.height=item.height; video.x=(width-video.width)/2;video.y=(height-video.height)/2; if(item.duration)duration=formatSeconds(item.duration);} privatefunctiononCuePoint(item:Object):void{//Itemhasfourproperties:name,time,parameters,typelog("cuepoint"+item.name+"reached");} privatefunctiononPlayStatus(item:Object):void{if(item.code=="NetStream.Play.Complete"){timer.stop();updateTimeDisplay(duration);}} privatefunctiononNetStatus(event:NetStatusEvent):void{varmsg:String=""; if(event.info.code)msg+=event.info.code; if(event.info.level)msg+=",level:"+event.info.level; log(msg);} privatefunctionlog(msg:String,showUser:Boolean=true):void{trace(msg);if(showUser)logger.text+=msg+"\n";}` 清单8–32。播放,暂停,NetStream读取属性 `privatefunctiononPlayPause():void{if(playBtn.selected){ns.resume();timer.start();}else{ns.pause();timer.stop();}} privatefunctiononTimer(event:TimerEvent):void{updateTimeDisplay(formatSeconds(ns.time));} privatefunctionupdateTimeDisplay(time:String):void{if(duration)time+="/"+duration; timeDisplay.text=time;} privatefunctionformatSeconds(time:Number):String{varminutes:int=time/60;varseconds:int=int(time)%60; returnString(minutes+":"+(seconds<10"0":"")+seconds);}` Figure8–11显示了所有这些代码在低dpiAndroid设备上运行的结果。像这样的小型播放器更适合这种类型的屏幕。 图8-11。运行在低dpi设备上的基于NetStream的最小视频播放器 正如你所看到的,在创建我们基于极简NetStream的视频播放器的过程中,涉及了更多的代码。但是,如果您需要轻量级视频播放器实现的终极灵活性,NetStream和Video类的组合将提供您需要的所有功能。 在播放视频这一节的开始,我们简单地提到了StageVideo。一旦在Android上得到支持,它将允许您基于NetStream的视频播放器利用H.264视频的硬件加速解码和渲染。Adobe提供了一个非常有用的“入门”指南来帮助你转换你的NetStream代码以使用StageVideo而不是Video显示对象。如果你喜欢不费吹灰之力就让自己适应未来,你可以利用第三个选项在Android上编写视频播放器:OSMF库。这是我们下一节的主题,当它在Android上可用时,它将自动利用StageVideo。 开源媒体框架是Adobe发起的一个项目,旨在创建一个库,收集编写基于Flash的媒体播放器的最佳实践。它是一个全功能的媒体播放器,被抽象成一些易于使用的类。该库允许您快速创建用于Flex和Flash应用的高质量视频播放器。OSMF包含在Flex4SDK中,但是您也可以从项目网站下载最新版本。10清单8–33显示了OSMFVideoView的MXML代码。这里显示的用户界面代码与NetStreamVideoView的清单8–29中的代码几乎完全相同。本质上,我们只是用基于OSMF的MediaPlayer实现替换了基于NetStream的后端。 清单8–33。《MXML宣言》为OSMFVideoView skinClass="spark.skins.spark.mediaClasses.normal.PlayPauseButtonSkin"/> 清单8–34显示了将用于实现视频播放器的OSMF类的初始化代码。我们将包含电影URL的实例URLResource传递给LightweightVideoElement构造函数。OSMFMediaElement是正在播放的媒体类型的接口。LightweightVideoElement是一个代表视频的专门化,支持渐进式下载和简单的RTMP流。还有一个名为VideoElement的类支持更多的流协议,但是对于我们的目的来说,LightweightVideoElement拥有所有需要的功能。 一旦LightweightVideoElement被创建,它就被传递给OSMFMediaPlayer类的构造函数。MediaPlayer是一个类,通过它你可以控制视频的播放。它能够调度许多不同的事件,这些事件可以用来获取关于MediaPlayer的状态和状况的信息。在接下来显示的示例代码中,我们处理了mediaSizeChange事件以使视频显示在View上居中,处理了timeChange和durationChange事件以更新timeDisplayLabel,处理了complete事件以通知我们视频何时结束播放。 MediaPlayer本身不是显示对象。相反,它提供了一个可以添加到显示列表中的displayObject属性。在本例中,它被添加为videoContainerUIComponent的子节点。我们做的最后一点初始化工作是使用currentTimeUpdateInterval属性请求我们每秒只更新一次视频播放器的currentTime,而不是默认值的每250毫秒。视频将自动开始播放,因为MediaPlayer的autoPlay属性的默认值是true。 清单8–34。初始化代码为MediaPlayer `importorg.osmf.elements.VideoElement;importorg.osmf.events.DisplayObjectEvent;importorg.osmf.events.MediaElementEvent;importorg.osmf.events.TimeEvent;importorg.osmf.media.MediaPlayer;importorg.osmf.media.URLResource;importorg.osmf.net.NetLoader; privatevarplayer:MediaPlayer;privatevarduration:String; privatefunctiononInitialize():void{varelement:LightweightVideoElement;element=newLightweightVideoElement(newURLResource(sourceURL)); player=newMediaPlayer(element);videoContainer.addChild(player.displayObject); player.addEventListener(DisplayObjectEvent.MEDIA_SIZE_CHANGE,onSize);player.addEventListener(TimeEvent.CURRENT_TIME_CHANGE,onTimeChange);player.addEventListener(TimeEvent.DURATION_CHANGE,onDurationChange);player.addEventListener(TimeEvent.COMPLETE,onVideoComplete);player.currentTimeUpdateInterval=1000;} privatefunctiononViewDeactivate():void{if(player)player.stop();} privatefunctiononPlayPause():void{if(player.playing){player.play();}else{player.pause();}}` 在刚刚显示的onViewDeactivate处理程序中,我们确保当View被停用时停止播放器。您还可以看到播放/暂停ToggleButton的click处理程序。它只是调用了MediaPlayer的play和pause方法,这取决于玩家当前是否在玩游戏。 清单8–35。OSMF事件处理者 `privatefunctiononSize(event:DisplayObjectEvent):void{player.displayObject.x=(width-event.newWidth)/2;player.displayObject.y=(height-event.newHeight)/2;} privatefunctiononDurationChange(event:TimeEvent):void{duration=formatSeconds(player.duration);} privatefunctiononTimeChange(event:TimeEvent):void{updateTimeDisplay(formatSeconds(player.currentTime));} privatefunctiononVideoComplete(event:TimeEvent):void{trace("Thevideoplayedallthewaythrough!");} 与滚动你自己的基于NetStream的视频播放器相比,有了OSMF,你可以用更少的代码获得所有的功能。您还可以利用视频专家编写的代码。如果你需要它提供的所有功能,在OSMF上构建你的视频播放器是不会错的。运行时,这个基于OSMF的视频播放器的外观和行为与图8–11中所示的一模一样。 本章的最后一个例子是前面提到的录音机的视频模拟。VideoRecorder应用将使用Android摄像头接口来捕获视频文件,然后允许用户立即在Flex应用中播放它。本例的源代码可以在本书源代码的examples/chapter-08目录下的VideoRecorder示例应用中找到。 你可能还记得第七章中的提到过,CameraUI类可以用来通过原生的Android摄像头接口捕捉视频和图像。 这个例子将使用一个OSMFMediaPlayer来播放捕获的视频。清单8–36显示了CameraUI类和MediaPlayer类的初始化代码。 清单8–36。初始化CameraUI和MediaPlayer类 `importflash.media.CameraUI;importorg.osmf.elements.VideoElement;importorg.osmf.events.DisplayObjectEvent;importorg.osmf.events.MediaElementEvent;importorg.osmf.events.TimeEvent;importorg.osmf.media.MediaPlayer;importorg.osmf.media.URLResource;importorg.osmf.net.NetLoader; privatevarcameraUI:CameraUI;privatevarplayer:MediaPlayer;privatevarduration:String; privatefunctiononInitialize():void{if(CameraUI.isSupported){cameraUI=newCameraUI();cameraUI.addEventListener(MediaEvent.COMPLETE,onCaptureComplete); player=newMediaPlayer(); player.addEventListener(DisplayObjectEvent.MEDIA_SIZE_CHANGE,onSize);player.addEventListener(TimeEvent.CURRENT_TIME_CHANGE,onTimeChange);player.addEventListener(TimeEvent.DURATION_CHANGE,onDurationChange);player.addEventListener(TimeEvent.COMPLETE,onVideoComplete); player.currentTimeUpdateInterval=1000;player.autoPlay=false;} captureButton.visible=CameraUI.isSupported;}` 像往常一样,我们检查以确保设备支持CameraUI类。如果是这样,就会创建一个新的CameraUI实例,并为它的complete事件添加一个处理程序。您在第七章中了解到,当图像或视频捕获成功完成时,CameraUI会触发此事件。接下来我们创建我们的MediaPlayer并附加通常的事件监听器。注意,autoPlay属性被设置为false,因为我们想要在这个应用中手动开始回放。 清单8–37显示了使用原生Android界面启动视频捕获的代码,以及在捕获成功完成时得到通知的处理程序。 清单8–37。开始并完成视频捕捉 `privatefunctiononCaptureImage():void{cameraUI.launch(MediaType.VIDEO);} privatefunctiononCaptureComplete(event:MediaEvent):void{player.media=newVideoElement(newURLResource(event.data.file.url));player.play();playBtn.selected=true;playBtn.visible=true; if(videoContainer.numChildren>0)videoContainer.removeChildAt(0); videoContainer.addChild(player.displayObject);}` 当用户点击按钮开始捕获时,onCaptureImage处理程序启动本地摄像机UI来捕获视频文件。如果成功,onCaptureComplete处理程序接收一个包含MediaPromise作为其data属性的事件。MediaPromise包含一个文件的引用,捕获的视频存储在该文件中。我们可以使用文件的URL来初始化一个新的VideoElement,并将其分配给MediaPlayer的media属性。然后,我们可以开始播放视频,并调整playBtn的属性,使其与应用的状态保持一致。如果videoContainer已经添加了一个displayObject,我们删除它,然后添加玩家新的displayObject。 大多数事件处理代码与上一节给出的OSMFVideoView代码相同。清单8–38中显示了两个不同之处。 清单8–38。对MediaPlayer事件的处理略有不同 `privatefunctiononSize(event:DisplayObjectEvent):void{if(player.displayObject==null)return; varscaleX:int=Math.floor(width/event.newWidth);varscaleY:int=Math.floor(height/event.newHeight);varscale:Number=Math.min(scaleX,scaleY); player.displayObject.width=event.newWidth*scale;player.displayObject.height=event.newHeight*scale; player.displayObject.x=(width-player.displayObject.width)/2;player.displayObject.y=(height-player.displayObject.height)/2;} privatefunctiononVideoComplete(event:TimeEvent):void{player.seek(0);playBtn.selected=false;}` 在这种情况下,onSize处理程序将尝试缩放视频尺寸,使其更接近显示器的尺寸。注意检查player.displayObject是否是null。当从一个捕获的视频切换到下一个视频时,可能会发生这种情况。因此,我们必须小心不要试图在displayObject不存在时对其进行缩放。另一个区别在于onVideoComplete处理程序。由于用户可能希望多次观看他们捕获的视频剪辑,我们通过将播放头重新定位到开头并重置播放/暂停按钮的状态来重置视频流。Figure8–12显示了在Android设备上运行的应用。 图8–12。抓拍短视频后的录像机示例应用 在下一章中,我们将继续编写真实的Flex移动应用的主题,看看在团队中工作和利用设计师-开发人员工作流的一些方面。 无论您是设计师还是开发人员,现在都是进入移动开发的激动人心的时刻,这是一个充满潜力和机会的年轻行业。但是移动开发行业确实面临着其他软件开发项目所面临的同样的挑战,那些通信和工作流的挑战。图9–1取笑软件项目中的沟通和解释问题。这幅漫画与许多公司的做法相差无几。一个项目可能有许多实际的需求,但是大多数参与的人只会表达那些他们关心或感兴趣的需求。 项目可能会在很多地方失败。一个智能的工作流可以真正帮助缓解这些痛点,这样客户要求的就是设计师设计的,开发者执行的。但是首先必须理解设计者和开发者的角色,以及他们使用的工具。 设计师的角色是理解客户的需求,将这些转化为应用,并为其创造视觉设计。设计师与客户讨论应用应该如何工作,GUI如何完成用户故事,以及为什么它会这样工作。这是一条双行道,因为客户的输入也被考虑在内。设计师也根据开发者的需求调整视觉设计。有时候,开发人员可以预见设计人员没有意识到的技术挑战,在这种情况下,他们可以也应该合作解决问题。有时候合作只是澄清事情是如何运作的。其他时候,它可能导致设计和技术限制之间的折衷。 “设计是一种有意识的、直觉的努力,旨在建立有意义的秩序。” –维克多·帕帕内克,设计师兼教育家 AdobeDeviceCentralCS5.5与大多数设计程序集成,包括Photoshop、Illustrator、Fireworks和Flash,使您能够利用手机数据,从移动项目的开始到最终启动提高工作效率。 开始新的移动项目时,DeviceCentralCS5.5是一个不错的起点。当您启动DeviceCentral时,会出现一个欢迎屏幕(参见Figure9–2)。 图9–2。从CS5中部设备启动新的Fireworks文档;从DeviceCentralCS4开始,添加了Captivate和Fireworks文件格式。 图9–5。从测试设备面板双击设备配置文件。单击右上角的Create,基于该配置文件创建一个新文档。 新文档会自动设置为适合目标设备的正确显示尺寸和屏幕方向。现在,您已经准备好创建自己的手机设计了。在分析设备和模拟内容外观方面,DeviceCentral的帮助再大也不为过。这有助于加快设计工作流程,肯定比购买许多不同的设备要好。 就组织和生产力而言,Fireworks的一个流行功能是能够在单个文件中创建具有不同尺寸、屏幕方向甚至文档分辨率的多个页面。这意味着您可以轻松地同时处理纵向和横向布局,这在针对多点触摸设备和使用加速度计时非常方便。你甚至可以将应用图标和你的主要内容保存在同一个文件中。除了Fireworks,没有任何Adobe产品能做到这一点。 为移动设备进行设计时,在设计过程中有些时候,您可能希望在实际的手机环境中预览您的作品。最快、最简单的方法是从Photoshop、Illustrator、Flash或Fireworks中启动预览。 图9–6。在DeviceCentral中预览三星GalaxyS上的设计,选择室内反射 您可能想要创建自定设备描述文件有几个原因: 创建自定描述文件的第一步是制作现有设备描述文件的副本,用作模板。我建议选择尽可能与您想要的自定义配置文件相似的内容。原始配置文件和您的自定义配置文件之间的相似之处越多,您以后在编辑单个数据点时需要做的工作就越少。 图9–7。创建概要文件的可编辑副本 请注意,如果您计划与他人共享您的自定义配置文件,您应该给他们起一个既独特又有描述性的名称。此外,尽可能完整地填写所有字段。这是一个显而易见的最佳实践,有助于为整个社区的共同利益发展一个准确和完整的数据集。 在右边,您现在应该会看到一个圆形,在设备皮肤的正上方有一个铅笔,表示该配置文件现在是可编辑的。类似地,当您将指针悬停在任何属性上时,比如输入控件或语言,会出现相同的铅笔图标。如果悬停时属性不显示铅笔图标,则该属性不可编辑。 接下来,您可以直接从CS5设备中心编辑设备配置文件: 您选择的语言现在应该显示在您的自定义配置文件中。 AdobePhotoshopCS5非常注重摄影,但也用于创建应用设计,因为它在设计创建和图像编辑方面非常灵活。AdobePhotoshopCS5拥有卓越的图像选择、图像润饰和逼真绘画的突破性功能,以及广泛的工作流程和性能增强。 一旦在Photoshop中创建了设计(图9–8),工作流程中的下一步将是将这些图形引入FlashProfessional或FlashBuilder进行进一步开发。这可以通过分别导出每个图像,或将Photoshop文件(.psd)直接导入FlashProfessional来完成。 图9–8。一个在CS5AdobePhotoshop中创建的应用设计,包括形状层、文本和智能对象,仅举几个例子 FlashProfessionalCS3中引入的一个令人兴奋的功能是导入PSD文件的能力(图9–9)。在导入时,FlashProfessional使您能够确定如何导入每个层。例如,您可以在FlashProfessional中将文本层作为可编辑文本导入。在FlashProfessional中,形状层也可以转换为可编辑的形状。甚至电影剪辑也可以从光栅图形创建,包括实例名称。Photoshop中的图层在FlashProfessional中可以显示为图层,并带有仍可编辑的图层效果。甚至物品的位置都可以保持。最终结果是FlashProfessional中的完整设计,可以制作动画并进一步开发用于移动设备。 图9–9。将原Photoshop文件,导入到FlashProfessional中;每个层可以不同方式导入,保持文本、形状层,甚至层效果。 尽管导入Photoshop文件非常容易而且非常有用,但是您必须注意一些事情。请注意,当导入许多层时,文件会变大,请考虑合并它们。例如,如果在构成背景的不同图层上有多个图形,请考虑在导入之前在Photoshop中合并这些图层。此外,考虑在Flash中绘制矢量元素,而不是导入它们。这将使您在编辑时有更多的控制权。如果有帮助的话,您甚至可以从Photoshop中导入一个图形作为向导,同时在FlashProfessional中将所有部分创建为矢量元素。 如果Photoshop文件相当复杂,由多个图层组成背景,请考虑将这些图层合并为一个背景图层。一般的规则是,如果图形不动,看看能不能和其他图形合并。 FlashBuilder不像FlashProfessional那样导入Photoshop文件。相反,需要从Photoshop中导出单独的图像。最有效的方法是将每个元素分离成自己的Photoshop文件,并导出适当的文件类型(图9–10)。 图9–10。单独PSD文件中的单个图形准备导出为PNG、JPG或GIF。请务必保留原始PSD文件,以防以后需要进行更改。 在Photoshop中,导出图形的最佳方式是使用“文件”菜单下的“存储为Web和设备所用格式”选项。这使您能够选择想要导出的格式并查看其质量(Figure9–11)。在FlashBuilder中,您可以导入适当的文件类型,无论它是JPG、GID、PNG、SWF还是FXG。 基本上有四种不同的文件类型可以在Flash应用中使用。您选择哪一个取决于图形的内容。 PNG-24可能是富图形最流行的图形文件类型之一,因为它允许不同级别的透明度和24位颜色。还有一个PNG-8,它不允许透明,但文件大小更小,因为颜色深度是8位(256色)。 GIF是一种8位文件格式,允许多达256种颜色,这使文件大小保持较小。由于颜色数量有限,gif适用于边缘锐利的线条艺术和平面颜色,如徽标。相反,该格式不用于摄影或带有渐变的图像。gif可以用来存储游戏的低颜色精灵数据。gif也可以用于小动画,因为它们可以包含多个帧。GIF文件也可以有透明度,但不像PNG-24文件那样有不同的透明度。GIF中的每个像素要么不透明,要么透明。 JPG文件通常用于摄影图像。这种格式具有有损压缩,这意味着图像可以被压缩,导致文件较小,但这可能会导致图像质量的一些损失。将图像压缩到JPG是在保持图像质量的同时保持文件大小较小的一个很好的平衡。 AdobeFlashPlatform基于XML的图形交换格式使设计人员能够为web、交互式和RIA项目的开发人员提供更多可编辑、可工作的内容。FXG用作跨应用文件支持的图形交换格式。它基于XML,可以包含图像、文本和矢量数据。FlashProfessional、Fireworks和Illustrator都可以创建FXG文件。然后,这些文件可以在FlashProfessional或FlashBuilder中使用(参见Figure9–12)。 图9–12。在FlashBuilder中打开的一个FXG文件,包含矢量、文本和位图数据 所有这些文件格式都可以从大多数图像编辑程序中创建。所使用的程序在很大程度上取决于设计师最喜欢什么,但是如果我们更客观地看一看,你会注意到每个程序在移动Flash开发方面都有自己独特的优势。例如,FXG格式非常灵活,可以在FlashBuilder中向开发人员展示各种文本和矢量图形元素。PNG-24文件格式在设计师需要具有不同透明度的像素级完美图形时非常有用。如果照片有多种颜色和阴影,并且不需要透明度,JPG格式非常适合。最后,GIF非常适合平面图形,比如徽标。 AdobeIllustrator帮助设计师为几乎所有项目创建矢量作品。Illustrator拥有复杂的绘图工具、自然笔刷和大量内置的省时工具,可用于矢量图像编辑。IllustratorCS5允许用户在文件的像素网格上精确地创建和对齐矢量对象,以获得干净、清晰的光栅图形。用户还可以利用光栅效果,如投影、模糊和纹理,并跨媒体保持其外观,因为图像与分辨率无关。这使得Illustrator成为一个很好的开始创建图形的地方,不管输出是什么。 使用Illustrator,您可以创建移动设计,并将单个图形转换为电影剪辑元件。元件的每个实例都可以有一个实例名,就像在Flash中一样。可以将影片剪辑元件实例复制并粘贴到FlashProfessional中。Flash维护电影剪辑甚至实例名称(参见图9–13)。 图9–13。在Illustrator(左)中,您可以创建可以直接复制并粘贴到FlashProfessional(右)中的电影剪辑元件。符号和实例名称保持不变。 AdobeFireworksCS5软件提供了为Web或几乎任何设备创建高度优化的图形所需的工具。Fireworks允许您创建、导入和编辑矢量和位图图像。 在Fireworks中创建图形后(参见Figure9–14),可以将其导出为最流行的图形格式,包括FXG和MXML,专门用于FlashBuilder。以基于XML的FXG格式导出有助于确保为AdobeFlashBuilder精确转换丰富的应用设计。FXG和MXML都是基于XML的格式,可以包含可以在FlashBuilder中打开和编辑的矢量图形和文本(参见图9–15)。基于位图的图像被外部引用。 图9–14。烟花中的画面设计 图9–15。FXG-和MXML-创建的文件在FlashBuilder中打开;请注意第12行的文本引用以及第5–7行的文本标签属性。位图图像在文件外部。 从技术角度来看,开发人员应该能够将最基本的设计和技术规范转化为实际的应用。优秀的开发人员在许多方面不同于他们更普通的同事。一些重要的例子如下: 开发人员的工具箱有限。他们应该知道他们所选择的一种或多种语言的开发环境(包括编译器和调试器),以及开发团队的每个成员都需要使用的一些常用工具。这些工具通常被集成到一个平台中,该平台既充当编译器又充当调试器。这通常是用于学习语言的相同工具,因此学习开发环境通常不是一个大挑战。以下开发环境通常用于移动Flash开发。 AdobeFlashProfessionalCS5.5是制作富有表现力的交互式内容的领先创作环境,是设计人员和开发人员共享的工具。ActionScript是使用的编码语言,可以在二进制FLA文件格式中编写,其中可以包含图形、声音、字体、动画,有时还可以包含设计人员添加的视频。代码片段是在FlashProfessionalCS5中引入的,可用于加速开发。ActionScript也可以在外部ActionScript文件(.as)中编写,这是为Document类和其他对象类例行完成的。通常这取决于项目类型来决定动作脚本将被写在哪里。对于较小的项目,在FLA中编写ActionScript就可以了。对于较大的项目,许多开发人员更喜欢外部ActionScript文件来帮助组织他们的代码。 FlashProfessionalCS5.5包括舞台元件栅格化,以提高移动设备上复杂矢量对象的渲染性能。此外,还添加了20多个新的代码片段,包括用于创建移动和AIR应用的代码片段。在通过USB电缆连接的支持AdobeAIR的设备上可以进行源代码级调试,直接在设备上运行内容。 图9–16。典型的客户、设计师、开发人员工作流程 AdobeFlashBuilder4.5(以前称为AdobeFlexBuilder)是一个基于Eclipse的开发工具,用于使用ActionScript和开源Flex框架快速构建富有表现力的移动、web和桌面应用。FlashBuilder4.5允许开发人员为一个或多个移动平台(Android、BlackBerry或iOS)构建独立的Flex/ActionScript应用。设计和代码视图支持使用移动就绪组件进行移动开发。使用移动AdobeAIR运行时仿真器在桌面上测试移动应用,或者使用一键式打包、部署和启动流程在本地连接的移动设备上进行测试。开发人员可以将所需资源部署、打包和签名为特定于平台的安装程序文件,以便上传到移动应用分发站点或商店。 FlashBuilder可以导入许多流行的图形文件格式(参见Figure9–17)。内容应该决定将使用什么类型的文件。对于摄影,可以使用JPG。如果有动画,将需要一个SWF文件。大概最灵活的文件格式是FXG。它是一种基于XML的格式,公开了大量内容,使开发人员能够进一步编辑或在需要时进行动态更改。 图9–17。将图形文件导入FlashBuilder 在Flex框架中使用FlashBuilder时,情况与FLA工作流略有不同。首先,没有Fla。Flex就像传统的web开发一样。你所有的文件都在一个文件夹中,由开发人员来组织它们并将它们全部签入到源代码控制中(如果正在使用的话)。代码也在适当的文件夹中公开和组织,或者作为MXML(Flex框架)文件或者作为(ActionScript)文件。因此,设计师目前无法轻松地在他们自己的设计“沙盒”中游戏,就像他们可以在FlashProfessional中使用自己的FLA一样。这有利有弊。好处是没有设计师可以编辑开发者的作品。缺点是设计师不能检验他或她的设计。决定使用FlashBuilder还是FlashProfessional工作流取决于开发人员的技能和偏好。 一个好的工作流程真的可以决定一个项目成功还是失败。你可以在一个项目中拥有最好的设计师和开发人员,但是如果他们不能有效地一起工作,交换想法和素材,所有这些都很容易失去。设计师设想的可能不是开发人员执行的,项目经理解释的可能不是最初要求的。你可以很容易地看到一个项目在很多地方可能会失败。一个好的工作流可以缓解过程中的许多痛点,并且可以很容易地确定一个项目是否失败。 业界认为闪存技术发展缓慢。媒体上的负面言论进一步强化了这一点,例如苹果公司首席执行官史蒂夫·乔布斯在他的《关于Flash的思考》中称“Flash在移动设备上的表现不佳” 虽然可以用Flash或任何其他移动技术编写运行缓慢的应用,但使用正确的工具和技术,您也可以创建快速、响应迅速的应用。就像本机应用一样,Flash技术让您可以利用硬件加速的视频播放和GPU加速的渲染。正确使用这些技术可以显著提高应用的性能,减少电池消耗。 还很容易陷入这样一个陷阱:将针对桌面应用优化的现有内容用于移动应用。移动设备的屏幕更小,处理器更慢,内存更少,网络连接通常更慢或不可靠。如果您在构建应用时考虑到这些约束,并经常在目标移动设备上进行测试,那么您将会获得更好的结果。 在本章中,您将详细了解Flash运行时是如何工作的,从而理解限制您的应用性能的关键因素。然后,我们将深入研究几个不同的性能敏感领域,包括图像、媒体和文本。在此过程中,我们将回顾ActionScript和Flex中专门为优化移动内容而引入的新API,您应该充分利用这些API。 总会有一些写得很差的Flash应用让批评者指出Flash不适合移动设备的原因。但是,通过遵循本章中的建议和准则,您将确保您的应用不是其中之一。 性能调优移动应用与桌面应用并没有太大的不同,并分为相同的三个基本考虑事项: 内存是应用运行时使用的设备RAM的数量。这通常会随着应用的执行而增长,直到达到一个稳定状态,不再创建其他对象,或者新对象的数量大致等于被释放对象的数量。内存的持续增长可能表明存在内存泄漏,即没有释放资源或者没有取消对不可见/屏幕外对象的引用。 移动设备增加了额外的复杂性,主系统和GPU上都有内存限制。垃圾收集也是一个因素,因为当收集器复制活动对象以释放未使用的内存时,使用的内存通常是实际需要的两倍。 应用的大小也是一个重要的考虑因素,因为它会影响应用从AndroidMarket的初始下载及其启动性能。编译后的ActionScript实际上非常紧凑,因此静态素材(如嵌入到项目中的图像和视频)通常会决定应用的大小。 如果您编写了一个广泛使用的应用,您可能会遇到用户对性能不满意的情况。对于每一个抱怨性能缓慢的用户,都有数十或数百人放弃或停止使用该应用,而不是报告问题。 那么你的应用需要多快才能让用户满意呢?根据本·施奈德曼的说法,3你应该保持在以下界限内: 那么这对您的Flash应用意味着什么呢? Flash应用通常利用动画和过渡来改善用户体验。如果你打算利用这些,它们需要有相对较高的帧速率,以便给用户留下应用响应迅速的印象。这些的目标应该是大约每秒24帧或大约42毫秒,这是用户感觉动画流畅的最小帧速率。在下一节中,我们将更多地讨论如何调整渲染性能来达到这个目标。 在其核心,Flash运行时是一个基于帧的动画引擎,处理保留模式图形。即使您正在使用Flex之类的高级框架构建应用,了解FlashPlayer的渲染基础也会有所帮助,这样您就可以优化处理和内容以获得最佳性能。 Flash引擎的心跳是每秒帧数设置,它控制每秒在屏幕上绘制的帧数。虽然性能瓶颈可能会导致每秒帧数减少,但处理的帧数永远不会超过这个数字。 许多图形工具包使用所谓的即时模式渲染来绘制到屏幕上。在即时模式呈现中,应用实现了一个回调,它必须在每个时钟周期重新绘制屏幕内容。虽然这在概念上很简单,并且接近硬件实现的内容,但是它将保存状态和为动画提供连续性的工作留给了应用开发人员。 Flash使用保留模式图形,您可以创建一个将在屏幕上渲染的所有对象的显示列表,并让框架负责在每个时钟周期渲染和传输最终图形。这更适合动画和图形应用,但基于显示列表的大小和复杂性,可能会耗费更多资源。 TedPatrick为Flash播放器如何处理渲染提出了一个非常有用的概念模型,他称之为弹性跑道。4如图图10–1所示,该模型将每一帧中的工作在代码执行和渲染之间进行拆分。 图10–1。闪光播放器弹性跑道 Flash播放器的默认帧速率是24fps低于这个值的任何值对用户来说都是明显不稳定或滞后的。然而,用户可以轻松感知高达60fps的帧速率差异,特别是在有大量运动或滚动的任务中。拍摄高于60fps的帧速率通常是不值得的,尤其是考虑到大多数液晶显示器的刷新率上限为60hz,一些设备的最大帧速率上限为60。 值得研究的一些常见ActionScript代码性能最佳实践包括: 如果您正在编写一个Flex应用,您还需要了解以下内容: 如果在调优代码后,代码执行仍然是瓶颈,那么您可能希望将工作负载分散到多个框架上。例如,如果您正在执行命中检测算法,可能无法检查单个帧中的所有对象。但是,如果您可以按区域对对象进行分组,并对它们进行增量处理,则工作可以分布在多个帧上,从而提高应用的渲染速度。 在CPU上运行时,Flash使用高度优化的保留模式软件渲染器将图形绘制到屏幕上。为了渲染每一帧,它遍历DisplayList中所有对象的列表,以确定哪些是可见的,哪些需要绘制。 软件渲染器逐行扫描更新区域,通过查看显示列表中每个元素的顺序、位置和不透明度来计算每个像素的值。Figure10–2包含一个在Flash中创建的示例图形,该图形由几层文本和图形复合而成。 图10–2。内脏的样本闪现图示5 当放置在舞台中时,该场景将具有类似于图10–3所示的显示列表。 图10–3。显示样本闪光器官图形列表 在渲染阶段,Flash将使用这个显示列表来决定如何在屏幕上绘制每个像素。由于图形是不透明的,并且嵌套只有三层深,这将在屏幕上非常快地呈现。随着显示列表的复杂性增加,您需要特别注意应用中使用的对象类型,以及应用于它们的效果。 提高应用呈现性能的一些方法包括: 如果您正在开发一个基于Flex的应用,那么您将需要特别注意UIComponents、GraphicElements和FXG的使用。Table10–2列出了使用这些不同对象类型的利弊。 UIComponents是Flex中最复杂的对象类型,会显著影响渲染性能,尤其是在表格或列表渲染器中广泛使用时。和FXG都是非常轻量级的组件,渲染器可以对它们进行显著的优化。FXG有轻微的性能优势,因为它在应用构建时被编译成图形,而GraphicsElements则需要在运行时处理。 移动开发中的一个常见错误是专门在桌面模拟器中开发,并等到应用几乎完成时才开始在设备上测试。如果等到有了极其复杂的显示列表,就很难找出哪些元素导致了速度的下降。另一方面,如果您在构建应用时定期进行测试,那么就很容易诊断出哪些更改对性能影响最大。 另一种可以用来以牺牲内存为代价提高渲染性能的技术是场景位图缓存。Flash通过cacheAsBitmap和cacheAsBitmapMatrix属性提供内置支持,可以轻松捕捉和替换静态图像,以代替完整的场景层次。这在移动设备上尤其重要,因为在移动设备上,矢量图形操作要慢得多,并且会显著影响您的性能。 cacheAsBitmap是DisplayObject的boolean属性,通过扩展,你在Flash和Flex中使用的所有视觉元素包括Sprite和UIComponent都可以访问这个变量。当设置为true时,每次DisplayObject或它的一个子节点改变时,它将获取当前状态的快照并保存到屏幕外缓冲区。然后,对于未来的渲染操作,它将重新绘制保存的屏幕外缓冲区,这对于场景的复杂部分可以快几个数量级。 要在DisplayObject上启用cacheAsBitmap,您需要执行以下操作: cacheAsBitmap=true; FlexUIComponent有一个缓存策略,它会根据启发自动启用cacheAsBitmap。您可以通过执行以下操作来覆盖此行为并强制启用cacheAsBitmap: cachePolicy=UIComponentCachePolicy.ON; 当您有不经常改变的复杂图形时,打开cacheAsBitmap是一项重要的技术,例如矢量渲染的背景。尽管背景是静态的,但是当围绕它移动的其他元素重叠并遮挡了部分背景时,也会触发更新。此外,简单的翻译,如滚动背景,将导致一个昂贵的重绘操作。 为了弄清楚应用重绘的每一帧中屏幕的哪些部分被重绘,可以使用下面的代码启用showRedrawRegions: flash.profiler.showRedrawRegions(true); 这将在正在更新的屏幕区域周围绘制红色矩形,并且可以通过编程打开和关闭。图10–4显示了一个CheckBox控件的例子,它可以让你打开和关闭重绘区域。该控件最近被单击过,所以它周围有一个红色的矩形。 图10–4。重绘区域调试功能的例子 此选项仅在调试播放器中可用,因此在测试应用时,它将在AIR调试启动程序中工作,但在运行时播放器(如移动设备)中部署时,它将不起作用。Figure10–4还展示了一个非常简单的每秒帧数监视器,可用于在开发过程中对您的Flex应用性能进行基准测试。这两者的完整代码将在下一节构建FlashMobileBench应用中显示。 虽然cacheAsBitmap是一个非常强大的优化应用重绘的工具,但如果使用不当,它也是一把双刃剑。在cacheAsBitmap设置为true的情况下,为每个DisplayObject保留并刷新一个全尺寸的屏幕缓冲区,如果在图形加速模式下运行,这会消耗大量设备内存或耗尽有限的GPU内存。 此外,如果你有一个频繁更新的对象或者应用了一个转换,那么cacheAsBitmap只会用不必要的缓冲操作来降低你的应用的速度。幸运的是,对于转换的情况,有一个改进版本的cacheAsBitmap,叫做cacheAsBitmapMatrix,你可以利用它。 cacheAsBitmapMatrix也是DisplayObject上的一个属性,和cacheAsBitmap一起工作。为了使cacheAsBitmapMatrix生效,cacheAsBitmap也必须开启。 如前所述,cacheAsBitmap在对对象应用旋转或倾斜等变换时不起作用。这样做的原因是将这样的变换应用到保存的Bitmap会产生缩放伪像,这会降低最终图像的外观。因此,如果您希望将缓存应用于应用了变换的对象,Flash要求您还为存储在cacheAsBitmapMatrix属性中的Bitmap指定一个变换矩阵。 在大多数情况下,将cacheAsBitmapMatrix设置为识别矩阵将会达到预期效果。屏幕外的Bitmap将被保存在未变换的位置,并且DisplayObject上的任何后续变换将被应用到那个Bitmap。以下代码显示了如何将cacheAsBitmapMatrix设置为识别转换: cacheAsBitmap=true;cacheAsBitmapMatrix=newMatrix(); 如果您利用一个cachePolicy在一个FlexUIComponent上做同样的事情,您将做以下事情: cachePolicy=UIComponentCachePolicy.ON;cacheAsBitmapMatrix=newMatrix(); 注意:如果你计划在多个对象上设置cacheAsBitmapMatrix,你可以重用同一个矩阵来消除矩阵创建的开销。 这样做的缺点是,最终的图像可能会出现一些轻微的锯齿,尤其是在图像被放大或直线被旋转的情况下。为此,您可以指定一个变换矩阵,在缓冲图像之前放大图像。同样,如果您知道最终的图形将总是以缩小的尺寸呈现,您可以指定一个变换矩阵来缩小缓冲的图像以节省内存使用。 如果你使用cacheAsBitmapMatrix来缩小图像尺寸,你需要注意不要以原始尺寸显示DisplayObject。Figure10–5显示了一个例子,如果你设置一个先缩小并旋转图像的缓存矩阵,然后尝试以其原始大小渲染对象,会发生什么。 图10–5。演示误用cacheAsBitmapMatrix对图像质量的影响 请注意,由于放大,最终图像有很多锯齿。即使您使用原始图像的一对一转换来显示它,Flash也会放大缓存的版本,从而产生低保真度的图像。 cacheAsBitmapMatrix的最佳用途是将其设置为比预期的变换稍大,这样您就有足够的像素信息来生成高质量的变换图像。 FlashMobileBench是一个简单的应用,可让您测试不同设置对您部署的移动应用的性能的影响。 它允许您测试的功能包括: 它还包括一个简单的FPS监控小部件,您可以在自己的应用中重用它。 为了强调运行该应用的设备的能力,我们必须做的第一件事是将帧速率从默认的24fps提高到更高的水平。根据对一些设备的测试,我们发现240fps是许多平台达到的上限,并选择它作为目标帧速率设置。请记住,这是一个测试理论性能的基准应用,但在大多数情况下,您不会希望将帧速率设置得这么高,因为您可能会处理比硬件所能显示的更多的帧。 为了改变帧速率,Application类中有一个名为frameRate的属性。清单10–1展示了如何在Flex移动应用中设置这一点。 清单10–1。闪光移动板凳ViewNavigatorApplication(MobileBench.mxml) 这遵循了用一个叫做MobileBenchHomeView的View构建Flex移动应用的ViewNavigatorApplication模式。这个View的布局在MXML完成,如清单10–2所示。 清单10–2。Flash移动工作台View布局代码(MobileBenchHomeView.mxml) 这为应用创建了基本的UI,包括一个填充FPS设置的地方,用于选择缓存策略的单选按钮,以及用于添加GraphicsElement以及开始和停止动画的按钮。 还有一个额外的复选框来显示重绘区域。该控件可以按原样放入您自己的应用中,并可以帮助您最小化重绘区域的大小,以优化渲染性能。请记住,此功能仅在AIR调试启动程序中有效,因此您不能在设备运行时使用它。 除了UI标签,FPS监视器的代码是相当独立的。它由一个绑定到ENTER_FRAME事件的事件监听器和一些簿记变量组成,以跟踪平均帧速率。代码如清单10–3所示。 清单10–3。动作脚本导入,初始化,以及FPS处理程序的代码 importflash.profiler.showRedrawRegions;importflash.utils.getTimer;importmx.core.UIComponentCachePolicy;importmx.graphics.SolidColor;importmx.graphics.SolidColorStroke;importspark.components.Group;importspark.primitives.Ellipse;`importspark.primitives.Rect;importspark.primitives.supportClasses.FilledElement; privatefunctioninit():void{addEventListener(Event.ENTER_FRAME,calculateFPS);addEventListener(Event.ENTER_FRAME,animateShapes);} //FPShandler privatevarlastTime:int=getTimer();privatevarframeAvg:Number=0;privatevarlastFPSUpdate:int=getTimer(); privatefunctioncalculateFPS(e:Event):void{varcurrentTime:int=getTimer();varduration:int=currentTime-lastTime;varweight:Number=(duration+10)/1000;frameAvg=frameAvg*(1-weight)+duration*weight;lastTime=currentTime;if(currentTime-lastFPSUpdate>200){fps.text="FPS:"+Math.round(1000.0/frameAvg).toString();lastFPSUpdate=currentTime;}}` 用于计算帧速率的算法针对以下特征进行了调整: 代码的下一个关键部分是场景中GraphicsElement的填充。清单10–4中的代码实现了这一点。 清单10–4。创作GraphicsElements的动作脚本代码 `[Bindable]privatevarshapes:Vector.=newVector.(); privatefunctionpopulateRandomShape(shape:FilledElement):void{shape.width=shape.height=Math.random()*20+20;shape.x=Math.random()*(tiles.width-20)-shape.width/2;shape.y=Math.random()*(height-bounds.y-20)-shape.width/2;shape.fill=newSolidColor(0xFFFFFF*Math.random());shape.stroke=newSolidColorStroke(0xFFFFFF*Math.random());shapes.push(shape);shapeGroup.addElement(shape);} privatefunctiongenerateCircles():void{for(vari:int=0;i<100;i++){populateRandomShape(newEllipse());}} privatefunctiongenerateSquares():void{for(vari:int=0;i<100;i++){populateRandomShape(newRect());}}` 形状的所有属性都是随机的,从填充和描边的颜色到大小和位置。Rect和Ellipse创建之间的重叠逻辑也被抽象成一个公共函数,以最大化代码重用。 为了制作形状的动画,我们使用了在清单10–5中找到的代码。 清单10–5。Rect和Ellipse形状动画的ActionScript代码 `privatevarmoving:Boolean;privatevarrotating:Boolean;privatevardirectionCounter:int; privatefunctionanimateShapes(e:Event):void{if(moving){shapeGroup.x+=1-((directionCounter+200)/400)%2;shapeGroup.y+=1-(directionCounter/200)%2;directionCounter++;}if(rotating){shapeGroup.rotation+=1;}}` 我们没有使用Flex动画类,而是选择通过一个简单的ENTER_FRAME事件监听器来实现。这使您可以灵活地扩展线束,以修改形状类上不是一级属性的变量。 最后,修改cacheAsBitmap设置的代码如清单10–6所示。 清单10–6。用于设置renderMode的应用描述符标签(加粗) `privatevaridentityMatrix:Matrix=newMatrix(); privatefunctioncacheOff():void{shapeGroup.cachePolicy=UIComponentCachePolicy.OFF;} privatefunctioncacheAuto():void{shapeGroup.cachePolicy=UIComponentCachePolicy.AUTO;} privatefunctioncacheAsBitmapX():void{shapeGroup.cachePolicy=UIComponentCachePolicy.ON;shapeGroup.cacheAsBitmapMatrix=null;} privatefunctioncacheAsBitmapMatrixX():void{shapeGroup.cachePolicy=UIComponentCachePolicy.ON;shapeGroup.cacheAsBitmapMatrix=identityMatrix;}` 在阅读了上一节之后,这段代码应该看起来非常熟悉。尽管我们只有一个对象实例来应用cacheAsBitmapMatrix,我们还是遵循了重用公共单位矩阵的最佳实践,以避免额外的内存和垃圾收集开销。 运行FlashMobileBench后,您将立即看到指定设备上的FPS计数器最大值。单击按钮将一些形状添加到场景中,将缓存设置为所需的设置,并查看设备的性能。图10–6显示了在摩托罗拉Droid2上运行的Flash移动工作台应用,使用cacheAsBitmapMatrix渲染了300个圆。 图10–6。运行在摩托罗拉Droid2上的Flash移动工作台 你的设备性能如何? Flash移动项目的默认设置是renderMode为“auto”,目前默认为cpu。您可以显式地将其更改为gpu渲染,看看您的应用是否获得了显著的性能提升。要在FlashProfessional中更改渲染模式,请打开AIRforAndroid设置对话框,并从渲染模式下拉列表中选择GPU,如图Figure10–7所示。 图10–7。FlashProfessional中的GPU渲染模式设置 要更改FlashBuilder项目中的renderMode,您需要编辑应用描述符文件,并在initialWindow下添加一个额外的renderMode标签,如清单10–7所示。 清单10–7。用于设置renderMode的应用描述符标签(加粗) 从gpu模式获得的结果会因您使用的应用特性和运行的硬件而有很大差异。在某些情况下,你会发现你的应用在gpu模式下比在cpu模式下运行得更慢。Table10–3列出了在MotorolaDroid2上运行FlashMobileBench的一些实验结果,在不同的缓存和gpu模式下运行100个圆和100个正方形。 正如您在这个特定设备上的这个场景的结果中所看到的,GPU没有提供任何优势,并且在没有矩阵集的情况下启用cacheAsBitmap的情况下速度明显较慢。 这强调了在应用中进行设计决策之前,使用不同设备进行测试的重要性。在这个特定的例子中,性能下降很可能是由于GPU将数据发送回CPU的回写开销。大多数GPU设备都针对从CPU接收数据进行了优化,以便快速将其写入屏幕。在某些设备上,从另一个方向发回数据进行处理的成本高得惊人。 然而,随着摩托罗拉ATRIX和XOOM上的英特尔Integra特性等新芯片组的推出,这种情况正在迅速改变,这些芯片组为双向通信优化了管道。此外,Flash团队正在开发一个优化的渲染管道,通过在处理器上做更多的工作来减少对CPU的写回需求。有关闪存团队正在进行的性能改进的更多信息,请参见本章后面的“闪存性能的未来”一节。 性能在关键应用领域的环境中得到最佳调整,这将被用户注意到。对于Flex移动应用来说,通过列表组织内容是非常常见的,但也带来了巨大的性能挑战。 由于滚动列表涉及动画,如果在交互过程中帧速率下降,这是非常明显的。同时,项目渲染器代码中的任何性能问题都会因渲染器在每个单独的列表单元格中重用而被放大。 为了演示这些概念,我们将构建一个简单的示例,显示所有Adobe用户组的列表,并在单击某个项目时导航到用户组网站。 清单10–8展示了基本的View代码,用于创建一个Flex列表和连接一个将打开浏览器页面的click事件处理程序。我们还利用之前开发的FPSComponent来跟踪我们开发应用的速度。 清单10–8。Adobe用户组应用View类 提示:对于移动应用,总是使用itemRenderer属性而不是itemRendererFunction属性。后者会导致创建项目渲染器的多个实例,并会对性能产生负面影响。 这个类引用了一个显示列表项的UserGroupRenderer。该渲染器的创建包括组合以下组件: 清单10–9展示了满足这些需求的ItemRenderer的简单实现。 清单10–9。未优化的ItemRenderer代码 在运行这个例子时,我们有一个非常实用的滚动列表,如图Figure10–8所示。 图10-8。使用自定义的Adobe用户组列表ItemRenderer Flash提供了几个不同的映像类,这些映像类提供不同的功能,并且具有非常不同的性能特征。根据您的应用需求使用正确的图像类可以带来巨大的性能差异。 可用的图像类别按性能升序排列如下: 对于最初版本的ItemRenderer,我们使用了FlexImage类。虽然这是一个不错的选择,但是我们也没有使用这个类的高级特性,所以我们可以通过使用Bitmap来提高性能。 此外,Flex4.5中添加的一个新特性是ContentCache类。当在一个Bitmap上设置为contentLoader时,它缓存远程获取的图像,在同一图像多次显示的情况下显著提高滚动性能。 清单10–10显示了项目渲染器类的更新版本,该类包含了这些改进以提高性能。 清单10–10。ItemRenderer【代码对图像进行了优化(粗体变化) 通过这些额外的改进,我们将滚动的帧速率提高到了19fps,投掷的帧速率提高到了12fps。后者仅用几行代码就提高了70%以上,而且没有损失任何功能。 您会注意到桌面和移动设备之间最显著的性能差异之一是文本的性能。当您能够使用映射到设备字体的文本组件和样式时,您将获得最佳性能。但是,使用自定义字体或组件来提供精确的文本控制和反走样会有很大的性能损失。 随着FlashPlayer10的发布,Adobe推出了一个新的低级文本引擎,称为Flash文本引擎(FTE)和一个建立在它之上的框架,称为文本布局框架(TLF)。与以前的文本引擎(通常称为经典文本)相比,TLF具有显著的优势,例如支持双向文本和印刷质量的排版。然而,这对移动应用来说是一个巨大的性能损失。 FlashPlayer获得高性能文本显示的最佳设置是将文本引擎设置为“经典文本”,并通过在文本属性窗格中选择“使用设备字体”来关闭抗锯齿,如Figure10–9所示。 图10–9。Flash专业优化手机文本设置 对于Flex应用,您有大量不同的文本组件,它们利用了从经典文本到TLF的所有内容,因此具有不同的性能特征。 表10–4中显示了可用的文本组件,以及它们所基于的文本框架和移动性能特征。 对于移动应用,使用Label、TextInput和TextArea组件可以获得最佳性能,应该尽可能使用它们。由于它们不支持双向文本和其他高级特性和样式,在某些情况下,您可能仍然需要使用RichEditableText或RichText。 由于用户组列表应用不需要任何高级文本特性,我们可以用Label代替RichText。更新后的代码如清单10–11所示。 清单10–11。ItemRenderer【代码对文本进行了优化(更改以粗体显示) 这次改动后,滚动速度为20fps,投掷速度为18fps,有了显著的提升。我们可以通过使用StyleableTextField来实现更高的速度,这正是Flash团队为他们的内置组件所做的。 在过去的几节中,我们在测试设备上将自定义项目渲染器的性能从低于10fps的完全不可接受的速度提升到大约20fps。我们可以通过进行以下一些额外的更改来继续优化渲染器: 但是,已经有一个组件包含了这些优化,并且可以开箱即用。 Flex团队提供了一个LabelItemRenderer和IconItemRenderer的默认实现,您可以使用和扩展。这些类中已经包含了很多您可以利用的功能,包括对样式、图标和装饰器的支持。它们也是高度优化的,利用了本章讨论的所有最佳实践。 清单10–12展示了您将使用内置IconItemRenderer替换我们的自定义项目渲染器的代码更改。 清单10–12。View代码利用内置的IconItemRenderer 运行这段代码的结果非常接近我们最初的项目渲染器,如图图10–10所示。如果您并排比较这些图像,您会注意到由于使用了StyleableTextComponent,文本中有细微的差异,但是没有显著的差异会影响应用的可用性。 图10-10。Adobe用户组列表使用内置的IconItemRenderer 在摩托罗拉Droid2上,使用内置组件的最终性能是滚动24fps,投掷27fps。这超过了Flex应用的默认帧速率,表明您可以用很少的代码在Flash中构建功能丰富、性能卓越的应用。 构建高性能移动应用的最佳秘诀是尽早并经常测试性能。通过在构建应用时识别性能问题,您将能够快速识别代码中对性能至关重要的部分,并在开发过程中对它们进行调优。 拥有正确的工具来获得绩效反馈会使这项工作变得更加容易。本节重点介绍了几个免费提供的工具,或者您的系统中可能已经有了这些工具,您可以从今天开始利用它们。 获得关于应用的帧速率、内存使用和整体性能的实时反馈对于确保在开发过程中不会出现性能倒退至关重要。虽然您可以滚动您自己的性能度量,但是如果您不小心的话,您可能会因为使用您自己的工具降低应用的速度而扭曲您的结果。 杜布先生的高分辨率!Stats为您提供了以下工具: 添加高分辨率!Stats添加到ActionScript项目中,可以使用以下代码: importnet.hires.debug.Stats;addChild(newStats()); 因为它是一个纯ActionScript组件,所以您需要做更多的工作来将其添加到Flex项目中,具体操作如下: importmx.core.IVisualElementContainer;importmx.core.UIComponent;importnet.hires.debug.Stats;privatefunctionaddStats(parent:IVisualElementContainer):void{varcomp:UIComponent=newUIComponent();parent.addElement(comp);comp.addChild(newStats());} 然后,要将它附加到一个View,只需用一个自引用从initialize方法调用它: 在统计数据下面,绘制了这些值的图表,让您了解应用的趋势。您还可以通过单击读数的顶部或底部来增加或减少应用帧速率。图10–11显示了高分辨率的放大版本!统计用户界面。 图10–11。高清放大截图!统计数据 一旦您确定了您的性能问题,追踪根本原因并确保一旦您修复了它,行为不会随着将来的变化而倒退是非常棘手的。 GrantSkinner采用了一种科学的方法来解决PerformanceTest的问题,为您提供纯ActionScriptAPIs来计时方法、分析内存使用情况,并创建可重复的性能测试场景。运行PerformanceTest工具的示例输出如图图10–12所示。 图10–12。运行性能测试工具的输出 由于输出是XML格式的,您可以轻松地将其与其他工具或报告集成,包括在编写代码时进行性能测试的TDD框架。有关PerformanceTestv2的更多信息,请参见以下URL: 对于堆和内存分析,最好的可用工具之一是内置于FlashProfessional中的探查器。FlashBuilderprofiler为您提供了内存使用情况的实时图表,允许您拍摄堆快照并将其与基准进行比较,还可以捕获应用的方法级性能计时。 虽然这在直接运行在移动设备上时目前不工作,但它可以用于在AIRDebugLauncher中运行时分析您的移动应用。要在profiler中启动应用,请从运行菜单中选择Profile。执行时,您将看到应用的实时视图,如图Figure10–13所示。 图10–13。在调试模式下针对Flash移动项目运行的FlashBuilder探查器 Adobe的Flash运行时团队一直在寻找新的方法来提高Flash应用在桌面和移动设备上的性能。这包括对您的应用透明的Flash和AIR运行时的性能增强,以及可让您在应用内更高效地工作的新API和功能。 注意:本节中的所有改进和更改都是针对闪存路线图提出的,但不是承诺的功能。最终的实现可能与所讨论的有很大的不同。 随着应用规模的增长,垃圾收集暂停会对应用的响应能力产生越来越大的影响。虽然垃圾收集的摊余成本非常低,但由于它提供了所有的好处,所以由全内存清理引起的偶然命中可能会对应用的感知性能造成毁灭性的影响。 从FlashPlayer8开始,Flash运行时就使用了标记和清除垃圾收集器。标记和清除垃圾收集器的工作方式是在从根对象遍历所有活动引用之前暂停应用,标记活动对象,如图Figure10–14所示。在该阶段未被标记的对象在算法的扫描阶段被标记为删除。最后一步是释放已释放的内存,这并不保证会立即发生。 图10–14。标记和清扫垃圾收集算法的可视化表示 Flashruntime团队正在考虑对垃圾收集算法进行多项改进,以提高性能: 虽然还不太为人所知,但反过来已经是可能的了。Flash已经有了手动触发垃圾收集的机制。要立即触发垃圾收集循环,需要调用两次System.gc()方法,一次强制标记,第二次强制清扫,如清单10–13所示。 清单10–13。代码强制垃圾回收(有意重复调用) flash.system.System.gc();flash.system.System.gc(); 提示:以前这个API只能从AIR获得,并且只能在调试模式下运行,但是现在它完全支持所有模式。 虽然标记和清扫收集器相当有效且易于实现,但它们不太适合交互式应用,并且倾向于破坏新创建的对象。实际上,长寿命对象很少需要收集,而新创建的对象经常被丢弃。分代垃圾收集器认识到了这种趋势,并根据对象的年龄将它们分成不同的代。这使得更频繁地在年轻一代上触发收集成为可能,允许以更少的工作量回收更大量的内存。 拥有一个高效的分代式垃圾收集器将极大地改变ActionScript的使用模式,不再需要过多的对象池和缓存策略,而这些策略目前通常用于提高性能。 您编写的Flash应用甚至平台本身中的库都是使用ActionScript编写的,因此ActionScript性能的增量改进可以对实际性能产生巨大影响。 闪存团队正在研究的一些将惠及所有应用的改进包括: Flash利用所谓的实时(JIT)编译器来动态优化Flash字节码。JIT编译器将性能关键的代码段翻译成可以直接在设备上运行的机器代码,以获得更高的性能。同时,它拥有关于代码执行路径的信息,可以利用这些信息来执行优化,从而加速应用。 计划中的一些新的JIT优化包括: 这些JIT优化的最终结果是,在不改变应用代码的情况下,您将受益于更快的性能。一般来说,您的应用受CPU限制越多,您获得的好处就越大。 此外,Flash团队还提议增加一个显式的float数字类型和匹配的Vector. 现代计算机拥有多个处理器和内核,可用于并行执行操作以提高效率。这一趋势还扩展到了移动应用,摩托罗拉ATRIX等现代设备能够将双核处理器封装在一个非常小的封装中。这意味着为了充分利用硬件,您的应用需要能够在多个线程上并行执行代码。 即使在多个处理器不可用的情况下,考虑在多个线程上并行执行的代码仍然是一个有用的抽象。这使您可以增量处理长期运行的任务,而不会影响需要频繁更新的操作,如渲染管道。 许多内置闪存操作已经在幕后多线程化,可以有效利用多个内核。这包括在后台执行I/O操作的网络代码,以及利用在不同线程中运行的本机代码的StageVideo。通过使用这些API,您可以隐式地利用并行性。 为了让您能够利用显式线程,Flash团队正在考虑两种不同的机制向开发人员公开这一点: 在这两种场景中,都使用了无共享并发模型。这意味着您不能访问变量或在不同线程中执行的代码之间更改状态,除非使用显式消息传递。无共享模型的优点是它可以防止竞争情况、死锁和其他难以诊断的线程问题。 通过在平台中内置显式并发机制,您的应用将受益于多核处理器的更高效使用,并可以在执行CPU密集型操作时避免动画和渲染暂停。 如今,Flash渲染管道是单线程的,这意味着它不能在较新的移动设备上利用多核,如摩托罗拉ATRIX。这在渲染图形和视频时尤其成问题,因为它们最终会被顺序处理,如图Figure10–15所示。 图10–15。单线程渲染流水线 线程渲染管道将视频处理卸载到第二个CPU,从而使视频能够流畅运行,而不管ActionScript执行或舞台渲染中的延迟。这使得多核系统上的可用资源得到了最佳利用,如图Figure10–16所示。 图10–16。多线程渲染流水线 我们可以更进一步,利用StageVideo将视频解码和合成卸载到图形处理器,这为您提供了优化的渲染管道,如图图10–17所示。 图10–17。多线程渲染流水线配合舞台视频 最终结果是,您能够在ActionScript代码中进行更多处理,而不会影响帧速率或视频回放。 图10–18。来自Away3D的Molehill演示(右上和右下)和AdobeMax(左下) 这些示例是使用名为Away3D的第三方3D工具包在Stage3D的预发布版本之上构建的。其他一些可以利用Stage3D的工具包包括Alternative3D、Flare3D、Sophie3D、Unity、Yogurt3D和M2D。 除了对游戏开发者有用之外,Stage3D还开启了拥有高度优化的2DUI工具包的可能性。正如前面讨论的GPU加速支持,图形处理器可以比CPU更快地完成许多操作,同时消耗更少的功率并延长电池寿命。通过将UI工具包完全卸载到图形处理器,CPU可以专用于应用和业务逻辑,而通过现有的3D场景图将显示列表管理、合成和渲染留给GPU。 正如您在本章中了解到的,通过遵循一些移动调优最佳实践,可以构建具有高级图形、高帧速率和流畅动画的高性能Flex应用。您已经获得性能调优知识的一些特定领域包括: 此外,您还了解了Flash运行时和图形处理能力的未来改进,您将能够在未来利用这些改进,而无需更改代码。 所有这些性能调整技术也适用于我们的最后一个主题,即将您的Flash和Flex应用扩展到平板电脑、电视等领域。 谷歌和Adobe正在努力分别扩展Android平台和AIR运行时的覆盖范围。Android已经扩展到摩托罗拉XOOM和三星GalaxyTab等平板电脑上,甚至通过谷歌电视进入你的客厅。这为您的forAndroid应用打开了更多潜在的平台!此外,以黑莓手机闻名的ResearchInMotion也发布了自己的平板电脑PlayBook。该行动手册完全兼容Flash,因此为您的Flex和Flash应用赢得新受众提供了又一个机会。 本章将探讨将移动应用转移到平板电脑和电视的大屏幕上时需要考虑的一些特殊因素。 屏幕越大,界面设计就越自由。更多的自由带来更多的责任。平板电脑用户希望你的应用能充分利用大屏幕提供的额外空间。图11–1显示了来自第八章的MusicPlayer应用在一台10.1英寸屏幕的摩托罗拉XOOM上运行。虽然该应用是可用的,但低像素密度和大屏幕的结合导致了小而长的控件和大量浪费的空间。我们能够并且将会做得更好。 这样做的动机来自这样一个事实,即自Android3.0推出以来,Android平板电脑领域正在爆炸式增长,Android3.0是专为平板电脑和电视的大屏幕而设计的Android版本。除了现有的Android2.2平板电脑——戴尔Streak和三星GalaxyTab——现在还有摩托罗拉XOOM和三星GalaxyTab10.1,它们都运行最新版本的Honeycomb(Android3.x的代号)。此外,东芝、索尼、华硕和亚马逊都有望在2011年发布蜂巢平板电脑。 显然,这是一个任何应用开发人员都想认真对待的细分市场。专门为支持这些更大的平板电脑屏幕而修改的应用将比那些不支持的应用有相当大的优势。 图11–1。运行在摩托罗拉XOOM平板电脑上的音乐播放器应用 第一步是让你熟悉硬件。大多数平板电脑拥有比普通智能手机更强大的处理器和更大的内存。Table11–1显示了目前市场上流行的Android平板电脑的显示屏对比。该表显示,大多数平板电脑的分辨率在160dpi左右,屏幕更大、分辨率更高。随着更强大的处理器和大屏幕的结合,您可能会认为您的应用会比在手机上运行得更快。这不是一个好的假设,尤其是如果你的应用是图形受限的而不是CPU受限的。除非他们利用硬件加速,否则图形密集型应用在平板电脑上的运行速度通常会更慢,因为更大的屏幕必须进行大量的像素计算。像往常一样,运行性能测试并根据需要进行优化。 请注意,尤其是在横向模式下(如图所示),应用都利用额外的屏幕空间来显示多个视图。与类似的手机应用不同,Flixster和Newsr在一个屏幕上同时显示主视图和详细视图,而不必转换到单独的详细视图。TweetComb利用额外的空间来显示多列推文,而MovieStudio为您提供了更大、更易于使用的控件。还要注意标题栏中包含了更多的动作(Flex应用中的ActionBar)。我们可以对我们的MusicPlayer应用进行类似的修改,从而将其转换为一个成熟的平板电脑界面,类似于图11–2中的图片。 当考虑对音乐播放器的平板版本进行修改时,立即想到的一件事是使用歌曲视图中的额外空间来显示额外的元数据,这在应用的手机版本中根本没有空间。这种简单的修改是第一种技术的理想选择,我们将研究如何将应用扩展到新的屏幕:基于状态的定制。 从技术上来说,HTCFlyer运行的是Android2.3(代号Gingerbread)而不是Android3.x,但你的AIRforAndroid程序也将运行在Gingerbread上。 我们已经展示了如何使用landscape和portraitView状态定制应用的UI布局。这种技术采用了这种思想并加以扩展。不仅仅是portrait和landscape,你需要定义四种状态组合来支持手机和平板电脑的每个方向。因此,您假设的MXML代码看起来类似于清单11–1。 清单11–1。首次尝试为手机和平板电脑添加独立状态 ` 清单11–2。返回定制View状态 `overridepublicfunctiongetCurrentViewState():String{varisPortrait:Boolean=height>width;varisTablet:Boolean=…//Acalculationbasedonscreensizeorresolution. varnewState:String=(isPortrait"portrait":"landscape")+(isTablet"Tablet":"Phone"); returnhasState(newState)newState:currentState;}` 新状态由两个布尔变量决定。通过比较View的宽度和高度,很容易确定isPortrait变量。isTablet这个变量稍微复杂一点。您可以通过测试来使用屏幕的分辨率,看看x或y维度是否大于960,这是目前手机上使用的最大分辨率。更可靠的方法是使用屏幕分辨率和像素密度来确定屏幕的物理尺寸。那么你可以假设任何超过5.5英寸的都是平板设备。这种计算的一个例子显示在清单11–4中的onViewActivate函数中。 现在我们可以回到从歌曲的元数据向UI添加更多信息的想法。有四样东西可以添加到平板电脑界面上:专辑名称、艺术家姓名、专辑出版年份以及专辑所属的流派。我们已经将albumTitle和artistName定义为SongViewModel类中的属性。这意味着我们只需要添加year和genres属性。清单11–3展示了实现这一点的代码。 清单11–3。向SongViewModel添加year和genre属性 `packageviewmodels{[Bindable]publicclassSongViewModelextendsEventDispatcher{publicvaralbumCover:BitmapData;publicvaralbumTitle:String="";publicvarsongTitle:String="";publicvarartistName:String="";publicvaryear:String="";publicvargenres:String=""; //… /***Calledwhenthesong'smetadatahasbeenloadedbytheMetaphile*library.*/privatefunctiononMetaData(metaData:IMetaData):void{varsongFile:MusicEntry=songList[currentIndex];varid3:ID3Data=ID3Data(metaData);artistName=id3.performerid3.performer.text:"Unknown";albumTitle=id3.albumTitleid3.albumTitle.text:"Albumby"+artistName;songTitle=id3.songTitleid3.songTitle.text:songFile.name;year=id3.yearid3.year.text:"Unknown";genres=id3.genresid3.genres.text:"Unknown"; //…}}` 我们的注意力现在转向如何将这些信息添加到我们的界面上。图11–3显示了新界面的两个模型,一个横向,一个纵向。手机界面将保持不变,但当我们检测到我们正在平板电脑上运行时,我们将进行以下更改: 根据设备的方向,新歌曲信息会出现在不同的位置,但这可以使用我们的自定义状态名称和组件的includeIn属性轻松实现。 图11–3。显示附加信息的设计模型,显示在平板电脑界面上 清单11–4中的代码显示了需要对原始View代码进行的第一次修改,以实现如图图11–3所示的新设计。 清单11–4。修改后的开始SongViewMXML [Bindable]privatevarisTablet:Boolean; overridepublicfunctiongetCurrentViewState():String{varisPortrait:Boolean=height>width;varnewState:String=(isPortrait"portrait":"landscape")+(isTablet"Tablet":"Phone"); returnhasState(newState)newState:currentState;} privatefunctiononViewActivate():void{varw:Number=Capabilities.screenResolutionX/Capabilities.screenDPI;varh:Number=Capabilities.screenResolutionY/Capabilities.screenDPI;isTablet=Math.max(w,h)>5.5; setCurrentState(getCurrentViewState());} privatefunctiononResize():void{setCurrentState(getCurrentViewState());} privatefunctiononInitialize():void{/*sameasbefore/}privatefunctiononViewDeactivate():void{/sameasbefore/}privatefunctiononSongEnded(event:Event):void{/sameasbefore*/}]]>` View的title属性使用一个到isTablet变量的绑定来决定是在ActionBar中显示歌曲标题还是专辑标题。记住,在较小的手机屏幕上,我们在ActionBar的标题区域显示歌曲标题,以避免SongView界面过度拥挤。如果使用更大的平板电脑屏幕,将专辑名称放在ActionBar中更有意义,并且在从一首歌曲转到下一首歌曲时更改歌曲信息。 在View的viewActivate事件的处理程序中设置了isTablet标志。当View激活时,onViewActivate处理器以英寸为单位计算设备屏幕的宽度和高度。如果其中任何一个尺寸超过5.5英寸,那么我们可以假设该应用正在平板设备上运行。然后,该函数调用我们被覆盖的getCurrentViewState方法来获取View的初始状态,并将结果传递给setCurrentState函数。 我们还为View的resize事件附加了一个处理程序来检测方向变化。onResize处理器将通过调用我们的getCurrentViewState函数来设置View的当前状态,并使用返回值来设置当前的View状态。 注意:覆盖getCurrentViewState函数来提供自定义状态确实有一个缺点,那就是它使得FlashBuilder的设计视图实际上毫无用处。 是时候将这种状态管理代码用于我们的MXML宣言了。清单11–5显示了根Group容器以及一组标签,它们组成了横向方向的歌曲信息部分。 清单11–5。View的根容器Group和景观元数据显示 ` 清单11–6显示了歌曲信息显示的肖像模式版本以及其余的控件。 清单11–6。肖像歌曲信息组和回放控件 ` 在纵向模式下,歌曲信息VGroup显示在专辑封面和播放控件之间——因此它在MXML文件中的位置是这样的,其includeIn属性指定了portraitTablet州。 作为点睛之笔,我们在歌曲信息组件的ViewNavigatorApplicationMXML文件中添加了一点CSS样式。我们现在来看看图11–4中的应用。我们的应用现在能够适应运行在最小和最大的移动设备上。这是定制的一个简单例子,通过明智地使用状态可以实现这一点。该应用的代码可以在MusicPlayerWithStates项目中找到,该项目位于本书示例代码的examples/chapter-11目录中。 图11–4。音乐播放器支持在小屏幕和大屏幕上运行的应用 这种基于状态的定制技术的主要优点是,它允许您将所有应用代码保存在一个项目中。这使得维护代码更容易,并简化了构建过程。然而,当您考虑当您想要开始支持其他平台时需要做什么时,缺点就变得很明显了。如果你想把你的市场扩大到包括iPhone、iPad和PlayBook,那么你需要开始调整用户界面,以适应这些平台上使用的所有不同的惯例。你将突然面临状态的组合爆炸。如果不同设备类别或平台的接口彼此差异太大,您也会遇到问题。在你拥有一份冗长、难读、难维护的MXML档案之前,各州只能带你走这么远。 如果你发现自己处于这个位置,你可以转向界面定制的第二个选择:基于项目的定制。 让我们假设我们的设计师已经看过了图11–2中显示的一些应用,并决定为我们的音乐播放器尝试一种新的外观。他们想出了一种新的横向模式下的平板电脑界面,看起来有点像图11–5。他们希望将歌曲信息移到屏幕的右侧,将播放控件放在专辑封面下,并将歌曲列表添加到屏幕的左侧。从列表中选择一首歌应该跳到那首歌。列表的选择高亮应该总是反映当前正在播放的歌曲。我们还将假装我们已经开始听到营销部门关于扩展以支持其他移动平台的传言。将所有这些放在一起,我们将决定是时候选择完全定制的能力了,通过将我们的代码库分割成单独的项目,其中一个公共库项目将被其余的共享。 图11–5。在平板电脑上以风景模式运行的音乐播放器的新界面原型 图11–6。在FlashBuilder4.5中创建新库项目 您必须为库项目指定一个名称(如MusicPlayerLib),正如我们在Figure11–6中所做的那样。因为我们并不关心在这个项目中支持web和桌面(还没有!),我们还在配置部分选择了“移动库”选项。 我们知道我们的展示模型将被放入这个项目。我们也知道其中一个依赖于中期库。因此,我们必须将Metaphile.swc文件添加到这个项目中,以便对其进行编译。我们创建了一个libs目录并将Metaphile.swc放在里面。然后,我们通过右键单击项目并选择Properties,将libs目录添加到构建路径中。将显示项目的属性对话框,它看起来类似于图11–7中所示。点按“Flex库构建路径”,然后点按“添加SWC文件夹…”按钮。在出现的对话框的文本字段中键入目录名“libs”,然后单击OK。你的对话框现在应该看起来像图11–7中的那样,这表明Metaphile.swc文件已经被添加到你的构建路径中。 图11–7。将Metaphile.swc文件添加到我们的库项目 创建我们的库项目的最后一步是从原始的MusicPlayer应用中复制必要的包结构,并将源代码和图形素材复制到正确的位置。Table11–2显示了已经添加的包以及每个包中的文件。 请注意,我们已经从原来的MusicPlayer项目的views包中取出了自定义的ProgressButton控件,并把它放到了共享库项目的一个新的components包中。库项目现在应该可以编译了,我们已经准备好创建新的项目,我们将使用这些项目来构建将在手机和平板电脑上运行的应用版本。 新项目现在应该可以编译和运行了,结果看起来和第八章中的原始音乐播放器一模一样。如果您有任何问题,可以查看本书示例代码的examples/chapter-11目录中的MusicPlayerPhone项目中的源代码。通过重复这些步骤来创建一个MusicPlayerTablet项目,您就可以开始使用MusicPlayer应用的新的自定义平板电脑界面了。 图11–8。【包浏览器】的查看菜单图标 通过单击查看菜单图标并选择“选择工作集…”选项,可以定义新的工作集。将显示“选择工作集”对话框。单击“新建”按钮将显示“新建工作集”对话框。选择“资源”作为工作集类型,然后单击“下一步”。在最后一个对话框中,键入工作集的名称,并选择希望成为工作集一部分的项目。然后单击完成。图11–9显示了对话框的顺序。 图11–9。创建新的工作集 要选择一个工作集,点击查看菜单并再次选择工作集。您定义的工作集将出现在列表中。选中要激活的工作集旁边的复选框,然后单击“确定”。一旦您选择了一个工作集,它的名称将直接出现在视图菜单上,使您只需点击两次就可以在工作集之间切换。当您的PackageExplorer视图开始被您正在处理的所有不同项目塞满时,能够快速定义工作集并在它们之间切换是一个巨大的好处。 在新的SongView界面中,歌曲列表会出现在屏幕的左侧。列表中的当前选择应该反映当前正在播放的歌曲。点击列表中的新条目应该会切换到该歌曲。我们在这里描述的是两个绑定:一个在模型中的歌曲列表和列表中的项目之间,另一个在列表的当前选择和模型中的当前歌曲索引之间。 清单11–7。改为SongViewModel是因为没有代码的六页实在是太长了! `[Bindable]publicclassSongViewModelextendsEventDispatcher{//Somevariablesremovedforbrevity… publicvaryear:String="";publicvargenres:String="";publicvarsongList:ArrayCollection; privatevar_currentIndex:Number=0; /**AcollectionofMusicEntryobjects.*/privatevarmusicEntries:ArrayCollection; publicfunctionSongViewModel(entries:ArrayCollection,index:Number){this.musicEntries=entries;this.currentIndex=index; loadCurrentSong();filterEntriesBySongs();} /*****TakesallsongsinmusicEntriesandputstheminsongList.***/privatefunctionfilterEntriesBySongs():void{songList=newArrayCollection(); for(vari:int=0;i 清单11–8显示了model类中的代码,该类提供对currentIndex属性的访问,并处理对应于currentIndex的歌曲的播放。currentIndex的get函数为View提供了对素材价值的访问。set函数存储新值并调用playSongAtCurrentIndex函数。 `publicfunctiongetcurrentIndex():Number{return_currentIndex;} publicfunctionsetcurrentIndex(value:Number):void{_currentIndex=value;playSongAtCurrentIndex();} /***Jumptothebeginningofthenextsonginthelist.Willwrapto*thebeginningofthesonglistifneeded.*/publicfunctionnextSong():void{incrementCurrentSongIndex();playSongAtCurrentIndex();} /***Movestheplaypositionbacktothebeginningofthecurrentsong*unlesswearewithin3secondsofthebeginningalready.Inthat*case,wejumpbacktothebeginningoftheprevioussong.Will*wraptotheendofthesonglistifneeded.*/publicfunctionpreviousSong():void{if(channel&&channel.position<3000){decrementCurrentSongIndex();playSongAtCurrentIndex();}else{percentComplete=0;}} /*****WillloadandplaythesongindicatedbythecurrentIndexvariable.***/publicfunctionplaySongAtCurrentIndex():void{loadCurrentSong(); if(isPlaying){pauseSong();playSong();}else{percentComplete=0;}}` playSongAtCurrentIndex功能将歌曲加载到内存中,如果模型处于“播放”模式,则停止当前歌曲并播放这首新歌。如果模型被暂停,那么percentComplete变量将被重置,这样下次调用模型的onPlayPause函数时,播放将从歌曲的开始处继续。我们还回到了模型的previousSong和nextSong功能,并将其更改为使用新的playSongAtCurrentIndex功能,以消除不必要的代码重复。边走边打扫! 清单11–9。修改为SongViewMXML支持新景观界面设计