安卓-Flash-高级教程-全-

安卓 Flash 高级教程(全)

原文:Pro Android Flash

协议:CC BY-NC-SA 4.0

零、简介

我们写这本书的目的是向每一个开发者打开移动和设备开发的奇妙世界。您不再需要学习定制的移动编程语言或成为移动应用设计专家来编写好看、专业的业务应用。我们相信,在未来,手机和平板电脑将只是应用开发人员的另一个部署目标,借助移动闪存和 Fle×技术,这一未来就在眼前。

对于刚接触 Flash 的人

这本书以对 Flash 工具链和底层技术的温和介绍开始,并通过例子教授编程概念。如果你有另一种基于 C 语言的经验,如 Java、JavaScript 或 Objective-C,这本书的进度将允许你在学习 Flash 和 Fle× mobile 概念和 API 的同时学习 ActionScript 和 MXML。

对于 Android 新手来说

Flash 和 Fle×平台利用了 Android 的所有强大功能,同时使程序员不必处理 Android APIs 和编程模型的复杂性。这意味着,只要终端用户对 Android 有简单的了解,你就可以成为一名应用开发人员,向 Android Market 发布你自己的基于 Flash 的应用。

对于我们所有的摇滚明星开发者来说

让我们面对现实吧——你拿起这本书不仅仅是为了成为另一个移动开发者。你想要拓展平台的极限,挖掘超越平均水平的特性和能力,并构建出酷毙了的应用。

我们与你同在,这就是为什么我们在开发这本书时将技术推向极限。在本书后面的章节中,您将学习如何利用原生 Android 功能,配置和调整您的应用以获得最佳性能,以及部署到除简单手机之外的各种不同设备。

你的团队写的

我们不是那些以写书为生的普通无名作者。我们是应用开发人员和技术极客,就像您一样。我们投资于我们讨论的技术、移动开发的未来,以及最重要的,您作为未来 Flash 平台开发人员的成功。

所有作者都在网上有大量的社区参与,包括领先的 Adobe 用户组和技术宣传。我们对这项技术感到兴奋,并乐于接受问题、询问和对话。我们不仅仅是另一个作者团队,而是您自己的个人 Flash 开发团队。

通过阅读这本书和编写练习,你会学到很多东西,但不要止步于此。开始与其他读者和 Flash 开发者对话。加入专门研究 Flash 和 Fle×技术的技术用户组。带着问题、想法、概念和猜想联系我们,作者。

最重要的是,让技术成为你自己的。

一、移动 Flash 简介

这本书,Pro Android Flash,是使用无处不在的 Flash 平台在移动设备上构建丰富、普及的用户体验的权威指南。我们将向您展示如何利用构成 Flash 平台的强大而成熟的技术、框架和工具来构建高度定制的应用,这些应用充分利用了用户要求其设备具备的所有移动功能。在阅读本书时,您将获得针对移动 Android 设备的基本知识,包括设备密度、硬件输入、本机集成和性能优化。

为什么选择安卓?

有许多不同的移动平台可供选择,也有大量的移动和平板设备可供消费者选择。与桌面市场不同,桌面市场已经有了大量的整合和巩固,移动市场也在不断发展,不断推出新的设备和功能。

显而易见的问题是,你的目标平台是哪个?我们的答案是从安卓开始;然后,通过利用闪存技术,您可以避免受限于任何特定的平台。

这本书着重于在运行 Android 操作系统的设备上创建应用。这是因为 Android 正在迅速成为世界上最受欢迎的移动操作系统,对不同硬件平台和多种外形的支持最好。

根据尼尔森公司的数据,Android 是 2010 年下半年购买智能手机的人的首选。黑莓 RIM 和苹果 iOS 在统计上处于第二位,如图图 1–1 所示。

images

图 1–1。 美国移动 OS 流量份额 1

这可能是由于许多不同的因素,包括平台是开源的这一事实,这吸引了设备制造商,Android Market、谷歌的设备内应用店面或谷歌体验提供的相对自由,谷歌体验为最终用户提供了 Gmail、谷歌地图、Gtalk、YouTube 和谷歌搜索的无缝集成。不管 Android 流行的原因是什么,很可能你的大部分客户已经拥有 Android 设备,或者正在考虑在不久的将来购买一台。

与此同时,您正在构建一个具有巨大横向增长潜力的平台。Android 只是 Flash 平台的开始,它受益于一个抽象的虚拟机和 API,这些 API 旨在跨多个不同的操作系统和设备工作。您可以利用 Flash 为所有移动应用带来的跨平台透明性。

其他平台上的 Flash

Adobe 启动了 Open Screen Project, 2 ,这是一项全行业的计划,旨在将 Flash 驱动的应用的优势带到您生活中的所有屏幕上。Adobe 已经宣布了支持 iOS、黑莓、Windows 7 和 webOS 的计划,将你从平台锁定中解放出来。

黑莓支持最初针对其平板电脑操作系统,第一个可用的设备是黑莓 PlayBook。预计这种支持将来会扩展到包括它的其他移动设备。


1 资料来源:尼尔森公司,[nielsen.com/](http://nielsen.com/),2010 年

2 Adobe,“开屏项目”,[www.openscreenproject.org/](http://www.openscreenproject.org/)

苹果仍然限制在浏览器中运行 Flash,但它已经开放了应用商店,允许第三方框架。这意味着对于 iOS 设备,您可以在任何 iOS 设备上部署 Flash 作为 AIR 应用,包括 iPod touch、iPhone 和 iPad。

您还可以在浏览器中支持 Flash 的任何设备上部署 Flash web 应用。这包括谷歌电视、webOS 和 Windows 7。未来,我们有望看到更多支持闪存技术的平台。

探索安卓系统

Android 是一个完整的移动堆栈,包括操作系统、服务和基础设施,以及一组核心应用。虽然您不需要成为 Android 方面的专家来有效地编写 Flash 应用并将其部署到 Android 设备上,但是熟悉 Android 的工作方式确实会有所帮助。

Android 的核心是基于 Linux 操作系统。它使用 Linux 内核的修改版本,该版本具有额外的驱动程序并支持移动硬件设备。

在此之上,有一组库和核心服务组成了基本的 Android 功能。你很少会直接与这些库进行交互,但是每当你播放一个媒体文件,浏览一个网页,甚至在屏幕上绘图,你都在经历一个核心的 Android 库。

原生 Android 应用是使用编译成 Dalvik 字节码的 Java 编程语言编写的。Dalvik 是 Android 特殊虚拟机的名称,它抽象了硬件并支持垃圾收集等高级功能。您运行的所有 Android 应用(包括 Adobe AIR 应用)都在 Dalvik 虚拟机中执行。

完整的 Android 系统架构,按 Linux 内核、库和运行时、应用框架和应用细分,如图 1–2 所示。

images

图 1–2。 安卓系统架构 3

除了拥有非常坚实的技术基础,Android 还在不断发展,以适应新的硬件进步。Android 平台的一些当前功能包括:

  • 移动浏览器(Mobile browser):WebKit,一个现代的框架,支持 HTML5 提出的所有扩展,并支持 Android 的内置浏览器
  • Flash player :从 Android 2.2 开始,你可以在网络浏览器中运行 Flash 内容,这是一项标准功能。
  • 多点触摸:所有安卓手机都支持触摸屏,大多数都有至少两个触摸点,你可以用它们来进行手势识别。
  • 摄像头:安卓手机被要求配备后置摄像头,现在许多手机也配备了前置摄像头。
  • GPS,指南针:所有安卓手机都要求有一个三向 GPS 和指南针,可用于导航应用。
  • 多任务处理 : Android 是第一个将应用切换和后台操作暴露给已安装应用的移动操作系统。
  • GSM 电话技术:作为电话使用的 Android 设备为您提供了 GSM 电话技术的全部功能。
  • 蓝牙、Wi-Fi 和 USB :所有 Android 设备都配有蓝牙和 Wi-Fi 连接,以及一个标准 USB 端口,用于数据传输和调试。
  • 音频和视频支持 : Android 支持播放网络上最常见的音频和视频格式,包括 MP3、Ogg 和 H.264。

3 转载自 Android 开源项目创建和共享的作品,并根据知识共享 2.5 归属许可中描述的条款使用:谷歌,“什么是 Android?”,[developer.android.com/guide/basics/what-is-android.html](http://developer.android.com/guide/basics/what-is-android.html),2011 年

这些功能使得 Android 平台成为构建移动应用的一个非常强大的基础。此外,Adobe Flash 和 AIR 构建在这些基础功能之上,使 Flash 成为开发 Android 应用的绝佳平台。

Flash 平台

Adobe Flash Platform 是一个完整的系统,集成了运行在不同操作系统、浏览器和设备上的工具、框架、服务器、服务和客户端。许多行业的公司都使用 Flash Platform 来消除设备和平台碎片,并开发出一致且富于表现力的交互式用户体验,不受设备限制。让我们来看看 Flash 平台的运行时和工具。

Flash 运行时

创建 Flash 应用时,您可以选择两个不同的部署目标。第一个是 Adobe Flash Player,这是一个嵌入式浏览器插件,第二个是 Adobe AIR,这是一个独立的客户端运行时。这两个选项都可以在桌面和移动设备上使用,并为您提供了很大的灵活性来定制您的应用部署,以满足最终用户的需求。

Adobe Flash 播放器

据 Adobe 称,Flash Player 安装在 98%的联网电脑和超过 4.5 亿台设备上, 4 为运行在客户端的应用提供了最广泛的应用。2011 年,Adobe 预计将有超过 1.32 亿部智能手机支持 Flash Player,并且已经有超过 2000 万部智能手机预装了 Flash Player。预计 2011 年还会有另外 50 款新的平板设备支持 Flash Player。

Adobe Flash Player 在浏览器中的安全容器中运行。这允许您将 Flash 内容与用 HTML 和 JavaScript 编写的其他 web 内容混合在一起。您还可以获得免安装操作的优势。


4 资料来源:Adobe,“富互联网应用的好处”,www.adobe.com/resources/business/rich_internet_apps/benefits/#,2009 年

Adobe AIR

目前为 Flash Player 发布内容的设计人员和开发人员也可以重新利用相同的内容来为 Adobe AIR 运行时制作应用。在撰写本文时,有 8400 万部智能手机和平板电脑可以运行 Adobe AIR 应用,Adobe 预计到 2011 年底将有超过 2 亿部智能手机和平板电脑支持 Adobe AIR 应用。

Adobe AIR 将 Flash 扩展到浏览器之外,允许您的内容从 Android Market 下载并作为一流的应用安装。此外,Adobe AIR 应用可以请求用户的许可,以访问受限的硬件,如照相机、麦克风和文件系统。

Table 1–1 总结了在 Flash Player 中部署或作为 Adobe AIR mobile 应用部署的优势。由于 AIR 是 Flash APIs 的适当超集,因此也可以创建部署在两者下的单个应用。

image

Adobe Flex

Flex 是一个开源软件开发工具包,专门用于在 Flash 平台上构建专业的业务应用。它包括一些额外的库,可以快速方便地构建带有布局、控件和图表的用户界面。此外,大多数 Flex UIs 都是用一种叫做 MXML 的 XML 方言以声明方式编写的,这使得构建嵌套的用户界面布局比直接的 ActionScript 更容易。

Adobe 非常积极地向 Flex 框架添加移动功能,如视图、触摸支持和移动优化皮肤。在本书中,我们将利用 Adobe Flex 技术来演示移动 API。同时,我们将演示纯 ActionScript APIs 的使用,如果您正在构建一个不包含 Flex SDK 的应用,则可以使用这些 API。

闪光工具

自从 Creative Suite 5.5 (CS5.5)发布以来,所有用于 Flash 和 Flex 开发的 Adobe 工具也支持移动开发。

Table 1–2 列出了 Adobe 提供的工具,您可以使用这些工具通过 Flash 和 Flex 开发移动应用。它们之间的互操作非常紧密,这使得利用每种工具的优势变得很容易。这扩展到 Adobe 设计工具,如 InDesign、Photoshop、Illustrator 和 Fireworks,它们可用于为您的应用开发内容,这些内容将直接插入到您的 Flash 和 Flex 应用中。

image

Adobe Flash 专业版

Adobe Flash Professional 为设计人员和开发人员提供了一套绘图工具、时间线以及添加交互性的能力,从而为多个平台创建丰富的交互式体验。它实际上起源于一个动画工具。当然,这意味着它的核心非常适合处理动画和图形。但是,从它卑微的开始,它已经成长为一个成熟的程序,能够创建丰富的身临其境的体验,并具有用 ActionScript 编写的高级交互性,可以发布到多个平台。

如果您是 Flash 开发的新手,Flash Professional 是一个很好的起点。它提供了一个可用于构建内容的图形电影和时间轴编辑器,以及一个非常实用的 ActionScript 编辑器,该编辑器具有代码模板、API 帮助和代码完成等高级功能。

需单独购买

Adobe Flash Builder 软件旨在帮助开发人员快速开发适用于 Flash 平台的跨平台富互联网应用和游戏。用户可以通过编写 ActionScript 代码来创建游戏,就像使用 Flash Professional 一样。借助 Flash Builder,您还可以使用 Flex 框架编写应用,Flex 框架是一个用于开发和部署富互联网应用(RIA)的免费、高效的开源框架。

如果您正在开发一个具有复杂 UI 和复杂算法或业务逻辑的大型应用,您肯定会希望添加 Flash Builder 4.5。它基于全功能的 Eclipse IDE,提供了您期望从专业开发环境中获得的一切,包括代码导航、键盘加速器和完整的 GUI 生成器。

设备中心

Device Central 是 Flash Professional 附带的补充应用,允许您在桌面上模拟不同的移动设备,包括对倾斜、多点触摸和加速度计的支持。它还让您可以访问一个巨大的信息库,其中列出了 Flash 平台支持的所有可用的移动和嵌入式设备,包括完整的规格和自定义仿真器。

注意:截至本文撰写时,Device Central 尚未更新到 AIR 2.6 以支持 Android 设备。

土坯闪光催化剂

Flash Catalyst 是 Adobe 的快速应用开发平台。它允许您将 Photoshop、Illustrator 或 Flash 中制作的艺术资源转化为一流的 UI 控件。Catalyst 的移动工作流是创建或修改包含您的组件和素材的 FXP 文件,然后在 Flash Builder 中打开它,添加业务逻辑并在移动平台上运行。

所有这些应用都可以免费试用;但是,如果您想使用纯开源堆栈进行开发,您可以使用 Flex SDK 直接从命令行进行 Flex 和 ActionScript 开发。作为 Flash Builder 和 Catalyst 基础的所有组件都是 Flex SDK 的一部分,可以通过编程方式访问。如果您正在配置一个自动构建来编译和测试您的 Flex 应用,这也是您想要使用的。

工作流程

除了已经列出的工具,Adobe 还有一个强大的工作流程,允许设计人员使用 Adobe InDesign、Adobe Photoshop、Adobe Illustrator 和 Adobe Fireworks 等程序将图形移动到 Flash Professional 或 Flash Builder 中进行进一步开发,如图 1–3 所示。这意味着在处理图形时很少出现转换问题,也不存在将图形从设计转移到开发的漫长过程。

images

图 1–3。 从设计到开发再到发布到多个平台/设备的 Flash 工作流程

我们将在第九章中更详细地讨论设计人员/开发人员的工作流程,给出如何在不同工具之间简化工作流程的真实例子。

从 Flash Professional 运行应用

开始编写 Flash 应用的最简单方法是使用 Adobe Flash Professional。它为构建简单的电影提供了一个可视化的环境,也为构建更复杂的逻辑提供了良好的 ActionScript 编辑功能。

创建新的 Android 项目

要为 Android 项目创建一个新的 AIR,从文件image New… 打开新项目对话框,点击模板选项卡。在这里你可以选择一个 AIR for Android 项目,并选择你的设备模板,如图 Figure 1–4 所示。

images

图 1–4。 Flash Professional 新建模板对话框

这将创建一个新项目,画布的大小完全适合纵向模式下的移动项目,并且允许您在 Flash Professional 中或通过 USB 在设备上测试您的应用。有关设备部署的更多信息,请参阅第五章“应用部署和发布”。

编写 Flash 功能报告

为了演示设备功能,我们将创建一个名为 Flash Capability Reporter 的简单应用。它将会有一个简单的滚动列表,列举出你正在运行的模拟器或设备的所有功能。

对于 ActionScript 代码,我们将使用来自CapabilitiesMultitouch类的静态常量。其中大多数返回 true 或false,但有些会返回stringinteger值。通过使用字符串连接操作符,我们可以很容易地对它们进行显示格式化,如清单 1–1 所示。

清单 1–1。 Flash 能力校验码

`import flash.system.Capabilities;
import flash.ui.Multitouch;

capabilityScroller.capabilities.text =
  "AV Hardware Disable: " + Capabilities.avHardwareDisable + "\n" +
  "Has Accessibility: " + Capabilities.hasAccessibility + "\n" +
  "Has Audio: " + Capabilities.hasAudio + "\n" +
  "Has Audio Encoder: " + Capabilities.hasAudioEncoder + "\n" +
  "Has Embedded Video: " + Capabilities.hasEmbeddedVideo + "\n" +
  "Has MP3: " + Capabilities.hasMP3 + "\n" +
  "Has Printing: " + Capabilities.hasPrinting + "\n" +
  "Has Screen Broadcast: " + Capabilities.hasScreenBroadcast + "\n" +
  "Has Screen Playback: " + Capabilities.hasScreenPlayback + "\n" +
  "Has Streaming Audio: " + Capabilities.hasStreamingAudio + "\n" +
  "Has Video Encoder: " + Capabilities.hasVideoEncoder + "\n" +
  "Is Debugger: " + Capabilities.isDebugger +  "\n" +
  "Language: " + Capabilities.language + "\n" +
  "Local File Read Disable: " + Capabilities.localFileReadDisable + "\n" +
  "Manufacturer: " + Capabilities.manufacturer + "\n" +
  "OS: " + Capabilities.os + "\n" +
  "Pixel Aspect Ratio: " + Capabilities.pixelAspectRatio + "\n" +
  "Player Type: " + Capabilities.playerType + "\n" +
  "Screen Color: " + Capabilities.screenColor + "\n" +
  "Screen DPI: " + Capabilities.screenDPI + "\n" +
  "Screen Resolution: " + Capabilities.screenResolutionX + "x"
  + Capabilities.screenResolutionY + "\n" +
  "Touch Screen Type: " + Capabilities.touchscreenType + "\n" +
  "Version: " + Capabilities.version + "\n" +
  "Supports Gesture Events: " + Multitouch.supportsGestureEvents + "\n" +
  "Supports Touch Events: " + Multitouch.supportsTouchEvents + "\n" +
  "Input Mode: " + Multitouch.inputMode + "\n" +
  "Max Touch Points: " + Multitouch.maxTouchPoints + "\n" +
  "Supported Gestures: " + Multitouch.supportedGestures;`

每行末尾的"\n"字符增加了换行符以提高可读性。然后将结果字符串分配给在capabilityScroller电影中定义的 ID 为capabilities的 Flash 文本字段。在 Flash 中使用嵌入的电影通过隐藏文本的滚动动画来清理主时间轴。

虽然这在功能上已经完成,但我们在完成的图书样本中添加了一些额外的图形细节,包括以下内容:

  1. 一个图形剪辑层:为了使文本在滚动时从图形后面出现,在滚动时消失,我们添加了一个带有纯色背景的附加层,并在文本应该可见的地方剪切了一部分。这代替了使用剪辑蒙版,所以我们可以获得使用设备字体的性能优势。
  2. 闪烁的灯光:一个简单的动画通过使用 Flash Deco 工具在左侧创建,使用砖块图案填充网格。选择了两种不同的颜色,并选中了“随机顺序”选项,以创建三帧动画上闪烁灯光的视觉外观。
    ** Android 标志和文字:没有一个 Android 应用是不完整的,没有一点吸引眼球的东西。借助 Android 上可用的全彩色高分辨率显示器,您可以对应用的图形外观做很多事情。在这种情况下,选择矢量图形来平滑缩放到任何大小的设备。*

*要运行完整的示例,请转到 AIR Debug Launcher (Mobile) 中的控制image测试影片image。这将在 AIR Debug Launcher (ADL)运行时运行应用,如图图 1–5 所示。

images

图 1–5。 桌面上 ADL 中运行的 Flash Capability Reporter 应用

您可以在自己的开发过程中使用该示例来比较桌面和移动设备的功能。您可以随意添加功能列表,并尝试在不同设备上运行。

您会注意到,即使我们在 ADL 的移动模式下运行,返回的值也与您在设备上运行时得到的值不一致。在本章的后面,我们将向你展示如何在 Android 模拟器中或者通过 USB 在设备上运行你的应用。

从 Flash Builder 运行应用

新版 Flash Builder 为移动设备构建 Flash 和 Flex 应用以及直接从 IDE 运行和调试这些应用提供了强大的支持。在本节中,我们将向您展示如何从头开始创建一个新的移动项目,演示 Flex 移动开发的基础知识,包括视图、控件和多点触摸手势识别。

我们将创建的应用称为手势检查。它允许您分析您的设备,以直观地发现支持哪些手势,并测试它们是否被成功识别。在创建此示例的过程中,您将全面了解 Flash Builder 的移动功能,包括如何创建新的 Flex 移动项目、使用 Flash Builder 调试器调试应用以及通过 USB 部署在设备上运行应用。

创建新的 Flex 移动项目

要创建新的 Flex 移动项目,从文件image新建image Flex 移动项目打开新建项目对话框。你会得到一个项目创建向导对话框,允许你输入项目名称,如图图 1–6 所示。

images

图 1–6。 Flex 移动项目创建向导

将项目命名为 GestureCheck,并选择一个文件夹来存储项目。

提示:如果您创建的项目名称中没有空格,Flex 将创建与您选择的名称相匹配的项目文件。如果您的名称包含空格、破折号或其他在 ActionScript 标识符中无效的字符,它将使用通用名称“Main”来代替。

完成后,点击下一步进入向导的移动设置页面,如图 Figure 1–7 所示。

images

图 1–7。??【移动设置】选项卡用于选择应用模板和设置

Flash Builder 附带了几个用于开发移动项目的内置模板,可用于快速启动新项目。其中包括一个简单的空白应用、一个从主页开始的基于视图的应用,以及一个允许您在不同命名视图之间切换的选项卡式应用。您可以在第三章中找到更多关于视图和选项卡导航的信息。

在本练习中,选择默认的基于视图的基本应用模板。您还可以选择重定向、全屏模式和密度缩放。确保禁用自动重定向,使应用停留在纵向模式。我们将在第二章中更深入地讨论纵向/横向切换。

在移动设置页面上完成后,单击完成创建您的移动应用。

首先,Flex 模板为您提供了以下项目结构(标有internal的文件您永远不要直接修改):

  • .actionScriptProperties: [internal]包含库、平台和应用设置的 Flash Builder 设置文件
  • .flexProperties: [internal] Flex 服务器设置
  • .project: [internal] Flex Builder 项目设置
  • .settings: [internal] Eclipse 设置文件夹
  • bin-debug:这是执行过程中存储 XML 和 SWF 文件的输出文件夹。
  • libs:资源库文件夹,您可以在其中添加自己的自定义扩展
  • src:包含您所有应用代码的源文件夹
    • views:为存储您的应用视图而创建的包
      • [AppName]HomeView.mxml:应用的主视图(由主视图Application引用)
    • [App-Name]-app.xml:包含移动设置的应用描述符
    • [AppName].mxml:项目的主要Application类和执行的切入点

我们将在本教程的剩余部分关注的文件都在src目录中。这包括您的应用描述符和主Application类,它们都在根包中,以及您的HomeView,它是在名为views的包中创建的。

编写 Flex 移动配置器

创建应用的第一件事是为 UI 构建一个声明性的 XML 布局。为此,我们将使用 Flex 的一些基本布局和 UI 类,包括:

  • H/VGroup:``HGroupVGroup类让你在一个简单的垂直或水平堆叠布局中安排一组组件。组件按顺序排列,间距由gap属性设置。
  • 显示不可编辑的文本字符串的简单组件;这通常用作表单中另一个控件的标签。
  • Image:``Image类让你显示一个可以从GIF, JPEG, PNG, SVGSWF文件中加载的图形。在这个例子中,我们将使用透明的PNGs
  • CheckBox:一种表单控件,其值为选中或未选中,带有可视指示器;它还包括文本描述作为显示的一部分。

使用这些布局和控件,我们可以构建一个简单的用户界面,显示设备上是否启用了特定的多点触摸手势,以及用户是否成功测试了手势。第一个“滑动”手势的代码显示在清单 1–2 中。这个代码应该在视图文件中更新,可以在[src/views/GestureCheckHomeView.mxml](http://src/views/GestureCheckHomeView.mxml)中找到。

清单 1–2。 第一次手势显示的 UI 元素

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark"     title="Supported Gestures" initialize="init()">   <s:VGroup paddingTop="15" paddingBottom="15"                     paddingLeft="20" paddingRight="20" gap="10">     <s:HGroup verticalAlign="middle" gap="20">       <s:Label text="Swipe" fontSize="36" width="110"/>       <s:Image source="@Embed('/gestures/swipe.png')" width="137"/>       <s:VGroup gap="10">         <s:CheckBox content="Enabled" mouseEnabled="false"/>         <s:CheckBoxcontent="Tested" mouseEnabled="false"/>       </s:VGroup>     </s:HGroup>   </s:VGroup> </s:View>

要运行此应用,请进入运行image作为image移动应用运行。这将调出运行配置对话框,如图 1–8 所示。

images

图 1–8。 Flash 移动运行配置对话框

首先,我们将使用桌面上的 AIR Debug Launcher (ADL)运行应用。为此,选择桌面启动方法,并选择一个合适的设备进行模拟(对于本例,您将需要选择一个具有高密度显示屏的设备,如 Droid X)。

单击 Run 按钮将在 ADL 中执行应用,向您显示您之前添加的 UI 元素,如图 Figure 1–9 所示。

images

图 1–9。 手势检查用户界面

这构建了基本的 UI 模式,但是没有连接任何应用逻辑来设置CheckBoxes的状态。为了实现这一点,我们将使用一个initialize函数,该函数遍历由Multitouch类报告的所有supportedGestures。这显示在清单 1–3 中。

清单 1–3。 检测手势支持和用法的附加代码以粗体突出显示

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    title="Supported Gestures" initialize="init()">
  fx:Script
    <![CDATA[
      import flash.ui.Multitouch;

private function init():void {
        for each(var gesture:String in Multitouch.supportedGestures) {
          this[gesture+"Enabled"].selected = true;
          addEventListener(gesture, function(e:GestureEvent):void {
            e.currentTarget[e.type+"Tested"].selected = true;
          });
        }
      }
    ]]>
</fx:Script>
<s:VGroup paddingTop="15" paddingBottom="15"
                  paddingLeft="20" paddingRight="20" gap="10">
  <s:HGroup verticalAlign="middle" gap="20">
    <s:Label text="Swipe" fontSize="36" width="110"/>
    <s:Image source="@Embed('/gestures/swipe.png')" width="137"/>
    <s:VGroup gap="10">         <s:CheckBox id="gestureSwipeEnabled" content="Enabled" mouseEnabled="false"/>
        <s:CheckBox id="gestureSwipeTested" content="Tested" mouseEnabled="false"/>
      </s:VGroup>
    </s:HGroup>
  </s:VGroup>
</s:View>`

注意,我们已经向CheckBoxes添加了一些 id,以便从initialize函数中引用它们。命名约定是手势名称附加“启用”或“已测试”字样。在设置selected状态的代码中使用了相同的命名约定。

当创建视图时,init函数被调用一次,并遍历所有的supportedGestures。它将启用的CheckBox的状态设置为 true,并添加一个事件监听器,当在应用中使用该手势时,该监听器会将测试的CheckBox的状态设置为 true。如果你想了解更多关于手势和事件监听器的功能,我们会在第二章中详细介绍。

如果您运行更新后的示例,您将获得相同的 UI,但也会触发一个错误。ActionScript 错误对话框显示在 Figure 1–10 中,虽然您可能很清楚程序中的问题是什么,但我们将利用这个机会演示 Flash Builder 调试器是如何工作的。

images

图 1–10。 更新后的应用执行时 ActionScript 出错

注意:只有在启用了手势支持的电脑上运行,比如带触摸板的 Macintosh,才会出现前述错误。相反,您可以在带有触摸屏的移动设备上运行,以重现相同的错误。

使用 Flash Builder 调试器

在上一节中,我们在运行应用时遇到了一个错误,但是错误窗口在识别发生了什么或者让我们检查当前状态方面并没有特别大的帮助。事实上,如果我们在移动设备上运行应用,它会继续执行,甚至不会让我们知道发生了错误。虽然这种行为对于生产应用来说是理想的,如果执行可以安全地继续,您不希望小错误困扰您的最终用户,但是这使得调试应用具有挑战性。

幸运的是,Flash Builder 附带了一个内置的调试器,您可以使用它来诊断您的应用。要使用调试器,您必须通过RunimageDebug AsimageMobile Application命令启动应用。

当您这样做时,与正常应用运行的唯一显著区别是,您现在将在控制台面板中获得跟踪输出和错误。当试图诊断应用行为时,这本身就非常有用;但是,如果在执行过程中遇到错误,系统会询问您是否要切换到 Flash Debug 透视图,如图 Figure 1–11 所示。

images

图 1–11。Flash 调试透视图突出显示手势检查应用中的错误

Flash Debug 透视图使您能够在应用执行时查看其内部,这是非常强大的。在左上角的调试窗格中,您可以启动和停止您的应用,以及导航堆栈框架,例如我们遇到的错误情况。

当您在“调试”面板中选择一个帧时,它会在右上角的“变量”窗格中显示执行上下文的状态,并在中间的面板中显示相关的源代码。这使得很容易识别出我们在将 enabled CheckBox设置为true的调用中失败了,因为没有带有id“gestureZoom”的CheckBox。这是因为我们还没有添加 UI 元素来处理额外的手势。

由于我们已经确定了问题,您可以停止应用,并通过使用 Flash Builder 窗口右上角的透视图选择器切换回代码透视图。

正如您将在第二章中了解到的,Android 上的 Flash 支持五种不同的手势事件。这些措施如下:

  • 平移:在显示屏上拖动两个手指。
  • 旋转:将两个手指放在显示屏上,顺时针或逆时针旋转。
  • 缩放:使用两个手指,同时分开或并拢。
  • 滑动:用单个手指在显示屏上水平或垂直按压和滑动。
  • 双指轻击:同时用两个手指触摸显示屏。

清单 1–4 展示了完整的应用,它将让您尝试这些手势。

清单 1–4。 手势检查示例应用的完整代码清单

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    title="Supported Gestures" initialize="init()">
  fx:Script
    <![CDATA[
      import flash.ui.Multitouch;

privatefunction init():void {
        for each(var gesture:String in Multitouch.supportedGestures) {
          this[gesture+"Enabled"].selected = true;
          addEventListener(gesture, function(e:GestureEvent):void {
            e.currentTarget[e.type+"Tested"].selected = true;
          });
        }
      }
    ]]>
  </fx:Script>
  <s:VGroup paddingTop="15" paddingBottom="15"
                    paddingLeft="20" paddingRight="20" gap="10">   <s:HGroup verticalAlign="middle" gap="20">
    <s:Label text="Pan" fontSize="36" width="110"/>
    <s:Image source="@Embed('/gestures/pan.png')" width="137"/>
    <s:VGroup gap="10">
      <s:CheckBox id="gesturePanEnabled" content="Enabled" mouseEnabled="false"/>
      <s:CheckBox id="gesturePanTested" content="Tested" mouseEnabled="false"/>
    </s:VGroup>
  </s:HGroup>
  <s:HGroup verticalAlign="middle" gap="20">
    <s:Label text="Rotate" fontSize="36" width="110"/>
    <s:Image source="@Embed('/gestures/rotate.png')" width="137"/>
    <s:VGroup gap="10">
      <s:CheckBox id="gestureRotateEnabled" content="Enabled" mouseEnabled="false"/>
      <s:CheckBox id="gestureRotateTested" content="Tested" mouseEnabled="false"/>
    </s:VGroup>
  </s:HGroup>
  <s:HGroup verticalAlign="middle" gap="20">
    <s:Label text="Zoom" fontSize="36" width="110"/>
    <s:Image source="@Embed('/gestures/zoom.png')" width="137"/>
    <s:VGroup gap="10">
      <s:CheckBox id="gestureZoomEnabled" content="Enabled" mouseEnabled="false"/>
      <s:CheckBox id="gestureZoomTested" content="Tested" mouseEnabled="false"/>
    </s:VGroup>
  </s:HGroup>
  <s:HGroup verticalAlign="middle" gap="20">
    <s:Label text="Swipe" fontSize="36" width="110"/>
    <s:Image source="@Embed('/gestures/swipe.png')" width="137"/>
    <s:VGroup gap="10">
      <s:CheckBox id="gestureSwipeEnabled" content="Enabled" mouseEnabled="false"/>
      <s:CheckBox id="gestureSwipeTested" content="Tested" mouseEnabled="false"/>
    </s:VGroup>
  </s:HGroup>
  <s:HGroup verticalAlign="middle" gap="20">
    <s:Label text="Two-Finger Tap" fontSize="36" width="110"/>
    <s:Image source="@Embed('/gestures/twoFingerTap.png')" width="137"/>
    <s:VGroup gap="10">
      <s:CheckBox id="gestureTwoFingerTapEnabled"
                  content="Enabled" mouseEnabled="false"/>
      <s:CheckBox id="gestureTwoFingerTapTested"
                  content="Tested" mouseEnabled="false"/>
      </s:VGroup>
    </s:HGroup>
    <s:Label text="Graphics courtesy of GestureWorks.com" fontSize="12"/>
  </s:VGroup>
</s:View>`

如果您从 ADL 桌面模拟器测试此应用,您将会根据您的桌面手势支持获得不同的结果。对于不支持多点触摸的机器,不会启用任何手势;然而,如果你足够幸运,拥有一台带有支持多点触摸的触摸板的台式机或笔记本电脑,你或许可以对该应用进行一些有限的测试,如图 Figure 1–12 所示。

images

图 1–12。 在配有触控板的 MacBook Pro 上运行时,手势支持有限

虽然它报告五个手势中有四个是启用的,但在我们用来执行这个示例的计算机上,实际上只可能执行平移、旋转和缩放。正如我们将在下一节中看到的,在完全支持所有多点触摸手势的设备上运行它会有趣得多。

在设备上运行闪存

Flash Builder 使在移动设备上执行应用变得非常容易。只需点击一下,它就可以部署应用,在设备上启动它,甚至还可以连接一个远程调试器。

要在一个物理设备上运行你的应用,你需要确保它被正确设置用于 USB 调试。在大多数设备上,你可以通过进入设置image应用image开发来启用 USB 调试,在那里你会找到图 1–13 所示的选项。

images

图 1–13。 安卓开发设置屏幕

确保在此页面上启用了 USB 调试。您可能还想同时启用保持清醒的支持,这样您就不必在手机每次进入睡眠状态时都不断地重新登录。

一旦启用了 USB 调试,并且您已经通过 USB 电缆将手机连接到计算机,您的设备应该对 Flash Builder 可见。要切换到在设备上运行,请转到运行image运行配置 …,并返回参考图 1–8,您可以选择在设备上启动的选项。一旦选中,每次运行您的应用时,它都会在您连接的 Android 设备上启动,如 Figure 1–14 所示。

如你所见,在一个真实的设备上,锻炼所有的手势事件是可能的。当测试不同的设备以查看它们支持什么手势以及它们如何响应这些手势时,这个应用应该会派上用场。

images

图 1–14。 完成了在 Android 移动设备上运行的手势检查应用

如果您的 Android 手机无法连接到电脑,以下是您可以遵循的一些故障诊断步骤:

  • 确保您的设备通过 USB 连接到电脑。如果成功,您的 Android 设备上的通知区域会显示已通过 USB 连接。
  • 您还需要确保通过前面的步骤启用了设备调试。同样,当它正常工作时,会在通知区域列出。
  • 如果没有 USB 连接,可能是驱动程序的问题。麦金塔电脑不需要驱动程序;但是,在 Windows 上,您可能需要为您的手机安装一个特殊的 USB 驱动程序。
  • 这也可能是与电脑的连接有问题。尝试使用不同的电缆或不同的 USB 端口。
  • 如果您有 USB 连接,但设备调试不工作,您可能需要在 Android 设备上更改您的 USB 连接模式。寻找一个写着“充电模式”或“禁用大容量存储”的选项

如果您仍然遇到问题,您应该验证您的手机是否在您正在使用的 Flash Builder 版本的支持设备列表中,并与您的制造商联系以确保您拥有正确的驱动程序和设置。

从命令行运行应用

除了从 Flash Professional 和 Flash Builder 中运行之外,您还可以使用 AIR Debug Launcher (ADL)从命令行启动应用。如果您在没有工具支持的情况下直接使用 Flex,这也是您测试应用的方式。

要使用 ADL,您必须下载免费的 Flex 4.5 SDK,或者导航到 Flash Builder 安装的sdks/4.5.0文件夹。确保 Flex SDK 的 bin 文件夹在您的路径中,以便您可以调用 ADL 命令行工具。

ADL 工具的语法如下:

adl ( -runtime <runtime-dir> )? ( -pubid <pubid> )? -nodebug? ( -profile PROFILE )? ( - extdir <extensions-dir> )? ( -screensize SCREEN_SIZE )? <app-desc><root-dir>? ( -- … )?

ADL 支持许多可选参数,其中大部分是可选的。以下是对所有论点的简要描述,对移动开发重要的论点以粗体突出显示:

  • runtime:指定备用空气运行时间的可选参数;默认情况下,将使用 SDK 中包含的运行时。
  • pubid:用于指定应用 ID 的已弃用参数;您应该在应用描述符中使用 ID 标记。
  • nodebug:禁用调试支持,产生轻微的性能提升和更接近生产的应用行为
  • **profile**:您正在调试的应用的类型;对于移动开发,我们将使用mobileDevice概要文件。以下是完整的值列表:
    • mobileDevice, extendedMobileDevice, desktop, extendedDesktop, tv, extendedTV
  • extdir:action script 扩展的可选目录
  • **screensize**:应用窗口的大小,可以是 Table 1–3 中列出的关键字之一或以下格式的字符串:
    • <width>×<height>:<fullscreen width>×<fullscreen height>
  • **app-desc**:这是 ADL 运行时唯一需要的参数,应该引用您要执行的 AIR 程序的应用描述符。
  • 默认情况下,ADL 会将应用的根目录设置为存储应用描述符的文件夹,但是您可以通过将其设置为另一个路径来覆盖它。
  • (-):最后,您可以通过在两个破折号后添加参数来将参数传入您的应用。

image

image

要运行您之前开发的手势检查应用,请导航到根项目文件夹并执行以下命令:

adl -profile mobileDevice -screensize Droid bin-debug/GestureCheck-app.xml

这将使用摩托罗拉 Droid 的移动配置文件和屏幕大小在 AIR Debug Launcher 中执行手势检查应用。由于手势检查应用在其应用描述符中没有将全屏设置为 true,因此 ADL 使用的窗口大小将为 480×816。

执行后,您应该会得到与图 1–12 中的所示相同的结果,与您在 Flash Builder 中执行的早期运行相匹配。

总结

这是一个开始移动开发的激动人心的时刻。智能手机的采用,尤其是基于 Android 的设备,正在呈指数级增长,您最终可以使用具有完整创作工具支持的现代开发框架,如 Flash 和 Flex。

在第一章的短短时间里,你已经学会了如何做以下事情:

  • 使用 Flash Professional 和 Flex Builder 创建移动应用
  • 在 AIR 调试启动程序中运行应用
  • 通过 USB 连接在 Android 设备上部署和测试
  • 使用 Flash Builder 调试器诊断您的应用
  • 从命令行用不同的屏幕尺寸测试您的应用

这只是 Flash Android 移动开发的冰山一角。在接下来的章节中,我们将向您展示如何构建引人入胜、身临其境的 Flash 应用,充分利用所有移动功能。然后,我们演示如何将您的应用部署和发布到 Android Market。最后,我们将讨论一些高级主题,如原生 Android 集成、性能调优以及将您的应用扩展到平板电脑、电视等。*

二、针对移动配置文件的目标应用

与台式机相比,移动设备的资源非常有限。移动处理器正迅速赶上昨日台式机的速度,但内存和存储仍处于溢价状态。与此同时,用户希望移动应用能够瞬间启动,并对任何时候的硬崩溃或软崩溃具有完全的容错能力。

例如,为了节省内存资源,Android 操作系统可以随时选择关闭后台应用。当用户访问应用时,它依赖于最后已知的活动状态来启动它。如果应用重新启动的时间超过一秒钟,用户会注意到延迟,因为他们认为应用仍在后台运行。

虽然许多相同的概念适用于桌面应用开发,例如使用的工具和编程语言、可用的服务器通信协议以及可用于 UI 开发的控件和外观,但移动设备有一些独特的特征会影响 UI 和应用设计,例如屏幕大小、输入法和部署。

许多相同的空间、占用空间和启动时间的限制在 Web 上已经存在了很长时间。Flash 浏览器应用通常被期望适合一个有限的网页,快速下载,共享有限的计算资源,并即时启动。因此,您现有的 Flash 和 Flex 应用可能是移植到移动设备的良好候选。在本章中,我们将向您展示如何构建充分利用 Android 移动平台的应用。

屏幕尺寸

Android 是操作系统和软件栈,不是硬件平台。Google 提供了一个开源平台,包括一个修改过的 Linux 内核和基于 Java 的应用,可以在各种硬件平台上运行。然而,他们并不能控制运行 Android 的最终设备的确切特征。这意味着设备的确切配置变化很大,屏幕尺寸是分辨率、物理尺寸和像素密度有很大差异的一个方面。Table 2–1 显示了终端用户可能在其上运行您的应用的各种常见 Android 设备的屏幕特征。

images

在表 2–1 中,分辨率是水平和垂直方向的物理像素数,尺寸是屏幕的对角线尺寸,密度是每英寸的像素数(ppi)。Type 为屏幕分辨率提供了一个通用名称,它是下列之一:

  • QVGA(四分之一视频图形阵列) : 240×320 像素或 VGA 显示器(480×640)分辨率的四分之一
  • HVGA(半视频图形阵列) : 320×480 或 VGA 显示器分辨率的一半
  • WVGA(宽视频图形阵列) : 480×800,与 VGA 高度相同,但宽度为 800(横向观看时)
  • FWVGA(全宽视频图形阵列) : 480×854,与 VGA 高度相同,但比例为 16:9,用于显示未剪辑的高清视频
  • qHD(四分之一高清) : 540×960 或四分之一 1080p 显示屏,16:9 比例,用于显示未剪辑的高清视频

你的应用的可用区域也会因为 Android 状态栏的高度而减少。中密度显示器(如 HTC Hero)的条形高度为 25 像素,高密度显示器(如 Nexus One)的条形高度为 38 像素,超高密度显示器的条形高度为 50 像素。当显示器从纵向模式切换到横向模式时,这也会发生变化。比如 Nexus One 在人像模式下的可用面积是 480×762,而在风景模式下变成了 442×800。

您可能只有一两个设备需要测试,但这并不意味着您的应用不能支持所有的设备。Flash 可以自动缩放应用以适应屏幕大小,并且很容易获得屏幕分辨率来以编程方式修改界面。清单 2–1 展示了如何从 ActionScript 代码中检索屏幕分辨率和密度。

清单 2–1。 程序化屏幕分辨率和密度捕捉

var resY = Capabilities.screenResolutionX; var resX = Capabilities.screenResolutionY; var dpi = Capabilities.screenDPI; trace("Screen Resolution is " + resX + "x" + resY + " at " + dpi + "ppi");

注意:术语每英寸点数(dpi)和每英寸像素(ppi)是等价的度量。这些在整个 ActionScript APIs 中可以互换使用。

屏幕分辨率与密度

虽然应用开发人员更可能关注屏幕分辨率的差异,但屏幕密度也同样重要。您的应用需要能够扩展到更大或更小的设备,以便文本仍然可读,目标足够大以便操作。图 2–1 比较了几款不同特性手机的物理尺寸和屏幕分辨率。

虽然 Xperia X10 mini 的屏幕分辨率与 Nexus One 相比微不足道,但屏幕的物理尺寸仅小 30%。这意味着用户界面中的所有图形都需要大幅缩小以适合屏幕。另一方面,在为 Xperia X10 mini 构建时,由于像素太大,即使很小的目标也可以被用户轻松操纵。对于 Nexus One,你需要将目标做得更大。

在 2006 年完成的一项研究中,奥卢大学和马里兰大学的研究人员发现,用拇指操纵触摸屏的最小目标尺寸在 9.2 毫米到 9.6 毫米之间。


1 佩卡·帕尔希、艾米·k·卡尔森和本杰明·b·彼得森,“在小型触摸屏设备上单手使用拇指的目标尺寸研究”, http://portal.acm.org/citation.cfm?id=1152260 ,2006 年

images

图 2–1。 几种安卓设备的物理尺寸和分辨率

例如,为了实现触摸交互,你需要在 Xperia X10 mini 上将目标尺寸调整为 57 像素宽,或者在 Nexus One 上调整为 92 像素宽。通过调整用户界面的大小以考虑密度,您可以确保用户界面仍然可用,同时最大化活动设备的屏幕空间。

在 Flash 中模拟与设备无关的像素

Android 有一个与设备无关的像素的概念,可以用来做布局,即使显示器的物理大小不同,也会出现相似的布局。它基于 160 dpi 屏幕的参考平台,相当于每英寸大约一个 13×13 像素的正方形。如果你指定了一个 Android 布局,使用与设备无关的像素,平台会根据你的应用运行的设备自动调整。

Flash 没有设备无关像素的概念,但是用代码模拟非常容易。基本公式是 dips =像素* (160 /密度)。清单 2–2 演示了如何在 ActionScript 中进行计算。

清单 2–2。 ActionScript 函数计算设备无关像素(dips)

function pixelsToDips(pixels:int) {     return pixels * (160 / Capabilities.screenDPI); } trace("100 pixels = " + pixelsToDips(100) + " dips");

使用模拟的设备无关像素,您可以在 Flash 应用中重现与原生 Android 应用相似的布局行为。

如果您计划根据当前设备密度缩放应用图形,请确保您的应用没有设置为自动调整大小以填充屏幕或在旋转时居中显示内容。有关如何操作的更多信息,请参见本章后面的“Flash 中的自动方向翻转”一节。

Flex 应用中的密度

Flex 有内置的支持来缩放应用的用户界面,包括图形、字体和控件。它支持三种常见显示密度的离散比例因子,而不是任意缩放。Table 2–2 列出了所有不同的显示密度,以及用于为当前设备选择密度的映射 DPI 范围。

images

为了利用 Flex density 支持,将您的Application对象上的applicationDPI属性设置为应用最初设计的比例。在运行时,您的应用将根据设备屏幕的密度自动缩放。一个 240 dpi 的应用描述符的例子包含在清单 2–3 中。

清单 2–3。 应用描述符设置applicationDPI

<s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark" firstView="views.MainHomeView"     **applicationDPI="240"**> </s:ViewNavigatorApplication>

applicationDPI的唯一有效值是文本字符串“160”、“240”和“320”,对应于三种支持的密度。只能通过 MXML 设置applicationDPI属性。

根据作者密度与设备密度的比率,使用矢量图形和文本构建的应用部分会根据需要平滑地放大或缩小。就字体而言,调整字体大小,确保文本在任何显示器上都易于阅读。

位图图形也将被缩放,但放大时可能看起来模糊,缩小时可能会丢失细节。为了确保您的位图大小适合不同的密度,您可以通过使用MultiDPIBitmapSource类提供基于显示密度自动换入的替代图像。

密度浏览器应用

为了更好地了解密度如何影响您的 Flex 应用,我们将指导您创建密度浏览器应用。该应用允许您输入应用dpi和设备dpi作为参数,并计算将在不同设备上使用的灵活调整的设备密度和比例因子。

首先,使用移动应用模板创建一个名为“密度浏览器”的新 Flex 移动项目。这将自动生成一个标准项目模板,其中包括一个 Adobe AIR 应用描述符(DensityExplorer-app.xml)、一个 ViewNavigatorApplication ( DensityExplorer.mxml)和一个初始视图(DensityExplorerHomeView.mxml)。

第一步是打开DensityExplorerHomeView.mxml并添加一些控件,让你设置作者密度和设备 DPI。我们将在第五章中更详细地介绍 Flex 控件,但是对于这个应用来说,几个标签、单选按钮和一个水平滑块就足够了。

清单 2–4 显示了允许使用RadioButtonHSlider类输入作者密度和设备 dpi 的基本代码。

清单 2–4。 密度浏览器控件用于applicationDPIdeviceDPI条目

<fx:Script>   <![CDATA[     [Bindable]     protectedvarapplicationDPI:Number;     [Bindable]     publicvardeviceDPI:Number;   ]]> </fx:Script> <s:VGroup paddingTop="20" paddingLeft="15" paddingRight="15" paddingBottom="15"           gap="20" width="100%" height="100%">   <s:Label text="Application DPI:"/>   <s:HGroup gap="30">     <s:RadioButton id="ad160" content="160" click="applicationDPI = 160"/>     <s:RadioButton id="ad240" content="240" click="applicationDPI = 240"/>     <s:RadioButton id="ad320" content="320" click="applicationDPI = 320"/>   </s:HGroup>   <s:Label text="Device DPI: {deviceDPI}"/>   <s:HSlider id="dpiSlider" minimum="130" maximum="320" value="@{deviceDPI}"              width="100%"/> </s:VGroup>

首先,引入一些可绑定的脚本变量来保存applicationDPIdeviceDPI。这些并不是显示基本 UI 所必需的,但是它们将使以后连接输出部分变得更加容易。主控件在VGroup中垂直组织,而RadioButtons使用HGroup水平组织。

使用一个简单的click处理器将RadioButtons连接到applicationDPI。当滑块改变时,双向数据绑定表达式(前缀为@操作符)用于更新dpi的值。为了完成 UI 的这一部分,设备 dpi 文本包含一个对 DPI 的绑定引用,以便您可以在滑块的值发生变化时看到它。

运行这个程序会给你一个简单的 Flex 应用,如图 2–2 所示。您可以通过移动滑块来验证功能,这将更新deviceDPI设置。

images

图 2–2。 密度探索者第一部分:基本控件

此应用的目标是计算 Flex 将使用的调整后的设备密度和比例因子。幸运的是,有一个新的 Flex 4.5 API 可以通过 ActionScript 公开这些信息。我们需要调用的类叫做DensityUtil,可以在mx.utils包中找到。DensityUtil有两个带有以下签名的静态方法:

  • getRuntimeDPI():Number:该函数返回applicationDPI如果置位,否则当前运行时应用的 DPI 分类;它将始终返回下列值之一:160、240 或 320。
  • 在给定应用 DPI(源)和设备 DPI(目标)的情况下,此函数计算 Flex 将使用的比例。

除了这些函数,我们还需要知道当前的applicationDPI和设备 dpi 值,这样我们就可以填充 UI 控件的初始值。这些可以通过以下 API 进行查询:

  • Application.applicationDPI:对象上的成员变量,可以被查询以获得初始的applicationDPI
  • Capabilities.screenDPI:返回屏幕 dpi 数值的Capabilities对象的静态方法

利用这些 API,我们可以扩充之前的代码,添加初始化逻辑以及密度和规模的读数。清单 2–5 用粗体显示了更新后的代码。

清单 2–5。 更新了密度探测器代码,并进行初始化和输出

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:mx="library://ns.adobe.com/flex/mx"     xmlns:s="library://ns.adobe.com/flex/spark"     title="Density Explorer" **initialize="init()"**>   <fx:Script>     <![CDATA[       **import mx.binding.utils.BindingUtils;**       **import mx.utils.DensityUtil;**       [Bindable]       protectedvarapplicationDPI:Number;       [Bindable]       publicvardeviceDPI:Number;       **[Bindable]**       **protectedvardpiClassification:Number;**       **protectedfunction updateDensity(dpi:Number):void {**         **dpiClassification = dpi < 200 ? 160 : dpi >= 280 ? 320 : 240;**       **}**       **protectedfunction init():void {**         **applicationDPI= parentApplication.applicationDPI;**         **if (applicationDPI!= null) {**           **this["ad" + applicationDPI].selected = true;**         **}**         **BindingUtils.bindSetter(updateDensity, this, "deviceDPI");**         **deviceDPI= Capabilities.screenDPI;**       **}**     ]]>   </fx:Script>   <s:VGroup paddingTop="20" paddingLeft="15" paddingRight="15" paddingBottom="15"                     gap="20" width="100%" height="100%">     <s:Label text="ApplicationDPI:"/>     <s:HGroup gap="30">       <s:RadioButton id="ad160" content="160" click="applicationDPI= 160"/>       <s:RadioButton id="ad240" content="240" click="applicationDPI= 240"/>       <s:RadioButton id="ad320" content="320" click="applicationDPI= 320"/>     </s:HGroup>     <s:Label text="Device DPI: {dpi}"/>     <s:HSlider id="dpiSlider" minimum="130" maximum="320" value="@{deviceDPI}"                width="100%"/>     **<s:Group width="100%" height="100%">**       **<s:BorderContainer bottom="0" minHeight="0"width="100%" borderStyle="inset"**                  **backgroundColor="#d0d0d0"borderColor="#888888" backgroundAlpha=".6">**         **<s:layout>**           **<s:VerticalLayout gap="10"paddingLeft="10" paddingRight="10"**                     **paddingTop="10" paddingBottom="10"/>**         **</s:layout>**         **<s:Label text="Adjusted Device Density: {dpiClassification}"/>**         **<s:Label text="Scale Factor: {DensityUtil.getDPIScale(applicationDPI,**                                                            **dpiClassification)}"/>**       **</s:BorderContainer>**     **</s:Group>**   </s:VGroup> </s:View>

在由View.initialize调用的方法内部执行初始化,以确保所有值都可用。首先从parentApplication对象更新applicationDPI,并通过对返回的字符串执行 ID 查找来选择正确的RadioButton。接下来从Capabilities对象设置dpi。为了确保初始值赋值和滑块后续更新对dpi的所有更新都将重新计算deviceDensity,bind setter 被配置为触发对dpi的所有更新。

为了显示deviceDensity的当前值和计算出的刻度,在View的末端增加了一个带有几个LabelsBorderContainer。通过使用一个BorderContainer作为环绕组,很容易改变样式,使输出在视觉上不同于输入。

最后一步是添加一个额外的群组,它将随着dpi设置的更新而淡入设备图片。为了确保图像针对不同密度的显示器进行优化缩放,我们使用了一个MultiDPIBimapSource,它指的是不同的预缩放伪影。该代码如清单 2–6 所示。

清单 2–6。*【MXML】用于显示代表性设备图像的代码使用了 MultiDPIBitmapSource*

<s:Group id="phones" width="100%" height="100%">   <s:Image alpha="{1-Math.abs(deviceDPI-157)/20}" horizontalCenter="0">     <s:source>       <s:MultiDPIBitmapSource         source160dpi="@Embed('assets/xperia-x10-mini160.jpg')"         source240dpi="@Embed('assets/xperia-x10-mini240.jpg')"         source320dpi="@Embed('assets/xperia-x10-mini320.jpg')" />     </s:source>   </s:Image>   <s:Image alpha="{1-Math.abs(deviceDPI-180)/20}" horizontalCenter="0">     <s:source>       <s:MultiDPIBitmapSource         source160dpi="@Embed('assets/htc-hero160.jpg')"         source240dpi="@Embed('assets/htc-hero240.jpg')"         source320dpi="@Embed('assets/htc-hero320.jpg')" />     </s:source>   </s:Image>   <s:Image alpha="{1-Math.abs(deviceDPI-217)/20}" horizontalCenter="0">     <s:source>       <s:MultiDPIBitmapSource         source160dpi="@Embed('assets/htc-evo-4g160.jpg')"         source240dpi="@Embed('assets/htc-evo-4g240.jpg')"         source320dpi="@Embed('assets/htc-evo-4g320.jpg')" />     </s:source>   </s:Image>   <s:Image alpha="{1-Math.abs(deviceDPI-252)/20}" horizontalCenter="0">     <s:source>       <s:MultiDPIBitmapSource         source160dpi="@Embed('assets/nexus-one160.jpg')"         source240dpi="@Embed('assets/nexus-one240.jpg')"         source320dpi="@Embed('assets/nexus-one320.jpg')" />     </s:source>   </s:Image>   <s:Image alpha="{1-Math.abs(deviceDPI-275)/20}" horizontalCenter="0">     <s:source>       <s:MultiDPIBitmapSource         source160dpi="@Embed('assets/atrix160.jpg')"         source240dpi="@Embed('assets/atrix240.jpg')"         source320dpi="@Embed('assets/atrix320.jpg')" />     </s:source>   </s:Image> </s:Group>

所有选择的图片都是手机标准新闻图片的缩放版本。为了在接近 dpi 值时慢慢淡入设备,将一个简单的数学公式应用于 alpha:

1-Math.abs(deviceDPI-{physicalDPI})/{threshold}

对于每个电话,实际的dpi代替该设备的physicalDPI,并且阈值被设置为足够低的值,使得对于目标 dpi 值不会有两个电话重叠。对于所选择的设备,阈值 20 低于任何电话dpi值之间的差。

完成的密度浏览器应用如图 2–3 所示。这是试验应用 dpi 和设备 dpi 的不同值的好机会,以查看它们对您部署的应用的影响。

images

图 2–3。 完成了密度探测器的应用

为了进行对比,Figure 2–4 展示了在物理设备上以 160、240 和 320 dpi 运行的 Density Explorer 的屏幕截图。请注意,尽管屏幕的物理尺寸差异很大,但应用的布局和图形的质量都保持不变。通过将作者密度设置为 240,可以保证您的应用在任何密度的设备上都具有相同的外观和感觉,而无需修改代码。

images

图 2–4。 密度浏览器在分类为 160 dpi(左)、240 dpi(中)和 320 dpi(右)的设备上运行时的并排比较

CSS 中的密度支持

虽然 Flex 中的applicationDPI设置为您提供了一种简单的机制来为一种密度编写您的应用,并让 Flex 负责调整大小,但是当您在不同的设备上查看应用时,它并不能让您对应用的精确布局和样式进行细粒度的控制。将applicationDPI设置为常量对于简单的应用来说很好,但是随着 UI 复杂性的增加,这通常不够好。这就是 CSS 媒体查询的来源。

Flex 媒体查询允许您在 CSS 中对不同设备上的样式进行细粒度控制。它们基于 W3C CSS 媒体查询候选推荐标准, 2 ,但仅包含与 Flex 和移动应用最相关的功能的子集。

Flex 支持两种类型的选择器。第一种类型允许您根据设备类型选择样式。清单 2–7 中的代码演示了如何根据运行设备的类型来改变字体颜色。


2 www.w3.org/TR/css3-mediaqueries/

清单 2–7。 演示设备媒体选择器的代码示例

`@namespace s "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选择器的使用,我们将更新 Density Explorer 示例,使用样式表来替换图像,而不是将其嵌入到带有MultiDPIBitmapSource的代码中。应用图像的简化应用代码如清单 2–8 所示。

清单 2–8。 更新了 DensityExplorer 代码用于整合 CSS 媒体查询

<s:Group id="phones" width="100%" height="100%">   <s:Image alpha="{1-Math.abs(deviceDPI-157)/20}" horizontalCenter="0"         source="{phones.getStyle('xperiaX10Mini')}"/>   <s:Image alpha="{1-Math.abs(deviceDPI-180)/20}" horizontalCenter="0"         source="{phones.getStyle('htcHero')}"/>   <s:Image alpha="{1-Math.abs(deviceDPI-217)/20}" horizontalCenter="0"         source="{phones.getStyle('htcEvo4g')}"/>   <s:Image alpha="{1-Math.abs(deviceDPI-252)/20}" horizontalCenter="0"         source="{phones.getStyle('nexusOne')}"/>   <s:Image alpha="{1-Math.abs(deviceDPI-275)/20}" horizontalCenter="0"         source="{phones.getStyle('atrix')}"/> </s:Group>

注意,我们在父对象上使用了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 应用类对整合媒体查询的支持

<?xml version="1.0" encoding="utf-8"?> <s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"               xmlns:s="library://ns.adobe.com/flex/spark"               splashScreenImage="@Embed('ProAndroidFlash400.png')"               firstView="views.DensityExplorerHomeView">   <fx:Style source="DensityExplorer.css"/> </s:ViewNavigatorApplication>

在不同设备上运行该程序的输出几乎与之前在图 2–4 中的结果相同,只是间距略有不同。原因是 Flex control 团队也在他们的控件中放入了dpi提示,这样它们就可以根据目标设备自动调整大小,即使没有将applicationDPI固定为一个常量值。

既然你已经学习了 CSS 媒体选择器,你就有了一个强大的工具来从你的代码中提取样式,甚至是对密度敏感的应用。

屏幕方向

移动设备的一个独特之处是可以在手中旋转。在桌面世界中,这相当于将显示器翻转过来。虽然旋转桌面显示器有一些创造性的用途,如图 2–5 所示,但这肯定不是一个常见的用例。

images

图 2–5。 独特的利用监视器旋转来制造光弧 3

在移动设备中,旋转是一个重要的 UI 范例,它让您可以充分利用有限的屏幕空间。一个表现良好的移动应用应该在旋转时调整用户界面的大小,让用户停留在他或她喜欢的方向,通常显示一个完全不同的视图,这是为那个方向定制的。


3Tim prit love 拍摄的知识共享许可照片:
www.flickr.com/photos/timpritlove/123865627/

Flex 中的纵向/横向切换

要在 Flex 项目中打开自动定向,有两种方法。最方便的方法是在从标准模板创建新的 Flex 移动应用时,只需选中“自动重定向”复选框。图 2–6 显示了选中“自动重定向”选项的项目创建向导的屏幕截图。

images

图 2–6。 选中了“自动重定向”选项的 Flex builder 项目向导

如果您有一个现有的项目或者想要手动更改自动定向,您需要在应用描述符中设置autoOrients属性。应用描述符位于根项目目录中名为*-app.xml的文件中,其中的autoOrients属性应该被创建为initialWindow元素的子元素,并被设置为true,如清单 2–11 所示。

清单 2–11。 应用描述符更改,允许载物台自动定位

<initialWindow>   <content>[This value will be overwritten by Flash Builder in the output app.xml]</content>   **<autoOrients>true</autoOrients>** </initialWindow>

这会旋转舞台并调整其大小,还会引发事件,您可以通过监听这些事件来更改应用布局。

然而,简单地打开自动定向通常会产生不理想的结果。例如,如果您在 Density Explorer 应用上启用自动定向,用户界面的底部会被裁剪掉,如 Figure 2–7 所示。

images

图 2–7。

对于 Density Explorer 应用的横向方向,理想的布局是将手机图片放在控件的左侧。在 Flex 应用中有两种方法可以实现这一点。第一个是为动态改变布局的旋转事件添加一个事件处理程序。由于这是一种纯粹的 ActionScript 方法,它在 Flash Professional 中也同样适用。第二个是利用新的肖像和风景状态,这只能从 MXML 访问。在下面的小节中,我们将演示这两种方法。

随着事件进行纵向/横向切换

每次旋转 Flash 移动设备时,都会触发方向事件来通知任何侦听器。方向事件处理程序通过标准的addEventListener方法添加到Stage对象上。定向事件的事件类为StageOrientationEvent,事件类型为StageOrientationEvent.ORIENTATION_CHANGE

注意:StageOrientationEvent类上也有一个ORIENTATION_CHANGING事件类型,但这不适用于 Android 设备。

StageOrientationEvent有两个变量对处理方向变化特别有用:

  • beforeOrientation:当前旋转事件触发前的手机方向
  • afterOrientation:当前手机方向

将所有这些放在一起,您可以修改密度浏览器,以根据手机的方向改变布局。

第一步是更新声明性的 MXML UI,以包含一个额外的用于横向的HBox,用惟一的 id 命名外部的HBox和内部的VBox,并连接一个addedToStage事件监听器。完成这项工作的代码如清单 2–12 所示。

清单 2–12。 MXML 新增支持舞台布局变更

**<s:HGroup id="outer" width="100%" height="100%" addedToStage="stageInit()">**   <s:VGroup paddingTop="20" paddingLeft="15" paddingRight="15"                     paddingBottom="15" gap="20" width="100%" height="100%">     …     <s:Group **id="inner"** width="100%" height="100%">       <s:Group id="phones" width="100%" height="100%">         …   </s:VGroup> **</s:HGroup>**

下一步是实现stageInit函数来连接一个方向改变事件监听器。除了连接侦听器之外,用当前方向触发一个初始事件通常也很有帮助。这将确保即使您的应用以横向模式启动,它也将遵循相同的代码路径,就像它以纵向模式打开,然后被用户旋转一样。这个动作脚本显示在清单 2–13 中。

清单 2–13。 实现 stageInit orientationChange 功能

**protected function** 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**;   } } **protected function** stageInit():**void** {   stage.addEventListener(StageOrientationEvent.ORIENTATION_CHANGE, orientationChange);   orientationChange(**new** StageOrientationEvent(                                 StageOrientationEvent.ORIENTATION_CHANGE, **false**, **false**,                                 **null**, stage.orientation)); }

在这种情况下,向右和向左旋转的行为是相同的,尽管如果你想变得有创意,你可以根据手机旋转的方式将设备显示屏放在屏幕的不同侧。

运行修改后的密度浏览器应用的结果如图 2–8 所示。正如您所看到的,设备显示屏以可用的大小显示,其余的控件不再在很宽的显示屏上展开。最令人印象深刻的是,当你旋转手机时,布局会动态更新,以优化纵向和横向。

images

图 2–8。 改进了景观布局的密度浏览器应用

纵向/横向状态切换

在 Flex 中可以用于纵向/横向切换的第二种技术是利用两种内置状态,这两种状态在设备旋转时触发,称为portraitlandscape。虽然这只能从 MXML 访问,并且不像事件 API 那样提供那么多关于设备方向的信息,但实现这一点的代码更简单,本质上更具声明性,更易于阅读和维护。

要公开新的纵向和横向状态,您需要在视图代码中添加一个定义了portraitlandscape状态的states声明,如清单 2–14 所示。

清单 2–14。 状态声明为 portrait landscape 模式

<s:states>   <s:State name="portrait"/>   <s:State name="landscape"/> </s:states>

这些states将在设备改变方向时自动触发。要修改布局,您可以利用includedIn属性和Reparent标签来移动手机图像的位置。你需要做的代码修改如清单 2–15 所示。

清单 2–15。 当状态改变时,用户界面改变以恢复手机图像

**<s:HGroup width="100%" height="100%">**   **<fx:Reparent target="phones" includeIn="landscape" />**   …     <s:Group width="100%" height="100%">       <s:Group id="phones" width="100%" height="100%" **includeIn="portrait"**>         …   </s:VGroup> **</s:HGroup>**

最终结果是,用 8 行 MXML 代码就可以完成用事件方法需要 21 行代码才能完成的事情。运行该应用的结果与在图 2–8 中获得的结果相同。

Flash 中的自动方向翻转

Flash 应用也可以配置为在设备旋转时自动翻转舞台的方向。要在 Flash 项目中启用自动方向切换,需要勾选 Android 发布设置对话框的空中自动方向复选框,如图 Figure 2–9 所示。

images

图 2–9。??【闪光 CS5.5 自动定向设置】带圈

设置此选项将导致应用的纵横比在用户旋转设备时自动从横向翻转到纵向。方向改变时,载物台将旋转,使其垂直定向,旋转后调整大小以填充新尺寸,然后在显示器内居中。

如果您想要更改内容的布局以填充屏幕并完全控制舞台的大小,您需要禁用自动缩放和定位。这可以在 ActionScript 中通过改变Stage对象的scaleModealign属性来完成,如清单 2–16 所示。

清单 2–16。 从 ActionScript 中禁用舞台缩放和对齐

stage.scaleMode = StageScaleMode.NO_SCALE;  // turn off scaling stage.align = StageAlign.TOP_LEFT;  // align content to the top-left of the stage

这可以添加到应用启动时执行的任何关键帧中,并保持舞台左上角对齐,不调整内容的大小。然后,您可以在方向改变时添加事件侦听器,以便修改您的应用来适应屏幕大小。

旋转笑脸 Flash 定向示例

为了演示如何在 Flash CS5.5 中快速创建调整方向的内容,您将创建一个小的示例应用,该应用在方向改变时将一张快乐的笑脸图片变形为一张魔鬼图片。

首先,您需要创建一个新的 AIR for Android 项目,大小为 480×480 像素。选择与较小设备尺寸大小相等的正方形画布的原因是为了确保旋转时不会发生额外的缩放。

图 2–10 显示了快乐笑脸图片的开始状态,画出的方框表示横向和纵向模式的范围。两个框的交叉点是 480×480 的画布,附加图形水平和垂直溢出。当方向改变时,这些会被裁剪掉,使笑脸很好地居中。

images

图 2–10。 快乐笑脸图片起始状态,用方框显示横向和纵向范围

你可以随心所欲地创作自己的图形,但要将元素放在单独的图层中,这样便于以后制作动画和变形。

下一步是创建大约一秒钟的魔鬼笑脸关键帧。这应该包括从快乐的笑脸平滑过渡的运动或形状补间。图 2–11 显示了一些延时帧,这些帧显示了笑脸以及一些背景场景元素的动画。

images

图 2–11。 从快乐到恶魔般的笑脸的动画

同时,也创建反向动画,在大约两秒钟后回到快乐的笑脸。虽然有一些在 Flash 中反转动画的 ActionScript 技术,但是它们不直观,并且通常会带来性能损失。

现在您已经完成了图形,您需要添加 ActionScript 代码来在设备旋转时制作笑脸动画。这应该添加到时间轴中的第一个关键帧,并且应该包括停止动画自动播放和将方向事件监听器附加到场景,如清单 2–17 所示。

清单 2–17。 响应方向改变事件的 ActionScript 代码

`import flash.events.StageOrientationEvent;

stop();

stage.addEventListener(StageOrientationEvent.ORIENTATION_CHANGE, onChanged);

function onChanged(event:StageOrientationEvent):void {
  play();
}`

注意,onChanged事件处理程序只需在每个方向改变事件上播放电影。动画的所有重担都已经被时间线承担了。

所需代码的最后一点是在这个邪恶的笑脸框上添加一个stop()调用,这样它就会在一个旋转事件之后停止。

另外,如果用户的设备不支持定向事件,例如用户在桌面或电视上运行,您可以向用户添加警告。给出反馈的最简单方法是创建一个隐藏层,在检查方向支持的基础上使其可见,如清单 2–18 所示。

清单 2–18。 定位校验码隐藏/显示错误页面

if (Stage.supportsOrientationChange) {   orientationNotSupported.visible = false; }

在设备上运行的完整旋转笑脸应用如图 2–12 所示。虽然这个例子相当简单,但它展示了向应用添加方向支持是多么容易。

images

图 2–12。 完成纵向(左)和横向(右)旋转笑脸示例

多点触摸和手势

用户界面长期以来一直受到桌面鼠标的限制。第一只鼠标是道格拉斯·恩格尔巴特在 1963 年发明的。它有两个垂直的轮子用于跟踪运动,还有一条长绳子,类似于啮齿动物或老鼠的尾巴。在这之后,出现了内置球、光学跟踪和多个按钮的鼠标,如 80 年代初生产的 Dépraz 鼠标。这两种早期装置都显示在图 2–13 中。

images

图 2–13。 恩格尔巴特鼠(右下)和德普拉兹鼠(左上)的照片

现代鼠标包括诸如滚轮、激光跟踪和无线操作等功能。然而,所有的鼠标都有一个共同的限制,那就是它们一次只能在屏幕上支持一个光标。

移动界面最初也有指针驱动的单点触摸交互的限制。然而,它们已经进化到利用人类的体质。我们有两只手,总共十个手指,每个手指都能够单独与设备上的触摸点进行交互和操作。

大多数 Android 设备支持至少两个同步触摸点,这是处理所有移动手势所需的最低要求。这也是移动设备由手指支撑并用两个拇指操作的最常见的使用场景。然而,支持几乎无限数量的接触点的新设备正在被引入。

您可以通过查询Multitouch对象来检索您的设备支持的触摸点数量,如清单 2–19 所示。

清单 2–19。 通过 ActionScript 检索触摸点数

trace("Max Touch Points: " + Multitouch.maxTouchPoints);

在本节中,您将学习如何利用多点触摸和用户手势,改善用户体验和 Flash 应用的可用性。

移动手势

使用多点触控最简单的方法是利用 Flash 支持的预定义手势。对于任何至少有两个触摸点的 Android 设备,您将能够使用表 2–3 中的手势。

images


4 由 Gestureworks ( www.gestureworks.com )提供的知识共享许可插图

images

在不支持手势的情况下,在应用中提供一些其他机制来完成相同的行为通常是一个好主意;然而,移动用户已经开始期待手势提供的便利和速度,因此在应用设计中适当地映射它们是很重要的。

要发现运行您的应用的设备是否支持手势,并动态查询手势功能,您可以在Multitouch类上调用以下静态方法:

  • Multitouch.supportsGestureEvents:您运行的设备是否支持发射手势事件
  • Multitouch.supportedGestures:字符串列表,每个支持的多点触摸事件对应一个字符串

在使用手势事件之前,需要将touchMode设置为手势输入模式,如清单 2–20 所示。

清单 2–20。 启用手势识别支持的动作脚本代码

Multitouch.inputMode = MultitouchInputMode.GESTURE;

在您期望接收手势事件之前,应该在您的程序中调用它。

Flash 剪贴簿示例

为了演示如何处理手势事件,我们将通过一个示例程序来构建一个响应多点触摸事件的图片剪贴簿。为简单起见,我们将图像作为资源加载(参见第七章了解更多关于从相机胶卷中检索图像的信息)。

以下是我们将支持的一些多点触摸事件:

  • 缩放:放大或缩小图片
  • 旋转:将图像旋转到任意角度
  • 平移:将整页图像作为一个单元移动
  • 滑动:滑动标题,在图像页面之间切换
  • 双指轻击:用两个手指轻击图像,在自己的视图中打开它

此外,虽然它不是多点触摸手势,但我们将挂接拖动监听器,以便您可以通过用单个手指拖动图像来定位它们。

在 Flash 应用中有两种不同的方法来连接多点触摸监听器。第一种是通过纯 ActionScript,在基于 Flash 或 Flex 的应用中同样适用。第二种是通过使用InteractiveObject类上的事件挂钩,如果您正在使用 Flex 组件,这是最方便的选择。我们将在这一部分展示两者的例子。

缩放和旋转手势处理

Flash 剪贴簿示例的核心将是一个MultitouchImage组件,它扩展了 spark Image类以添加大小调整和旋转功能。对于这个类,我们将使用addEventListener机制来连接缩放和旋转手势的多点触摸监听器。代码如清单 2–21 所示。

清单 2–21。 MultitouchImage 类添加旋转和调整大小支持

`package com.proandroidflash {
  import flash.events.TransformGestureEvent;
  import flash.geom.Point;
  import flash.ui.Multitouch;
  import flash.ui.MultitouchInputMode;
  import mx.events.ResizeEvent;
  import spark.components.Image;

publicclassMultitouchImageextends Image {
    publicfunctionMultitouchImage() {
      addEventListener(ResizeEvent.RESIZE, resizeListener);
      addEventListener(TransformGestureEvent.GESTURE_ROTATE, rotateListener);
      addEventListener(TransformGestureEvent.GESTURE_ZOOM, zoomListener);
      Multitouch.inputMode = MultitouchInputMode.GESTURE;
    }     protectedfunction resizeListener(e:ResizeEvent):void {
      transformX = width/2;
      transformY = height/2;
    }

protectedfunction rotateListener(e:TransformGestureEvent):void {
      rotation += e.rotation;
    }

protectedfunction zoomListener(e:TransformGestureEvent):void {
      scaleX *= e.scaleX;
      scaleY *= e.scaleY;
    }
  }
}`

在构造函数中,我们通过调用addEventListener方法并传入GESTURE_ROTATEGESTURE_ZOOM常量来添加旋转和缩放监听器。旋转回调简单地获取TransformGestureEvent的旋转参数,并将其添加到节点的当前旋转中,将值保存回来。在这两种情况下,旋转都以度数表示为数值。缩放监听器类似地获取TransformGestureEventscaleXscaleY参数,并将它们乘以节点的scaleXscaleY以获得新值。在这两种情况下,手势事件为您提供了自上次调用侦听器以来的增量,因此您可以简单地增量调整节点值。

为了确保旋转和缩放发生在图像节点的中心,我们将transformXtransformY设置在resizeListener中节点的中点。这也是通过构造函数中的addEventListener连接的,因此每次节点大小改变时都会触发。

我们做的最后一件事是将inputMode设置为MultitouchInputMode.GESTURE,这样事件监听器将被触发。根据我们的需要频繁地设置这个值是安全的,所以我们在每次调用构造函数时都这样做。

对于其余的手势事件,我们将利用InteractiveObject事件挂钩,通过 MXML 轻松连接;然而,你也可以通过遵循表 2–4 中的类和常量信息,使用addEventListener机制连接所有其他手势。

images

按住并拖动鼠标事件

我们将使用的另一个助手类是DraggableGroup类。这实现了一个标准的指向和拖动隐喻,作为 spark 组的扩展,如清单 2–22 所示。除了有利于封装之外,从手势事件中提取鼠标事件允许您同时处理多个事件。

清单 2–22。 DraggableGroup 实现点和拖动力学的类

`package com.proandroidflash {
  import flash.events.MouseEvent;
  import mx.core.UIComponent;
  import spark.components.Form;
  import spark.components.Group;

public class DraggableGroup extends Group {
    public function DraggableGroup() {
      mouseEnabledWhereTransparent = false;
      addEventListener(MouseEvent.MOUSE_DOWN, mouseDownListener);
      addEventListener(MouseEvent.MOUSE_UP, mouseUpListener);
    }

protected function mouseDownListener(e:MouseEvent):void {
      (parent as Group).setElementIndex(this, parent.numChildren-1);
      startDrag();
    }

protected function mouseUpListener(e:MouseEvent):void {
       stopDrag();
       // fix for bug in Flex where child elements don't get invalidated
       for (var i:int=0; i<numElements; i++) {
         (getElementAt(i) as UIComponent).invalidateProperties();
       }
     }
   }
}`

DraggableGroup的代码是一个相当简单的 Flex 组件实现,使用与手势代码相同的addEventListener/回调范例。虽然您可以使用触摸事件实现相同的代码,但是坚持使用鼠标抬起和鼠标放下事件的好处是,即使在没有触摸支持的情况下,UI 的这一部分也可以工作。

代码中的一些微妙之处值得指出。

  • 默认情况下,Group 类为其边界区域中的任何单击触发事件。通过将mouseEnabledWhereTransparent设置为false,您可以避免孩子范围之外的误点火。
  • 要在单击时引发该对象,需要更改父容器中的顺序。这个实现假设父节点是一个Group,并使用setElementIndex函数将这个节点推到前面。
  • Flex 中有一个 bug,在拖动后子元素的布局属性不会失效。在所有子节点上手动调用invalidateProperties可以解决这个问题。例如,如果没有此修复,您会注意到旋转/缩放的中心不会随着拖动而平移。
滑动手势处理

为了显示图像,我们将使用一个简单的视图,将各个图像页面的呈现委托给一个ItemRenderer。首先,我们将看看组成剪贴簿示例主屏幕的View类。完整的代码如清单 2–23 所示。

清单 2–23。 Flash 剪贴簿主视图代码,滑动事件处理程序以粗体突出显示

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark"     xmlns:mx="library://ns.adobe.com/flex/mx"     xmlns:proandroidflash="com.proandroidflash.*"     title="Home" backgroundColor="#333333" destructionPolicy="never">   <fx:Script>     <![CDATA[       import mx.core.IVisualElement;       [Bindable]       protected var page:int = 0;       **protected function swipe(e:TransformGestureEvent):void** {         page = (page + e.offsetX + pages.numElements) % pages.numElements;         updateVisibility();       }       protected function updateVisibility():void {         for (var i:int=0; i<pages.numElements; i++) {           var element:IVisualElement = pages.getElementAt(i);           if (element != null) {             element.visible = i == page;           }         }      }   ]]> </fx:Script> <s:layout>   <s:VerticalLayout horizontalAlign="center" paddingTop="10"                     paddingLeft="10" paddingRight="10" paddingBottom="10"/> </s:layout> <fx:Declarations>   <s:ArrayList id="images">     <fx:Object image1="@Embed(source='img/cassandra1.jpg')"                      image2="@Embed(source='img/cassandra2.jpg')"                      image3="@Embed(source='img/cassandra3.jpg')"/>     <fx:Object image1="@Embed(source='img/cassandra4.jpg')"                      image2="@Embed(source='img/cassandra5.jpg')"                      image3="@Embed(source='img/cassandra6.jpg')"/>     <fx:Object image1="@Embed(source='img/cassandra7.jpg')"                      image2="@Embed(source='img/cassandra8.jpg')"                      image3="@Embed(source='img/cassandra9.jpg')"/>     <fx:Object image1="@Embed(source='img/cassandra10.jpg')"/>   </s:ArrayList> </fx:Declarations> <s:VGroup **gestureSwipe="swipe(event)"**>   <s:Label text="Flash Scrapbook" fontSize="32" color="white"/>   <s:Label text="Drag, Rotate, and Zoom with your fingers." fontSize="14"            color="#aaaaaa"/> </s:VGroup> <s:DataGroup id="pages" itemRenderer="com.proandroidflash.ScrapbookPage"              dataProvider="{images}" width="100%" height="100%"              added="updateVisibility()"/> </s:View>

虽然有相当多的代码来创建视图,但是实际的视图定义本身只包含五行来创建标题的VGroup和显示图像的DataGroup。剩下的主要是一大块代码,用于简单地将嵌入的图像加载到一个ArrayList中,以及一些嵌入在脚本标签中的辅助函数。

挂钩 swipe 事件处理程序所需的代码以粗体突出显示。通过利用InteractiveObject上的gesture*事件属性,您可以像这样快速地将事件监听器连接到您的应用。每当用户滑动标题 VGroup 时,将调用 protected swipe 方法,其中包含滑动方向的信息。与旋转和缩放事件在手势持续期间不断调用不同,swipe 在手势结束时只被调用一次。您可以通过检查offsetXoffsetY属性来解读滑动的方向。

  • offsetX=0, offsetY=-1: Swipe Up
  • offsetX=0, offsetY=1: Swipe Down
  • offsetX=-1, offsetY=0: Swipe Left
  • offsetX=1, offsetY=0: Swipe Right

值得注意的是,刷卡将总是在水平或垂直方向。不支持对角线滑动,不会被识别为手势。此外,您需要确保您将滑动监听器钩到的组件足够宽或足够高,以提供足够的移动距离来识别手势。一旦用户的手指离开组件,手势识别就会结束。

添加平移和双指轻敲事件监听器

现在我们有了一个带有DataGroup的主页,我们需要实现引用的ItemRenderer来构建剪贴簿页面。这也将是MultitouchImage, DraggableGroup和我们之前定义的主视图之间的链接。

首先创建一个新的 MXML 文件,该文件有一个外部的ItemRenderer元素,其中声明了页面内容。在这个类中,我们将连接两个新的手势事件监听器。在外部的BorderContainer上,我们将连接一个平移事件监听器,这样用户就可以用一个手势拖动整个页面和页面上的所有图像。此外,在每个MultitouchImages上,我们将添加一个双指点击事件监听器,用于切换到全屏视图。

剪贴簿页面实现的完整代码如清单 2–24 所示。

清单 2–24Flash ScrapbookPage项目渲染器代码,平移和双指点击事件处理程序以粗体突出显示

<?xml version="1.0" encoding="utf-8"?> <s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"         xmlns:s="library://ns.adobe.com/flex/spark"         xmlns:proandroidflash="com.proandroidflash.*"         autoDrawBackground="false" width="100%" height="100%">   <s:BorderContainer backgroundColor="#cccccc" borderColor="#555555"                      **gesturePan="pan(event)"** rotation="5" x="50" width="100%"                      height="100%">     <fx:Script>       <![CDATA[         import spark.components.View;         import views.ImageView;         **protected function pan(e:TransformGestureEvent):void {**           **e.target.x += e.offsetX;**           **e.target.y += e.offsetY;**         **}**         **protected function expand(source:Object):void {**           **(parentDocument as View).navigator.pushView(ImageView, source);**         **}**       ]]>     </fx:Script>     <proandroidflash:DraggableGroup>       <proandroidflash:MultitouchImage source="{data.image1}" y="-70" x="10" width="350"                                rotation="-3" **gestureTwoFingerTap="expand(data.image1)**"/>     </proandroidflash:DraggableGroup>     <proandroidflash:DraggableGroup>       <proandroidflash:MultitouchImage source="{data.image2}" y="100" x="40" width="350"                                rotation="13" **gestureTwoFingerTap="expand(data.image2)**"/>     </proandroidflash:DraggableGroup>     <proandroidflash:DraggableGroup>   <proandroidflash:MultitouchImage source="{data.image3}" y="300" x="5" width="350"                                rotation="-8" **gestureTwoFingerTap="expand(data.image3)**"/>     </proandroidflash:DraggableGroup>   </s:BorderContainer> </s:ItemRenderer>

平移和双指轻敲事件侦听器的连接方式与我们之前连接滑动事件侦听器的方式类似。平移手势碰巧使用了与滑动手势相同的offsetXoffsetY变量,但是具有非常不同的含义。平移事件在用户手势的持续时间内不断被调用,并为offsetXoffsetY传递以像素为单位的增量。

对于双指点击手势,我们选择不传入事件,而是替换为包含要显示的图像的上下文相关变量。然后,这作为ViewNavigator's pushView方法的数据参数传入。

Flash 剪贴簿图片查看

最后一步是实现在pushView方法调用中引用的ImageView类。因为 Flex 为我们处理了所有的视图导航逻辑,所以实现非常简单。我们增加的唯一额外功能是另一个双指轻击手势,这样你就可以导航回主视图,而不用点击 Android 的后退按钮。

ImageView类的代码如清单 2–25 所示。

清单 2–25。 ImageView 代码,用粗体字突出显示的双指点击事件处理程序

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark"     title="Image Viewer" backgroundColor="#333333">   <s:Image source="{data}" width="100%" height="100%"            **gestureTwoFingerTap="navigator.popView**()"/> </s:View>

简单表达式可以内联在事件处理程序中,就像我们在这里所做的那样。这避免了创建Script标签的需要,使得代码清单非常简洁。

这也完成了 Flash 剪贴簿应用的最后一个文件,所以你现在可以给它一个测试驱动器。启动应用后,您应该会看到类似于图 2–14 所示的屏幕。

在应用的此页面上,尝试执行以下操作:

  • 通过用一个手指按住并拖动来在画布上拖动图像—这将练习DraggableGroup
  • 用两个手指按下并向相反的方向拖动来放大图像——这可以锻炼MultitouchImage上的缩放收听者。
  • 用两个手指按下并在圆圈中拖动来旋转图像——这将练习MultitouchImage上的旋转监听器。
  • 用一个手指在“Flash 剪贴簿”上水平滑动以改变页面,这可以锻炼主视图上的滑动听者。
  • 将图像拖到一边,这样你就可以看到背景,并用两个手指拖过背景,平移场景——这练习了ScrapbookPage上的平移监听器。
  • 用两个手指点击其中一个图像并切换到ImageView—这将练习连接到每个MultitouchImage的两个手指点击监听器。

images

图 2–14。 首页查看页面完成 Flash 剪贴簿申请

完成最后一步后,您将进入应用的ImageView页面,如图图 2–15 所示。要返回主视图,你可以用两个手指再次点击图像,或者使用 Android 的后退按钮。

images

图 2–15。 完成了图片浏览器页面上的 Flash 剪贴簿示例

通过完成这个简单的示例,您已经成功探索了 Flash Android 上所有可用的手势事件。尝试在自己的应用中以创新的方式使用这些手势!

接触点 API

处理多点触摸输入的另一种方法是使用触摸点 API 来直接访问设备上生成的事件。这使您可以根据自己的应用需求进行定制的多点触摸处理。要确定您的设备是否支持触摸事件处理,您可以查询Multitouch对象,如清单 2–26 所示。

清单 2–26。 打印出是否支持触摸事件的代码片段

trace("Supports touch events: " + Multitouch.supportsTouchEvents);

由于处理触摸事件与手势识别直接冲突,您需要更改应用的输入模式来开始接收触摸点事件。这可以通过将Multitouch.inputMode变量设置为TOUCH_POINT来实现,如清单 2–27 所示。

清单 2–27。 启用触摸点事件的代码片段

Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT;

注意:inputMode设置为TOUCH_POINT将禁止识别任何手势,如缩放、旋转和平移。

在被调度的事件的数量和类型方面,接触点 API 是相当低级的。您可以注册并监听表 2–5 中列出的任何事件,只要目标对象扩展InteractiveObject

images

大多数触摸事件都是不言自明的,但是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事件。

记住这些如何工作的最简单的方法是将滚动事件与滚动效果的实现相关联。对于翻转效果,如果节点或其任何子节点被触摸,您通常希望显示该效果,这是touchRollOver的语义。

卡特彼勒发电机示例

作为触摸点 API 的一个简单示例,我们将指导您如何创建一个支持多点触摸的 Flash 应用,当您在屏幕上拖动手指时,该应用会生成毛虫。

首先,我们将创建一些可用于构建示例的艺术素材:

  • 背景:创建一个名为 Background 的图层,作为应用的背景。我们选择使用图案画笔来绘制一个供毛毛虫爬行的虚拟花园,但要尽可能有创意。
  • 绿球:制作一个名为绿球的简单电影剪辑,它将组成毛毛虫的身体。为此,我们在一个椭圆形的图元上做了一个简单的径向渐变。
  • Blue Ball: 制作一个名为 Blue Ball 的简单电影剪辑,它将组成毛毛虫的备用身体。为此,我们在椭圆上做了另一个径向渐变。
  • 红球:创建一个名为红球的电影剪辑,其中包含一系列将在毛毛虫上显示的面孔。确保在每一帧上编码一个stop(),这样我们就可以一次一个地浏览它们。

应用逻辑的机制非常简单。当用户在屏幕上拖动他或她的手指时,我们将在当前的一个或多个触摸位置为毛虫的身体不断创建新的球对象。一旦用户的手指离开屏幕,我们将绘制毛毛虫的头部。此外,如果用户点击其中一个卡特彼勒头,我们将播放电影来改变所显示的面孔。

为了完成这个,我们需要引入TouchEvent类,它由每个触摸事件回调函数返回。TouchEvent上与本例相关的变量包括:

  • stageX/stageY:在全局坐标中指定的触摸事件发生的位置;要获得相对于当前Sprite,的位置,请使用localX/localY来代替。
  • pressure:显示屏上使用的压力量(通常与尺寸有关);这是依赖于设备的,所以你不能指望它在所有设备上都可用。
  • 正在交互的对象
  • isPrimaryTouchPoint:这是注册的第一个触摸点还是后来添加的附加触摸点;我们将用这个给毛毛虫涂上不同的颜色。

卡特彼勒发电机应用的完整代码列表如列表 2–28 所示。你可能想把它放在一个叫做 Actions 的单独层的第一帧,以区别于程序中的图形元素。

清单 2–28。 卡特彼勒发电机应用的代码列表

`import flash.ui.Multitouch;
import flash.ui.MultitouchInputMode;
import flash.events.TouchEvent;
import flash.events.KeyboardEvent;
import flash.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);

var lastScale:Number;
var startX:Number;
var startY:Number;

function beginListener(event:TouchEvent):void {
  lastScale = 0;
}

function moveListener(event:TouchEvent):void {
  var ball;
  if (event.isPrimaryTouchPoint) {
    ball = new GreenBall();
  } else {
    ball = new BlueBall();
  }
  ball.x = event.stageX;
  ball.y = event.stageY;
  lastScale = Math.max(lastScale, event.pressure*7);
  ball.scaleX = lastScale;
  ball.scaleY = lastScale;
  addChild(ball);
}

function endListener(event:TouchEvent):void {
  var ball = new RedBall();
  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);
}

function ballMoveListener(event:TouchEvent):void {
  event.stopImmediatePropagation();
}

function changeFace(event:TouchEvent):void {
  event.target.play();
}

function keyListener(event:KeyboardEvent):void {
  if (event.keyCode = Keyboard.MENU) {
    clearAll();
  }
}

function clearAll():void {
  for (var i:int=numChildren-1; i>=0; i--) {
    if (getChildAt(i).name != "background") {
      removeChildAt(i);
    }
  }
}`

注意,我们将事件监听器添加到了Stage中,而不是背景中。这样做的原因是,当在手指下添加额外的节点以组成卡特彼勒身体时,它们会遮挡背景,从而防止触发额外的移动事件。但是,stage 会接收所有事件,而不管它们发生在哪个对象中。

Stage添加事件侦听器是一把双刃剑,因为这也意味着接收任何点击事件极其困难。为了防止 caterpillar 脸上的移动事件蔓延到舞台,我们称之为event.stopImmediatePropogation().,这允许在不受舞台事件监听器干扰的情况下处理点击手势。

我们使用的另一个技术是通过使用Math.max函数来确保每个后续添加的球都比前一个大。这确保了即使当用户将他或她的手指从屏幕上移开时压力减小,卡特彼勒视角也保持不变。

在设备上运行时,最终应用看起来应该类似于图 2–16。

images

图 2–16。??【毛虫发电机】应用显示杂草中的几条毛虫

这个应用也可以作为一个粗略的性能测试,因为它不断地生成Sprites并将其添加到 stage 中。您可以通过按下手机上的菜单按钮随时清除场景和重置应用,该按钮已连接到clearAll功能。

尝试为pressure使用不同的乘数来调整应用,以在您的设备上获得最佳性能。

总结

在本章中,您学习了如何设计和构建充分利用移动平台的应用。您将能够在未来的移动项目中应用的一些要点包括:

  • 不仅要考虑屏幕分辨率,还要考虑密度
  • 如何计算设备无关像素并利用applicationDPI
  • 为纵向和横向模式定制您的应用布局
  • 通过多点触控手势提高应用的可用性
  • 如何在应用中使用和处理原始触摸事件

我们将在整本书中继续使用这些概念来构建更加复杂和强大的应用,但是您应该已经有了一个设计自己的移动用户界面的良好开端。

三、为 Android 构建 Flash 和 Flex 应用

第一章和第二章介绍了如何使用 Flash 和 Flex 作为创建移动应用的平台。现在,您已经了解了选择 Flash 平台的原因,以及为使用触摸手势作为主要用户输入形式的各种屏幕设备编写应用时需要考虑的一些事项。下一步是着手编写自己的应用。在本章结束时,您将知道如何在各种类型的 Flex 应用之间做出选择,如何编写自己的View以及如何使用 Flex SDK 中的移动就绪控件为这些View提供丰富的内容。

简而言之,是时候向您展示将您的应用想法变为现实所需的一切了!

用 Flex 构建移动用户界面

由于其便利性和开发人员生产力特性,MXML 是定义 Flex 移动应用主用户界面的首选方式。然而,MXML 的便利性是以性能为代价的。因此,有些任务,比如List项目渲染器,最好在纯 ActionScript 中完成。我们将在第十章中更深入地讨论这个特殊的话题。

由于屏幕尺寸较小,大多数移动应用被分成多个View应用。因此,大多数 Android 应用的 AIR 都是用ViewNavigatorApplicationTabbedViewNavigatorApplication构建的也就不足为奇了。这些应用容器负责初始化和连接所有与应用View相关的东西。这包括一个或多个以下组件:

  • ViewNavigator:这个类处理将一组View链接在一起,来回传递数据,并在View之间转换。ViewNavigator还拥有应用的ActionBar,它显示当前View的标题、可选的动作控件(通常是按钮)和可选的导航控件(通常是 Home 或 back 按钮)。
  • 这些 Flex 组件提供了应用的大部分实际接口。每个View都有一个对其ViewNavigatorActionBar的引用。每个View可以用自己的内容填充ActionBar,甚至完全隐藏它。View s 根据用户交互使用ViewNavigator触发其他View s 的显示。

图 3–1 显示了纵向和横向手机的基本构造ViewNavigatorApplication。源代码可以在位于本书示例代码的chapter-03目录中的 ViewAppAnatomy 示例项目中找到。

images

图 3–1。 屈曲移动的基本解剖ViewNavigatorApplication

在图 3–1 中,应用的ActionBar在屏幕顶部伸展。由三个区域组成:导航区域、标题区域和动作区域。图 3–1 中的ActionBarActionBar导航区域包含一个标记为 Nav 的按钮,而标题区域显示的是“ActionBar字符串。ActionBar的操作区包含两个标记为 A1 和 A2 的按钮。View的内容区域由ActionBar下方的屏幕其余部分组成。请记住,尽管View使用ActionBar来显示它的标题和控件,但这两者在组件层次结构中是兄弟。除非ViewNavigatoroverlayControls属性设置为true,否则Viewwidthheight不包括ActionBar所占的面积。如果overlayControls设置为true,那么ActionBar以及TabbedViewNavigatorApplication的标签栏将部分透明,这样它们下面的任何View内容都是可见的。

作为这种基于View的应用结构的替代方案,您也可以从一个普通的Application MXML 文件开始创建一个完全定制的界面,就像您对基于 web 或基于桌面的应用所做的那样。

如果您使用的是 Flash Builder 4.5,您可以单击应用的文件菜单并选择新建images Flex 移动项目。在你命名你的新项目并点击 Next 按钮后,你可以选择从一个普通的旧Application、一个ViewNavigatorApplication或者一个TabbedViewNavigatorApplication开始。我们将在接下来的几节中研究这三种选择之间的区别。

查看导航应用

ViewNavigatorApplication创建一个单一的ViewNavigator来管理整个移动应用的View之间的转换。应用容器还有一个firstView属性,该属性决定应用启动时将显示哪个View组件。清单 3–1 显示了一个非常基本的ViewNavigatorApplication的代码。这段代码来自本书示例代码的examples/chapter-03目录中的 HelloView 示例项目。

清单 3–1。 一个简单的开始:你的第一个 Flex 移动ViewNavigatorApplication

`

<s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                            xmlns:s="library://ns.adobe.com/flex/spark"
                            splashScreenImage="@Embed('assets/splash.png')"
                            firstView="views.FirstView">
</s:ViewNavigatorApplication>

<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Hello World">

<s:VGroup width="100%" horizontalAlign="center" gap="20" top="20" left="10"
            right="10">
    <s:Label text="This is a ViewNavigatorApplication." width="100%"
             textAlign="center"/>
    <s:Button label="Next View" click="navigator.pushView(SecondView)"/>
  </s:VGroup>
</s:View>

<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Hello Again">   <s:Label text="Press the back button to return to the first view." top="20"
                    left="10" right="10"/>
</s:View>`

太神奇了!在大约 20 行代码中,我们有了一个全功能的移动应用,它有多个View和它们之间的动画转换。这就是为什么 Flex 是移动开发的一个令人信服的选择。Adobe 的团队让您可以轻松快速地开始开发 Android 移动应用。

使用 XML 属性将ViewNavigatorApplication容器的firstView属性设置为views.FirstView。创建项目时,Flash Builder 会在应用的默认包下创建一个视图包。所以views.FirstViewFirstView.mxml文件的全限定路径名。此外,我们已经在我们的ViewNavigatorApplication上指定了一个splashScreenImage属性。这通常是一个好主意,因为移动应用有时需要一段时间才能启动。@Embed指令将这个图像嵌入到应用中,这样它就可以在启动时显示。

在清单 3–1 中的应用容器的正下方显示了FirstView的源代码。文件中的根组件是一个 Spark View组件。<s:View>标签的title属性指定了当View被激活时将在ActionBar中显示的字符串。像任何 MXML 文件一样,View标签的子元素指定了构成View用户界面的各种 Spark 组件。在这种情况下,我们有一个火花LabelButton布置在一个垂直组或VGroup内。

注意,Buttonclick事件处理程序调用了navigator对象的pushView函数,这是View对应用的ViewNavigator实例的引用。这个方法的第一个参数是应该显示的View的类名。在这种情况下,我们告诉navigator接下来显示SecondView。而SecondView则只是简单的指示用户使用 Android 内置的“返回”按钮返回到FirstView。在SecondView的代码中没有对navigator对象进行显式调用。这是可能的,因为ViewNavigator自动添加一个监听器到 Android 的后退按钮。由于ViewNavigator还维护一个已经显示的View的堆栈,它可以从堆栈中弹出最近的View并返回到前一个View以响应“后退”按钮的按下,而无需应用开发人员编写任何额外的代码。第一个 Hello World 应用在图 3–2 中运行。

images

图 3–2。 一个简单的ViewNavigatorApplication Hello World 节目

视图生命中的重要事件

事件是任何 Flex 和 Flash 应用的生命线。它们不仅允许您对应用中正在发生的事情做出反应,而且知道事件到达的顺序也很重要,这样您就可以选择适当的处理程序来放置程序逻辑。图 3–3 显示了在三个应用阶段接收一些更重要事件的顺序:启动、关闭和从一个View到另一个View的转换。图中代表应用容器接收到的事件的方框是深色的,而View s 接收到的事件是浅色的。

images

图 3–3。 应用及其View接收重要事件的顺序

在创建第一个View之前,应用接收到了initialize事件。因此,我们知道如果您需要以编程方式而不是作为 XML 属性中的简单字符串来设置ViewNavigatorApplicationfirstViewfirstViewData属性,那么initialize处理程序是一个很好的地方。一个方便的例子是,当您在关机期间保存数据,并希望在下次启动时读回数据并恢复应用的View状态。

在应用接收到initialize事件后,第一个View将接收到它的initializecreationCompleteviewActivate事件。当设置你的View s 时,记住这个顺序是很重要的。如果你需要在你的控件上编程设置一些初始状态,如果可能的话,最好在initialize处理程序中完成。如果您一直等到调用creationComplete处理程序,那么控件实际上被初始化了两次。这可能不会造成明显的延迟,但在为移动平台开发时,意识到性能问题总是值得的。同样,viewActivate事件将是你在View启动序列中发表意见的最后机会。

一旦第一个View完成其初始启动序列,应用将接收其creationCompleteactivate事件。那么第一个View也会收到最后一个activate事件。只有应用的第一个View会收到activate事件。只有当应用运行时第一个创建的是View时,该事件处理程序才是您想要运行的代码的合适位置。

在一个View转换序列中,新的View将在旧的View接收其viewDeactivate事件之前接收其initializecreationComplete事件。尽管旧的View仍然有效,但是您应该避免View之间的相互依赖。任何需要从一个View传递到下一个View的数据都可以使用新的Viewdata参数来传递。我们将在本章的后面向您展示如何做到这一点。View转换序列的最后一步是新的View接收其viewActivate事件。关于这个序列需要记住的重要事情是,在ViewNavigator播放到新View的动画过渡之前,新View将接收到initializecreationComplete事件。viewActivate事件将在过渡播放后接收。如果你想让新的View控件在转场播放时处于某个特定状态——并且它们对用户可见——你需要使用initializecreationComplete事件。同样,initialize是首选,这样控件就不会被初始化两次。另一方面,在过渡播放之前做大量的处理将导致用户输入和View过渡开始之间的明显滞后,这将导致您的界面感觉迟钝。因此,尽可能将处理延迟到viewActivate事件是一个好主意。

当应用关闭时,应用容器将接收到deactivate事件,随后是View堆栈中的每个View。如果一个View被实例化多次,它将接收多个deactivate事件,每个实例一个。在移动环境中,关闭并不总是意味着应用从内存中删除。例如,在 Android 中,当一个应用正在运行时按下“home”按钮将导致该应用接收其停用事件。但是应用还没有退出;它只是在后台运行。如果您真的希望您的应用在停用时退出,您可以从应用容器的deactivate处理程序中调用NativeApplication类中的exit函数,如清单 3–2 所示。

清单 3–2。 使移动应用在停用时完全退出

<?xml version="1.0" encoding="utf-8"?> <s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"                             xmlns:s="library://ns.adobe.com/flex/spark"                             deactivate="onDeactivate()"                             firstView="views.FirstView">   <fx:Script>     <![CDATA[       privatefunction onDeactivate():void {         NativeApplication.nativeApplication.exit();       }     ]]>   </fx:Script> </s:ViewNavigatorApplication>

选项卡式视图导航应用

一个TabbedViewNavigatorApplication允许你划分你的应用,这样每一组与特定应用功能相关的View就有了自己的标签。例如,股票投资组合应用可能有一个选项卡,允许您查看您的投资组合,其中一个View显示您拥有的所有股票,另一个详细的 View允许您详细检查某一特定股票。另一个标签可能会显示市场新闻,其中的View会列出新闻,而另一个标签会让你查看单个新闻。最后,您可能会有一个帐户选项卡,让您管理您的帐户设置。

在这个场景中,每个选项卡都有自己的ViewNavigator,负责管理与该选项卡相关联的View。您在应用容器的 MXML 文件中定义这些ViewNavigator,如清单 3–3 所示。

清单 3–3。 宣告一个TabbedViewNavigatorApplication

`
<s:TabbedViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                                  xmlns:s="library://ns.adobe.com/flex/spark">

<s:ViewNavigator label="Hello" width="100%" height="100%"
                   icon="@Embed('assets/smiley.png')"
                   firstView="views.HelloView"/>

<s:ViewNavigator label="World" width="100%" height="100%"
                   icon="@Embed('assets/globe.png')"
                   firstView="views.WorldView"/>

</s:TabbedViewNavigatorApplication>`

您可以将您的ViewNavigator声明包含在一个<s:navigators>标记中,但是这是不必要的,因为navigators属性是TabbedViewNavigatorApplication的默认属性。这允许我们将我们的ViewNavigator声明为TabbedViewNavigatorApplication的直接子元素。ViewNavigator s 的widthheight设置为 100%。如果您想要正确调整View的大小,这是必需的。否则,它们将只有容纳其内容所需的大小。一个ActionBar只延伸到屏幕顶部的一部分,看起来有点奇怪!图标的大小也很关键。选项卡组件不会尝试调整图像的大小。如果它太大,你的标签会占据整个屏幕。虽然在这个简单的例子中我们没有这样做,但是在实际的应用中,你会希望使用一个MultiDPIBitmapSource来定义你的标签图标,这样它们在所有的设备屏幕上都很好看。

与常规ViewNavigatorApplication的另一个区别是firstView属性是在ViewNavigator上定义的,而不是在TabbedViewNavigatorApplication中定义的。这是有意义的,因为每个ViewNavigator管理自己的一组View。在 MXML 文件中声明的第一个ViewNavigator是应用启动时默认激活的。清单 3–4 显示了组成该应用 Hello 选项卡的两个View的 MXML,即HelloViewLanguageView

清单 3–4。 我们的TabbedViewNavigatorApplication 中 Hello 页签的两个视图

`

<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        initialize="onInitialize()"
        title="Hello" >
  fx:Script      <![CDATA[
       import spark.events.IndexChangeEvent;

privatefunction onChange(event:IndexChangeEvent):void {
         data.selectedIndex = event.newIndex;
         navigator.pushView(LanguageView, listData.getItemAt(event.newIndex));
       }

/**
        * Initializes the data object if it does not exist. If it does,
        * then restore the selected list index that was persisted.
*/
       privatefunction onInitialize():void {
         if (data == null) {
           data = {selectedIndex: -1};
         }

helloList.selectedIndex = data.selectedIndex;
      }
    ]]>
  </fx:Script>

<s:List id="helloList" width="100%" height="100%" labelField="hello"
          change="onChange(event)">
    <s:ArrayCollection id="listData">
      <fx:Object hello="Hello" lang="English"/>
      <fx:Object hello="Hola" lang="Spanish"/>
      <fx:Object hello="nuqneH" lang="Klingon"/>
      
    </s:ArrayCollection>
  </s:List>
</s:View>

<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        initialize="onInitialize()"
        title="Language">

fx:Script
    
  </fx:Script>

<s:VGroup horizontalAlign="center" width="100%" paddingTop="20">
    <s:Label id="hello" fontSize="36"/>
    <s:Label id="lang" fontSize="36"/>
  </s:VGroup>
</s:View>`

HelloView用两个属性定义了对象的静态ArrayCollection:一个hello属性包含用某种特定语言写的单词“Hello ”,另一个lang属性指定该语言是什么。这个ArrayCollection然后被用作View中显示的火花ListdataProvider。关于这个View要注意的第一件事是,当显示其他View时,它使用它的data属性为自己保存数据。这在onInitialize功能中完成。如果Viewdata对象为空,换句话说,如果这是第一次初始化View,那么使用 ActionScript 的对象文字符号创建一个新的data对象。否则,现有的data对象——在其他View显示时保持不变——用于检索之前在List中选择的项目的索引,并在View重新激活时重新选择它。

HelloView源代码还演示了如何将数据传递给另一个View,正如在ListonChange处理程序中所做的那样。当用户在HelloViewList中选择一个项目时,onChange处理程序会首先将IndexChangeEventnewIndex属性保存在自己的data对象中,这样在下次激活View时可以恢复List选择。然后,处理函数使用同一个newIndex属性从ArrayCollection中获取对所选对象的引用。它将对象作为ViewNavigatorpushView函数的第二个参数传递给LanguageViewdata属性。在清单 3–4 的底部,您可以看到LanguageView的代码用两个Label组件向用户显示了data对象的hellolang属性,这两个组件的text属性被绑定到data对象的属性。

图 3–4 显示了在 Hello TabbedView 示例应用中运行的 Hello 选项卡的这两个View。这个项目的源代码可以在本书示例代码的examples/chapter-03目录中找到。

images

图 3–4。Hello WorldTabbedViewNavigatorApplication的 Hello 标签下的View

那关于世界选项卡呢?世界标签包含一个View,它的名字很有创意,叫做WorldView。不直观地说,它不包含地球的图片。相反,它展示了基于 GUI 的 Hello World 程序的另一个主要部分:问候消息。图 3–5 展示了这种View的作用。

images

图 3–5。Hello WorldTabbedViewNavigatorApplication的 World 标签下的View

这个特定实现的独特之处在于,它演示了ActionBar可以包含任何种类的 Spark 控件,而不仅仅是按钮。清单 3–5 展示了这是如何实现的。

清单 3–5。 一个问候消息的简单实现

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark">
  fx:Script
    <![CDATA[
      import spark.events.TextOperationEvent;

privatefunction onChange(event:TextOperationEvent):void {
        viewLabel.text = "Hello, "+textInput.text;
      }
    ]]>
  </fx:Script>

<s:titleContent>
    <s:TextInput id="textInput" prompt="Enter your name..." width="100%"
                 change="onChange(event)"/>
  </s:titleContent>

<s:VGroup horizontalAlign="center" width="100%" paddingTop="20">
    <s:Label id="viewLabel" text="Hello, World" fontSize="44"/>
  </s:VGroup>
</s:View>`

通常显示View标题字符串的ViewtitleContent,已经被一个 Spark TextInput控件取代。TextInputchange处理程序只是复制已经输入到Labeltext属性中的字符。这是一个很好的例子,说明了定制ActionBar的灵活性。

只是一个应用

创建新的 Flex mobile 项目的第三个选项是从空白应用开始。如果您正在处理一个独特的移动应用,它的界面不使用多“视图”的典型模式,那么您可以选择这个选项。如果您正在处理一个只有一个View的应用,因此您不需要ViewNavigator和它带来的所有东西,那么您也可以利用这个选项。当您选择从一个空白的应用开始时,您将得到的正是这个,如清单 3–6 所示。

清单 3–6。 一个空白的手机应用

<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"                xmlns:s="library://ns.adobe.com/flex/spark">   <fx:Declarations>     <!-- Place non-visual elements (e.g., services, value objects) here -->   </fx:Declarations> </s:Application>

然而,您从一个空白应用开始并不意味着您不能利用 Flex 4.5 中包含的特定于移动设备的控件。举个例子,你正在开发一个只有一个屏幕的应用。为一个屏幕设置一个完整的基于View的系统是不值得的。但是这并不意味着你不能使用一个ActionBar来让你的应用看起来更像一个传统的移动应用!清单 3–7 展示了一个应用,它最初是一个空白的应用,现在看起来像任何其他 Flex 移动应用。

清单 3–7。 一款没有ViewNavigator的 Flex 移动应用

<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"                xmlns:s="library://ns.adobe.com/flex/spark">   <fx:Declarations>     <s:SkinnablePopUpContainer id="myAlert" x="{(width-myAlert.width)/2}"                                y="{(height-myAlert.height)/2}">       <s:Panel title="Nice Button Click!">         <s:VGroup horizontalAlign="center" paddingTop="20" paddingBottom="20"                   paddingLeft="20" paddingRight="20" gap="20" width="100%">             <s:Label text="You clicked on an ActionBar button."/>             <s:Button label="OK" click="myAlert.close()"/>           </s:VGroup>         </s:Panel>       </s:SkinnablePopUpContainer>     </fx:Declarations>
`  <s:ActionBar id="ab" left="0" right="0" top="0" title="Just an App"
               titleAlign="center">
    <s:navigationContent>
      <s:Button label="Nav" click="myAlert.open(this, true)"/>
    </s:navigationContent>
    <s:actionContent>
      <s:Button label="Act" click="myAlert.open(this, true)"/>
    </s:actionContent>
  </s:ActionBar>

<s:VGroup top="100" horizontalCenter="0" horizontalAlign="contentJustify">
    <s:Label text="ActionBars are just another component.  
They can even be placed on the:"
             width="400" fontSize="32" textAlign="center"/>
    <s:Button label="Top" click="ab.top=0;ab.bottom=null;ab.left=0;ab.right=0"/>
    <s:Label text="or" fontSize="32" textAlign="center"/>
    <s:Button label="Bottom" click="ab.top=null;ab.bottom=0;ab.left=0;ab.right=0"/>
  </s:VGroup>

</s:Application>`

这个应用声明了它自己的ActionBar,就像它声明任何其他组件一样。事实上,该应用允许您将ActionBar放在屏幕的顶部或底部。在这个清单中定义的ActionBar在其navigationContentactionContent中也包含显示弹出容器的按钮。SkinnablePopupContainer是可用于移动应用的新型火花控制器之一。Android 界面的一个常见设计模式是,长时间点击组件可以显示一个弹出容器,允许用户选择更多选项。SkinnablePopupContainer是如何在 Flex 移动应用中实现这种模式。我们将在本章后面更详细地介绍 Android 设计模式和 Flex 移动组件。Figure 3–6 显示了该应用运行时,底部的ActionBar和弹出容器是可见的。

不过,要明确的是,ActionBar s 属于屏幕的顶部;不要在家里尝试,我们是专业人士!在前面的例子中,我们已经多次提到了ViewNavigatorViewActionBar。在下一节中,我们将更深入地探讨移动 Flex 应用的这些主要部分。

images

图 3–6。 一个带有ActionBar组件和一个可见弹出容器的应用

视图导航器和视图

ViewNavigator是一个可设置皮肤的容器,它保存了一堆View对象,其中在任何给定的时间,只有栈顶的View是可见的和活动的。将新的View压入堆栈会自动播放动画过渡并显示新的View。要返回到前一个View,应用只需将顶部的View弹出堆栈。

ViewNavigator还显示一个ActionBar,它显示由激活的View定义的上下文信息。每当显示新的View时,ViewNavigator自动更新ActionBarViewNavigator类中主要感兴趣的方法如下:

  • pushView:推一个新的View到堆栈上,自动使其在屏幕上可见;这个函数的第一个参数是要显示的View的类。该方法还有另外三个可选参数:一个将被传递给新的Viewdata对象,一个由ViewNavigator存储并可由新的View读取的context对象,以及一个在View之间播放的transition。我们将在本章的后面详细介绍这三个可选参数。
  • popView:从堆栈中删除当前View,显示前一个View;该函数有一个可选参数:在View之间播放的transition
  • popToFirstView:从堆栈中移除所有的View,除了第一个,然后第一个变成可见的View;该函数也接受一个transition参数。
  • popAll:从堆栈中删除所有View并显示一个空白屏幕;transition参数也是可选的。
  • hideActionBar / showActionBar:隐藏或显示ActionBar;一些移动应用可以通过点击ActionBar中的控件来选择全屏显示。在全屏模式下,轻击屏幕会再次显示ActionBar。这些方法可用于在您的 Flex 应用中实现这样的系统。默认情况下,ActionBar的隐藏和显示是动态的,但是您可以向这些函数传递一个Boolean参数来关闭它。

ViewNavigator将自动处理 Android 后退按钮的按压,并代表应用调用popViewActionBarViewViewNavigator类都合作为移动开发者提供了许多这样的特性。本节的其余部分将探讨这些类如何协同工作,为您提供一个高效、灵活、健壮的框架来开发您的移动应用。

动作栏

在移动应用中,ActionBarView标题和控件的传统位置。ActionBar有三个不同的区域:导航区域、标题区域和动作区域。回头参考图 3–1 中显示这些区域的示例。这三个区域都可以包含任意控件,但是默认情况下,标题区域将显示一个标题字符串。尽管标题区域也可以显示任意的控件,但是如果给了它可显示的替代内容,它就不会显示标题字符串。

每个ViewNavigator都有一个由导航器实例化的View共享的ActionBar控件。因此,一个ViewNavigatorApplication对于整个应用将只有一个ActionBar,而一个TabbedViewNavigatorApplication对于应用中的每个ViewNavigator将有一个单独的ActionBarActionBar有七个属性决定了它的内容和布局。

  • actionContent:决定在ActionBar的动作区域(标题的右边)显示什么的控件数组
  • actionLayout:Spark 布局,允许由actionContent数组指定的控件的自定义布局
  • navigationContent:一个控件数组,决定在ActionBar的导航区域(标题的左边)显示什么
  • navigationLayout:Spark 布局,允许由navigationContent数组指定的控件的自定义布局
  • title:如果titleContent为空,将在标题区显示的字符串
  • titleContent:决定在ActionBar的标题区域(ActionBar的中心)显示什么的控件数组
  • titleLayout:Spark 布局,允许由actionContent数组指定的控件的自定义布局

这七个属性在ViewNavigatorApplicationViewNavigatorView类中被复制。如果您在ViewNavigatorApplication中为这些属性赋值,您实际上是为整个应用定义了ActionBar的默认外观。在ViewNavigator中定义这些属性作为所有View的默认属性,这些属性将由ViewNavigator显示。在TabbedViewNavigatorApplication s 中,这是指定默认ActionBar设置的唯一方式——每个ViewNavigator一次。当显示一个新的View时,它的ViewNavigator将用那个ViewActionBar相关属性更新ActionBar,从而覆盖应用或导航器指定的任何默认值。此外,View s 还有一个额外的属性,actionBarVisible,它决定了在显示View时,是否应该显示ActionBar

我们已经展示了在导航和操作区域显示控件的示例应用,以及用TextField替换标题内容的应用,所以在本节中我们不再重复这些示例。您可能会发现有用的另外一条信息是影响ActionBar外观的两种特殊样式的使用。titleAlign风格允许您将标题字符串的对齐方式设置为leftrightcenterdefaultButtonAppearance风格可以设置为normalbeveled。在 Android 上,这些默认为左对齐的标题和正常的按钮外观。您可以根据应用的需要更改它们,或者如果您计划将应用移植到 iOS 平台,也可能需要更改它们。在这种情况下,iOS 上的通常会有倾斜的按钮和居中的标题。图 3–7 显示了这种情况。

images

图 3–7。 一个机器人ActionBar穿着 iPhone 去约会

将斜面样式应用到defaultButtonAppearance甚至为导航内容中的按钮添加了传统的 iOS 箭头形状。微小的触动会让一切变得不同。清单 3–8 显示了创建图 3–7 的ActionBar外观的代码。

清单 3–8。 一个 iOS 风格的ActionBar,有格调!

`
<s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                            xmlns:s="library://ns.adobe.com/flex/spark"
                            firstView="views.MainHomeView">
  fx:Style
    @namespace s "library://ns.adobe.com/flex/spark";
    s|ActionBar {
      titleAlign: "center";
      defaultButtonAppearance: "beveled";
      chromeColor: "0x7893AB"
    }
  </fx:Style>

<s:navigationContent>
    <s:Button label="Back"/>
  </s:navigationContent>
</s:ViewNavigatorApplication>`

我们已经利用了在 MXML 文件中直接为ViewNavigatorApplication定义navigationContent的能力。由于这种布局,后退按钮会出现在整个应用的每个View上。除了titleAligndefaultButtonAppearance样式,我们还为ActionBar定义了自定义颜色。ActionBar将使用chromeColor样式作为填充ActionBar背景的渐变的基础。为ActionBar定义一个定制chromeColor是定制一个移动应用以实现品牌或独特性的常见方式。

动画视图过渡

View过渡控制一个View替换另一个View时播放的动画。当按下一个新的View时,默认的过渡是向左一个SlideViewTransition,而弹出一个View会向右一个SlideViewTransition。这两种默认转换都使用其push模式。然而,您可以用无数不同的方式定制View过渡动画。Table 3–1 显示了转换及其模式和方向。

images

当你在组合中加入不同的 easers 时,你真的有大量的组合可以玩。例如,uncover模式中的SlideViewTransition和向下方向看起来像当前的View正在向下滑动离开屏幕,以显示位于其下方的新的View。再放一个Bounce画架,顶部的View会滑下来,碰到屏幕底部会反弹。您甚至可以通过扩展ViewTransitionBase类来编写自己的自定义过渡,通过实现IEaser接口来编写自己的 easers。你真的只是被你的想象力所限制!

您可以通过将您的转换作为第四个参数传递给pushViewreplaceView函数来指定ViewNavigator将使用的转换。ViewNavigatorpopViewpopAllpopToFirstView也采用单个可选参数,指定在更改View s 时播放哪个过渡。啊,但是如果用户按下 Android 的后退按钮呢?在这种情况下,我们不能显式地调用pop函数,所以相反,您必须将ViewNavigatordefaultPopTransition属性设置为您希望它默认播放的过渡。如果您没有为pop函数指定过渡参数或设置defaultPopTransition属性,则ViewNavigator将在弹出View时播放其默认的幻灯片过渡。即使您使用自定义过渡来推动View,当View弹出时,ViewNavigator也不会尝试反向播放推动过渡。还需要注意的是ViewNavigator有对应的defaultPushTransition。您可以使用这两个属性为特定的ViewNavigator播放的所有过渡设置默认值。

现在唯一合理且有趣的事情是编写一个应用来尝试这些转换组合,对吗?没错。清单 3–9 显示了 ViewTransitions 示例程序的TransitionListView的代码。这个View显示了所有内置View过渡的List,每个过渡都显示了一些不同的模式、方向和画师的组合。ViewTransitions 项目可以在本书第三章的示例代码中找到。

清单 3–9。 展示了几个不同品种的每一个View跃迁

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"         xmlns:s="library://ns.adobe.com/flex/spark"         title="Home">   <fx:Declarations>     <s:Bounce id="bounce"/>     <s:SlideViewTransition id="slide"/>     <s:SlideViewTransition id="slideBounce" easer="{bounce}" duration="1000"/>     <s:SlideViewTransition id="slideCover" mode="cover"/>     <s:SlideViewTransition id="slideUncover" mode="uncover"/>     <s:SlideViewTransition id="uncoverBounce" mode="uncover" easer="{bounce}"                            direction="down" duration="1000"/>     <s:FlipViewTransition id="flip"/>     <s:FlipViewTransition id="flipBounce" easer="{bounce}" duration="1000"/>     <s:FlipViewTransition id="flipCube" mode="cube"/>     <s:CrossFadeViewTransition id="fade"/>     <s:ZoomViewTransition id="zoom"/>     <s:ZoomViewTransition id="zoomBounce" easer="{bounce}" duration="1000"/>     <s:ZoomViewTransition id="zoomIn" mode="in"/> `    <s:ZoomViewTransition id="zoomInBounce" mode="in" easer="{bounce}"
                          duration="1000"/>
  </fx:Declarations>

fx:Script
    <![CDATA[
      import spark.events.IndexChangeEvent;
      import spark.transitions.ViewTransitionBase;

privatefunction onChange(event:IndexChangeEvent):void {
        var selectedItem:Object = transitionList.selectedItem;
        var transition:ViewTransitionBase = selectedItem.transition;
        var data:Object = {name: selectedItem.name};

navigator.defaultPopTransition = transition;
        navigator.pushView(TransitionedView, data, null, transition);
      }

]]>
  </fx:Script>

<s:navigationContent/>

<s:List id="transitionList" width="100%" height="100%" labelField="name"
          change="onChange(event)">
    <s:ArrayCollection>
      <fx:Object name="Default - Push Slide" transition="{slide}"/>
      <fx:Object name="Push Slide with Bounce" transition="{slideBounce}"/>
      <fx:Object name="Cover Slide" transition="{slideCover}"/>
      <fx:Object name="Uncover Slide" transition="{slideUncover}"/>
      <fx:Object name="Uncover Slide with Bounce" transition="{uncoverBounce}"/>
      <fx:Object name="Flip" transition="{flip}"/>
      <fx:Object name="Flip with Bounce" transition="{flipBounce}"/>
      <fx:Object name="Cube Flip" transition="{flipCube}"/>
      <fx:Object name="Fade" transition="{fade}"/>
      <fx:Object name="Zoom Out" transition="{zoom}"/>
      <fx:Object name="Zoom Out with Bounce" transition="{zoomBounce}"/>
      <fx:Object name="Zoom In" transition="{zoomIn}"/>
      <fx:Object name="Zoom In with Bounce" transition="{zoomInBounce}"/>
    </s:ArrayCollection>
  </s:List>
</s:View>`

<fx:Declarations>部分用于声明一个Bounce更容易和各种不同的过渡。一些过渡使用它们的默认设置,而另一些则指定特殊的模式或方向。几个转换也使用duration属性来指定转换需要的总毫秒数。内置转换的默认持续时间范围是 300 到 400 毫秒。这对于一个有效的弹跳动画来说有点太快了,所以那些使用Bounce easer 的过渡有更长的持续时间。

当从List中选择一个过渡时,onChange处理函数检索所选对象,并将过渡的名称传递给下一个Viewdata.name属性。包含在List中的对象也保留了对所需过渡的引用。所以这个转换属性作为第四个参数传递给导航器的 pushView方法。但是请注意,这个转换也用于在调用pushView之前设置导航器的defaultPopTransition属性。这将确保如果在推送过程中播放了一个FlipViewTransition,当返回到TransitionListView时将播放相同的过渡。这有一点欺骗,因为当你通常想要在特定的一对View的推和弹出上播放相同类型的过渡时,你通常会颠倒弹出过渡的方向。这在常规应用中很容易实现,但在这种情况下,不值得在示例代码中为每个当前转换对象定义一个相反方向的转换。图 3–8 显示了在立方体模式下FlipViewTransition期间捕获的 ViewTransitions 示例程序。

images

图 3–8。 使用立方体翻转过渡从一个View到下一个

如果您在 ViewTransitions 示例程序运行时仔细观察ActionBar,或者如果您检查图 3–8 中的中间图像,您将会注意到ActionBar没有随着View进行转换。这是因为ActionBar被认为属于整个应用——或者整个选项卡——而不仅仅是一个View。但是有些时候,如果ActionBar参与进来,这种转变看起来会更好。在这些情况下,您可以将View转换类的transitionControlsWithContent属性设置为true。因此,如果我们如下更改示例应用中的立方体翻转声明,我们就可以获得如图 Figure 3–9 所示的效果。

<s:FlipViewTransition id="flipCube" mode="cube"            transitionControlsWithContent="true"/> images

图 3-9。 在立方体翻转过渡上用View过渡ActionBar

查看菜单

所有 Android 设备都有一个内置的菜单按钮,可以显示屏幕菜单。AIR 通过ViewMenuViewMenuItem类支持这一功能。ViewMenu充当一个ViewMenuItem s 单级的容器;不支持子菜单。应用中的每个View都可以定义自己的ViewMenu。清单 3–10 展示了如何为View声明一个ViewMenu

清单 3–10。 宣告一个ViewMenu

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"         xmlns:s="library://ns.adobe.com/flex/spark"         title="MenuItem Example"> `  fx:Script
    
  </fx:Script>

<s:viewMenuItems>
    <s:ViewMenuItem label="Pro" click="onClick(event)"
                    icon="@Embed('assets/ProAndroidFlashIcon36.png')"/>
    <s:ViewMenuItem label="Android" click="onClick(event)"
                    icon="@Embed('assets/android.png')" />
    <s:ViewMenuItem label="Flash" click="onClick(event)"
                    icon="@Embed('assets/flash.png')" />
    <s:ViewMenuItem label="Book" click="onClick(event)"
                    icon="@Embed('assets/book.png')" />
  </s:viewMenuItems>

<s:Label id="message" width="100%" top="20" textAlign="center"/>
</s:View>`

一个ViewMenuItem实际上只是另一种按钮。它甚至扩展了 Spark ButtonBase类。因此,就像任何其他按钮一样,您可以定义标签和图标属性以及一个click事件处理程序。在这个例子中,每个ViewMenuItem都有相同的onClick处理程序,它使用ViewMenuItem的标签向用户显示选择。使ViewMenu容器和ViewMenuItem按钮与普通 Spark 不同的是,它们的布局设计模仿了原生 Android 菜单。

提示:记住 Windows 上的 Ctrl+N 和 Mac 上的 Cmd+Non 会在桌面模拟器中显示 Viewmenu。

图 3–10 显示了在 Android 设备上运行时产生的ViewMenu的样子。

images

图 3-10。 一个ViewMenu带图标

在视图之间传递数据

为了节省资源,ViewNavigator将确保在任何给定时间内存中只有一个View。当一个View被推到栈顶时,旧的Viewdata对象被自动保存,如果一个新的data对象作为pushView函数的第二个参数被提供,它将被传递给新的View。如果没有提供data对象,那么新的Viewdata属性将为空。由于当一个新的View被压入堆栈时,一个Viewdata对象被持久化,所以当由于其他View被弹出堆栈而导致View被重新激活时,那个data对象将被恢复。

那么,通过data对象的通信似乎是单向的:一个View可以将一个data对象传递给它正在推入堆栈的View,但是当它弹出堆栈时,View无法将数据返回给原始的View。那么,如果需要的话,新的View如何将数据返回给原来的View?答案是新的View将简单地覆盖ViewcreateReturnObject功能。该函数返回一个保存在ViewNavigatorpoppedViewReturnedObject属性中的对象,其类型为ViewReturnObject。所以为了访问新的View返回的对象,原来的View将访问navigator.poppedViewReturnedObject.object

您也可以通过使用context对象将数据传递给新的View。你可以传递一个context对象作为ViewNavigatorpushView函数的第三个参数。context的行为很像data对象;通过访问navigator.context属性,新的View可以随时访问它。当顶部的ViewViewNavigatorView栈中弹出时,先前的Viewcontext也被恢复。弹出的Viewcontext对象也存在于navigatorpoppedViewReturnedObject.context属性中。

datacontext对象的使用在某种程度上是可以互换的,在这种情况下,你应该更喜欢使用data对象。context对象对于那些有一个View的情况很有用,根据用户导航到View的方式,这个【】的显示会略有不同。例如,您可能有一个显示一个人的联系信息的细节View。有时,根据用户如何导航到“View”——无论是通过点击“查看”按钮还是“编辑”按钮——View应该相应地调整其显示。这是一个使用context对象来区分包含在data对象中的联系信息是应该简单地呈现给用户还是应该可编辑的好地方。

保存视图和会话数据

我们已经在清单 3–4 中看到,并且在上一节中简要讨论过,当ViewViewNavigator弹出函数之一重新激活时,Viewdata对象被恢复。因此,View数据的一个持久性策略是在调用pushView之前将值存储在它的data对象中,或者存储在viewDeactivate事件的处理程序中。如果新的View调用其中一个弹出函数,那么先前由原来的View存储的数据将可以通过它的data对象再次访问。这种策略只对正在运行的应用中的View之间的持久化数据有效。如果用户触发调用NativeApplicationexit功能的动作或 Android 操作系统关闭应用,那么所有Viewdata对象都将丢失。

PersistenceManager类用于在应用运行之间保存数据。ViewNavigatorApplicationTabbedViewNavigatorApplication容器引用了一个persistenceManager实例,该实例可用于在应用启动或关闭时保存和加载持久化数据。清单 3–11 展示了一个使用persistenceManager保存应用启动次数的简单例子。这段代码是名为简单持久性的第三章示例项目的一部分。

清单 3–11。 在应用运行之间保存数据

<?xml version="1.0" encoding="utf-8"?> <s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"                             xmlns:s="library://ns.adobe.com/flex/spark"                             splashScreenImage="@Embed('assets/splash.png')"                             initialize="onInitialize()"                             deactivate="onDeactivate()"                             applicationDPI="160"> `  fx:Script
    <![CDATA[
      import views.PersistenceCountView;
      privatestaticconst RUN_COUNT:String = "runCount";
      privatefunction onInitialize():void {
        var rc:Number = Number(persistenceManager.getProperty(RUN_COUNT));
        navigator.pushView(views.PersistenceCountView, rc);
      }

privatefunction onDeactivate():void {
        var rc:Number = Number(persistenceManager.getProperty(RUN_COUNT));
        persistenceManager.setProperty(RUN_COUNT, ++rc);
        NativeApplication.nativeApplication.exit(0);
      }
    ]]>
  </fx:Script>
</s:ViewNavigatorApplication>`

当应用被放到后台或被 Android 操作系统关闭时,应用的onDeactivate处理程序被调用。所以在这个处理程序中,我们增加了运行计数,并调用persistenceManagersetProperty函数来保存它。然后我们调用NativeApplicationexit函数来确保应用在两次运行之间被关闭。这确保了我们的数据真正被持久化和恢复。

当应用的onInitialize处理程序被触发时,使用getProperty函数从persistenceManager中检索应用的运行计数。getProperty函数接受一个参数,该参数是要检索的属性的键String。该运行计数被转换为一个Number并传递给应用的第一个View,在此显示如图 3–11 所示。

images

图 3–11。 显示应用的持续运行次数

基于View导航器的应用也有一个名为persistNavigatorState的属性,当设置为true时,将自动保存ViewNavigator的状态和View堆栈。持久化的数据会在下次运行程序时自动加载回来。除了ViewNavigator数据,应用的版本和数据保存的时间也被保存。使用PersistenceManagergetProperty函数和“应用版本”和“时间戳”键可以访问这两个数据。

既然我们已经探索了移动应用容器,是时候关注移动 Flex 应用可用的控件了。

视觉控制

任何 Flex 应用的核心都是一组丰富的 UI 控件,让您可以在 UI 中表达常见元素。我们已经在本书前面的代码和应用中使用了其中的许多;然而,在本节中,我们将特别关注可用于 mobile Flex 应用的不同类型的控件。

对于移动应用,性能和外观对于确保用户界面的可用性都非常重要。出于这个原因,强烈建议远离 Flex 中的旧 MX 包,并专注于具有移动皮肤的 Spark 控件。

Spark 控件的完整列表显示在表 3–2 中,以及它们对移动应用的适用性。

images

images

许多控件目前没有移动优化外观,因此目前不应在移动设备上使用。例如,ComboBoxNumericStepperDropDownList如果在移动设备上使用它们的桌面皮肤,就不会有一致的外观和交互。如果您需要一个具有这些功能之一的控件,您可以创建自己的自定义外观,以匹配您的移动应用的样式。

一些可用的组件也没有针对移动设备上的性能进行优化。我们在第十章的中更详细地讨论了这个话题,但是如果你遵循前面的指导方针,使用TextAreaTextInput而不是RichTextRichEditableText,你应该没问题。对于Image类也是如此,重复使用时可以是重量级的,比如在ListItemRenderers中。

该列表是截至 Flex 4.5 的最新版本,但是 Adobe 正在为其余控件添加额外的移动外观,因此请参考 API 文档以了解有关移动控件兼容性的最新信息。

在本章的其余部分,我们将详细介绍如何使用每个支持移动设备的控件的全部功能。

文本控件

三个为移动设备优化并能给你带来最佳性能应用的控件是LabelTextAreaTextInput。每个控件都可以通过 CSS 样式高度定制。

Label让您能够以统一的格式显示单行或多行文本。它在幕后使用 Flash 文本引擎(FTE),这使它快速而轻量,但不如使用全文布局框架(TLF)的RichText控件灵活。Labels应该用在任何你想在屏幕上显示不可修改的文本的地方,比如控件标签或者章节标题。

TextInputTextArea分别提供单行和多行使用的文本输入。当在移动设备上使用时,它们在幕后使用StyleableTextField类,这使它们具有极高的性能,但功能有限。在桌面上,这些控件由 TLF 支持,为您提供国际语言支持、改进的排版和嵌入的 CFF 字体。如果你在移动设备上需要这些功能,你将不得不使用RichEditableText控件,这会带来很大的性能损失。

三个推荐文本组件的移动样式如表 3–3 所示。虽然在桌面配置文件上运行时还有其他样式属性可用,如kerninglineBreakrenderingMode,但由于移动主题中使用的轻量级文本引擎,这些属性在移动设备上不受支持。

images

images

正如您所看到的,这些文本组件支持的样式几乎是相同的,只是为内容区域、边框以及TextInputTextArea的焦点添加了一些样式。

使用不同文本组件的最大区别在于使用不同的属性进行文本编辑。这些属性中有许多在TextInputTextArea上可用,但对于Label并不需要,因为它仅用于文本渲染。Table 3–4 列出了三个文本组件的所有可用属性。

images

为了展示文本组件的不同风格和功能,我们构建了一个小的示例应用,它呈现了美国独立宣言的前几段以及一个可编辑的签名列表。该应用的代码如清单 3–12 所示。

清单 3–12。显示独立宣言的文本组件示例代码

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark" title="Text">   <fx:Style>     @namespace s "library://ns.adobe.com/flex/spark";     .title {       fontFamily: Times;       fontSize: 30;       fontWeight: bold;     }     .body {       color: #222222;       fontFamily: Times;       fontSize: 12;       fontStyle: italic;       textAlign: justify;     }     .main-signature {       fontFamily: Helvetica;       fontSize: 18;     }     .state-signatures {       fontFamily: Helvetica;       fontSize: 12;     }   </fx:Style>   <s:VGroup left="15" top="15" right="15" width="100%" gap="12">     <s:Label styleName="title" text="Declaration of Independence"/>     <s:Label styleName="body" width="100%"          text="When in the Course of human events, it becomes necessary for one people to …"          />     <s:Label styleName="body" width="100%" maxDisplayedLines="12"          text="We hold these truths to be self-evident, that all men are created equal, that they are …"          />     <s:HGroup verticalAlign="baseline" width="100%">     <s:Label styleName="main-signature" text="President of Congress:"/>     <s:TextInput styleName="main-signature" text="John Hancock" editable="false" width="100%"/>     </s:HGroup>     <s:Label styleName="main-signature" text="State Representatives:"/>     <s:TextArea styleName="state-signatures" width="100%"          text="Josiah Bartlett, William Whipple, Matthew Thornton, Samuel Adams, John Adams, …"         />   </s:VGroup> </s:View>

注意使用内联 CSS 从代码中抽象出样式。也可以将文本组件上的样式直接声明为 XML 属性,尽管为了模块化,您可能更愿意反其道而行之,将 CSS 完全提取到一个单独的文件中。

运行这个例子会得到如图 Figure 3–12 所示的输出。

images

图 3–12。 独立宣言测试样本

作为测试文本组件的一个练习,尝试对应用进行以下更改:

  • TextArea组件上使用密码保护。
  • 更改TextInput的大小,使其与初始文本大小完全匹配。
  • TextInput默认文本改为用户开始输入时消失的提示。
  • 更改TextInput的样式和交互性,以匹配Label的样式和交互性。

提示:使用一个禁用了可编辑性且样式类似于LabelTextInput比直接使用一个Label组件性能更高,这是因为使用了幕后的StyleableTextField实现。

软键盘支持

当使用文本组件时,Android 软键盘会像你所期望的那样在焦点上自动触发。然而,有时您需要更精细地控制软键盘何时被触发,以及当它被激活时会发生什么。

Flex 中的软键盘由应用焦点控制。当一个将needsSoftKeyboard属性设置为true的组件获得焦点时,软键盘将出现在前面,舞台将滚动,以便选定的组件可见。当该组件失去焦点时,软键盘将消失,舞台将返回其正常位置。

有了对焦点的理解,你可以通过做以下事情来控制软键盘:

  • 以声明方式显示软键盘:为你的组件设置needsSoftKeyboardtrue
  • 以编程方式显示软键盘:在已经设置了needsSoftKeyboard的组件上调用requestSoftKeyboard()
  • 隐藏软键盘:在没有设置needsSoftKeyboard的组件上调用setFocus()

这对于通常不触发软键盘的组件来说工作良好;但是,对于自动升高键盘的组件,将needsSoftKeyboard设置为false没有任何作用。防止键盘在这些组件上弹出的一个解决方法是侦听激活事件,并用如下代码抑制它:

<fx:Script>   <![CDATA[     private function preventActivate(event:SoftKeyboardEvent):void {       event.preventDefault();     }   ]]> </fx:Script> <s:TextArea text="I am a text component, but have no keyboard?"   softKeyboardActivating="preventActivate(event)"/>

这段代码捕获了TextArea组件上的softKeyboardActivating事件,并取消了提升软键盘的默认动作。

除了在激活时获取事件,您还可以捕捉softKeyboardActivatesoftKeyboardDeactivate事件,以便根据软键盘状态执行操作。

清单 3–13 展示了一个软键盘示例应用,它展示了所有这些技术一起使用来完全控制软键盘。

清单 3–13。 软键盘交互示例代码

`
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
         xmlns:s="library://ns.adobe.com/flex/spark"
         splashScreenImage="@Embed('ProAndroidFlash400.png')">
  fx:Script
    <![CDATA[
      [Bindable]
      privatevar state:String;

[Bindable]
      privatevar type:String;

privatefunction handleActivating(event:SoftKeyboardEvent):void {
        state = "Activating...";
        type = event.triggerType;
      }

privatefunction handleActivate(event:SoftKeyboardEvent):void {
        state = "Active";
        type = event.triggerType;
      }

privatefunction handleDeactivate(event:SoftKeyboardEvent):void {
        state = "Deactive";
        type = event.triggerType;
      }

privatefunction preventActivate(event:SoftKeyboardEvent):void {
        event.preventDefault();
      }
    ]]>
  </fx:Script>
  <s:VGroup left="20" top="20" right="20" gap="15"
        softKeyboardActivating="handleActivating(event)"
        softKeyboardActivate="handleActivate(event)"
        softKeyboardDeactivate="handleDeactivate(event)">
    <s:HGroup>
      <s:Label text="Keyboard State: " fontWeight="bold"/>
      <s:Label text="{state}"/>
    </s:HGroup>
    <s:HGroup>
      <s:Label text="Trigger Type: " fontWeight="bold"/>
      <s:Label text="{type}"/>
    </s:HGroup>
    <s:Button id="needy" label="I Need the Keyboard" needsSoftKeyboard="true" emphasized="true"/>
    <s:TextArea text="I am a text component, but have no keyboard?"
          softKeyboardActivating="preventActivate(event)"/>
    <s:HGroup width="100%" gap="15">
      <s:Button label="Hide Keyboard" click="{setFocus()}" width="50%"/>
      <s:Button label="Show Keyboard" click="{needy.requestSoftKeyboard()}" width="50%"/>
    </s:HGroup>
  </s:VGroup>
</s:Application>`

这段代码创建了几个控件,并为它们附加了动作,这样你就可以随意隐藏和显示软键盘,还可以看到当前软键盘的状态,就像涓流事件所报告的那样。运行该应用后,您将会看到如图图 3–13 所示的 UI。

images

图 3–13。 演示如何控制软键盘的示例应用

请注意,通常触发软键盘的TextArea控件不再弹出软键盘,而高亮按钮在获得焦点时会立即弹出软键盘。底部显示和隐藏键盘的两个按钮仅仅是玩焦点把戏,让 Flash 随意显示和隐藏键盘。

你可以在你的应用中使用同样的技术来完全控制软键盘。

按钮控件

也许任何用户界面最基本的元素之一就是按钮。自从 1973 年施乐 Alto 上出现第一个图形用户界面以来,它就一直存在。Figure 3–14 展示了一张桌子大小的 Alto 的图片,以及其文件管理器中使用的按钮样式。自那时以来,我们已经走过了漫长的道路,但基本概念并没有多大变化。

images

图 3–14。 施乐 Alto 的图像(左)和其 GUI 中使用的按钮样式的复制品(右)

Flex 有几个内置的按钮控件,具有移动优化的样式,可以在设备上以可用的大小呈现它们。其中包括以下按钮类型:

  • Button
  • CheckBox
  • RadioButton
  • ButtonBar

标准的Button控件是高度可定制的,包括嵌入图像图标的能力。CheckBox控件提供了一个带有标签的定制按钮和一个可视开关,可以通过单击来启用或禁用。RadioButton类似于CheckBox,但是使用一个圆形指示器,并与一组相关的RadioButton一起工作,一次只能选择其中一个。最后,ButtonBar将一组切换按钮合并成一行,在给定时间只能选择其中一个,类似于RadioButton组。

所有这些控件都有相似的样式,可以用来自定义它们。由于手机皮肤的不同,并不是所有的桌面风格都可用,比如cffHinting``direction``renderingMode。Table 3–5 列出了按钮类上支持移动的样式。

images

images

所有按钮类型都支持其中的大多数样式,包括嵌入在类型为ButtonBarButtonButtonBar中的按钮。有一些例外,样式被明确地从子类中排除,比如textAlignicon,它们在CheckBoxesRadiobuttons中都不被支持。对于在ButtonBarButtons上设置样式,通常只需在ButtonBar上设置样式,并让 CSS 继承将它应用到创建的子按钮上。

由于功能的不同,操作按钮的可用属性也略有不同。Table 3–6 列出了可用的公共属性,包括它们适用于哪个按钮类。

images

除了这些属性之外,button 类上还有一些事件和方法有助于交互性。其中最重要的是clickHandler函数,每当用户在按钮上按下并释放鼠标时,该函数就会被调用。此外,您可以监听一个buttonDown事件,并覆盖buttonReleasedmouseEventHandler函数来进行更高级的交互。可切换按钮子类(CheckBoxRadioButtonButtonBarButton)上的另一个可用事件是change事件,每当selected属性改变时就会触发该事件。

为了演示不同按钮控件的使用,我们制作了一个小按钮示例,模仿了现代微波炉上的复杂控件集。本例的代码如清单 3–14 所示。

清单 3–14。 现代微波的代号举例

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"   xmlns:s="library://ns.adobe.com/flex/spark" title="Buttons"> <fx:Style>   @namespace s "library://ns.adobe.com/flex/spark";   .number {     touchDelay: 500;   }     .header {       color: #660000;     }   </fx:Style>   <s:VGroup left="60" right="60" top="20" width="100%">     <s:ButtonBar styleName="header" requireSelection="true" width="100%">       <s:dataProvider>         <s:ArrayCollection source="['Defrost', 'Cook', 'Irradiate']" />       </s:dataProvider>     </s:ButtonBar>     <s:RadioButton label="Meat" color="#404040" symbolColor="green"/>     <s:RadioButton label="Poultry" color="#404040" symbolColor="yellow"/>     <s:RadioButton label="Alien Fish" color="#d02525" symbolColor="#d02525"/>     <s:CheckBox label="Trigger Meltdown" symbolColor="red"/>     <s:HGroup width="100%">       <s:Button styleName="number" label="9" width="100%"/>       <s:Button styleName="number" label="8" width="100%"/>       <s:Button styleName="number" label="7" width="100%"/>     </s:HGroup>     <s:HGroup width="100%">       <s:Button styleName="number" label="6" width="100%"/>       <s:Button styleName="number" label="5" width="100%"/>       <s:Button styleName="number" label="4" width="100%"/>     </s:HGroup>     <s:HGroup width="100%">       <s:Button styleName="number" label="3" width="100%"/>       <s:Button styleName="number" label="2" width="100%"/>       <s:Button styleName="number" label="1" width="100%"/>     </s:HGroup>   <s:HGroup width="100%">       <s:Button styleName="number" label="?" width="100%"/>       <s:Button styleName="number" label="0" width="100%"/>       <s:Button styleName="number" icon="@Embed('alien.gif')" width="100%"/>     </s:HGroup>     <s:HGroup width="100%">       <s:Button label="End" width="100%"/>       <s:Button label="Start" width="100%" emphasized="true"/>     </s:HGroup>   </s:VGroup> </s:View>

当在移动设备上运行时,该应用将类似于图 3–15 中所示。

images

图 3–15。 运转现代微波输出的例子

要使用您所学的一些新样式和属性进行练习,请尝试以下方法:

  • 更改数字按钮中标签字体的大小和颜色。
  • 增加一个clickHandler,微波炉启动时会播放声音。
  • 添加一个Label,重复地将按钮上的数字追加到文本中。

弹性列表

s 可能是移动应用中最重要的控件之一。由于有限的屏幕空间,它们取代了数据网格,通常用于通过菜单或层次结构进行向下导航。

FlexList控件已经针对移动应用进行了彻底的改进,其行为类似于你对移动设备的期望。这包括带有图标和装饰的大图形,以及当你通过一个List的开始或结束时的滚动“反弹”。

最简单的方法是,你可以创建并显示一个 Flex List,只需给它一个要渲染的对象集合,如清单 3–15 所示。

清单 3–15。 代码从一个ArrayCollection 创建一个List

<s:List width="100%" height="100%">   <s:ArrayCollection source="['Alabama', 'Alaska', 'Arizona']" /> </s:List>

上述代码将默认的dataProvider属性设置为字符串的ArrayCollection。默认情况下,它将使用LabelItemRenderer,它只是在一个StyleableTextField中显示List的每个条目。执行该程序将产生一个基本的List,如图 3–16 中的所示。

images

图 3–16。 基本List例题使用LabelItemRenderer

要创建一个更复杂的List,你可以改变itemRenderer来使用一个更复杂的渲染器。Flex 4.5 附带了第二个名为IconItemRenderer的内置渲染器,它具有显示以下项目组件的附加功能:

  • 图标:显示在文本左侧的图形图标,通过设置iconField或分配iconFunction来选择
  • 标签:以大字体显示的单行文本,通过设置labelField或分配labelFunction来选择
  • 消息:以较小字体显示的多行描述,通过设置messageField或分配messageFunction来选择
  • Decorator:显示在图像右侧的图标,设置在decorator属性上

为了演示IconItemRenderers的用法,我们制作了一个示例,让您浏览所有 50 个州的格言和纪念币列表。该示例的代码如清单 3–16 所示。

清单 3–16。 *IconItemRenderer* 样本应用代码

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark" title="List">   <fx:Declarations>     <s:ArrayCollection id="stateInfo">       <fx:Object state='Alabama' coin="@Embed('coins/Alabama.jpg')"         motto="Audemus jura nostra defendere -- We dare defend our rights"/>       <fx:Object state='Alaska' coin="@Embed('coins/Alaska.jpg')"         motto="Futurum aquilonem -- North to the future"/>       <fx:Object state='Arizona' coin="@Embed('coins/Arizona.jpg')"         motto="Ditat Deus -- God enriches"/>       <fx:Object state='Arkansas' coin="@Embed('coins/Arkansas.jpg')"         motto="Regnat populus -- The people rule"/>       <fx:Object state='California' coin="@Embed('coins/California.jpg')"         motto="Eureka (??????) -- I have found it"/>       …     </s:ArrayCollection>   </fx:Declarations>   <s:Group width="100%" height="100%">     <s:List dataProvider="{stateInfo}" width="100%" height="100%">       <s:itemRenderer>         <fx:Component>           <s:IconItemRenderer labelField="state" messageField="motto" iconField="coin"                decorator="@Embed('usflag.png')"/>         </fx:Component>       </s:itemRenderer>     </s:List>     <s:Label text="United States Mint images." fontSize="10" left="2" bottom="2"/>   </s:Group> </s:View>

注意,我们没有嵌套dataProvider,而是将其抽象为一个声明,并由id引用。通常,您的数据将由 web 服务或数据库查询提供,在这种情况下,您只需用您的dataProvider替换示例代码中使用的那个。

对于IconItemRenderer,我们使用Component标签创建了一个内联实例,并将其直接分配给itemRenderer属性。还要注意,我们选择使用*Field版本来选择标签、消息和图标。出于性能原因,这是更可取的,因为这意味着IconItemRenderer知道值是静态的,可以进行更多的缓存来提高性能。

Figure 3–17 显示了在移动设备上运行的状态信息示例。

images

图 3–17。 国家信息应用展示纪念币和格言

通过利用IconItemRenderer类的样式属性,您可以进一步定制列表。这里有一些关于你可以尝试的改变的建议:

  • Change the font family, size, and color to further distinguish the motto from the state name.

    提示:IconItemRenderer上设置的样式将被标签继承,而messageStyleName类可以用来改变消息的样式。

  • 通过以编程方式更改硬币图形的宽度和缩放模式来增加硬币图形的大小。

  • 使用allowMultipleSelection属性在List组件上启用多选。

滑块、滚动条和忙碌指示器控件

在创建移动应用时,您会发现其他几个控件也很有用。这些控件包括HSliderScrollerBusyIndicator控件。

元首

HSlider是一个标准的水平滑块控件,允许您指定用户可以选择的值范围。HSlider的一些特性包括:

  • 移动大小的滑动条
  • 显示精确值的数据提示
  • 支持可配置的值范围和步进

滚动条本身的样式仅限于几个简单的属性,包括focusAlphafocusColorliveDraggingslideDuration。前两种样式与其他控件上的样式相同。此外,您可以通过将liveDragging设置为false来禁用它,这将强制该值仅在用户放开鼠标按钮时更新。slideDuration样式控制滑块背景被按下时拇指移动的时间,默认为 300 毫秒。

除此之外,您可以使用几个属性来控制数据提示的显示,包括将Numeric值转换为字符串的dataTipFormatFunctiondataTipPrecision来选择小数位数,以及让您完全禁用数据提示的showDataTip。数据提示文本使用与上一节中提到的按钮组件相同的文本样式属性。对于移动使用,支持以下文本样式:fontFamilyfontStyleleadingletterSpacinglocaletextDecoration

为了控制滑块的范围,有几个属性可用,包括minimummaximumstepSizesnapInterval。这些都是Numeric值,让您控制滑块的范围以及步进和捕捉行为。您可以使用value属性设置滑块的初始值。当用户与滑块交互时,该属性也会动态更新。

最后,在使用滑块时,您可以跟踪几个事件来进行交互行为。这包括changechangingthumbDragthumbPressthumbRelease的事件。

滚动条

Scroller是一个支持移动的控件,允许用户在大于可视区域的内容周围翻页。移动皮肤是一个完整的重新设计,它使用触摸事件而不是静态滚动条来平移视口。这使得在触摸屏显示器上操作更加容易,同时提供了等效的功能。

Scroller的子节点必须实现IViewport接口,该接口包括 Spark 库中的GroupDataGroupRicheditableText组件。下面的示例代码展示了如何创建一个新的Scroller实例来浏览静态图像:

<s:Scroller width="100%" height="100%">   <s:VGroup>     <s:BitmapImage source="@Embed('/ProAndroidFlash.png')"/>   </s:VGroup> </s:Scroller>

在这个例子中,viewport的默认属性被设置为一个VGroup,它包含一个大的BitmapImageGroup只是用来包装BitmapImage,确保外部组件是IViewport类型,否则用户看不到。在运行这个示例时,用户将能够在屏幕上拖动鼠标来浏览图像。

除此之外,由于简化的移动用户界面,真的没有什么可定制的。文本或滚动条样式都不适用,包括字体、颜色以及隐藏或显示滚动条。

忙碌指示器

最后一个组件显示一个简单的繁忙指示器小部件,带有一组圆形的旋转叶片。当图形显示在屏幕上时,它会不断地使图形产生动画效果,表明诸如加载之类的活动正在后台进行。

BusyIndicator的直径计算为高度和宽度的最小值,四舍五入为 2 的倍数。另外两个专用于BusyIndicator组件的刻度盘是一个用于控制动画速度(以毫秒为单位)的rotationInterval和一个用于改变等待图形颜色的symbolColor

合并样本

为了演示这三个控件的用法,我们制作了一个快速演示,它使用所有控件来提供图像的缩放和平移。HSlider控件用于改变图像的缩放级别,Scroller组件在图像被放大时提供平移,而BusyIndicator在这些动作发生时显示活动。

这个例子的完整代码显示在清单 3–17 中。

清单 3–17。 示例代码演示了HSliderScrollerBusyIndicator控件的使用

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark" title="Misc">   <fx:Script>     <![CDATA[       [Bindable]       private var scale:Number = .5;       [Bindable]       private var busy:Boolean = false;     ]]>   </fx:Script>   <s:VGroup top="15" left="15" right="15" bottom="15" gap="10" width="100%" height="100%">     <s:HGroup width="100%" verticalAlign="middle">       <s:Label text="Scale:"/>       <s:HSlider width="100%" value="@{scale}" minimum=".01" maximum="1" stepSize="0"              changeStart="{busy=true}" changeEnd="{busy=false}"/>       <s:BusyIndicator visible="{busy}"/>     </s:HGroup>     <s:Scroller width="100%" height="100%"           mouseDown="{busy=true}" mouseUp="{busy=false}" mouseOut="{busy=false}">       <s:VGroup>         <s:BitmapImage source="@Embed('/ProAndroidFlash.png')"                  scaleX="{scale}" scaleY="{scale}"/>       </s:VGroup>     </s:Scroller>   </s:VGroup> </s:View>

在运行这个示例时,您将看到类似于 Figure 3–18 的输出,这是在Scroller拖动操作的中途捕获的。

images

图 3-18。 *Scroller* 平移操作期间捕获的示例

尝试操作控件来缩放和平移图像,并随意用您选择的图像替换此示例中的图像。

在阅读了前面几节关于所有可用控件的内容并对示例进行了实验之后,您现在已经对 Flex toolkit 的 UI 功能有了很好的理解。

总结

本章详细分析了 mobile Flex 应用。有了这些知识,您现在应该能够开始编写自己的移动应用了。以下主题现在是您开发工具的一部分:

  • TabbedViewNavigatorApplicationViewNavigatorApplication应用容器以及何时使用其中之一
  • ViewNavigatorView的关系
  • 如何指定View之间的动画过渡
  • 如何在View之间来回传递数据
  • 在运行之间保持应用状态时使用的正确方法
  • 哪些控件针对移动应用进行了优化
  • 如何使用文本控件显示用户的输出和集合输入
  • 如何控制 Android 的软键盘
  • 使用和设计移动Button控件的一些技巧和提示
  • 如何使用IconItemRenderer在你的List控件中显示丰富的内容
  • 如何使用滑块输入范围内的值
  • 使用Scroller在比移动设备的受限屏幕更大的内容中平移
  • 如何使用BusyIndicator通知用户正在进行的操作

您现在知道如何创建移动应用。在下一章中,我们将向你展示如何用图形、动画和图表来增加一些活力!

四、图形和动画

图形对所有年龄段的人来说都是一种有趣的“取悦大众”的方式。如果您和我们一样喜欢图形,您会很高兴地发现,在桌面浏览器上运行的基于 Flex 的图形代码示例也可以在移动设备上运行。此外,当您为移动设备创建基于图形的应用时,您可以利用与触摸相关的事件和手势(在第二章中讨论过)。

本章的第一部分向你展示了如何渲染各种二维形状,如矩形、椭圆、贝塞尔曲线和路径。本章的第二部分包含一个使用线性渐变和径向渐变渲染几何对象的代码示例。本章的第三部分提供了一个代码示例,说明如何使用滤镜效果,包括BlurDropShadowGlow

您还将看到移动代码示例,说明如何对本章第一部分中讨论的图形形状执行变换(平移、缩放、旋转和剪切)。接下来,您将学习如何渲染图表和图形(使用 MX 组件),然后是本章的最后一个示例,它向您展示了如何创建一个草图绘制程序,将本章前面介绍的各种图形相关概念联系在一起。这个草图程序还包括触摸事件,在 JPG 文件上绘制草图的能力,以及一个保存选项,使您可以在移动设备上将草图保存为 JPG。

阅读完本章后,您将对移动设备的图形相关功能有一个很好的认识,并且本章中的一些代码示例可能会启发您编写自己美观的图形代码!

为 2D 形状使用火花原语

本节中的移动代码示例演示了如何呈现各种 2D 形状,如矩形、椭圆形、贝塞尔曲线、多边形和路径。此外,一些代码示例包含采用各种阴影技术的多个图形图像,这将使您能够对图形图像的代码进行并排比较。

绘制矩形和椭圆形

让我们从渲染两个矩形和一个椭圆开始,这是两个大家都很熟悉的 2D 形状。使用移动应用模板创建一个名为 RectEllipse1 的新 Flex 移动项目,并添加如清单 4–1 所示的代码。

清单 4–1。 渲染两个矩形和一个椭圆

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Rectangle and Ellipse">
   <s:Rect id="rect1" x="10" y="10" width="250" height="200">
      <s:fill>
         <s:SolidColor color="0xFF0000"/>
      </s:fill>
      <s:stroke>
        <s:SolidColorStroke color="0xFFFF00" weight="4"/>
      </s:stroke>
   </s:Rect>

<s:Ellipse id="ellipse1" x="10" y="220" width="250" height="200">
      <s:fill>
         <s:SolidColor color="0x0000FF"/>
      </s:fill>
      <s:stroke>
         <s:SolidColorStroke color="0xFF0000" weight="4"/>
      </s:stroke>
   </s:Ellipse>

<s:Rect id="rect2" x="10" y="460" width="250" height="100">
    <s:fill>
      <s:SolidColor color="0xFFFF00"/>
    </s:fill>
    <s:stroke>
      <s:SolidColorStroke color="0x0000FF" weight="8"/>
    </s:stroke>

fx:Declarations
      
   </fx:Declarations>
</s:View>`

清单 4–1 以一个 XML Rect元素开始,该元素指定了属性idxywidthheight的值。注意,XML Rect元素包含一个 XML fill元素和一个 XML stroke元素,而不是一个fill属性和一个stroke属性,这与 SVG 不同,SVG 通过属性指定fillstroke值。但是,XML stroke元素包含一个 XML SolidColorStroke子元素,它将colorweight指定为属性,而不是 XML 元素的值。注意,SVG 使用了一个stroke和一个stroke-width属性,而不是一个color属性和一个weight属性。

清单 4–1 还包含一个 XML Ellipse元素,它定义了一个椭圆,具有与 XML Rect元素几乎相同的属性和值,但是生成的输出是一个椭圆而不是矩形。

第二个 XML Rect元素类似于第一个Rect元素,但是颜色不同,在屏幕上的位置也不同。

图 4–1 显示了两个矩形和一个椭圆。

images

图 4–1。 两个矩形和一个椭圆

使用线性和径向渐变

Flex 移动应用支持线性渐变和径向渐变。顾名思义,线性渐变以线性方式计算起始色和结束色之间的中间色。例如,如果线性渐变从黑色变化到红色,那么初始颜色是黑色,最终颜色是红色,颜色的阴影线性“过渡”在黑色和红色之间。

径向梯度不同于线性梯度,因为过渡以径向方式发生。想象一颗扔进池塘的鹅卵石,观察半径增加的圆圈的波纹效果,这让你对径向渐变是如何渲染的有所了解。

作为一个示例,下面的移动代码呈现一个具有线性渐变的矩形和一个具有径向渐变的椭圆。使用移动应用模板创建一个名为 LinearRadial1 的新 Flex 移动项目,并添加如清单 4–2 所示的代码。

清单 4–2。 使用线性渐变和径向渐变

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        title="Linear and Radial Gradients">

<s:Panel title="Linear and Radial Gradients">
    <s:Group>
      <s:Rect id="rect1" x="10" y="10"
              height="250" width="300">
         <s:fill>
           <s:LinearGradient>
             <s:GradientEntry color="0xFF0000"
                              ratio="0"   alpha=".5"/>
             <s:GradientEntry color="0xFFFF00"
                              ratio=".33" alpha=".5"/>
             <s:GradientEntry color="0x0000FF"
                              ratio=".66" alpha=".5"/>
           </s:LinearGradient>
         </s:fill>

<s:stroke>
           <s:SolidColorStroke color="0x000000" weight="2"/>
         </s:stroke>
      </s:Rect>

<s:Ellipse id="ellipse1" x="10" y="270"
                      height="300" width="250">
        <s:fill>
          <s:RadialGradient>
            <s:GradientEntry color="0xFF0000"
                             ratio="0"  alpha="1"/>
            <s:GradientEntry color="0xFFFF00"
                             ratio=".9" alpha="1"/>
         </s:RadialGradient>
       </s:fill>

<s:stroke>
         <s:SolidColorStroke color="0x000000" weight="2"/>
       </s:stroke>
     </s:Ellipse>
   </s:Group>   </s:Panel>

fx:Declarations
    
  </fx:Declarations>
</s:View>`

清单 4–2 包含一个 XML Panel元素,该元素包含一个 XML Group元素,其属性指定面板的布局。XML Group元素包含两个 XML 子元素:一个 XML Rect元素和一个 XML Ellipse元素。XML Rect元素定义了一个带有线性渐变的矩形,如下所示:

`    <s:Rect id="rect1" x="10" y="10"
                  height="100" width="200">
       <s:fill>
          <s:LinearGradient>
             <s:GradientEntry color="0xFF0000"
                              ratio="0"   alpha=".5"/>
             <s:GradientEntry color="0xFFFF00"
                              ratio=".33" alpha=".5"/>
             <s:GradientEntry color="0x0000FF"
                              ratio=".66" alpha=".5"/>
          </s:LinearGradient>
       </s:fill>

<s:stroke>
          <s:SolidColorStroke color="0x000000" weight="2"/>
       </s:stroke>
    </s:Rect>`

前面的 XML Rect元素指定了属性idxywidthheight的值。接下来,XML Rect元素包含一个 XML fill元素(正如您在前面的示例中看到的),该元素又包含一个 XML LinearGradient元素,该元素指定了三个 XML GradientEntry元素,每个元素都为ratioalpha属性指定了一个十进制值(在01之间)。XML Rect元素的最后一部分包含一个 XML stroke元素,该元素包含一个 XML SolidColorStroke元素,该元素指定属性colorweight的值。

清单 4–2 还包含一个 XML Ellipse元素,它定义了一个带有径向渐变的椭圆。这段代码包含与 XML Rect元素几乎相同的属性和值,除了它表示一个椭圆而不是矩形。

图 4–2 显示了一个带有线性渐变的矩形和一个带有径向渐变的椭圆。

images

图 4–2。??【线性渐变的矩形】和径向渐变的椭圆

绘制三次贝塞尔曲线

Flex 支持三次贝塞尔曲线(有两个端点和两个控制点)和二次贝塞尔曲线(有两个端点和一个控制点)。您可以轻松识别三次贝塞尔曲线,因为它以字母“C”(或“C”)开头,二次贝塞尔曲线以字母“Q”(或“Q”)开头。大写字母“C”和“Q”指定“绝对”位置,而小写字母“C”和“Q”指定相对于 XML Path元素中前面一点的位置。

三次或二次贝塞尔曲线的点中列出的第一个点是第一个控制点,在三次贝塞尔曲线的情况下,后面是另一个控制点,然后是第二个端点。二次和三次贝塞尔曲线中的第一个端点是 XML Path元素中指定的前一个点;如果未指定点,则将原点(0,0)用作第一个端点。

您也可以使用字母“S”(对于三次贝塞尔曲线)或字母“T”(对于二次贝塞尔曲线)来指定贝塞尔曲线序列。

使用移动应用模板创建一个名为 BezierCurves1 的新 Flex 移动项目,并添加如清单 4–3 所示的代码,该代码显示了四条贝塞尔曲线的代码:一条三次贝塞尔曲线、一条二次贝塞尔曲线、两条组合的三次贝塞尔曲线以及一条组合的三次和二次贝塞尔曲线。

清单 4–3。 渲染三次和二次贝塞尔曲线

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Cubic and Quadratic Bezier Curves">

<s:Panel width="500" height="500"
                  title="Cubic and Quadratic Bezier Curves">
     
     <s:Path data="C 100 150 200 20 300 100">
       <s:fill>
       <s:LinearGradient rotation="90">
         <s:GradientEntry color="#FFFFFF" alpha="0.5"/>
         <s:GradientEntry color="#FF0000" alpha="0.5"/>
       </s:LinearGradient>
       </s:fill>
       <s:stroke>
         <s:SolidColorStroke color="0x0000FF" weight="4"/>
       </s:stroke>
     </s:Path>


     <s:Path data="Q 250 200 100 300">
       <s:fill>
         <s:RadialGradient rotation="90">
           <s:GradientEntry color="#000000" alpha="0.8"/>
           <s:GradientEntry color="#0000FF" alpha="0.8"/>
         </s:RadialGradient>
       </s:fill>

<s:stroke>
         <s:SolidColorStroke color="0xFF0000" weight="8"/>
       </s:stroke>
     </s:Path>


     <s:Path data="C 100 300 200 20 300 100 S 250 200 300 250">
       <s:fill>
         <s:LinearGradient rotation="90">
           <s:GradientEntry color="#FF0000" alpha="0.5"/>
           <s:GradientEntry color="#FFFF00" alpha="0.5"/>
         </s:LinearGradient>
       </s:fill>

<s:stroke>          <s:SolidColorStroke color="0x00FF00" weight="2"/>
       </s:stroke>
     </s:Path>


     <s:Path data="C 250 400 200 150 350 100 T 250 250 400 280">
       <s:fill>
         <s:LinearGradient rotation="90">
           <s:GradientEntry color="#FFFF00" alpha="0.5"/>
           <s:GradientEntry color="#FF0000" alpha="0.5"/>
         </s:LinearGradient>
       </s:fill>

<s:stroke>
         <s:SolidColorStroke color="0x000000" weight="4"/>
       </s:stroke>
     </s:Path>
   </s:Panel>
</s:View>`

清单 4–3 包含一个 XML Panel元素,该元素又包含四个 XML Path元素,这些元素指定带有各种阴影的贝塞尔曲线。第一个 XML Path元素指定了一条三次贝塞尔曲线,如下所示:

    <s:Path data="C 100 300 200 20 300 100 S 250 200 300 250">      [other elements omitted]     </s:Path>

此三次贝塞尔曲线的第一个端点是(0,0),因为没有指定点;控制点为(100,300)和(200,20);并且目的地端点是(300,100)。

这个 XML Path元素包含一个 XML LinearGradient元素,该元素从白色到红色变化,不透明度为0.5,后跟宽度为4的蓝色笔划,如下所示:

    <s:LinearGradient rotation="90">       <s:GradientEntry color="#FFFFFF" alpha="0.5"/>       <s:GradientEntry color="#FF0000" alpha="0.5"/>     </s:LinearGradient>     </s:fill>     <s:stroke>       <s:SolidColorStroke color="0x0000FF" weight="4"/>     </s:stroke>

第二个 XML Path元素指定了一条二次贝塞尔曲线,该曲线的第一个端点是(0,0 ),因为没有指定点;这条二次贝塞尔曲线的单个控制点是(250,200);而目的地端点是(100,300)。这个 XML Path元素包含一个 XML LinearGradient元素,从黑色到蓝色变化,不透明度为0.8

第三个 XML Path元素指定了一条与第二条三次贝塞尔曲线“连接”的三次贝塞尔曲线,如下所示:

    <s:Path data="C 100 300 200 20 300 100 S 250 200 300 250">      [other elements omitted]     </s:Path>

这条三次贝塞尔曲线的两个控制点是(100,300)和(20,300),目的端点是(300,100)。这个 XML Path元素的第二部分指定了一条二次贝塞尔曲线,它的控制点是(250,200),目标端点是(300,250)。

这个 XML Path元素包含一个指定从黄色到红色的线性渐变的 XML LinearGradient元素,后面是一个指定黑色和宽度为4单位的 XML stroke元素。

最后一个 XML Path元素指定了一条三次贝塞尔曲线,后跟第二条三次贝塞尔曲线,如下所示:

    <s:Path data="C 250 300 200 150 350 100 T 250 250 400 280">       [other elements omitted]     </s:Path>

这条三次贝塞尔曲线的控制点是(250,300)和(200,150),目的端点是(350,100)。这个 XML Path元素的第二部分指定了一条二次贝塞尔曲线,它的控制点是(250,250),目标端点是(400,280)。

这个 XML Path元素包含一个 XML LinearGradient元素,它指定从黄色到红色的线性渐变,不透明度为0.5,后面是一个 XML stroke元素,它指定黑色和线宽为4单位。

图 4–3 显示了三次、二次和组合贝塞尔曲线。

images

图 4–3。 三次、二次和组合贝塞尔曲线

另一个路径元素示例

在前面的例子中,您看到了如何使用Path元素来呈现一组贝塞尔曲线。元素还可以让你组合其他的 2D 形状,比如线段和带有线性渐变和径向渐变的贝塞尔曲线。使用移动应用模板创建一个名为 Path1 的新 Flex 移动项目,并添加如清单 4–4 所示的代码。

清单 4–4。 结合线段和贝塞尔曲线

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Path-based Lines and Bezier Curves">

<s:Panel width="500" height="500"
                  title="Path-based Lines and Bezier Curves">
     <s:Path data="M 50 50 L150 50 350 150 50 150z
             C 250 300 200 150 350 100 T 250 250 400 500">
       <s:fill>
         <s:LinearGradient rotation="90">
           <s:GradientEntry color="#FF0000" alpha="1"/>
           <s:GradientEntry color="#0000FF" alpha="1"/>
         </s:LinearGradient>
       </s:fill>

<s:stroke>
         <s:SolidColorStroke color="0x000000" weight="8"/>
       </s:stroke>
     </s:Path>
   </s:Panel>
</s:View>`

清单 4–4 中的 XML Panel元素包含一个 XML Path元素,它使用线段来呈现一个梯形,后跟一对三次贝塞尔曲线。XML Path元素的data属性如下所示:

    <s:Path data="M 50 50 L150 50 350 150 50 150z             C 250 300 200 150 350 100 T 250 250 400 280">

data属性的第一部分(以字母M开始)指定一个梯形;data属性的第二部分(以字母C开始)呈现一条三次贝塞尔曲线;data属性的第三部分(以字母T开始)指定了另一条三次贝塞尔曲线。

图 4–4 显示了一条梯形和两条三次贝塞尔曲线。

images

图 4–4。 基于路径的梯形和贝塞尔曲线

使用火花过滤器

Flex 滤镜效果对于在基于 Flex 的应用中创建丰富的视觉效果非常有用,这些效果可以真正增强应用的吸引力。Spark 原语支持多种滤镜,包括Blur滤镜,一个DropShadow滤镜,一个Glow滤镜,都属于spark.filters包。

使用移动应用模板创建一个名为 RectLGradFilters3 的新 Flex 移动项目,并添加如清单 4–5 所示的代码。

清单 4–5。 用火花滤镜画矩形

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Rectangle: Gradient and Filters">

<s:Rect id="rect1" x="50" y="50" height="300" width="250">
    <s:fill>
      <s:LinearGradient>
        <s:GradientEntry color="0xFF0000"
                         ratio="0"   alpha=".5"/>
        <s:GradientEntry color="0xFFFF00"
                         ratio=".33" alpha=".5"/>
        <s:GradientEntry color="0x0000FF"
                         ratio=".66" alpha=".5"/>
      </s:LinearGradient>
    </s:fill>
    <s:stroke>
      <s:SolidColorStroke color="0xFF0000" weight="2"/>
    </s:stroke>
    <s:filters>
      <s:DropShadowFilter distance="80" color="#0000FF"/>
      <s:BlurFilter/>
      <s:GlowFilter/>
    </s:filters>
  </s:Rect>
</s:View>`

清单 4–5 包含一个 XML Rect元素,它定义了一个用线性渐变渲染的矩形。ratio属性是一个介于01之间的十进制数,它指定了颜色过渡从起点到终点的距离的分数。在清单 4–5 中,第一个GradientEntry元素有一个 ratio 属性,其值为0,这意味着矩形用颜色0xFF0000(红色的十六进制值)呈现。第二个GradientEntry元素有一个 ratio 属性,它的值是0.33,这意味着矩形是用颜色0xFFFF00(黄色的十六进制值)从初始位置到目的地的 33%的位置呈现的。第三个GradientEntry元素有一个值为0.66ratio属性,因此矩形从初始位置到目的位置的 66%处用颜色0x0000FF(蓝色的十六进制值)呈现。

alpha属性是不透明度,是介于0(不可见)和1(完全可见)之间的十进制数。清单 4–5 中的三个GradientEntry元素有一个0.5的 alpha 属性,所以矩形是部分可见的。尝试比率属性和 alpha 属性的不同值,以便可以找到创建令人愉悦的视觉效果的组合。

XML Rect元素的最后一部分包含一个 XML stroke元素,该元素指定红色和描边宽度2,后跟三个火花过滤器,如下所示:

    <s:filters>       <s:DropShadowFilter distance="80" color="#0000FF"/>       <s:BlurFilter/>       <s:GlowFilter/>
    </s:filters>

本例中的三个 Spark 过滤器具有直观的名称,表明当您将它们包含在代码中时可以创建的效果。第一个 Spark 过滤器是一个向 XML Rect元素添加“投影”的DropShadowFilter。第二个火花过滤器是一个BlurFilter,它增加了模糊效果。第三个也是最后一个火花过滤器是一个GlowFilter,它创建了一个辉光过滤器效果。

Figure 4–5 显示了一个带有线性渐变和三个火花过滤器的矩形。

images

图 4–5。 一个带有线性渐变和三个火花滤镜的矩形

将变换应用于几何形状

本章的这一节介绍了如何将变换应用于几何对象,包括本章上一部分讨论的对象。Spark 原语支持以下效果和变换:

  • Animate
  • AnimateColor
  • AnimateFilter
  • AnimateShaderTransition
  • AnimateTransform
  • Fade
  • Move
  • Resize
  • Rotate
  • Scale
  • Wipe
  • CrossFade

这些 Spark 原语在spark.effects包中,它们可以应用于 Spark 组件,也可以应用于 MX 组件;mx.effects包(包含在 Flex 4 SDK 中)包含可以应用于 MX 组件的相应功能。

以下小节包含一个 Flex 代码示例,它说明了如何在 Flex 中创建缩放效果。

创建缩放效果

缩放效果(即,扩展或收缩形状)对于面向游戏的应用非常有用,并且在基于 Flex 的应用中非常容易创建。使用移动应用模板创建一个名为 ScaleEffect1 的新 Flex 移动项目,并添加如清单 4–6 所示的代码。

清单 4–6。 用线性渐变创建缩放效果

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Scale Effect">

fx:Library
     <fx:Definition name="MyRect1">
       <s:Rect x="50" y="50" height="40" width="20">
         <s:fill>
           <s:LinearGradient>
             <s:GradientEntry color="0xFF0000"
                              ratio="0"   alpha=".5"/>
             <s:GradientEntry color="0xFFFF00"
                              ratio=".33" alpha=".5"/>
             <s:GradientEntry color="0x0000FF"
                              ratio=".66" alpha=".5"/>
           </s:LinearGradient>
         </s:fill>
         <s:stroke>
            <s:SolidColorStroke color="0xFF0000" weight="1"/>
         </s:stroke>
         <s:filters>
            <s:BlurFilter/>
            <s:GlowFilter/>
         </s:filters>        </s:Rect>
     </fx:Definition>

<fx:Definition name="MyEllipse1">
        <s:Ellipse x="200" y="200" height="40" width="80">
          <s:fill>
            <s:LinearGradient>
               <s:GradientEntry color="0xFF0000"
                             ratio="0"   alpha=".5"/>
               <s:GradientEntry color="0xFFFF00"
                             ratio=".33" alpha=".5"/>
               <s:GradientEntry color="0x0000FF"
                             ratio=".66" alpha=".5"/>
            </s:LinearGradient>
          </s:fill>
          <s:stroke>
            <s:SolidColorStroke color="0xFF0000" weight="1"/>
          </s:stroke>
          <s:filters>
            <s:DropShadowFilter distance="20" color="#FF0000"/>
          </s:filters>
        </s:Ellipse>
     </fx:Definition>
  </fx:Library>

<s:Group>
    <fx:MyRect1    scaleX="6" scaleY="4"/>
    <fx:MyEllipse1 scaleX="3" scaleY="8"/>
    <fx:MyRect1    scaleX="2" scaleY="2"/>
    <fx:MyEllipse1 scaleX="2" scaleY="2"/>
  </s:Group>
</s:View>`

清单 4–6 包含一个 XML Definition元素,它指定一个带有矩形定义的 XML Rect元素,以及另一个 XML Definition元素,它指定一个带有椭圆定义的 XML Ellipse元素。XML Group元素包含两个对矩形的引用和两个对椭圆的引用,如下所示:

<s:Group>   <fx:MyRect1    scaleX="6" scaleY="4"/>   <fx:MyEllipse1 scaleX="3" scaleY="8"/>   <fx:MyRect1    scaleX="2" scaleY="2"/>   <fx:MyEllipse1 scaleX="2" scaleY="2"/> </s:Group>

第一个 XML 元素通过为属性scaleXscaleY指定值63来缩放先前定义的矩形。第二个 XML 元素通过为属性scaleXscaleY指定值38来缩放先前定义的矩形。

图 4–6 显示了两个缩放的矩形和两个缩放的椭圆。

images

图 4–6。 两个缩放的矩形和椭圆

在 Spark 中创建动画效果

本节包含的移动代码展示了如何将动画效果应用到几何对象上,包括本章上一部分讨论的那些对象。动画效果的火花原语如下:

  • Animate
  • AnimateColor
  • AnimateFilter
  • AnimateShaderTransition
  • AnimateTransform
  • CrossFade
  • Fade
  • Move
  • Resize
  • Rotate
  • Scale
  • Wipe

以下部分提供了移动代码示例,说明如何使用 XML Animate元素以及如何并行和顺序定义动画效果。

使用动画元素

对于面向游戏的应用来说,动画效果显然非常受欢迎,而且它们也可以有效地用于其他类型的应用。同时,请记住,在以业务为中心的应用中谨慎使用动画效果可能是个好主意。

使用移动应用模板创建一个名为 AnimPropertyWidth 的新 Flex 移动项目,并添加如清单 4–7 所示的代码。

清单 4–7。 动画显示矩形的宽度

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Animate Rectangle Width">

fx:Declarations
    <s:Animate id="MyAnimate1">
       <s:motionPaths>
         <s:MotionPath property="width">
           <s:keyframes>
             <s:Keyframe time="0"    value="200"/>
             <s:Keyframe time="2000" value="400"/>
           </s:keyframes>
         </s:MotionPath>
       </s:motionPaths>
     </s:Animate>
   </fx:Declarations>

<s:VGroup>
    <s:Rect id="rect1" x="10" y="50" height="300" width="200">
      <s:fill>
         <s:LinearGradient>
            <s:GradientEntry color="0xFF0000"
                             ratio="0"   alpha=".5"/>
            <s:GradientEntry color="0xFFFF00"
                             ratio=".33" alpha=".5"/>
            <s:GradientEntry color="0x0000FF"
                             ratio=".66" alpha=".5"/>
         </s:LinearGradient>
       </s:fill>
       <s:stroke>          <s:SolidColorStroke color="0xFF0000" weight="2"/>
      </s:stroke>
    </s:Rect>

<s:Button id="MyButton1" label="Animate Width"
              click="MyAnimate1.play([rect1])"
              bottom="150" right="50">
    </s:Button>
   </s:VGroup>
 </s:View>`

清单 4–7 包含一个 XML Declarations元素,该元素又包含一个定义动画特定细节的 XML Animate元素。XML Animate元素有一个值为MyAnimate1id属性,该属性在本节稍后描述的点击处理事件中被引用。

清单 4–7 包含一个 XML VGroup元素,该元素又包含一个 XML Rect元素,其内容类似于您在本章中已经看到的例子。

清单 4–7 包含一个 XML Button元素,使您能够开始动画效果。每当用户单击或点击这个按钮时,代码将执行事件处理程序,其id属性为MyAnimate1,这在前面的代码示例中已定义。动画效果很简单:矩形宽度在两秒钟(2000 毫秒)内从 200 个单位增加到 400 个单位。

图 4–7 和图 4–8 显示了当用户点击按钮时,一个矩形在屏幕上水平移动的两个快照。

images

图 4–7。 一个带有动画的矩形(初始位置)

images

图 4–8。 一个带动画的矩形(最终位置)

动画:并行和顺序

Flex 支持两种动画效果。并行动画效果涉及同时发生的两个或多个动画效果。另一方面,顺序动画效果涉及两个或更多按顺序出现的动画效果,这意味着在任何给定时间只出现一个动画效果。记住这一点,使用移动应用模板创建一个名为 SequentialAnimation1 的新 Flex 移动项目,并添加如清单 4–8 所示的代码。

清单 4–8。 创造连续动画效果

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Sequential Animation">

fx:Declarations
    <s:Sequence id="transformer1" target="{button1}">
      <s:Move xFrom="50" xTo="150"
              autoCenterTransform="true"/>
      <s:Rotate angleFrom="0" angleTo="360"
              autoCenterTransform="true"/>
      <s:Scale scaleXFrom="1" scaleXTo="2"
              autoCenterTransform="true"/>
    </s:Sequence>

<s:Sequence id="transformer2" target="{button2}">
      <s:Move xFrom="50" xTo="150"              autoCenterTransform="true"/>
    <s:Scale scaleXFrom="1" scaleXTo="2"
             autoCenterTransform="true"/>
    <s:Rotate angleFrom="0" angleTo="720"
             autoCenterTransform="true"/>
    </s:Sequence>
  </fx:Declarations>

<s:Rect id="rect1" x="10" y="10" width="400" height="400">
    <s:fill>
      <s:SolidColor color="0xFF0000"/>
    </s:fill>
    <s:stroke>
      <s:SolidColorStroke color="0x0000FF" weight="4"/>
    </s:stroke>
  </s:Rect>

<s:Button id="button1" x="50" y="100" label="Transform Me"
                  click="transformer1.play()"/>

<s:Button id="button2" x="50" y="200" label="Transform Me Too"
                  click="transformer2.play()"/>
</s:View>`

清单 4–8 包含一个 XML Declarations元素,该元素又包含两个 XML Sequence元素,这两个元素指定了三种转换效果。动画效果从 XML Move元素开始(提供翻译效果),然后是 XML Rotate元素(提供旋转效果),最后是 XML Scale元素(提供缩放效果)。当用户点击第一个 XML Button元素时,这将调用 XML Sequence元素中定义的动画效果,该元素的id属性的值为transformer1

类似的注释也适用于第二个 XML Sequence元素和第二个按钮,只是动画效果包含两次完整的旋转,而不是一次旋转。

请注意,通过用 XML Parallel元素替换 XML Sequence元素,您可以轻松地将动画效果从顺序改为并行,如下所示:

     <s:Parallel id="transformer" target="{button}">        <s:Move xFrom="50" xTo="150"                autoCenterTransform="true"/>        <s:Rotate angleFrom="0" angleTo="360"               autoCenterTransform="true"/>        <s:Scale scaleXFrom="1" scaleXTo="2"                 autoCenterTransform="true"/>      </s:Parallel>

图 4–9 和图 4–10 显示了两个按顺序经历动画效果的按钮。由于截图只捕获了初始和最终的动画效果,因此在移动设备上启动这个移动应用,这样您还可以看到滑动效果和旋转效果。

images

图 4–9。一个带有连续动画的按钮(初始)

*images

图 4–10。 一个带有连续动画的按钮(后)

创建 3D 效果

Flex 支持多种 3D 效果,包括移动、旋转和缩放 JPG 文件。3D“移动”效果包括移动 JPG 图像以及减小图像的尺寸,而 3D 缩放效果包括将 JPG 图像的宽度和高度从起始值(通常为 1)增加(或减小)到最终值(可以大于或小于 1)。3D“旋转”效果包括旋转 JPG 图像,使其看起来像在三维空间中旋转。

清单 4.9 中的以下代码示例向您展示了如何在基于移动设备的应用中创建移动、旋转和缩放 JPG 文件的 3D 效果。

Figure 4–11 显示了卡桑德拉·陈(斯蒂芬·陈的女儿)的 JPG 形象Cassandra4.jpg,该形象用于说明这三种 3D 动画效果的代码示例中。

images

图 4–11。3D 特效的 JPG

清单 4–9。 制作 3D 动画效果

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Creating 3D Effects">

fx:Declarations
    <s:Move3D id="moveEffect" target="{targetImg}" xBy="100" zBy="100"               repeatCount="2" repeatBehavior="reverse"
              effectStart="playMoveButton.enabled=false"
              effectEnd="playMoveButton.enabled=true;"/>

<s:Rotate3D id="rotateEffect" target="{targetImg}"
                angleYFrom="0" angleYTo="360"
                repeatCount="4" repeatBehavior="reverse"
                effectStart="playRotateButton.enabled=false;"
                effectEnd="playRotateButton.enabled=true;"/>

<s:Scale3D id="atScale" target="{targetImg}"
               scaleXBy="-.45" repeatCount="2"
               repeatBehavior="reverse"
               effectStart="playScaleButton.enabled=false"
               effectEnd="playScaleButton.enabled=true;"/>
  </fx:Declarations>

<s:VGroup width="100%" height="100%" >
    <s:Image id="targetImg"
             horizontalCenter="0"
             verticalCenter="0"
             source="@Embed(source='img/Cassandra4.jpg')"/>

<s:HGroup>
      <s:Button id="playMoveButton"
                left="10" bottom="25"
                label="Move"
                click="moveEffect.play();"/>

<s:Button id="playRotateButton"
                left="110" bottom="25"
                label="Rotate"
                click="rotateEffect.play();"/>

<s:Button id="playScaleButton"
                left="222" bottom="25"
                label="Scale" click="atScale.play();"/>
    </s:HGroup>
  </s:VGroup>

</s:View>`

清单 4–9 包含一个 XML Declarations元素,该元素包含三个 3D 效果元素,以及三个 XML Button元素,用户可以单击这些元素来创建 3D 效果。XML Move3D元素通过属性xByzBy指定目标位置,还有一个值为 2 的repeatCount(执行动画效果两次),以及一个值为reverse(每次都返回到原始位置)的repeatBehavior。相应的 XML Button元素包含一个值为Movelabel属性和一个值为moveEffect.play()click属性,后者调用在 XML Declarations元素中定义的 XML MoveEffect元素中指定的移动动画效果。

旋转效果通过 XML Rotate3D元素处理,其属性angleYFromangleYTo分别指定0360的开始和结束角度(即一次完整的旋转)。这种旋转效果会出现四次。XML Button元素包含一个值为Rotatelabel属性和一个值为rotateEffect.play()click属性,该属性调用在 XML Declarations元素中定义的 XML Rotate3D元素中指定的缩放动画效果。

缩放效果(这是第三个也是最后一个效果)是通过 XML Scale3D元素处理的,该元素包含几个属性,这些属性的值指定了同一个 JPG 图像的动画行为的细节。id属性的值为atScale,用于在代码的其他地方引用这个元素。属性target引用了 XML 元素,其id的值为targetImg,引用了 JPG 图像。scaleXBy属性的值为-0.25,它将 JPG 图像缩小 25%。repeatCount属性的值为4,repeatBehavior属性的值为reverse,表示动画效果出现四次,从左到右来回交替。另外两个属性是effectStarteffectEnd,它们指定动画开始和结束时的行为,在本例中是禁用然后启用 playButton。

注意,XML Image元素指定了Cassandra4.jpg,的位置,它位于这个移动项目的顶层目录的images子文件夹中。出于布局目的,XML Image元素在 XML VGroup元素中指定,该元素还包含一个 XML HGroup元素,该元素包含三个 XML Button元素。

Figure 4–12 显示了经过 3D“移动”效果后的 JPG。

images

图 4–12。 一个 3D 移动后的 JPG 效果

图 4–13 显示了经过 3D“旋转”效果后的 JPG。

images

图 4–13。 一个 3D 旋转后的 JPG 效果

图 4–14 显示了经过 3D“缩放”效果后的 JPG。

images

图 4–14。 一个 JPG 经过 3D 缩放后的效果

创建火花皮肤

当您希望在移动应用的某些方面创建更丰富的视觉效果时,自定义外观非常有用。例如,您可以创建多个自定义外观,将图形效果(包括您在本章前面学习的那些)应用于按钮。我们将要讨论的代码示例清楚地展示了创建 Spark 自定义皮肤效果的过程。

清单 4–10 到 4–12 分别显示CustomSkinHomeView.mxmlButtonSkin1.mxmlButtonSkin2.mxml中的代码内容。

在本节讨论 MXML 文件之前,让我们看一下下面的将文件ButtonSkin1.mxml(在skins包中)添加到项目中的步骤列表。

  1. 将新文件夹skins添加到您的项目中。
  2. 右键单击您的项目,并导航到New->MXML Skin
  3. 指定skins作为新皮肤的包名。
  4. 指定ButtonSkin1作为皮肤的名称。
  5. 指定spark.components.Button作为组件的名称。
  6. 取消选择标签“Create as a copy of:”左侧的复选框。

对定制皮肤ButtonSkin2.mxml重复前面的一组步骤,并对您想要添加到这个项目中的任何额外的定制皮肤重复这些步骤。现在让我们看看CustomSkin.mxml的内容,显示在清单 4–10 中

清单 4–10。 创建自定义火花皮肤

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Custom Skins">
<s:VGroup>
    <s:Label text="This is a Normal Button:" x="10" y="0"/>
    <s:Button label="Button1" x="10" y="25"/>

<s:Label text="First Skinned Button:"   x="10" y="60"/>
    <s:Button skinClass="skins.ButtonSkin1" x="10" y="85"/>

<s:Label text="Second Skinned Button:"  x="10" y="100"/>
    <s:Button skinClass="skins.ButtonSkin2" x="10" y="125"/>

<s:Label text="Third Skinned Button:"   x="10" y="140"/>
    <s:Button skinClass="skins.ButtonSkin1" x="10" y="165"/>

<s:Label text="Fourth Skinned Button:"  x="10" y="180"/>
    <s:Button skinClass="skins.ButtonSkin2" x="10" y="205"/>
</s:VGroup>
</s:View>`

清单 4–10 包含一个 XML VGroup元素,该元素包含十个“成对的”XML 元素,用于呈现一个标准 XML Label元素和一个标准 XML Button元素,其中第一个是一个普通的按钮,如下所示:

      <s:Label text="This is a Normal Button:" x="10" y="0"/>       <s:Button label="Button1" x="10" y="25"/>

前面的 XML 元素很简单:第一个是标签(“This is a Normal Button”),第二个呈现按钮。

第一对包含皮肤按钮的 XML 元素显示标签“First Skinned Button:”,第二个元素基于包skins中 Flex 皮肤ButtonSkin1的内容呈现一个 XML Button元素。类似地,下一对包含皮肤按钮的 XML 元素显示标签“Second Skinned Button:”,这一对中的第二个元素基于包skins中 Flex 皮肤ButtonSkin2的内容呈现一个 XML Button元素。类似的注释也适用于其他两个自定义按钮。

现在让我们看看清单 4–11 中ButtonSkin1.mxml的内容,它包含了渲染第二个按钮(这是第一个皮肤按钮)的数据。

清单 4–11创建带有图形的按钮皮肤

`<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark" >

fx:Metadata
     [HostComponent("spark.components.Button")]
   </fx:Metadata>

<s:states>
     <s:State name="disabled" />
     <s:State name="down" />
     <s:State name="over" />
     <s:State name="up" />
   </s:states>

<s:Rect id="rect1" x="0" y="0" height="40" width="100">
     <s:fill>
       <s:LinearGradient>
         <s:GradientEntry color="0xFF0000"
                          ratio="0"   alpha=".5"/>
         <s:GradientEntry color="0xFFFF00"
                          ratio=".33" alpha=".5"/>
         <s:GradientEntry color="0x0000FF"
                          ratio=".66" alpha=".5"/>
       </s:LinearGradient>
     </s:fill>

<s:stroke>
       <s:SolidColorStroke color="0x000000" weight="2"/>
     </s:stroke>
   </s:Rect>
</s:Skin>`

清单 4–11 包含一个 XML Skin根节点,其中有三个 XML 子元素定义了定制皮肤的行为。第一个子元素是 XML Metadata元素,如下所示:

     <fx:Metadata>        [HostComponent("spark.components.Button")]      </fx:Metadata>

前面的 XML 元素指定了Button类的包名,这也是您在项目中添加自定义皮肤ButtonSkin1.mxml时指定的名称。

第二个子元素是 XML states元素,如下所示:

   <s:states>      <s:State name="disabled" />      <s:State name="down" />      <s:State name="over" />      <s:State name="up" />    </s:states>

前面的 XML states元素包含对应于一个按钮状态和三个鼠标相关事件的四个子元素,如果您想要处理这些状态,您可以包含额外的代码。第三个子元素是 XML Rect元素,它为阴影效果和黑色边框指定了线性渐变。

清单 4–12。 创建第二个按钮皮肤

`<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark" >

fx:Metadata
     [HostComponent("spark.components.Button")]
   </fx:Metadata>
   <s:states>
     <s:State name="disabled" />
     <s:State name="down" />
     <s:State name="over" />
     <s:State name="up" />
   </s:states>

<s:Path data="M 0 0 L 100 0 L 100 40 L 0 40 Z ">
     <s:fill>
       <s:SolidColor color="#FF0000" alpha="1"/>
     </s:fill>
     <s:stroke>
       <s:SolidColorStroke color="#0000FF" weight="4"/>
     </s:stroke>
   </s:Path>
</s:Skin>`

注意,清单 4–12 和清单 4–11 的唯一区别是 XML Path元素而不是 XML Rect元素。

XML Path元素很简单:它包含一个数据属性,该属性的值是一组指定矩形的线段,矩形的颜色是#FF0000(红色),边框是#0000FF(蓝色),宽度是4

如你所见,Flex 使得定义自定义皮肤变得非常容易。然而,更复杂(也更有趣)的自定义皮肤通常指定鼠标事件(如鼠标按下、鼠标抬起等)的行为以及相应的状态变化方面的触摸事件。您可以“绑定”在这些事件期间执行的 ActionScript 函数(由您编写),以便更改应用各个方面的可视显示。

Figure 4–15 显示了一个标准的 Flex 按钮和四个使用自定义皮肤的按钮。

images

图 4–15。 一个标准按钮和四个带有自定义火花皮肤的按钮

在 Spark 中生成 2D 图表和图形

Flex 4 为以下 2D 图表和图形提供了良好的支持:

  • 面积图
  • 柱形图
  • 条形图
  • 曲线图
  • 饼图
  • 散点图

在下面的示例中,您将学习如何编写用于呈现 2D 条形图和 2D 饼图的移动代码示例,您还将看到具有动画效果并可以处理鼠标事件和触摸事件的代码示例。请注意,Flex 使用术语“条形图”表示水平条形图(即每个条形元素从左到右水平呈现),术语“柱形图”指垂直条形图。

创建 2D 条形图

条形图非常受欢迎,尤其是在面向业务的应用中,因为它们使您能够轻松地看到数据的趋势,否则可能很难从数据的表格显示中看出这些趋势。在接下来的示例中,您将学习如何创建一个移动应用,该应用从 XML 文档中读取基于 XML 的数据,然后在 2D 条形图中呈现这些数据。这些数据是为了举例说明,显然你会使用你自己的真实数据,而不是包含在清单 4–12 中的“虚构”数据。还要记住,本书中示例的完整来源可以从本书的网页上在线获得。

现在,使用移动应用模板创建一个名为 BarChart1 的新 Flex 移动项目,添加一个名为chartdata的新顶级文件夹,然后在这个名为ChartData.xml的文件夹中添加一个新的 XML 文档,其中包含了清单 4–13 中显示的数据。

清单 4–13。定义基于 XML 的图表数据

<?xml version="1.0"?> <chartdata>   <data>     <month>January</month>     <revenue>1500</revenue>   </data>   <data>     <month>February</month>     <revenue>1400</revenue> </data> [data omitted for brevity]   <data>     <month>November</month>     <revenue>1900</revenue>   </data>   <data>     <month>December</month>     <revenue>1800</revenue>   </data> </chartdata>

清单 4–13 包含一个 XML chartdata元素,该元素包含 12 个 XML data元素,每个元素保存一年中某个月的图表相关数据。清单 4–13 中的每个 XML data元素包含一个 XML month元素和一个 XML revenue元素。例如,第一个 XML data元素指定了一个值为1500revenue元素和一个值为Januarymonth元素(没有指定货币单位)。

现在让我们看一下清单 4–14,它包含使用清单 4–13 中基于 XML 的数据呈现条形图的代码。

清单 4–14。 创建条形图

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"         xmlns:mx="library://ns.adobe.com/flex/mx"         xmlns:s="library://ns.adobe.com/flex/spark"
`        title="Bar Chart">


   fx:Declarations
     <fx:Model id="chartModel" source="chartdata/ChartData.xml"/>
     <s:ArrayCollection id="chartData" source="{chartModel.data}"/>
     <mx:NumberFormatter id="nf" precision="1" rounding="nearest"/>
   </fx:Declarations>

fx:Style
     @namespace s  "library://ns.adobe.com/flex/spark";
     @namespace mx "library://ns.adobe.com/flex/mx";
     mx|ColumnChart
     {
       font-size:12;
       font-weight:bold;
     }
   </fx:Style>


   <mx:ColumnChart dataProvider="{chartData}"
                   height="70%" width="100%">
        mx:horizontalAxis
       <mx:CategoryAxis dataProvider="{chartData}"
                        categoryField="month"/>
     </mx:horizontalAxis>
     mx:series
       <mx:ColumnSeries xField="month" yField="revenue"/>
     </mx:series>
   </mx:ColumnChart>
</s:View>`

清单 4–14 定义了 XML 文档ChartData.xml在 XML Model元素中的位置,以及由基于 XML 的数据组成的ArrayCollection和一个简单的数据格式化程序。清单 4–14 包含一个 XML Style元素,该元素为两个 CSS 属性font-sizefont-weight指定值,分别为12bold,用于在饼图中呈现文本。

XML ColumnChart元素指定了一个柱形图,以及属性dataProviderheightweight的适当值,它们的值分别是chartData75%80%。注意,chartData是一个在 XML Declarations元素中定义的ArrayCollection变量,而chartData是用 XML 文档ChartData.xml中指定的数据值填充的。

heightweight属性的值被指定为呈现饼图的屏幕尺寸的百分比;根据您希望条形图占据屏幕的百分比来调整这些属性的值(50%表示半宽或半高,25%表示四分之一宽或四分之一高,依此类推)。

XML ColumnChart元素包含两个重要的元素。首先,有一个 XML horizontalAxis元素指定了水平轴的month值(在ChartData.xml中指定)。其次,有一个 XML series元素,引用条形图水平轴的month值和垂直轴的revenue值。

图 4–16 显示了一个基于 XML 文件ChartData.xml中数据的条形图,显示在清单 4–13 中。

images

图 4–16。2D 条形图

请记住图 4–16 缺少一些有用的信息,例如收入的货币、当前年份、公司的名称和位置以及收入数据的地区(或国家)。如果您添加此类额外信息,请对清单 4–14 中的代码进行适当的修改,以确保修改后的代码指定了访问收入相关数据的正确路径。

创建 2D 饼图

饼图也很受欢迎,因为它以一种更容易理解数据元素之间关系的方式显示数据。我们将创建一个饼图,它使用了清单 4–13 中的 XML 文档ChartData.xml中的数据,这些数据与上一个示例中用于呈现条形图的数据相同。使用移动应用模板创建一个名为 PieChart1 的新 Flex 移动项目,并添加如清单 4–15 所示的代码。

清单 4–15。 创建饼状图

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="Pie Chart">


   fx:Declarations
     <fx:Model id="chartModel" source="chartdata/ChartData.xml"/>
     <s:ArrayCollection id="chartData" source="{chartModel.data}"/>
     <mx:NumberFormatter id="nf" precision="1" rounding="nearest"/>
   </fx:Declarations>

fx:Style
      @namespace s  "library://ns.adobe.com/flex/spark";
      @namespace mx "library://ns.adobe.com/flex/mx";
      mx|PieChart
      {
         font-size:12;
         font-weight:bold;
      }
   </fx:Style>


   fx:Script
      
   </fx:Script>


   <mx:PieChart dataProvider="{chartData}"
                height="50%" width="80%"
                horizontalCenter="0" verticalCenter="0">
      mx:series
         <mx:PieSeries field="revenue"
                       labelFunction="getWedgeLabel"
                       labelPosition="callout"
                       explodeRadius="0.05"/>
      </mx:series>
   </mx:PieChart>
</s:View>`

清单 4–15 包含一个 XML Declarations元素和一个 XML Style元素,它们与清单 4–14 相同。定义私有函数 getWedgeLabel的 XML Script元素返回由每个饼图楔形区的name:value对组成的字符串,如下所示:

<fx:Script>    <![CDATA[       private function getWedgeLabel (item:Object,                                       field:String,                                       index:Number,                                       percentValue:Number):String       {          return item.month+": "+item.revenue;       }    ]]> </fx:Script>

XML PieChart元素指定了一个饼图,以及其值指定如何呈现饼图的属性。例如,heightwidth属性都有值80%,这意味着图表的高度和宽度是屏幕尺寸的 80%。根据您希望饼图占据屏幕的百分比来调整这些属性的值(就像您对条形图所做的那样)。

XML PieChart元素还包含一个 XML PieSeries元素,该元素又包含四个属性,使您能够指定如何呈现饼图数据和饼图扇区。field属性的值为revenue,这意味着 XML revenue元素的数据值呈现在饼图中。

labelFunction属性的值为getWedgeLabel,这是一个 ActionScript 函数(在前面的fx:Script元素中定义),它指定了饼图中每个饼图“楔形”的标签。

labelPosition属性的值为callout,这意味着每个饼图扇区的标签都呈现在饼图扇区之外,从饼图扇区到其标签之间有一条“断开的”线段。注意,labelPosition属性可以有另外三个值:insideoutsideinsideWithCallout。尝试这些值,看看它们如何改变饼图的呈现。

最后,explodeRadius属性的值为0.05,它呈现的饼图在相邻的饼图扇区之间留有空间,产生了一种“爆炸”效果。

图 4–17 显示了一个 2D 饼图。

images

图 4–17。??【2D】饼状图

使用带火花的 FXG

第三章包含了对 FXG 的简要介绍,这一节包含了一个代码示例,演示了如何将清单 4–1(包含了渲染矩形和椭圆的代码)转换成使用 FXG 的 Flex 项目。

使用移动应用模板创建一个名为 FXG1 的新 Flex 移动项目,创建一个名为components的顶层文件夹,然后在这个名为RectEllipse1.fxg的文件夹中创建一个文件,其内容如清单 4–16 所示。

清单 4–16。 使用 FXG 定义图形元素

`

  
    
      
    

    
      
    

  


    
      
    
    
      
    

  


    
      
    

    
      
    

  

`

XML Graphic元素包含两个 XML 元素,它们的数据值与清单 4–1 中的 XML Rect元素和 XML Ellipse元素相同,除此之外还有以下区别:

  • 元素不包含命名空间前缀。
  • 这些元素属于默认的名称空间。
  • 颜色属性使用“#”符号而不是“0x”前缀。

清单 4–17 展示了如何引用清单 4–16 中定义的元素。

清单 4–17。 引用 FXG 组件

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:comps="components.*">

<s:VGroup>
      <comps:RectEllipse1 id="rect1"/>
</s:VGroup>
</s:View>`

清单 4–17 包含一个引用 FXG 文件RectEllipse1.fxg的名称空间,该文件位于components子目录中。XML VGroup元素在comps名称空间中包含一个 XML RectEllipse1元素,该元素引用一个 XML 元素,该元素的id属性的值为rect1,该值在 FXG 文件RectEllipse1.fxg中定义,如清单 4–16 所示。

图 4–18 显示一个椭圆和两个矩形,与图 4–1 中的相同。

images

图 4–18。??【一个长方形和一个椭圆】??

从这个例子可以推测,FXG 使您能够模块化 Flex 项目中的代码。此外,以下 Adobe 产品使您能够将项目导出为 FXG 文件,然后您可以将这些文件导入 Flex 项目:

  • Adobe Photoshop 中
  • Adobe Illustrator 中
  • Adobe Fireworks

你可以在第九章中看到 FXG 文件更复杂的例子。

一个素描程序

您将在本节中看到的移动代码向您展示了如何创建一个草图程序,该程序将本章前面介绍的各种图形相关概念结合在一起,并提供触摸事件、在 JPG 文件上绘制草图以及在移动设备上将草图保存为 JPG 的能力。

使用 Mobile Flex 应用模板创建一个名为 Sketch1 的新 Flex 移动项目,并添加显示在清单 4–18 中的代码。出于讨论的目的,代码以较小的代码块呈现,请记住完整的代码可以从本书的网站下载。

清单 4–18。 渲染并保存草图

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark" title="HomeView">
   fx:Script
     <![CDATA[
       import flash.ui.Multitouch;
       import flash.ui.MultitouchInputMode;
       import flash.events.TouchEvent;

import mx.graphics.ImageSnapshot;
       import mx.graphics.SolidColor;
       import mx.graphics.codec.JPEGEncoder;

private var colors:Array = [0xFF0000, 0x00FF00, 0xFFfF00, 0x0000FF];
       private var singleTapCount:int = 0;
       private var touchMoveCount:int = 0;
       private var widthFactor:int = 0;
       private var heightFactor:int = 0;
       private var currentColor:int = 0;
       private var rectWidth:int = 20;
       private var rectHeight:int = 20;

Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT;

function touchMove(event:TouchEvent):void {
          //event.stopImmediatePropagation();
          ++touchMoveCount;

if (event.isPrimaryTouchPoint) {
            currentColor = colors[touchMoveCount%colors.length];
          } else {
            currentColor = colors[(touchMoveCount+2)%colors.length];
          }

var myRect:Rect = new Rect();
          myRect.x = event.localX;
          myRect.y = event.localY;
          myRect.width  = rectWidth;
          myRect.height = rectHeight;
          myRect.fill = new SolidColor(currentColor);

var myGroup1:Group = event.target as Group;
          myGroup1.addElement(myRect);
       }`

清单 4–18 以一个 XML Script元素开始,该元素包含各种导入语句和在一些 ActionScript 3 方法中使用的恰当命名的变量的定义(例如,用于跟踪触摸事件)。

MultiTouch.inputMode设置为多点触摸模式,因此当您在屏幕上拖动多个手指时,会呈现多个矩形。如果你需要刷新一下关于多点触摸的记忆,你可以阅读第二章中的相关章节。

函数touchMove包含处理移动事件的代码。该函数首先递增变量touchMoveCount,然后使用该变量作为数组colors的索引,从而呈现一组矩形,其颜色遍历该数组。该函数中的其余代码在触摸事件的位置创建一个小矩形。这实际上是图形渲染代码的“心脏”,但是其他函数处理其他事件。

下一个代码块包含函数touchEnd()的代码,它实际上是可选的,但是它向您展示了一个在这个事件处理程序中可以做什么的例子。

`      function touchEnd(event:TouchEvent):void {
         ++touchMoveCount;

if (event.isPrimaryTouchPoint) {
           currentColor = colors[touchMoveCount%colors.length];
         } else {
           currentColor = colors[0];
         }

widthFactor = (touchMoveCount%3)+1;
         heightFactor = (touchMoveCount%3)+2;

var myRect:Rect = new Rect();
         myRect.x = event.localX;
         myRect.y = event.localY;
         myRect.width  = rectWidthwidthFactor;
         myRect.height = rectHeight
heightFactor;
         myRect.fill = new SolidColor(currentColor);

var myGroup1:Group = event.target as Group;
         myGroup1.addElement(myRect);
      }`

函数touchEnd中处理“touch up”事件的代码递增变量touchMoveCount,然后使用该变量作为数组colors的索引,但是在这种情况下,执行一些简单的算法来呈现不同尺寸的矩形。

`function touchSingleTap(event:TouchEvent):void {
          var myRect:Rect = new Rect();
          myRect.x = event.localX;
          myRect.y = event.localY;

++singleTapCount;
          if (event.isPrimaryTouchPoint) {
            currentColor = colors[singleTapCount%colors.length];
            myRect.width  = rectWidth3;
            myRect.height = rectHeight
2;
          } else {
             currentColor = colors[(singleTapCount+1)%colors.length];
             myRect.width  = rectWidth2;
             myRect.height = rectHeight
3;           }

myRect.fill = new SolidColor(currentColor);

var myGroup1:Group = event.target as Group;
          myGroup1.addElement(myRect);
      }`

处理单击事件的逻辑在函数touchSingleTap中。该函数递增变量touchSingleTapCount,然后应用一些简单的逻辑来确定在单击事件位置呈现的矩形的尺寸。

`      function touchMoveHandlerImage(event:TouchEvent):void {
        touchMove(event);
      }

function touchTapHandlerImage(event:TouchEvent):void {
        touchSingleTap(event);
      }

private function saveImageToFileSystem():void {
         var jPEGEncoder:JPEGEncoder = new JPEGEncoder(500);
         var imageSnapshot:ImageSnapshot = ImageSnapshot.captureImage(imgPanel, 0,
                                                                      jPEGEncoder);

var fileReference:FileReference = new FileReference();
         fileReference.save(imageSnapshot.data, "fingersketch.jpg");
       }
     ]]>
   </fx:Script>`

两个函数touchMoveHandlerImagetouchTapHandlerImage(顾名思义)处理 JPG 文件fingersketch.jpg的移动事件和单击事件,该文件存储在 Flex 应用的images子目录中。这两个函数包含一行调用相应函数touchMovetouchTapHandler的代码,这在本节前面已经讨论过了。

每当您单击 Save Sketch 按钮时,就会调用函数saveImageToFileSystem,它包含将当前草图保存到移动设备的文件系统的代码。将出现一个弹出对话框,其中包含 JPG 文件的默认位置和名称,在保存当前草图之前,您可以更改这两个位置和名称。

`   <s:Panel id="imgPanel" title="Finger Sketching For Fun!" width="100%" height="100%" >
      <s:Button id="saveImage"
                left="150" bottom="5"
                label="Save Sketch"
                click="saveImageToFileSystem();"/>

<s:Group name="myGroup1" width="500" height="500"
               touchMove="touchMove(event)"
               touchEnd="touchEnd(event)"
               touchTap="touchSingleTap(event)">
        <s:Ellipse id="ellipse1" x="10" y="10" width="100" height="50">
          <s:fill> <s:SolidColor color="0xFFFF00"/> </s:fill>
          <s:stroke> <s:SolidColorStroke color="red" weight="5"/> </s:stroke>           </s:Ellipse>
          <s:Rect id="rect1" x="110" y="10" width="100" height="50">
            <s:fill> <s:SolidColor color="0xFF0000"/> </s:fill>
             <s:stroke> <s:SolidColorStroke color="blue" weight="5"/> </s:stroke>
          </s:Rect>
          <s:Ellipse id="ellipse2" x="210" y="10" width="100" height="50">
            <s:fill> <s:SolidColor color="0xFFFF00"/> </s:fill>
            <s:stroke> <s:SolidColorStroke color="red" weight="5"/> </s:stroke>
          </s:Ellipse>
          <s:Rect id="rect2" x="310" y="10" width="100" height="50">
            <s:fill> <s:SolidColor color="0xFF0000"/> </s:fill>
             <s:stroke> <s:SolidColorStroke color="blue" weight="5"/> </s:stroke>
          </s:Rect>

<s:Path data="C100 300 200 20 300 100 S 250 200 300 250">
             <s:fill>
                <s:LinearGradient rotation="90">
                   <s:GradientEntry color="#FF0000" alpha="0.8"/>
                   <s:GradientEntry color="#0000FF" alpha="0.8"/>
             </s:LinearGradient>
           </s:fill>

<s:stroke>
              <s:SolidColorStroke color="0x00FF00" weight="2"/>
           </s:stroke>
        </s:Path>

<s:Path data="C 350 300 200 150 350 100 T 250 250 400 280">
           <s:fill>
              <s:LinearGradient rotation="90">
                 <s:GradientEntry color="#FFFF00" alpha="0.5"/>
                 <s:GradientEntry color="#FF0000" alpha="0.5"/>
              </s:LinearGradient>
           </s:fill>

<s:stroke>
              <s:SolidColorStroke color="0x000000" weight="4"/>
           </s:stroke>
        </s:Path>
     </s:Group>

<s:Image id="img" width="480" height="320" source="img/fingersketch.jpg"
              touchMove="touchMoveHandlerImage(event)"
              touchTap="touchTapHandlerImage(event)"
              horizontalCenter="-10" verticalCenter="60"/>
   </s:Panel>
</s:View>`

代码的下一个主要部分由一个 XML Panel元素组成,该元素包含一个用于保存当前草图的 XML Button元素,随后是一个 XML Group元素,该元素指定与触摸相关的事件处理程序touchMovetouchEndtouchTap,它们在本示例的前面已经讨论过。

XML Group元素还包含各种图形对象的定义,包括椭圆、矩形和贝塞尔曲线,这在本章前面已经学过。这些图形对象显然是可选的,它们只是为了让你知道如何制作一个对用户有吸引力和视觉吸引力的草图程序。

XML Image元素指定了 JPG 文件fingersketch.jpg,它位于这个 Flex 应用的images子目录中。XML Image元素指定了用于触摸动作事件的函数touchMoveHandlerImage和用于轻击相关事件的函数touchTapHandlerImage。尝试使用属性horizontalCenterverticalCenter的不同值,这将改变 JPG 图像的水平和垂直布局位置。

图 4–19 显示了在移动设备中启动草图程序后的草图样本。

images

图 4–19。 一张样图

总结

在本章中,您学习了如何使用 Spark 组件在面向移动设备的图形应用中呈现各种 2D 图形形状。如果您已经熟悉渲染基于 Flex 的图形,那么您可以快速轻松地利用现有知识来创建使用图形的移动应用。

您使用的图形图像和图形效果取决于应用特定的要求,您可以在自己的移动项目中使用的一些效果包括:

  • 渲染基本形状,如矩形、椭圆形和线段
  • 为需要更多“艺术”非线性视觉效果的移动应用渲染二次和三次贝塞尔曲线
  • 应用线性渐变、径向渐变和滤镜效果来产生更丰富、更引人注目的视觉效果
  • 应用变换(平移、缩放、旋转和剪切)
  • 创建自定义皮肤来替换“标准”按钮
  • 结合触摸事件使用并行或顺序动画效果

使用条形图和饼图执行有效的数据可视化*

五、应用部署和发布

至此,我们已经向您展示了如何在 Flash 平台上构建引人入胜的应用,这些应用利用了 Flash Professional 和 Flash Builder 中的移动功能。然而,为了展示您新开发的应用,您需要知道如何准备您的应用进行部署,将它们安装在开发设备上,并将您的应用部署到 Android Market,最终用户可以在那里下载它们。

在本章中,我们将首先向您展示如何安装和配置 Android SDK,并在 Android 模拟器中运行。这是在一系列不同设备类型和操作系统组合上试验您的应用的好方法,这通常需要专门的设备测试实验室。

接下来,我们将向您展示如何从 Flash Professional 和 Flash Builder 部署您的应用,并使用您在前面章节中开发的一些应用作为示例。这是对高级主题的补充,如证书创建、命令行部署和 Android 模拟器的打包。

最后,我们向您展示如何将您的应用发布到 Android Market 和 Amazon Appstore。一旦您成功发布了一个应用,它将像商店中的任何其他本机应用一样出现,因为它是在 Flash 平台上构建的,这一事实对您的最终用户是完全透明的。

设置 Android 模拟器

如果您没有现成的 Android 设备,或者正在寻找一种方法在新的或不同的硬件上部署和测试您的代码,Android Emulator 是一个很好的选择。SDK 附带的 Android 模拟器尽可能接近于运行真实的东西,包括运行完整的 Android 操作系统堆栈,并支持类似的开发人员与 USB 连接设备的交互。

Table 5–1 比较了在设备上运行、在仿真器中运行和在 AIR Debug Launcher (ADL)中运行的体验。

images

正如你所看到的,ADL 是一种在开发过程中测试 Flash 应用的方便方法,但它不是一个完整的 Android 环境。相比之下,Android 模拟器在虚拟设备上运行完整版本的 Android 操作系统,因此您可以测试您的应用在不同的操作系统版本和屏幕组合上的表现。

在桌面模拟器中运行时有一些限制。最值得注意的是,你没有多点触摸支持。此外,一些 Android 按钮和功能只能通过命令行选项或按键绑定来使用,这将在下一节“模拟器按键绑定”中详细介绍

尽管有这些限制,Android 模拟器是在多种不同设备和 Android 操作系统版本上测试您的应用的一种非常经济有效的方式,也是一种您不想失去的工具。

安装 Android SDK

在设备上安装和运行 Flash 的先决条件是安装 Android SDK。为了运行模拟器,您需要下载并安装 Java SDK 和 Android SDK。您可以在此下载适用于您的平台的最新版本的 Java:

http://java.sun.com/javase/downloads

注: Java 预装在 Mac OS X 上

Android SDK 可免费用于个人和商业用途,并可通过以下网址从 Google 下载:

http://developer.android.com/sdk

初始下载相对较小,可以解压缩到您选择的目录中。要完成安装,您必须在主目录中运行 SDK 安装程序。这将提示您一个软件包安装对话框,如图 Figure 5–1 所示。

images

图 5–1。 AIR for Android 软件包安装对话框

您可以选择想要的软件包,方法是分别选择它们并单击“接受”按钮,或者只需单击“全部接受”。一旦您点击安装按钮,您接受的软件包将被下载和安装。

注意: Windows 用户必须安装一个额外的 USB 连接包,才能使用带有 USB 连接线的手机。如果你使用的是谷歌 Android 开发者手机,你可以在这里找到驱动:[developer.android.com/sdk/win-usb.html](http://developer.android.com/sdk/win-usb.html).

一个可选的步骤是安装 Eclipse 和 Eclipse 的 Android 开发工具(ADT)插件。正如下一章所讨论的,如果你想做任何原生 Android 开发,这是很有帮助的。ADT Eclipse 插件可以从以下 URL 下载:

http://developer.android.com/sdk/eclipse-adt.html

也可以使用您选择的 IDE 来开发原生 Android 应用。您只需使用 Android SDK 附带的命令行工具来编译和打包您的项目。

创建 Android 虚拟设备

Android 模拟器的核心是 Android 虚拟设备(AVDs)。每个 AVD 指定该设备特有的设置,包括 API 版本、屏幕大小和硬件属性。使用 AVDs,您可以拥有自己的私有虚拟设备实验室,针对要测试应用的每个目标设备进行不同的配置。

首先,您需要创建第一个 AVD 来运行 Flash 平台应用。这是在 Android SDK 和 AVD 管理器中完成的,在第一次安装 SDK 时运行。

您可以通过导航到sdk/tools目录并启动android可执行文件,从命令行重新启动 Android SDK 和 AVD 管理器。

片刻之后,Android 将启动 SDK 管理器。在这里,您可以通过执行以下步骤来创建一个新的 AVD。

  1. 导航到虚拟设备窗格。
  2. 点击 New…按钮打开如图 Figure 5–2 所示的 AVD 创建对话框。
  3. 在名称输入字段中指定“MyAndroidDevice”。
  4. 在尺寸输入字段中输入“50”。
  5. 从目标下拉列表中选择“Android 2.3.3 - API Level 10”(或更高版本)。
  6. 选择名为“WVGA854”的内置皮肤。
  7. 单击创建 AVD 按钮。

images

图 5–2。 对话框创建新的 Android 虚拟设备

步骤 3 中 AVD 的名称只是一个建议,所以您可以用另一个名称替换这个字符串。

要启动新创建的 AVD,请在列表中选择它,然后单击 Start…按钮。它会显示标准的 Android 启动屏幕,然后是一个锁定屏幕。拖动锁形符号解锁仿真器后,您将看到熟悉的主屏幕,如图图 5–3 所示。

images

图 5–3。??【Android 2 . 3 . 3】运行在桌面上的仿真器

模拟器的 Android 标准皮肤在左边显示你的设备屏幕,在右边显示完整的 Android 按钮和按键。有些键,比如拨号和挂断键,可能并不是在每个 Android 设备上都可以找到,但是模拟器仍然可以让你测试你的应用在这些键被按下时的行为。

几乎所有你能在普通 Android 设备上做的事情都可以在模拟器上实现,所以在继续安装你自己的应用之前,先熟悉一下用户界面。您可以启动预装的应用,如浏览器、联系人或电子邮件,或者来自 Android Market 的新应用。默认情况下,模拟器映像带有所有启用的开发选项,如 USB 调试、“保持清醒”和模拟位置,但熟悉设置应用也是值得的。

在仿真器内安装空气

当您通过 USB 调试在物理设备上运行时,如果您的 AIR SDK 包含较新版本,Flash Builder 将自动提示您升级已安装的 AIR 版本。您还可以选择直接从 Android Market 下载并安装 AIR 的发布版本,这正是在您运行没有安装 AIR 的 Flash Android 应用时会发生的情况。

然而,在模拟器的情况下,你不能直接在 Android Market 之外使用 AIR 的版本,因为它们不兼容。此外,由于 Flash Builder 不直接与 Android 模拟器集成,因此您也不能使用自动更新机制来安装 AIR。

解决方法是从 SDK 手动安装 AIR 运行时。AIR SDK 可以在 Flash Builder 安装的sdks/<version>文件夹中找到(截至发稿时,最新版本为 4.5.0)。在 AIR SDK 文件夹中,您可以在以下位置找到模拟器运行时:

runtimes/air/android/emulator/Runtime.apk

注意:设备和模拟器有单独的 AIR 运行时,因此请确保为此选择模拟器运行时。

可以从命令行使用 Android Debug Bridge (ADB)程序安装该文件。ADB 是 Android SDK 附带的工具之一,可以在platform-tools文件夹中找到。清单 5–1 展示了在 Mac OS X 的默认安装位置安装模拟器 APK 的命令

***Listing 5–1.** Installation Command for the AIR Emulator Runtime*

adb install "/Applications/Adobe Flash Builder 4.5/sdks/4.5.0/runtimes/air/android/emulator/Runtime.apk"

在 Windows 上,该命令非常相似,只是 Flash Builder 安装的路径位置不同。

提示:您还可以使用 AIR 调试工具(ADT)来安装 AIR 运行时。配置 ADT 将在本章后面的“设置 ADT”一节中介绍。使用 ADT 安装 AIR 运行时的命令如下:

adt -installRuntime -platform android

仿真器按键绑定

在 Android 模拟器中运行时,您可以选择使用普通的桌面键盘作为输入。除了 Android 设备上有几个特殊的键没有到桌面键盘的正常映射之外,这种方式工作得相当好。例如电源按钮、音量和相机按钮。

为了便于在 Android 设备上按下这些按钮,默认的设备皮肤在物理仿真器面板上包含了这些按钮,因此您可以用鼠标单击它们。然而,不能保证将来你自己安装的 Android 皮肤或自定义皮肤会有完整的按钮。一个这样的皮肤,如图图 5–4 所示,给你一个接近照片质量的 Nexus S 设备外观,1但是缺少一些按钮。

images

图 5–4。 Nexus S 安卓模拟器皮肤


1 黑科·伯伦斯的 Nexus S Skin:[heikobehrens.net/2011/03/15/android-skins/](http://heikobehrens.net/2011/03/15/android-skins/)

为了克服这个限制,Android 模拟器提供了完整的按键绑定。Table 5–2 列出了从普通 Android 键到桌面键盘修饰键的映射,当使用任何仿真器皮肤时,您都可以键入这些修饰键。

images

除了重新映射 Android 按钮之外,模拟器还有一些隐藏的功能,只能通过按键绑定来访问。Table 5–3 显示了一些特殊的键绑定,在仿真器中测试应用时,您会发现它们很有用。

为了充分利用前面的键绑定,您需要知道如何从命令行启动模拟器并传入参数。从命令行启动 Android 模拟器是对emulator可执行文件的直接调用,该文件可以在sdk/tools目录中找到:

emulator -avd <Virtual Device Name>

您替换的虚拟设备名称与 Android 工具中定义的名称完全相同,如前一节所示。然后,您可以添加任何想要使用的附加选项,例如-trace-onion

部署 AIR 应用

如果您一直使用设备通过 USB 测试您的应用,那么您已经在开发过程中进行了有限形式的部署。然而,您可能使用的是调试版本,当您的最终用户获得一个完全打包的应用时,不必担心对他们来说很重要的许多事情,比如权限、适当的证书和图标。

在这一节中,我们将更详细地研究应用描述符,演示如何微调您的应用部署,以改善用户体验并为您的公司形象树立品牌。

设置 ADT

虽然可以通过 Flash Professional 和 Flash Builder 完成整个发布工作流,但对于自动化和脚本编写而言,能够从命令行完成相同的活动非常有用。Adobe AIR SDK 提供了一个名为 AIR Developer Tool (ADT)的命令行,它允许您从脚本或构建文件执行任何操作。

要从命令行使用 ADT,必须预先设置以下内容:

  • 为您的平台安装 AIR SDK(这是使用 Flash Builder 自动安装的)。
  • 安装有效的 Java 运行时环境(JRE)。
  • 将 Java 运行时添加到您的PATH环境变量中。
    • 对于 Windows,这将是%JRE_HOME%\bin
    • 对于麦克·OS X 来说,这将是$JRE_HOME/bin.
    • 其中JRE_HOME是 JRE 安装位置的完全限定路径
  • 将 AIR SDK 添加到您的PATH环境变量中。
    • 对于 Windows,这将是%AIR_SDK%\bin
    • 对于麦克·OS X 来说,这将是$AIR_SDK/bin
    • 其中AIR_SDK是 AIR SDK 安装位置的完全限定路径

设置完成后,您可以使用 ADT 从命令行完成许多不同的打包和部署活动。其中包括以下内容:

  • 创建签名证书 : ADT 允许您从命令行创建代码签名证书,这些证书可以在打包应用时使用。
  • 打包应用:通过传入一个项目文件列表和一个有效的证书,你可以打包一个 APK 文件部署到 Android 设备上。这支持创建一个有或没有调试符号的 APK 文件,也允许你以 Android 模拟器为目标。
  • 安装应用:打包步骤创建的 APK 文件可以安装在设备上。这需要一个到 Android SDK 的路径。
  • 启动应用 : ADT 也可用于在设备上启动应用。这也需要一个通往 Android SDK 的路径。

在本章中,我们将利用 ADT 来展示 Flash 工作流的自动化潜力。

应用权限

用户对您的应用的第一印象将是它在安装时请求的不同权限的列表。因此,您应该确保您所请求的权限对您的应用有意义,并且是您可以交付功能的最小集合。

请求太大的权限可能会让用户暂停安装应用。例如,Twitter 客户端没有理由需要写入外部存储,因此请求该权限可能会阻止精明的用户出于安全考虑安装您的应用。

提示:您可能已经默认启用的权限之一是INTERNET权限。应用的 USB 调试需要该权限,因此在开发过程中启用该权限非常重要。大多数应用还需要访问 Internet 运行时,因此很可能您发布的应用版本也需要此权限;但如果没有,记得禁用这个。

在 Flash Professional 中更改权限

Flash Professional 有一个专用的用户界面来管理所有部署选项,包括权限。若要打开“设置”面板,请从“文件”菜单中选择“Air for Android 设置…”。然后点击 Permissions 选项卡,您将得到一个对话框,其中每个权限都有复选框,如图 Figure 5–5 所示。

images

图 5–5。??【Flash 职业权限】选项卡中的空中安卓设置对话框

您还可以通过选择顶部的复选框来手动设置应用描述符文件中的权限。如果您想这样做,请参阅“手动更改应用描述符中的权限”一节。

在 Flash Builder 中设置初始权限

Flash Builder 允许您在首次创建项目时设置权限。为此,点击新建移动项目向导第二页中的权限选项卡,如图 Figure 5–6 所示。

images

图 5–6。 新建项目向导中的 Flash Builder 权限选项卡

请注意,当您进入对话框时,INTERNET权限是预先选定的。这是 USB 设备调试工作所必需的。如果您需要任何额外的权限,可以在开始项目之前设置它们。

一旦创建了项目,就不能再通过项目设置对话框更改权限。相反,您可以按照下一节中的说明直接编辑为您创建的应用描述符文件。

手动更改应用描述符中的权限

如果您选择手动管理权限(对于 Flash Professional)或在项目创建后修改权限(对于 Flash Builder),则您需要知道如何修改应用描述符文件来更改权限。

应用描述符文件通常位于项目的源根目录中,并按照惯例<project-name>-app.xml命名。它被格式化为 XML 标记的标记,其中包含您可以以声明方式控制的所有不同应用设置的部分。权限设置可以在文件底部的android标签下找到,如清单 5–2 所示。

清单 5–2。 示例 AIR 应用描述符的权限部分

<android>   <manifestAdditions>     <manifest android:installLocation="auto">       <![CDATA[         <uses-permission android:name="android.permission.**PERMISSION_NAME** " />       ]]>     </manifest>   </manifestAdditions> </android>

对于您想要启用的每个权限,您可以复制uses-permission标记并用适当的权限名称替换PERMISSION_NAME占位符。清单 5–3 以适当的格式显示了所有可用的 Android 权限的例子,直接包含在应用描述符中。

清单 5–3。 全套可用安卓权限

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.DISABLE_KEYGUARD" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

在这些权限中,有一些可以作为一个组来启用和禁用,例如:

  • AIR 的SystemIdleModeAPI 需要DISABLE_KEYGUARDWAKE_LOCK权限。
  • AIR 的NetworkInfoAPI 同时需要ACCESS_NETWORK_STATEACCESS_WIFI_STATE

因此,如果您计划使用这两种 API 中的任何一种,请确保同时启用这两种权限。

图标和资源

每次用户打开你的应用,他们都会看到你选择的 Android 启动图标,所以这是很专业的,并且代表你的应用,这一点很重要。

从 Android 2.0 开始,他们标准化了面向前的图标设计,建议选择应用的一个方面,并用全尺寸的描述来强调这一点。图 5–7 突出显示了一些启动器图标,它们是推荐的 Android 外观和感觉的模型示例。

images

图 5–7。 示例 Android 应用启动器图标 2

为了更容易地构建符合这些标准的应用图标,Android 团队提供了一个包,其中包含不同大小图标的样本材料和模板。您可以从以下网址下载 Android 图标模板包:

http://developer.android.com/guide/practices/ui_guidelines/icon_design.html#tem platespack

该软件包包括 Photoshop 模板,您可以使用这些模板在边框内精确排列图形,以及配置为应用适当效果(如图标投影)的滤镜。

图 5–8 显示了本书示例中使用的 Pro Android Flash 图标的 Photoshop 文件。它采用“网络水果”图形作为封面艺术的中心,并使用这一单一元素作为书籍的代表性图标。


2 转载自 Android 开源项目创建和共享的作品,并根据知识共享 2.5 归属许可中描述的条款使用:[developer.android.com/guide/practices/ui_guidelines/icon_design_launcher.html](http://developer.android.com/guide/practices/ui_guidelines/icon_design_launcher.html)

images

图 5–8。Adobe Photoshop 中的安卓图标模板

因为我们使用的形状是圆形的,所以它可以接触到外部的蓝色边界(1/6 的间隔)。方形图标不应超出橙色边界线(2/9 英寸的距离)。我们也使用推荐的高密度投影设置,2 像素距离,5 像素大小,90 度角。Table 5–4 列出了不同密度图标的图标尺寸、边框大小和阴影设置。

images

要完成图标的准备工作,请隐藏用于创建图标的任何参考图层,并将其保存为透明的可移植网络图形(PNG)文件。如果您使用的是 Photoshop,最好的方法是使用“文件”菜单中的“存储为 Web 和设备所用格式”命令。这可以确保您的图像文件尽可能小,删除任何不必要的头信息。

一旦你创建了你的图形,你可以将它们包含在你的应用描述符中,这样它们将与你的应用捆绑在一起,并显示在你部署的应用的启动器和菜单中。Flash Professional 有一个配置页面,允许您选择图标并将它们链接到您的应用,如图 5–9 所示。

images

图 5–9。Flash 专业设置对话框的图标选择选项卡

您选择的每个图形文件都将被移动到名为AppIconsForPublish的文件夹中,该文件夹位于您的项目文件位置。部署后,这些图标将被复制到生成的.apk文件中,并作为相应的密度素材进行链接。

如果您使用 Flash Builder 或手动管理应用描述符,则必须手动编辑 XML。在文本编辑器中打开应用描述符后,添加一个icon部分,列出应用支持的不同密度图标的绝对或相对路径。清单 5–4 展示了应用描述符的icon部分应该是什么样子。

清单 5–4。 空中应用描述符的示例icon部分

<icon>   <image36x36>ProAndroidFlashIcon36.png</image36x36>   <image48x48>ProAndroidFlashIcon48.png</image48x48>   <image72x72>ProAndroidFlashIcon72.png</image72x72> </icon>

icon标签应该直接位于文件的外部application标签之下。在本例中,所有图标资源都与应用描述符文件位于同一个文件夹中,因此路径是一个简单的文件名。您可以将文件命名为任何名称,只要它们与描述符文件中的内容相匹配。

代码签名证书

Android 要求所有部署的应用都要签名。为了将应用部署到 Android Market,您不必从证书颁发机构购买昂贵的代码签名证书。所需的只是一个简单的自签名证书,因为谷歌负责检查在其市场上销售应用的各方的身份。

Flash Professional 和 Flash Builder 都提供用户界面来快速轻松地创建证书。您也可以使用 AIR Developer Tool (ADT)从命令行创建证书。所有这些机制创建的证书都是相同的,可以在工具之间互换使用。

若要在 Flash Professional 中创建证书,请从“文件”菜单中打开 Air for Android 设置…。在此对话框的“部署”选项卡上,您可以单击“创建…”按钮,通过弹出窗口生成新的证书,如图 5–10 所示。

images

图 5–10。 Flash 职业证书创建对话框

我们将在前面更详细地讨论创建证书的字段,但是如果您是为了开发目的而创建证书,那么您可以输入您想要的任何内容。Flash Professional 要求您在继续之前填写所有字段。

Flash Builder 有一个相同的表单,可以从 Google Android images数字签名部分的项目属性对话框中访问。再次点击 Create…按钮打开证书创建对话框,如图 Figure 5–11 所示。

images

图 5–11。 Flash Builder 证书创建对话框

Flash Builder 对话框与 Flash Professional 对话框几乎相同,只是省略了有效期。这将自动默认为 25 年。

使用 ADT 创建证书

若要从命令行创建证书,可以使用 AIR 开发工具(ADT)。有关设置 ADT 的更多信息,请参阅本章前面的“设置 ADT”一节。

要通过命令行创建代码签名证书,您需要键入以下命令:

adt -certificate -cn <name> ( -ou <org-unit> )? ( -o <org-name> )? ( -c <country> )? ( - validityPeriod <years> )? <key-type> <pfx-file> <password>

括号中的参数是可选的,可以省略。您可以自己选择的值用尖括号括起来。所有参数的描述和有效值在表 5–5 中列出。

注意: Android Market 要求证书的有效期必须超过 2033 年 10 月 22 日。因此,建议有效期至少为 25 年。

例如,以下命令将创建一个有效的 Android 代码签名证书:

adt -certificate -cn ProAndroidFlash -validityPeriod 25 1024-RSA proandroidflash.p12 superSecretPassword

然后,您可以通过运行checkstore命令来验证证书是否有效:

adt -checkstore -storetype pkcs12 -keystore proandroidflash.p12 -storepass superSecretPassword

如果证书创建成功,该命令将返回"valid password"

从 Flash Professional 发布

一旦您设置了适当的权限、图标和证书,从 Flash Professional 发布应用就像按一个按钮一样简单。事实上,您可以选择几个按钮或菜单项:

  • Android 设置对话框中的发布按钮
  • “发布设置”对话框中的“发布”按钮
  • “文件”菜单中的“发布”菜单项

这三个位置在图 5–12 中描述。除了在错误输入或信息不完整的情况下(例如证书密码丢失),您将被重定向到 AIR for Android 设置对话框,它们的工作方式完全相同。

images

图 5–12。 AIR for Android 部署设置对话框

在 AIR for Android 设置对话框的部署选项卡上有几个部署选项,我们还没有谈到,但它们在发布时很重要。首先是选择一个DeviceEmulatorDebug版本。如果您正在创建一个供最终用户使用的 Android 包,请确保选择一个Device版本。

如果您计划在 Android 模拟器中测试您的应用,那么Emulator版本是必需的,所以如果这是您计划测试您的应用的方式,请确保选择此选项。然而,记住切换回一个Device版本以分发给最终用户。

一个Debug版本是当你通过 USB 调试测试你的应用时通常会构建的项目类型。与Device版本相比,这种版本的性能较慢,并且在错误条件下的行为略有不同,因此不建议将其用于分发目的。

您还可以选择应用下载 AIR 运行时的位置。目前支持的两个应用商店是谷歌 Android Market 和亚马逊 Appstore。如果您计划将应用部署到这两个应用商店,则应该使用不同的 AIR 运行时设置为每个商店创建单独的版本。

最后一组选项将自动在第一个连接的 USB 设备上安装和启动您的应用包。这些对于以发布形式测试新构建的应用非常方便。如果您正在运行模拟器,Flash Professional 会将其视为连接的设备,并且还可以自动部署到该设备。

点击发布,一个 Android 包文件(APK)将为你的应用创建。APK 文件是一个自包含的安装包,您可以将其部署到设备或通过应用商店发布。APK 文件的位置和名称在“发布设置”对话框的“输出文件”栏中设定。

从 Flash Builder 导出发布版本

Flash Builder 还能够为您的应用打包 APK 文件。从项目菜单中选择导出发布版本…开始导出过程,之后您将看到一个向导对话框,如图 5–13 所示。

images

图 5–13。??【Flash Builder 导出发布构建向导】

向导的第一页允许您选择平台、文件名和签名选项。对于移动应用,您通常希望选择第一个选项来为每个目标平台签署包,在我们的例子中只有一个。要进入第二页,请单击“下一步”按钮。

第二页包含几个选项卡,其中包含数字签名、包内容和部署的选项。如果您已经按照本章前面的讨论设置了您的签名,除了可能输入密码之外,您不必在第一个选项卡上进行任何更改。Package Contents 选项卡显示了所有资源的列表,将包含在 APK 文件中。除非您想明确排除任何文件,如未使用的图形,否则您不需要在此进行任何更改。最后,最后一个选项卡有一个选项,可以在连接的移动设备上自动部署和运行(如果可用),这是默认选择的。

单击“完成”按钮后,Flash Builder 将打包一个用于发布的 APK 文件,并可能在已安装的设备上部署和启动该文件。

在 Android 模拟器中运行 Flex 应用

与 Flash Professional 发布过程相比,您可能会注意到在 Flash Builder 对话框中没有提到 Android 模拟器。此外,如果您尝试在 Android 模拟器上安装由 Flash Builder 创建的 APK 文件,安装将会失败并出现错误。

但是,您可以从命令行使用 AIR Developer Tool (ADT)手动创建同一应用的模拟器友好版本。有关设置 ADT 的更多信息,请参阅本章前面的“设置 ADT”一节。

作为一个例子,清单 5–5 向您展示了如何为第一章中构建的手势检查项目构建一个仿真器友好的 APK。请确保在 Flash Builder 已经构建了 SWF 文件后,从bin-debug文件夹执行此命令。

***Listing 5–5.** Command to Build an APK File for the GestureCheck Project*

adt -package -target apk-emulator -storetype pkcs12 -keystore <certificate file> GestureCheck.apk GestureCheck-app.xml GestureCheck.swf ProAndroidFlashIcon*.png

这将构建一个与 Android 模拟器兼容的 APK 文件。在模拟器运行时,您可以使用 ADT 工具通过执行以下命令来安装它:

adt -installApp -platform android -package GestureCheck.apk

提示:您也可以使用 Android 调试桥(ADB)安装 APK 文件,如下所示:

adb install GestureCheck.apk

您的应用现在将出现在模拟器的应用菜单中。您可以通过菜单手动启动应用,或者使用以下命令以编程方式启动应用:

adt -launchApp -platform android -appid com.proandroidflash.GestureCheck.debug

请注意,appid与您的应用描述符中的id used相同,只是多了一个“.”。debug”追加到末尾。

提示:您也可以使用 ADB 启动 AIR 应用,如下所示:

adb shell am start -a android.intent.action.MAIN -n air.com.proandroidflash.GestureCheck.debug/.AppEntry

Figure 5–14 展示了一个在普通 Android 模拟器上运行的手势检查应用的真实例子。

images

图 5–14。 运行在桌面模拟器上的 Android 2 . 3 . 3

如前所述,Android 模拟器不支持多点触摸事件,这一点通过查看手势检查应用在模拟器中运行时的输出可以明显看出。

从命令行部署

虽然 Flash Professional 和 Flash Builder 使从工具内部部署应用变得非常方便,但是能够从命令行执行相同的部署通常也很有用。这允许您创建一个可重复的、自动化的过程,该过程是为您的开发工作流的确切需求而定制的。

我们将使用的命令行工具称为 AIR Developer Tool (ADT ),它可以自动执行多种不同的任务,从证书创建到应用打包再到设备部署。有关设置 ADT 的更多信息,请参见前面的“设置 ADT”一节。

ADT 中用于包装空气应用的主要标志是-package。这表明您将为桌面、移动或其他部署打包 AIR 应用。以下是用 ADT 打包 Android 应用的全部论据:

adt -package -target ( apk | apk-debug | apk-emulator ) ( -connect <host> | -listen <port>? )? ( -airDownloadURL <url> )? SIGNING_OPTIONS <output-package> ( <app-desc> PLATFORM-SDK-OPTION? FILE-OPTIONS | <input-package> PLATFORM-SDK-OPTION? )

Table 5–6 讨论了这些参数以及有效值。

images

images

虽然打包选项的排列看起来令人望而生畏,但是您只需要所需的参数就可以完成大多数任务。例如,下面将使用应用描述符中的信息打包一个简单的应用:

adt -package -target apk -storetype pkcs12 -keystore cert.p12 Sample-app.xml Sample.swf

这是从描述符和 SWF 文件打包应用的最小参数集。要构建调试版本,您应该执行以下操作:

adt -package -target apk-debug -listen -storetype pkcs12 -keystore cert.p12 Sample- app.xml Sample.swf

这将在启动时监听端口7936上的 USB 调试接口。

如果您知道要为同一个项目创建多个部署,AIR Intermediate (AIRI)文件会非常方便。您可以从 Flash Builder 导出一个 AIRI 文件,也可以在命令行上使用以下语法的prepare命令创建一个文件:

adt -prepare Sample.airi Sample-app.xml Sample.swf

然后,您可以使用package命令的input-package变体部署到多个不同的目标:

adt -package -target apk -storetype pkcs12 -keystore cert.p12 Sample-android.apk Sample.airi adt -package -target apk -airDownloadURL http://www.amazon.com/gp/mas/dl/android?p=com.adobe.air  -storetype pkcs12 -keystore cert.p12 Sample-amazon.apk Sample.airi

这将创建两个不同的 APK 文件,一个准备部署到 Android Market,另一个使用不同的 AIR 下载 URL 从 Amazon Appstore 获取运行时。

命令行部署练习

本练习将指导您逐步完成打包、签名、安装和启动 Flash Capability Reporter 示例的过程。

以下是练习的先决条件:

  1. AIR SDK 已安装并位于路径中
  2. Java 运行时环境(JRE)已安装并位于路径中
  3. 安装了 Android SDK

首先打开命令提示符或终端。您应该能够键入不带选项的命令adt,并获得命令参数的帮助。如果它找不到adt命令或者抱怨java不在路径中,验证你已经正确地更新了你的 path 环境变量。

创建代码签名证书

要创建代码签名证书,您可以发出以下命令,其中尖括号中的值应该替换为您的名称和密码:

adt -certificate -cn <YourName> -validityPeriod 25 1024-RSA exercise.p12 <YourPassword>

如果命令成功完成,它将返回退出代码 0。

打包应用

要打包该应用,请确保您已经在 Flash Professional 中运行了该应用一次,以便可以创建电影(* .swf)和应用描述符(* -app.xml)文件。然后使用以下命令将其打包成一个 APK 文件:

adt -package -target apk -storetype pkcs12 -keystore exercise.p12 FlashCapabilityReporter.apk FlashCapabilityReporter-app.xml FlashCapabilityReporter.swf AppIconsForPublish/

这将创建一个包含应用可部署版本的 APK 文件。

安装并启动应用

另外,您可以使用以下命令将应用安装并启动到通过 USB 连接的设备上:

adt -installApp -platform android -package FlashCapabilityReporter.apk adt -launchApp -platform android -appid com.proandroidflash.FlashCapabilityReporter

如果成功,Flash Capability Reporter 应用将安装并运行在您的 Android 手机上。

将 AIR 应用发布到 Android Market

Android Market 是谷歌为 Android 设备创建和运营的应用商店。与苹果应用商店或亚马逊应用商店等其他应用商店相比,安卓市场非常开放。它没有一个限制性的筛选过程,它允许最终用户试用一个应用一天,如果他们不喜欢它,可以选择全额退款。

谷歌向开发者收取 25 美元的费用,以便创建一个可以用来提交无限数量的应用的帐户。据谷歌称,这项费用旨在通过防止应用垃圾来提高市场质量。

本节概述了将 Adobe AIR 应用发布到 Android marketplace 的三步流程。

第一步:创建一个 Android Market 开发者账户

要创建 Android Market 开发者帐户,请访问以下网站:

http://market.android.com/publish/Home

您将被要求登录您想要链接到您的 Android Market 开发者帐户的 Google 帐户。如果你已经有一个谷歌帐户,你可以使用它,否则可以免费创建一个新帐户。但是,请记住,一旦您链接了您的 Google 帐户,在不创建新的 Android Market 开发者帐户的情况下,将来将无法更改它。

接下来,提供所需的信息,如您的全名、网站 URL 和电话号码,并按照提示操作,直到您到达 Google Checkout 网站,以便提交您的注册费。要完成这个过程,请在结帐后按照链接返回注册页面,并同意 Android Market 许可证。

第二步:打包你的应用

为了上传到 Android 商店,你需要一个被打包成 APK 文件的签名应用。您可以从 Flash Professional、Flash Builder 或命令行执行此操作,如前几章所述。

提交申请时,你需要记住以下几点:

  • 确保您提交的是应用的发布版本(而不是调试或仿真版本)。
  • 如果不需要调试所需的INTERNET权限,记得禁用它。
  • 确保在所有标准 Android 尺寸(36×36、48×48 和 72×72)的应用中包含自定义图标。
  • 确保您将 AIR 运行时 URL 设置为 Android Market(这是默认设置)。

一旦你建立了 APK 文件,你就可以开始发布你的应用到 Android Market 了。

步骤 3:上传您的 Adobe AIR 应用

Android Market 应用提交过程是完全自动化的,包括每个步骤的详细说明。Figure 5–15 展示了一个例子,展示了如果您使用第一章中的手势检查示例应用,提交过程会是什么样子。

images

图 5–15。 安卓市场提交流程

大部分的申请提交过程只需上传你的 APK 文件。这包括选择图标、设置权限和选择支持的平台。

除了 APK 文件之外,你还需要提交至少两张你的申请截图以及一个大尺寸图标(512 像素见方)。要截屏你的应用,你可以在桌面上的模拟器中运行它,然后把图片裁剪到合适的大小,或者使用截屏工具直接在你的 Android 设备上拍照。

在填写必填字段后,您可以提交您的应用,它将立即在 Android Market 中可用。图 5–16 显示了一个成功的应用部署到 Android Market 的成功结果。

images

图 5–16。 成功部署安卓市场应用

将 AIR 应用发布到亚马逊应用商店

亚马逊应用商店是第二个购买 Android 设备应用的市场。它与亚马逊的店面紧密集成,允许你从一个界面购买 Android 应用以及书籍、CD 和其他产品。此外,它使用亚马逊的专利一键式购买系统来简化在移动设备上购买应用的过程。

通过亚马逊应用商店发布应用的费用要高得多,每年订阅费用为 99 美元。幸运的是,亚马逊已经免除了同时注册亚马逊开发者计划的开发者第一年的费用。

发布到亚马逊 Appstore 的要求和流程与 Android Market 非常相似。设置您的帐户、打包您的应用并将其上传到商店的三个步骤仍然适用。

提交到亚马逊 Appstore 时,一定要设置 AIR 运行时 URL 指向亚马逊 Appstore 进行下载。这可以通过 Flash Professional UI 中的部署设置来完成,也可以通过命令行将 ADT 的-airDownloadURL property设置为以下内容来完成:

http://www.amazon.com/gp/mas/dl/android?p=com.adobe.air

Figure 5–17 展示了 Amazon Appstore 应用提交的一个例子。

images

图 5–17。 亚马逊应用商店提交流程

作为开发人员,在向 Amazon Appstore 提交应用时,您会注意到以下一些主要差异:

  • 您需要提交三张您的应用截图,并且它们的大小必须精确到 854×480 或 480×854。
  • 除了 512×512 的图标,亚马逊 Appstore 还要求 114×114 的图标。
  • 你不能立即看到你的应用出现在亚马逊应用商店,你必须等待它通过审查过程。

尽管存在这些差异,但大多数应用提交过程都非常相似,这使得将您的 AIR Android 应用部署到这两个应用商店非常容易。

总结

本章结束了 Flash 平台的端到端移动应用开发过程。现在,您已经知道如何将应用从初始阶段发展到最终用户可以从市场上下载的完全发布的 Android 应用。

在本章中,您学习了如何执行以下操作:

  • 设置 Android 模拟器并配置 Android 虚拟设备
  • 配置应用在安装时请求的权限
  • 指定启动器图标和其他资源作为应用的一部分
  • 从 Flash Professional、Flash Builder 和命令行发布 AIR 包
  • 在设备上或 Android 模拟器中测试您的应用包
  • 将您的应用发布到 Android Market 和 Amazon Appstore

在接下来的几章中,我们将进一步深入探讨与 Android 的原生集成、针对移动设备的性能调整以及与设计师的合作。

六、Adobe AIR 和原生 Android 应用

您已经学习了如何创建有趣的基于 Flex 的移动应用,在本章中,您将了解 Adobe AIR 中可用的其他有用功能,以及如何将 Android 特定的功能合并到 Adobe AIR 移动应用中。

首先,您将学习如何执行 Adobe AIR 中提供的两项操作:如何在 AIR 应用中启动本机浏览器,以及如何在 SQLite 数据库中存储特定于应用的数据。本章的下一部分深入探讨了 Android 的基础知识,你需要理解本章后面讨论的代码示例。本节将向您展示如何创建一个简单的原生 Android 应用,并讨论 Android 应用中的主要文件。您还将了解重要的 Android 特定概念,如活动、Intent s 和Service s。

本章的第三部分包含一个 Adobe AIR mobile 应用的示例,该应用调用外部 API 来提供用户已向外部服务注册的网站的状态信息。我们的 Adobe AIR 应用将每个网站的状态存储在 SQLite 数据库中,然后在数据网格中显示状态详细信息。这个移动应用还允许用户点击一个按钮,向原生 Android 代码发送更新,然后在 Android 通知栏中显示更新。本章的最后一部分包含将 Adobe AIR mobile 应用与本机 Android 代码集成所需的步骤。

关于本章的内容,有几点需要记住。首先,Android 内容旨在帮助您了解如何将原生 Android 功能集成到 Adobe AIR 应用中。因此,只涵盖了 Android 主题的一个子集,这不足以成为一名熟练的 Android 应用开发人员。其次,Adobe AIR 是一个不断发展的产品,因此 Adobe AIR 目前不可用的一些 Android 功能可能会在未来的版本中可用。第三,Adobe AIR 应用与原生 Android 功能的集成不受 Adobe 官方支持;因此,如果您在整合过程中遇到困难,没有正式的支持机制来帮助您解决这些困难。

另一个需要考虑的问题与 Adobe AIR mobile 应用的目标设备所支持的 Android 版本有关。例如,支持 Android 2.2 的移动设备的数量目前远远大于支持 Android 2.3.x 或 Android 3.0 的移动设备,这两种移动设备目前都仅限于几款平板电脑(如三星 Galaxy Tab 10.1 和摩托罗拉 Xoom)和一款智能手机(三星 Galaxy S II)。

另一方面,如果 Adobe AIR 支持您创建移动应用所需的所有功能和特性,那么您就不需要本章中说明如何将 Adobe AIR 应用与 Android 应用合并的任何代码示例。如果是这种情况,你可以跳过这些材料而不失去连贯性。

在 Adobe AIR 中调用 URI 处理程序

目前,Adobe AIR 中有五个与 URI 相关的处理程序,使您能够在 Adobe AIR mobile 应用中执行以下操作:

  • 电话(打电话)
  • sms(发送文本消息)
  • mailto(发送电子邮件)
  • 市场(进行市场搜索)
  • http 和 https(启动 web 浏览器)

每个处理程序的代码都非常简单,这使得在 Adobe AIR mobile 应用中嵌入这些处理程序非常简单。需要记住的一点是,Adobe AIR 不支持“地理”URI,但是您仍然可以导航到 maps.google.com,并且会提示用户在地图应用的浏览器会话版本中打开该 URL。这种“变通方法”使您能够在 Adobe AIR mobile 应用中支持与地图相关的功能。

使用移动应用模板创建一个名为 URIHandlers 的新 Flex 移动项目,并添加如清单 6–1 所示的代码。

清单 6–1。 调用 URI 处理程序

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark" title="Home">
  fx:Script
    <![CDATA[
      import flash.sensors.Geolocation;

[Bindable]
      public var tel:String;
      [Bindable]
      public var sms:String;
      [Bindable]
      public var mailto:String;
      [Bindable]
      public var search:String;
      [Bindable]
      public var http:String;
      [Bindable]
      public var geo1:String;

private var geo:Geolocation;

private function onTel():void {
         navigateToURL(new URLRequest("tel:"+tel));
      }

private function onSMS():void {
         navigateToURL(new URLRequest("sms:"+sms));
      }

private function onMailto():void {
         navigateToURL(new URLRequest("mailto:"+mailto+"?subject=Hello%20AIR"));
      }

private function onSearch():void {
         navigateToURL(new URLRequest("market://search?q=iReverse"));
      }

private function onHTTP():void {
         navigateToURL(new URLRequest(http));
      }

private function onGeo():void {
         this.geo = new Geolocation();
         this.geo.addEventListener(GeolocationEvent.UPDATE, onLocationUpdate);
      }

private function onLocationUpdate(e:GeolocationEvent):void  {
         this.geo.removeEventListener(GeolocationEvent.UPDATE,onLocationUpdate);
         var long:Number = e.longitude;
         var lat:Number = e.latitude;
         navigateToURL(new URLRequest("http://maps.google.com/"));
      }
    ]]>
  </fx:Script>

<s:VGroup>
   <s:Form backgroundColor="0xFFFFFF" width="300">
    <s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{tel}"/>
      <s:Button id="telID" width="250" height="50" label="(Call)" click="onTel();"/>
      </s:HGroup>
    </s:FormItem>

<s:FormItem>
      <s:HGroup left="0">      <s:TextInput width="180" height="50" text="{sms}"/>
      <s:Button id="smsID" width="250" height="50" label="(Text)" click="onSMS();"/>       </s:HGroup>
    </s:FormItem>

<s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{mailto}"/>
      <s:Button id="mailtoID" width="250" height="50" label="(EMail)"
                click="onMailto();"/>
      </s:HGroup>
    </s:FormItem>

<s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{search}"/>
      <s:Button id="searchID" width="250" height="50" label="(Search Market)"
                click="onTel();"/>
      </s:HGroup>
    </s:FormItem>

<s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{http}"/>
      <s:Button id="httpID" width="250" height="50" label="(Go)" click="onHTTP();"/>
      </s:HGroup>
    </s:FormItem>

<s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{geo1}"/>
      <s:Button id="geoID" width="250" height="50" label="(Geo)" click="onGeo();"/>
      </s:HGroup>
    </s:FormItem>
   </s:Form>
  </s:VGroup>
</s:View>`

清单 6–1 包含一个带有各种输入字段的表单,每个输入字段都有一个关联的事件处理程序,它使用不同的参数调用内置方法navigateToURL()。例如,当用户输入一个 URL,然后点击相关的按钮,方法onHTTP()用下面的代码行启动一个 URL:

navigateToURL(new URLRequest(http));

Figure 6–1 显示了一个带有各种输入字段的表单,说明了如何在 AIR mobile 应用中使用与 URI 相关的功能。

images

图 6–1。 使用 URI 相关功能

在 Adobe AIR 中启动自定义 HTML 页面

Adobe AIR 使您能够启动定制的 HTML 页面(如前面所示),还可以导航到任意 HTML 页面(如清单 6–2 所示)。

使用 ActionScript 移动应用模板创建一个名为 StageWebViewHTML1 的新 Flex 移动项目,并添加如清单 6–2 中所示的代码。

清单 6–2。 启动一个硬编码的 HTML 页面

`package {
  import flash.display.Sprite;
  import flash.display.StageAlign;
  import flash.display.StageScaleMode;
  import flash.geom.Rectangle;
  import flash.media.StageWebView;

public class StageWebViewHTML1 extends Sprite {
    public function StageWebViewHTML1() {
       super();

// support autoOrients
       stage.align = StageAlign.TOP_LEFT;
       stage.scaleMode = StageScaleMode.NO_SCALE;        var webView:StageWebView = new StageWebView();
       webView.stage = this.stage;
       webView.viewPort = new Rectangle( 0, 0,
                                         stage.stageWidth,
                                         stage.stageHeight );
       // create an HTML page
       var htmlStr:String = "" +
         "" +
         "" +
           "

An HTML Page in Adobe AIR

" +
           "

Hello from the Author Team:

" +
           "
"+
           "

Stephen Chin

" +
           "

Dean Iverson

" +
           "

Oswald Campesato

" +
           "

Paul Trani

" +
           "
"+
           "
"+
           "

This is the key line of code:

"+
           "

webView.loadString( htmlStr, 'text/html'; );

"+
           "

'htmlStr' contains the HTML contents

";
         "" +
         "";

// launch the HTML page
       webView.loadString( htmlStr, "text/html" );
    }
  }
}`

清单 6–2 包含几个import语句和自动生成的代码,变量htmlStr是一个字符串,其中包含一行代码启动的 HTML 页面的内容:

       webView.loadString( htmlStr, "text/html" );

如果您计划在移动应用中调用硬编码的 HTML 页面,请尝试使用不同的 HTML5 标签来创建 HTML 页面所需的样式效果。

图 6–2 显示了来自清单 6–2 的输出,它呈现了一个带有硬编码内容的 HTML 页面。

images

图 6–2。 启动一个硬编码的 HTML 页面

在 Adobe AIR 中导航到 HTML 页面

在前面的示例中,您学习了如何启动硬编码的 HTML 页面,在本节中,您将学习如何导航到任何 HTML 页面,然后在 Adobe AIR mobile 应用中启动该 HTML 页面。

使用 ActionScript 移动应用模板创建一个名为 StageWebViewLaunch2 的新 Flex 移动项目,并添加如清单 6–3 所示的代码。

清单 6–3。 启动用户指定的 URL

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        title="HomeView" >

<fx:Script source="StageWebViewLaunch.as"/>

<s:Label x="10" y="50" width="150" height="50" text="Enter a URL: "/>   <s:TextInput x="180" y="50" width="290" height="50" text="{url}"/>
  <s:Button x="10" y="120" width="300" height="50"
            label="Launch the URL" click="StageWebViewExample()" />
</s:View>`

清单 6–3 包含一个允许用户输入 URL 的输入字段,以及一个通过 ActionScript3 文件StageWebViewLaunch.as中定义的方法StageWebViewExample()启动指定 URL 的按钮。

现在创建文件StageWebViewLaunch.as并插入清单 6–4 中的代码。

清单 6–4。 启动 URL 的 ActionScript3 代码

`import flash.media.StageWebView;
import flash.geom.Rectangle;

import flash.events.ErrorEvent;
import flash.events.Event;
import flash.events.LocationChangeEvent;

[Bindable]
public var url:String = "http://www.google.com";

private var webView:StageWebView = new StageWebView();

public function StageWebViewExample() {
  webView.stage = this.stage;
  webView.viewPort = new Rectangle( 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);
  // launch the user-specified URL
  webView.loadURL( url );
}

// Dispatched after the page or web content has been fully loaded
protected function completeHandler(event:Event):void {
  dispatchEvent(event);
}

// Dispatched when the location is about to change
protected function locationChangingHandler(event:Event):void {
  dispatchEvent(event);
}

// Dispatched after the location has changed
protected function locationChangeHandler(event:Event):void {
  dispatchEvent(event);
}

// Dispatched when an error occurs
protected function errorHandler(event:ErrorEvent):void {   dispatchEvent(event);
}`

清单 6–4 定义了引用用户指定的 URL 的Bindable变量url,后面是包含特定于 URL 的功能的变量webView。方法StageWebViewExample()定义了各种事件处理程序,所有这些程序都调用内置方法dispatchEvent(),然后用户指定的 URL 通过这行代码启动:

  webView.loadURL( url );

图 6–3 显示了谷歌主页,这是清单 6–4 中的默认 URL。

images

图 6–3。 启动用户指定的网址

在 Adobe AIR 中访问 SQLite

Adobe AIR 支持访问存储在移动设备上的 SQLite 数据库中的数据。您也可以直接从原生 Android 代码访问 SQLite 数据库,但 Adobe AIR 提供了更高的抽象级别(代码也更简单)。

使用移动应用模板创建一个名为 SQLite1 的新 Flex 移动项目,并添加如清单 6–5 所示的代码。

清单 6–5。 查看 SQLite 数据库中的数据

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="HomeView" creationComplete="start()">

<fx:Script source="SQLiteAccess.as"/>

<s:Label x="10" y="10" width="400" height="50"
           text="Create New Person and Click 'Add'"/>
  <s:Label x="10" y="50" width="150" height="50" text="First name:"/>
  <s:TextInput x="250" y="50" width="200" height="50" id="first_name"/>

<s:Label x="10" y="100" width="150" height="50" text="Last name:"/>
  <s:TextInput x="250" y="100" width="200" height="50" id="last_name"/>

<s:Button x="10" y="160" width="200" height="50"
            label="Add" click="addItem()"/>

<s:Button label="Remove Selected Person" height="50" x="10" y="230"
                  click="remove()" enabled="{dg.selectedIndex != -1}"/>

<s:DataGrid id="dg" left="10" right="10" top="300" bottom="80"
              dataProvider="{dp}">
     <s:columns>
       <s:ArrayList>
         <s:GridColumn headerText="Index"
                       dataField="id"
                       width="100" />
         <s:GridColumn headerText="First name"
                       dataField="first_name"
                       width="150" />
         <s:GridColumn headerText="Last name"  
                       dataField="last_name"
                       width="150" />
       </s:ArrayList>
    </s:columns>
  </s:DataGrid>
</s:View>`

清单 6–5 包含一个 XML 脚本元素,该元素引用一个 ActionScript3 文件,该文件包含访问 SQLite 数据库的方法,其内容将显示在清单 6–6 中。标签和文本输入字段使用户能够输入将被添加到我们的数据库中的每个新人的名字和姓氏。

有一个 XML Button元素用于通过addPerson()方法添加一个新人员,还有一个 XML Button用于通过removePerson()方法从我们的数据库中删除一个现有人员。两种方法都在SQLiteAccess.as中定义。变量dp是一个Bindable变量,包含数据网格中显示的数据,由于用户可以添加和删除数据行,dp是一个Bindable变量。

因为我们要访问存储在 SQLite 数据库中的数据,所以我们需要在 ActionScript3 中定义几个方法来管理数据库内容的创建、访问和更新。在与文件SQLite1HomeView.mxml相同的目录下创建一个名为SQLiteAccess.as的新 ActionScript3 文件,并添加如清单 6–6 所示的代码。

清单 6–6。 在 ActionScript3 中定义数据库访问方法

`import flash.data.SQLStatement;
import flash.errors.SQLError;
import flash.events.Event;
import flash.events.SQLErrorEvent;
import flash.events.SQLEvent;
import flash.events.TimerEvent;
import flash.filesystem.File;
import flash.utils.Timer;

import mx.collections.ArrayCollection;
import mx.utils.ObjectUtil;

import org.osmf.events.TimeEvent;`

清单 6–6 包含各种与 SQL 相关的import语句和一个Timer类,当我们试图读取更新后的数据库的内容时,将会用到这个类。因为在任何给定的时间点只能执行一条 SQL 语句,所以Timer类给了我们“稍后再试”的能力(以毫秒为单位)。

`// sqlconn holds the database connection
public var sqlconn:SQLConnection = new SQLConnection();

// sqlstmt holds SQL commands
public var sqlstmt:SQLStatement = new SQLStatement();

// a bindable ArrayCollection and the data provider for the datagrid
[Bindable]
public var dp:ArrayCollection = new ArrayCollection();

// invoked after the application has loaded
private function start():void {
  // set 'people.db' as the file for our database (created after it's opened)
  var db:File = File.applicationStorageDirectory.resolvePath("people.db");

// open the database in asynchronous mode
  sqlconn.openAsync(db);

// event listeners for handling sql errors and 'result' are
  // invoked whenever data is retrieved from the database
  sqlconn.addEventListener(SQLEvent.OPEN, db_opened);
  sqlconn.addEventListener(SQLErrorEvent.ERROR, error);
  sqlstmt.addEventListener(SQLErrorEvent.ERROR, error);   sqlstmt.addEventListener(SQLEvent.RESULT, result);
}`

变量sqlconnsqlstmt使我们能够连接到 SQLite 数据库并执行 SQL 查询。start()方法将数据库名称指定为people.db,然后打开一个异步连接。请注意用于处理数据库相关操作和错误的各种事件处理程序。

`private function db_opened(e:SQLEvent):void {
  // specify the connection for the SQL statement
  sqlstmt.sqlConnection = sqlconn;

// Table "person_table" contains three columns:
  // 1) id  (an autoincrementing integer)
  // 2) first_name (the first name of each person)
  // 3) last_name (the last name of each person)
 sqlstmt.text = "CREATE TABLE IF NOT EXISTS person_table
                            ( id INTEGER PRIMARY KEY AUTOINCREMENT,
                             first_name TEXT, last_name TEXT);";

// execute the sqlstmt to update the database
  sqlstmt.execute();

// refresh the datagrid to display all data rows
  refreshDataGrid();
}

// function to append a new row to person_table
// each new row contains first_name and last_name
private function addPerson():void {
  sqlstmt.text = "INSERT INTO person_table (first_name, last_name)
                          VALUES('"+first_name.text+"','"+last_name.text+"');";
  sqlstmt.execute();

refreshDataGrid();
}`

方法db_opened()指定数据库的名称和包含我们的个人相关数据的表,然后执行refreshDataGrid()方法,该方法检索数据库的最新内容,以便在我们的移动应用的数据网格中显示该数据。请注意,在addPerson()插入一个新的人之后,refreshDataGrid()方法被调用,这样 datagrid 将自动显示新添加的人。

`// function to refresh the data in datagrid
private function refreshDataGrid(e:TimerEvent = null):void {
  // timer object pauses and then attempts to execute again
  var timer:Timer = new Timer(100,1);
  timer.addEventListener(TimerEvent.TIMER, refreshDataGrid);

if ( !sqlstmt.executing ) {
    sqlstmt.text = "SELECT * FROM person_table"
    sqlstmt.execute();
  } else {
    timer.start();   }
}

// invoked when we receive data from a sql command
private function result(e:SQLEvent):void {
  var data:Array = sqlstmt.getResult().data;

// fill the datagrid
  dp = new ArrayCollection(data);
}

// remove a row from the table
private function removePerson():void {
  sqlstmt.text = "DELETE FROM person_table WHERE id="+dp[dg.selectedIndex].id;
  sqlstmt.execute();
  refreshDataGrid();
}

// error handling method
private function error(e:SQLErrorEvent):void {
//  Alert.show(e.toString());
}`

方法refreshDataGrid()首先检查 SQL 语句当前是否正在执行;如果是,那么它暂停指定的毫秒数(在本例中是 100),然后从person_table中检索所有行(这将包括新添加的人员)。方法result()用刷新的数据集填充变量dpremovePerson()方法删除用户在数据网格中点击选中的行。

Figure 6–4 显示了一组存储在移动设备上的 SQLite 数据库中的行。

images

图 6–4。SQLite 数据库中的一组记录

学习 Android 的基本概念

Android 是一个用于开发 Android 移动应用的开源工具包,在撰写本文时,Android 3.0(“蜂巢”)是最新的主要版本;Android 的最新版本是 3.1。注意,Adobe AIR 2 . 5 . 1 for mobile applications 至少需要 Android 2.2,您可以在支持 Android 2.2 或更高版本的移动设备上安装 Adobe AIR 应用。

以下部分提供了一些关于 Android 3.0 的主要特性、从哪里下载 Android 以及 Android 中的关键概念的信息。当你读完这一节,你将理解如何创建原生 Android 应用。

Android 3.0 的主要特性

Android 的早期版本提供了对 UI 组件和事件处理程序、音频和视频、管理文件系统上的文件、图形和动画效果、数据库支持、web 服务以及电话和消息(SMS)的支持。

Google Android 3.0(2011 年初发布)为 Android 2.3(2010 年 12 月发布)中的功能提供了向后兼容的支持。Android 3.0 提供了比 2.3 版本更好的功能,以及以下新功能:

  • 平板电脑的新用户界面
  • 状态和通知的系统栏
  • 应用控制的操作栏
  • 可定制的主屏幕
  • 复制/粘贴支持
  • 更多连接选项
  • SVG 支援
  • 通用远程功能
  • 谷歌文档集成
  • 内置远程桌面
  • 改进的媒体播放器
  • 更好的 GPS 支持
  • 改进的多任务处理
  • 跟踪设备
  • 电池寿命/电源管理改进

Android 3.0 中的优秀功能改进包括更长的电池寿命、更快的图形渲染和更丰富的媒体功能(例如,延时视频、HTTP 直播和 DRM)。此外,Android 3.1 支持另一组新功能,包括 USB 附件的 API 和来自鼠标、轨迹球和操纵杆的新输入事件。但是,在本章中,我们将重点关注 Android 功能的一小部分,为了将 Adobe AIR 应用与原生 Android 应用合并,我们需要了解这些功能,因此我们不会深入研究 Android 3.x 的功能。导航到 Android 主页以获取有关|Android 3.x 中支持的新的和改进的功能套件的更多信息。

安卓下载/安装

为了在 Eclipse 中开发基于 Android 的移动应用,您需要下载并安装 Java、Eclipse 和 Android。注意 Java 是预装在 Mac OSX 上的,Java 也可以通过基于 Linux 的包管理器下载。你可以在这里下载适合你平台的 Java:[java.sun.com/javase/downloads](http://java.sun.com/javase/downloads)

如果您有一台 Windows 机器,您需要将环境变量JAVA_HOME设置为解压缩 Java 发行版的目录。

Android SDK 可以在这里下载:[developer.android.com/sdk/index.html](http://developer.android.com/sdk/index.html)

对于 Windows 平台,Android 发行版是一个具有以下类型名称的文件(这在本书出版时可能略有不同):android-sdk_r06-windows.zip

完成 Java 和 Eclipse 安装后,按照 Android 安装步骤在您的机器上安装 Android。

你还需要创建一个 AVD (Android 虚拟设备),具体操作步骤在第五章中。

Android 中的关键概念

尽管 Android 应用是用 Java 编写的,但 Java 代码被编译成 Dalvik 可执行文件,该文件(以及其他素材)是部署到 Android 设备的.apk应用文件的一部分。

除了支持标准 Java 语言特性之外,Android 应用通常还包含以下特定于 Android 的概念:

  • 活动
  • Intent年代
  • Service年代
  • Broadcast接收者

在您掌握了前面列表中的概念之后,您可以了解Intent FilterContent Provider(对这两个主题的全面讨论超出了本章的范围)以及如何使用它们来提供更细粒度的基于Intent的功能以及跨 Android 应用共享数据的能力。

每个 Android 应用的属性都在 XML 文档AndroidManifest.xml中指定,该文档是在创建每个 Android 应用时自动生成的。这个清单文件包含关于活动、IntentServiceBroadcast接收者和许可的信息,这些信息是相关联的 Android 应用的一部分。

一个 Android 应用可以包含多个 Android Activities,每个 Android Activity可以包含多个IntentIntent Filter,此外,一个 Android 应用可以包含一个 Android Service和一个 Android Broadcast receiver,它们都被定义为AndroidManifest.xml中 Android activity元素的兄弟元素。

清单 6–7 提供了您可能在AndroidManifest.xml项目文件中找到的内容的概要。在这种情况下,项目文件包含两个 Android 活动、两个 Android Service和两个 Android Broadcast接收者的“存根”。XML 元素的属性已经被省略,这样您就可以看到 Android 应用的整体结构,在本章的后面,您将看到一个完整的AndroidManifest.xml内容的例子。

清单 6–7。 一个轮廓AndroidManifest.xml

<manifest xmlns:android=http://schemas.android.com/apk/res/android>     <application>         <activity>             <intent-filter>             </intent-filter>         </activity>         <activity>         </activity>         <service>         </service>         <service>             <intent-filter>             </intent-filte>         </service>         <receiver>         </receiver>         <receiver>             <intent-filter>             </intent-filter>         </receiver>     </application> </manifest>

清单 6–7 包含两个 Android activity元素、两个 Android service元素和两个 Android receiver元素。在这三对中的每一对中,都有一个包含 Android intent-filter元素的元素,但是请记住,AndroidManifest.xml的内容可能有许多变化。项目文件的确切内容取决于您的 Android 应用的功能。

Android 还支持 Java 本地接口(JNI ),允许 Java 代码调用 C/C++ 函数。但是,您还需要下载并安装 Android 原生开发工具包(NDK),它包含一组工具,用于创建包含可从 Java 代码调用的函数的库。如果你需要比单独使用 Java 更好的性能(尤其是图形性能),你可以使用 JNI,但是这个主题的细节已经超出了本章的范围。

安卓活动

Android Activity对应于应用的一个屏幕或一个视图,Android 应用的主要入口点是一个 Android Activity,它包含一个onCreate()方法(覆盖其超类中的相同方法),每当您启动 Android 应用时都会调用该方法。当你启动你的 Android 应用时,它的 Android Activity会自动启动。例如,清单 6–8 显示了HelloWorld.java的内容,这是在本章稍后创建基于 Eclipse 的“Hello World”Android 应用时自动生成的。

清单 6–8。HelloWorld.java的内容

`package com.apress.hello;

import android.app.Activity;
import android.os.Bundle;

public class HelloWorld extends Activity
{
   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
   }
}`

在 Android 应用的项目创建步骤中,您指定包名和类名;其余生成的代码对于每个 Android 项目都是一样的。

注意,HelloWorld扩展了android.app.Activity,并且它也覆盖了onCreate()方法。正如你可能猜到的,Android Activity是一个包含一组方法(如onCreate())的 Android 类,你可以在你的 Android 应用中覆盖这些方法。一个Activity包含一个或多个属于 Android 应用的View

Android View是用户在屏幕上看到的东西,包括 Android 应用的 UI 小部件。HelloWorld Android 应用包含一个 Android 类,它扩展了 Android Activity类,并用您的自定义代码覆盖了onCreate()方法。注意,Android 应用也可以扩展其他 Android 类(比如Service类),它们也可以创建线程。

一个 Android 应用可以包含不止一个 Android Activity,正如您已经知道的,每个Activity都必须在 XML 文档AndroidManifest.xml中定义,XML 文档是每个 Android 应用的一部分。

HelloWorld Android 项目包含 XML 文档AndroidManifest.xml,其中 Android 类HelloWorld注册在 XML activity元素中,如清单 6–9 所示。

清单 6–9。AndroidManifest.xml的内容

`

    
        
          
            
            
          

        


    
`

请注意句号(“.”)位于清单 6–9 中的 Android Activity HelloWorld之前。这个句点是强制的,因为字符串.HelloWorld被附加到包名com.apress.hello(也在清单 6–9 中指定),所以这个 Android 项目中HelloWorld.java的完全限定名是com.apress.hello.HelloWorld

安卓意图

Android UI(用户界面)由IntentView组成。抽象地说,Android Intent表示在 Android 应用中要执行的动作(通常由动词描述)的细节。

一个Intent本质上是 Android 活动(或Service s)之间的通知。一个Intent使一个 Android Activity能够向其他 Android 活动发送数据,也能够从其他 Android 活动接收数据。

Android Intent类似于事件处理器,但是 Android 提供了处理多个Intent的额外功能,以及使用现有Inten ts 与启动新Intent的选项。Android Intent s 可以启动一个新的 Android Activity,它们还可以广播消息(由 Android Broadcast接收器处理)。下面的代码片段说明了如何通过一个Intent来启动一个新的Activity:

Intent intent = new Intent(action, data); startActivity(intent);

Android Activities 和Intent s 提供了一组松散耦合的资源,让人想起 SOA(面向服务的架构)。Android Activity的对等物是一个 web 服务,Android Activity可以处理的Intent类似于 web 服务提供给世界的“操作”或方法。其他 Android 应用可以显式地调用这些方法中的一个,或者它们可以发出一个“通用”请求,在这种情况下,“框架”决定哪些 web 服务将处理该通用请求。

你也可以广播Intent以便在组件之间发送消息。下面的代码片段说明了如何广播一个Intent:

Intent intent = new Intent(a-broadcast-receiver-class); sendBroadcast(intent);

这种类型的功能为 Android 应用提供了更大的灵活性和“开放性”。

Android 意图的类型

Android 有几种类型,每一种提供的功能都略有不同。定向的Intent是具有一个接收者的Intent,而广播的Intent可以被任何进程接收。一个显式?? 指定了需要调用的 Java 类。一个隐式 Intent是一个不指定 Java 类的Intent,这意味着 Android 系统将决定哪个应用将处理隐式Intent。如果有几个应用可以响应隐式的Intent,Android 系统会让用户选择其中一个应用。**

**Android 也有Intent Filter s 的概念,用于Intent分辨率。一个Intent Filter表示一个机器人Activity(或机器人Service)可以“消费”的Intent,细节在 XML intent-filter元素中指定。注意,如果应用不提供Intent Filter,那么它只能被显式的Intent调用(而不能被隐式的Intent)。

在文件AndroidManifest.xml的一个 Android Activity中指定了一个Intent Filter,如清单 6–10 所示。

清单 6–10。 安卓中的一个Intent Filter的例子

<intent-filter>       <action android:name="android.intent.action.MAIN" />       <category android:name="android.intent.category.LAUNCHER" /> </intent-filter>

清单 6–10 显示了AndroidManifest.xml内容的一个片段,如清单 6–9 所示。清单 6–10 中的 XML action元素指定默认值android.intent.action.MAIN,XML category元素指定android.intent.category.LAUNCHER(也是默认值),这意味着父Activity将显示在应用启动器中。

一个Intent Filter必须包含一个 XML action元素,并且可以选择包含一个 XML category元素或者一个 XML data元素。如您所见,清单 6–10 包含指定默认动作的强制 XML action元素,以及可选的 XML category元素,但不包含可选的 XML data元素。

一个Intent Filter是定义特定动作的一组信息;XML data元素指定要操作的数据,XML category元素指定执行动作的组件。

在一个Intent Filter中可以指定这三个 XML 元素的各种组合,因为其中两个元素是可选的,Android 使用一种基于优先级的算法来确定将为您在AndroidManifest.xml中定义的每个Intent Filter执行什么。如果您需要更详细地了解Intent Filter s,请查阅 Android 文档以获取更多信息。

如果你有兴趣了解免费提供的 Android Intent s,你可以访问 OpenIntents,这是一个由其他人捐赠的各种 Android Intent s 组成的开源项目,它的主页在这里:[www.openintents.org/en/](http://www.openintents.org/en)

OpenIntents 提供各种类别的 Android 应用,如实用程序、商业应用、教育和娱乐。这些应用以.apk文件的形式提供,有时这些应用的源代码也是免费的。此外,OpenIntents 提供了游戏引擎、图表包等 Android 库的链接,以及访问 CouchDB 服务器、Drupal、脸书等服务。OpenIntents 库可从这里获得:[www.openintents.org/en/libraries](http://www.openintents.org/en/libraries)

OpenIntents 还提供了一个可以被 Android 活动调用的公开可用的 Android Intent的注册表,以及它们的类文件和它们的服务描述。有关更多信息,请访问 OpenIntents 主页。

安卓服务

Android Service可用于处理后台任务和其他不涉及视觉界面的任务。由于 Android Service运行在主进程的主线程中,Android Service通常会在需要执行工作时启动一个新线程,而不会阻塞 Android 应用的 UI(在主线程中处理)。因此,Android 应用可以通过该服务公开的一组 API“绑定”到一个Service

Android Service是通过AndroidManifest.xml中的 XML service元素定义的,如下所示:

<service android:name=".subpackagename.SimpleService"/>

清单 6–11 显示了ServiceName.java的内容,它为自定义 Android Service类的定义提供了“框架”代码。

清单 6–11。SimpleService.java的内容

`public class SimpleService extends Service {
   @Override
   public IBinder onBind(Intent intent) {
      return null;
   }

@Override
   protected void onCreate() {
      super.onCreate();
      startservice(); // defined elsewhere
   }

@Override    protected void onCreate() {
      // insert your code here
   }

@Override
   protected void onStart() {
      // insert your code here
   }
}`

例如,如果您需要定期执行某件事情,那么您可以包含一个Timer类的实例,该实例根据您的应用的需要来调度和执行一个TimerTask

安卓广播接收器

Android Broadcast接收器的目的是“听”Android Intent s. 清单 6–12 在本章稍后讨论的基于小部件的 Android 应用的AndroidManifest.xml文件中显示了 Android Broadcast接收器的定义。

清单 6–12。 AndroidManifest.xml 中的接收人录入样本

<!-- Broadcast Receiver that will process AppWidget updates -->     <receiver android:name=".MyHelloWidget" android:label="@string/app_name">       <intent-filter>         <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />       </intent-filter>       <meta-data android:name="android.appwidget.provider"                  android:resource="@xml/hello_widget_provider" />     </receiver>

清单 6–12 包含一个 XML receiver元素,该元素将MyHelloWidget指定为这个小部件的 Java 类。这个 Android receiver 包含一个 XML intent-filter元素和一个 XML action元素,当需要更新AppWidget MyHelloWidget时,这个 XMLaction元素会导致一个动作发生,如下所示:

    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />

Android 生命周期

在本章的前面,您看到了HelloWorld.java的内容,它包含了覆盖超类中相同方法的onCreate()方法。事实上,onCreate()是组成 Android 应用生命周期的七种 Android 方法之一。

谷歌 Android 应用包含以下方法,这是 Android 应用生命周期中调用方法的顺序 1 :

  • onCreate()
  • onRestart()
  • onStart()
  • onResume()
  • onPause()
  • onStop()
  • onDestroy()

创建Activity时会调用onCreate()方法,其作用类似于其他语言中的init()方法。当从内存中移除一个Activity时,就会调用onDestroy()方法,它的作用实质上是 C++ 中的一个析构函数方法。当Activity必须暂停时(比如回收资源),调用onPause()方法。当重新启动Activity时,调用onRestart()方法。当Activity与用户交互时,调用onResume()方法。当Activity在屏幕上可见时,调用onStart()方法。最后,调用onStop()方法来停止Activity

方法onRestart()onStart()onStop()处于可见阶段;方法onResume()onPause()处于前台阶段。Android 应用在执行期间可以暂停和恢复多次;细节是特定于应用的功能的(也可能是用户交互的类型)。

创建 Android 应用

这一节描述了如何在 Eclipse 中创建 Android 应用,随后的一节向您展示了 Android 应用的目录结构,随后讨论了在每个 Android 应用中创建的主要文件。

启动 Eclipse 并执行以下步骤,以创建一个名为 HelloWorld 的新 Android 应用:

  1. 导航到文件images新建images Android 项目。
  2. 输入“HelloWorld”作为项目名称。
  3. 选择“Android 2.3”左侧的复选框作为构建目标。
  4. 输入“HelloWorld”作为应用名称。
  5. 输入“com.apress.hello”作为包名。
  6. 在创建活动输入字段中输入“HelloWorld”。
  7. 输入数字“9”表示最小 SDK 版本。
  8. 单击完成按钮。

Eclipse 将生成一个新的 Android 项目(其结构将在下一节描述)。接下来,通过右键单击项目名 HelloWorld 启动这个应用,然后选择 Run As images Android Application。您必须等待 Android 模拟器完成其初始化步骤,这可能需要一分钟左右的时间(但是您的应用的每次后续启动都会明显更快)。

图 6–5 显示了从 Eclipse 启动的 Android 模拟器中 HelloWorld 应用的输出。

images

图 6–5。hello world 安卓应用

Android 应用的结构

导航到您在上一节中创建的 HelloWorld 项目,右键单击项目名称以显示展开的目录结构。接下来的几个小节讨论了每个 Android 应用的目录结构和主文件的内容。

清单 6–13 显示了 Android 项目 HelloWorld 的目录结构。

清单 6–13。 一个安卓项目的架构

`+HelloWorld
  src/
    com/
      apress/
        hello/
          HelloWorld.java

gen/
    com/
      apress/
        hello/
          R.java
  Android 2.3/
    android.jar
  assets/
  res/
    drawable-hdpi/
      icon.png
    drawable-ldpi/
      icon.png
    drawable-mdpi/
      icon.png
    layout/
      main.xml
    values/
      strings.xml
  AndroidManifest.xml
  default.properties
proguard.cfg`

这个 Android 应用包含两个 Java 文件(HelloWorld.javaR.java)、一个 JAR 文件(android.jar)、一个图像文件(icon.png)、三个 XML 文件(main.xmlstrings.xmlAndroidManifest.xml)以及一个文本文件default.properties

Android 应用中的主要文件

这里列出了我们将在本节中讨论的 HelloWorld Android 应用中的文件(所有文件都是相对于项目根目录列出的):

  • src/com/apress/hello/HelloWorld.java
  • gen/com/apress/hello/R.java
  • AndroidManifest.xml
  • res/layout/main.xml
  • res/values/strings.xml

清单 6–14 显示了HelloWorld.java的内容,其中包含了这个 Android 应用所需的所有定制 Java 代码。

清单 6–14。HelloWorld.java的内容

`package com.apress.hello;

import android.app.Activity;
import android.os.Bundle;
public class HelloWorld extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        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-GENERATED FILE.  DO NOT MODIFY.
 *
 * This class was automatically generated by the
 * aapt tool from the resource data it found.  It
 * should not be modified by hand.
 */

package com.apress.hello;

public final class R {
    public static final class attr {
    }
    public static final class drawable {
        public static final int icon=0x7f020000;
    }
    public static final class layout {
        public static final int main=0x7f030000;
    }
    public static final class string {
        public static final int app_name=0x7f040001;
        public static final int hello=0x7f040000;
    }
}`

清单 6–15 中的整数值本质上是对应于 Android 应用素材的引用。例如,变量icon是对位于res目录的子目录中的icon.png文件的引用。变量main是对位于res/layout子目录中的 XML 文件main.xml(将在本节稍后介绍)的引用。变量app_namehello是对 XML 文件strings.xml(本节前面已经介绍过)中的 XML app_name元素和 XML hello元素的引用,该文件位于res/values子目录中。

既然我们已经研究了基于 Java 的项目文件的内容,让我们把注意力转向 Android 项目中基于 XML 的文件。清单 6–16 显示了AndroidManifest.xml的全部内容。

清单 6–16。AndroidManifest.xml的内容

`

    
        
          
             
            
          

        


    
`

清单 6–16 以一个 XML 声明开始,后面是一个包含子 XML 元素的 XML manifest元素,它提供了关于您的 Android 应用的信息。注意,XML manifest元素包含一个带有 Android 应用包名的属性。

清单 6–16 中的 XML application元素包含一个值为@drawable/iconandroid:icon属性,该属性引用位于res子目录中的一个图像文件icon.png。Android 支持三种类型的图像文件:高密度、中密度和低密度。对应的目录有drawable-hdpidrawable-mdpidrawable-ldpi,都是每个安卓应用根目录下res目录的子目录。

清单 6–16 中的 XML application元素还包含一个值为@string/app_nameandroid:label属性,该属性引用了位于res/values子目录中的文件strings.xml中的一个 XML 元素。

清单 6–16 包含一个 XML intent-filter元素,这在本章前面已经简要讨论过了。清单 6–10 的最后一部分指定了该应用所需的最低 Android 版本号,如下所示:

    <uses-sdk android:minSdkVersion="9" />

在我们当前的例子中,最低版本是 9,这也是我们在这个 Android 应用的创建步骤中指定的数字。

现在让我们看看清单 6–17,它显示了 XML 文件strings.xml的内容。

清单 6–17。strings.xml的内容

<?xml version="1.0" encoding="utf-8"?> <resources>     <string name="hello">Hello World, HelloWorld!/string>     <string name="app_name">HelloWorld</string> </resources>

清单 6–17 很简单:它包含一个 XML resources元素和两个 XML 子元素,用于显示字符串“HelloWorld,Hello World!”当你启动这个安卓应用。注意,XML 文档AndroidManifest.xml中的 XML application元素还引用了第二个 XML string元素,其 name 属性的值为app_name,如下所示:

<application android:icon="@drawable/icon"                     android:label="@string/app_name">

现在让我们看一下清单 6–18,它显示了 XML 文档main.xml的内容,该文档包含关于这个 Android 应用的View相关信息。

清单 6–18。main.xml的内容

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:orientation="vertical"     android:layout_width="fill_parent"     android:layout_height="fill_parent"     > <TextView     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:text="@string/hello"     /> </LinearLayout>

清单 6–18 包含一个 XML LinearLayout元素,这是 Android 应用的默认布局。Android 支持其他布局类型,包括AbsoluteLayoutFrameLayoutRelativeLayoutTableLayout(本章不讨论)。

XML LinearLayout元素包含一个fill_parent属性,该属性指示当前元素将与父元素一样大(减去填充)。属性layout_widthlayout_height指定了View的宽度和高度的基本值。

XML TextView元素包含属性layout_widthlayout_height,它们的值分别是fill_parentwrap_contentwrap_content属性指定View的大小刚好足够包含它的内容(加上填充)。属性text是指在 strings.xml文件(位于res/values子目录中)中指定的 XML hello元素,其定义如下所示:

<string name="hello">Hello World, HelloWorld!</string>

字符串“HelloWorld,Hello World!”是在部署此 Android 应用后,在 Android 模拟器或 Android 设备中启动“Hello World”Android 应用时显示的文本。

在 Android 应用中发送通知

正如你已经知道的,Adobe 不提供通知的内置支持,这在原生 Android 移动应用中是可用的。然而,本节中的示例将向您展示如何创建一个 Adobe AIR mobile 应用和一个原生 Android 应用,它们可以合并成一个单独的.apk文件,该文件将支持与Notification相关的功能。

詹姆斯·沃德(他为这本书写了前言)在这一部分贡献了基于套接字的代码,Elad Elrom 提供了将 Adobe AIR mobile 应用与原生 Android 应用合并的分步说明。

这一节很长,因为有一个初始设置序列(涉及六个步骤),两个 Adobe AIR 源文件,以及这个移动应用的 Android 源文件。第一部分描述了设置序列;本节的第二部分讨论了带有 Adobe AIR 代码的两个源文件;第三部分讨论了包含原生 Android 代码的两个源文件。

  1. 下载一个包,其中包含为 Android 扩展 AIR 所需的依赖项:www.jamesward.com/downloads/extending_air_for_android-flex_4_5-air_2_6–v_1.zip
  2. 在 Eclipse 中创建一个常规的 Android 项目(暂时不要创建Activity):指定 FooAndroid 的“项目名称:”,为“目标名称:”选择 Android 2.2,为“应用名称:”键入“FooAndroid”,为“包名称:”输入“com.proandroidflash”,为“最小 SDK 版本:”键入“8”,然后单击 Finish 按钮。
  3. 将步骤 1 中下载的 zip 文件中的所有文件复制到新创建的 Android 项目的根目录中。您需要覆盖现有的文件,Eclipse 将提示您更新启动配置。
  4. 删除res/layout目录。
  5. 通过右键单击文件将airbootstrap.jar文件添加到项目的构建路径,然后选择 build path 和 Add to Build Path。
  6. 启动项目,确认在 Android 设备上看到“Hello,world”。如果是这样,那么 AIR 应用被正确引导,并且assets/app.swf中的 Flex 应用被正确运行。

现在我们已经完成了初始设置步骤,让我们使用移动应用模板创建一个名为 Foo 的新 Flex 移动项目,并添加如清单 6–19 所示的代码。

清单 6–19。 接收数据并将数据发送给 Android 上的通知

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="HomeView" creationComplete="start()">

<fx:Script source="SQLiteAccess.as"/>


  <s:Button x="10" y="10" width="200" height="50"
            label="Get Statuses" click="invokeMontastic()">
  </s:Button>

<s:Button x="250" y="10" width="200" height="50"
            label="Clear History" click="removeAll()">
  </s:Button>

<s:DataGrid id="dg" left="10" right="10" top="70" bottom="100"
              dataProvider="{dp}">
    <s:columns>
      <s:ArrayList>
        <s:GridColumn headerText="ID"
                      dataField="id"
                      width="60" />
        <s:GridColumn headerText="Status"
                      dataField="status"
                      width="100" />
        <s:GridColumn headerText="URL"
                      dataField="url"
                      width="300" />
      </s:ArrayList>
    </s:columns>
  </s:DataGrid>

<s:Button label="Create Notification" x="10" y="650" width="300" height="50">
     <s:click>
        >![CDATA[
           var s:Socket = new Socket();
           s.connect("localhost", 12345);            s.addEventListener(Event.CONNECT, function(event:Event):void {
             trace('Client successfully connected to server');
             (event.currentTarget as Socket).writeInt(1);
             (event.currentTarget as Socket).writeUTF(allStatuses);
             (event.currentTarget as Socket).flush();
             (event.currentTarget as Socket).close();
           });
          s.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void {
            trace('error sending allStatuses from client: ' + event.errorID);
          });
          s.addEventListener(ProgressEvent.SOCKET_DATA,
function(event:ProgressEvent):void {
            trace('allStatuses sent successfully');
         });
        ]]>
     </s:click>
  </s:Button>
</s:View>`

清单 6–19 包含一个 XML 按钮,它调用 Montastic APIs 来检索在 Montastic 中注册的网站的状态。当用户单击此按钮时,网站的状态存储在 SQLite 数据库中,并且数据网格用新的一组行刷新。

第二个 XML 按钮使用户能够删除 SQLite 表中的所有行,这很方便,因为该表的大小可以快速增加。如果您想维护这个表的所有行,您可能应该为 datagrid 提供滚动功能。

当用户单击第三个 XML 按钮元素时,这将在端口 12345 上启动一个基于客户端套接字的连接,以便将网站的最新状态发送到运行在本地 Android 应用中的服务器端套接字。Android 应用读取客户端发送的信息,然后在 Android 通知栏中显示状态。

清单 6–20 中的 ActionScript3 代码类似于清单 6–8,因此您将能够快速阅读其内容,尽管对代码进行了各种特定于应用的更改。

清单 6–20。 接收数据并将数据发送给 Android 上的通知

`Import flash.data.SQLConnection;
import flash.data.SQLStatement;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.errors.SQLErrorEvent;
import flash.events.SQLEvent;
import flash.events.TimerEvent;
import flash.filesystem.File;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
import flash.utils.Timer;

import mx.collections.ArrayCollection;
import mx.utils.Base64Encoder;

//Montastic URL
private static const montasticURL:String =
         "https://www.montastic.com/checkpoints/show";

// sqlconn holds the database connection
public var sqlconn:SQLConnection = new SQLConnection(); // sqlstmt is a SQLStatement that holds SQL commands
public var sqlstmt:SQLStatement = new SQLStatement();

// a bindable ArrayCollection and the data provider for the datagrid
[Bindable]
public var dp:ArrayCollection = new ArrayCollection();
[Bindable]
public var allStatuses:String = "1:UP#2:UP#3:UP";

private var urlList:Array = new Array();
private var statusList:Array = new Array();`

清单 6–20 包含各种导入语句,后面是用于打开数据库连接和执行 SQL 语句的变量。Bindable变量提供对数据库表内容的访问,以及在 Montastic 注册的网站的 URL 和状态。

变量checkpointsXMLList包含您已经向 Montastic 注册的网站的“实时”数据。

`// invoked after the application has loaded
private function start():void {
  // set 'montastic.db' as the file for our database (created after it's opened)
  var db:File = File.applicationStorageDirectory.resolvePath("montastic.db");

// open the database in asynchronous mode
  sqlconn.openAsync(db);

// event listeners for handling sql errors and 'result' are
  // invoked whenever data is retrieved from the database
  sqlconn.addEventListener(SQLEvent.OPEN, db_opened);
  sqlconn.addEventListener(SQLErrorEvent.ERROR, error);
  sqlstmt.addEventListener(SQLErrorEvent.ERROR, error);
  sqlstmt.addEventListener(SQLEvent.RESULT, result);
}

private function db_opened(e:SQLEvent):void {
  // specify the connection for the SQL statement
  sqlstmt.sqlConnection = sqlconn;

// Table "montastic_table" contains three columns:
  // 1) id  (an autoincrementing integer)
  // 2) url (the url of each web site)
  // 3) status (the status of each web site)
  sqlstmt.text = "CREATE TABLE IF NOT EXISTS montastic_table ( id INTEGER PRIMARY KEY
AUTOINCREMENT, url TEXT, status TEXT);";

// execute the sqlstmt to update the database
  sqlstmt.execute();

// refresh the datagrid to display all data rows
  refreshDataGrid();
}`

方法start()db_opened()类似于本章前面的例子,除了数据库名称现在是montastic.db,并且当用户在移动应用中点击相关的Button时,数据库表montastic_table被更新。注意,montastic_table包含列idurlstatus,而不是列idfirst_namelast_name

`// function to append new rows to montastic table
// use a begin/commit block to insert multiple rows
private function addWebsiteInfo():void {
  allStatuses = "";
  sqlconn.begin();

for (var i:uint = 0; i < urlList.length; i++) {
     var stmt:SQLStatement = new SQLStatement();
     stmt.sqlConnection = sqlconn;

stmt.text = "INSERT INTO montastic_table (url, status) VALUES(:url, :status);";
     stmt.parameters[":url"] = urlList[i];
     stmt.parameters[":status"] = statusList[i];

stmt.execute();
  }

// insert the rows into the database table
  sqlconn.commit();

refreshDataGrid();
}

// refresh the Montastic data in the datagrid
private function refreshDataGrid(e:TimerEvent = null):void {
   // timer object pauses and then attempts to execute again
   var timer:Timer = new Timer(100,1);
   timer.addEventListener(TimerEvent.TIMER, refreshDataGrid);

if (!sqlstmt.executing) {
       sqlstmt.text = "SELECT * FROM montastic_table"
       sqlstmt.execute();
   } else {
       timer.start();
   }
}

// invoked when we receive data from a sql command
//this method is also called for sql statements to insert items
// and to create our table but in this case sqlstmt.getResult().data
// is null
private function result(e:SQLEvent):void {
   var data:Array = sqlstmt.getResult().data;

// fill the datagrid with the latest data
   dp = new ArrayCollection(data);
}

// remove all rows from the table
private function removeAll():void {
  sqlstmt.text = "DELETE FROM montastic_table";
  sqlstmt.execute();

refreshDataGrid(); }`

方法addWebsiteInfo()addPerson()方法的“对应物”,在 begin/end 块中执行数据库插入,以便在一个 SQL 语句中执行多行插入。这种技术使我们能够使用与两种方法refreshDataGrid()result()相同的逻辑,从数据库中检索最新的数据,而不会出现争用错误。

请注意,我们现在有一个从数据库表中删除所有行的方法removeAll(),而不是从数据网格中删除选定行的方法remove()

`// functions for Montastic
public function invokeMontastic():void {
   var loader:URLLoader = new URLLoader();
   loader.addEventListener(Event.COMPLETE, completeHandler);
   loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);

var request:URLRequest = new URLRequest( montasticURL );
   request.method = URLRequestMethod.GET;

var encoder:Base64Encoder = new Base64Encoder();
   encoder.encode("yourname@yahoo.com:insert-your-password-here");
   request.requestHeaders.push(new URLRequestHeader("Authorization",
                                                    "Basic " + encoder.toString()));
   request.requestHeaders.push(new URLRequestHeader("pragma", "no-cache"));
   request.requestHeaders.push(new URLRequestHeader("Accept",
                                                    "application/xml"));
   request.requestHeaders.push(new URLRequestHeader("Content-Type",
                                                    "application/xml"));
   loader.load(request);
}
private function completeHandler(event:Event):void {
   var loader:URLLoader = URLLoader(event.target);
   checkpointsXMLList = new XML(loader.data);

urlList = new Array();
   statusList = new Array();

for each (var checkpoint:XML in checkpointsXMLList.checkpoint) {
      statusList.push(checkpoint.status.toString());
      urlList.push(checkpoint.url.toString());

}

allStatuses = "1="+statusList[0]+"#2="+statusList[1];

addWebsiteInfo();
}`

当用户点击相关按钮(其标签为“Get Statuses”)时,方法invokeMontastic()被执行,这又调用 Montastic APIs,这些 API 返回 XML,其中包含关于用户已向 Montastic 注册的网站的状态相关信息。

注意,方法completeHandler()是在对 Montastic 网站的异步请求返回了基于 XML 的数据之后调用的。

变量allStatuses被适当地更新(我们需要将这个字符串发送到服务器套接字),然后方法addWebsiteInfo()被执行,它用我们从 Montastic 接收的数据更新数据库表montastic_table

private function ioErrorHandler(event:IOErrorEvent):void {    trace("IO Error" + event); } private function sqlError(event:SQLErrorEvent):void {    trace("SQL Error" + event); }

函数ioErrorHandler()sqlError()在相关错误发生时被调用,在生产环境中,您可以添加额外的错误消息来提供有用的调试信息。

正如您之前看到的,我们使用了一个硬编码的 XML 字符串,其中包含您在 Montastic 注册的网站的信息样本。目前,您可以通过从命令行调用“curl”程序来检索基于 XML 的网站状态信息,如下所示(作为单行调用):

curl -H 'Accept: application/xml' -H 'Content-type: application/xml' -u yourname@yahoo.com:yourpassword https://www.montastic.com/checkpoints/index

既然我们已经讨论了特定于 AIR 的代码,那么让我们把重点放在处理来自客户端的信息的基于套接字的原生 Android 代码上。套接字代码是我们命名为 FooAndroid 的 Android 应用的一部分。

在我们讨论这个应用的 Java 代码之前,让我们看一下清单 6–21,它包含了我们的 Android 应用的文件AndroidManifest.xml。注意清单 6–21 显示了这个配置文件的最终版本,而不是在 Android 项目 FooAndroid 的创建步骤中生成的内容。

清单 6–21。 AndroidManifest.xml为原生安卓应用

`


      
                         
                
                
            

        

        
    

`

清单 6–21 指定MainApp.java作为我们的 Android 应用的 Android Activity。正如你将看到的,Java 类MainApp.java(包含一些定制的 Java 代码)扩展了 Android ActivityAppEntry.java,它是 Android Activity类的子类。请注意,清单 6–21 指定了一个名为TestService.java的 Android Service类,它包含基于套接字的定制代码,用于处理从 Adobe AIR 客户端接收的信息。

现在在 Eclipse 中创建一个名为 FooAndroid 的原生 Android 应用,它带有一个 Java 类MainApp,该类扩展了类AppEntry。Java 类AppEntry.java是一个简单的预建 Java Activity,它是 Android Activity类和我们定制的 Java 类MainApp之间的中间类。清单 6–22 显示了 Java 类MainApp的内容。

清单 6–22。主安卓Activity

`package com.proandroidflash;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

public class MainApp extends AppEntry {
    /** Called when the activity is first created. */

@Override
    public void onCreate(Bundle savedInstanceState)
    {
      super.onCreate(savedInstanceState);

try {
          Intent srv = new Intent(this, TestService.class);
          startService(srv);
      }
      catch (Exception e)
      {
          // service could not be started
      }
    }
}`

Java 类MainApp(它是 Android Activity类的间接子类)在我们的 Android 应用启动时执行;MainApp 中的onCreate()方法启动我们的定制 Java 类TestService.java(稍后讨论),该类启动服务器端套接字,以便处理来自 Adobe AIR 客户端的数据请求。

如您所见,onCreate()方法调用了安卓Activity类中的startService()方法,以启动TestService Service。这个功能是可能的,因为MainApp是 Android Activity类的子类。

现在在com.proandroidflash包中创建第二个 Java 类TestServiceApp.java,并将代码插入清单 6–23 中。

清单 6–23。 一个 Android Service类,处理来自 AIR 客户端的数据

`package com.proandroidflash;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;

public class TestService extends Service {
  private boolean stopped=false;
  private Thread serverThread;
  private ServerSocket ss;

@Override
  public IBinder onBind(Intent intent) {
    return null;
  }
  @Override
  public void onCreate() {
     super.onCreate();

Log.d(getClass().getSimpleName(), "onCreate");

serverThread = new Thread(new Runnable() {
          public void run() {
             try {
                Looper.prepare();                 ss = new ServerSocket(12345);
                ss.setReuseAddress(true);
                ss.setPerformancePreferences(100, 100, 1);

while (!stopped) {
                   Socket accept = ss.accept();
                   accept.setPerformancePreferences(10, 100, 1);
                   accept.setKeepAlive(true);

DataInputStream _in = null;

try {
                       _in = new DataInputStream(new BufferedInputStream(
                             accept.getInputStream(),1024));
                   }
                   catch (IOException e2) {
                     e2.printStackTrace();
                   }

int method = _in.readInt();

switch (method) {
                     // send a notification?
                     case 1: doNotification(_in);
                             break;
                   }
                }
             }
             catch (Throwable e) {
                e.printStackTrace();
                Log.e(getClass().getSimpleName(), "** Error in Listener **",e);
             }

try {
                 ss.close();
               }
               catch (IOException e) {
                  Log.e(getClass().getSimpleName(), "Could not close serversocket");
               }
            }
       },"Server thread");

serverThread.start();
  }`

FooAndroid 的初始部分包含各种导入语句、私有套接字相关变量和onBind()方法。这个方法可以用于 Android Service类支持的其他功能(这超出了本例的范围),对于我们的目的,这个方法只是返回 null。

清单 6–23 的下一部分包含一个冗长的onCreate()方法,它启动一个服务器端套接字来处理 Adobe AIR 客户端请求。

onCreate()方法启动一个 Java Thread,其run()方法在端口 12345(与客户端套接字是同一个端口)上启动一个服务器端套接字。onCreate()方法包含一个while循环,它等待客户端请求,然后在try/catch块中处理它们。

如果客户端请求中的第一个字符是数字“1”,那么我们知道客户端请求来自我们的 AIR 应用,并且try/catch块中的代码调用方法doNotification()。如果有必要,您可以增强onCreate()(即处理其他数字、文本字符串等的出现),以便服务器端代码可以处理其他客户端请求。

`  private void doNotification(DataInputStream in) throws IOException {
    String id = in.readUTF();
    displayNotification(id);
  }
  public void displayNotification(String notificationString)
  {
    int icon = R.drawable.mp_warning_32x32_n;

CharSequence tickerText = notificationString;
    long when = System.currentTimeMillis();
    Context context = getApplicationContext();
    CharSequence contentTitle = notificationString;
    CharSequence contentText = "Hello World!";

Intent notificationIntent = new Intent(this, MainApp.class);
    PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
                                                   notificationIntent, 0);
    Notification notification = new Notification(icon, tickerText, when);
    notification.vibrate = new long[] {0,100,200,300};

notification.setLatestEventInfo(context, contentTitle,
                                    contentText, contentIntent);

String ns = Context.NOTIFICATION_SERVICE;
    NotificationManager mNotificationManager =
                          (NotificationManager) getSystemService(ns);

mNotificationManager.notify(1, notification);
  }
  @Override
  public void onDestroy() {
     stopped = true;
     try {
        ss.close();
     }
     catch (IOException e) {}

serverThread.interrupt();

try {
        serverThread.join();
     }
     catch (InterruptedException e) {}
  }
}`

doNotification()方法只是读取输入流中的下一个字符串(由客户端发送),然后调用方法displayNotification()。在我们的例子中,这个字符串是一个串联的字符串,包含每个注册网站的状态(“UP”或“DOWN”)。

您可能已经猜到了,displayNotification()方法包含了在 Android 通知栏中显示通知的 Android 代码。需要注意的关键点是,这个方法用 Java 类MainApp.java创建了一个 Android Intent。新的Intent使我们能够创建一个 Android PendingIntent,这反过来又允许我们创建一个 Android Notification实例。displayNotification()中的最后一行代码启动我们的通知,它在 Android 通知栏中显示注册网站的状态。

代码的最后一部分是onDestroy()方法,它停止在onCreate()方法上启动的服务器端套接字。

现在我们已经完成了这个应用的 Java 代码,我们需要处理这个应用的 XML 相关文件。首先,确保应用中的AndroidManifest.xml内容与清单 6–21 中的内容相同。其次,在 XML 文件strings.xml中包含以下字符串:

<string name="button_yes">Yes</string> <string name="button_no">No</string> <string name="dialog_title"><b>Adobe AIR</b></string> <string name="dialog_text">This application requires that you first install Adobe AIR®.\n\nDownload it free from Android Market now?</string>

现在我们已经完成了所有的 Adobe AIR 代码和原生 Android 代码,我们准备将文件合并到一个移动应用中,这样做的步骤将在本章的下一节中显示。

您可以在 Figure 6–6 中看到本节中调用示例应用的示例,该示例显示了一组由 URL 及其状态组成的记录,这些记录存储在移动设备上的 SQLite 数据库中。

images

图 6–6。 一组注册网站的状态记录

Adobe AIR 和原生 Android 集成

本节包含将原生 Android 功能(如本章中的示例)集成到 Adobe AIR mobile 应用的过程。请注意,Adobe 不支持完成此操作的过程,实际步骤可能会在本书出版时发生变化。该信息由 Elad Elrom 提供。

清单 6–24 中的命令使用实用程序adtapktooladb将 Adobe AIR 应用MyAIRApp的内容与原生 Android 应用AndroidNative.apk的内容合并,以创建 Adobe AIR 移动应用MergedAIRApp.apk

清单 6–24 显示了您需要调用的实际命令,以便创建一个新的.apk文件,该文件包含来自 Adobe AIR 移动应用和原生 Android 移动应用的代码。确保您更新了变量APP_HOME的值,以便它反映您的环境的正确值。

清单 6–24。 使用 Adobe AIR 和原生 Android 代码创建合并的应用

APP_HOME="/users/ocampesato/AdobeFlashBuilder/MyAIRApp" cd $APP_HOME/bin-debug adt -package -target apk -storetype pkcs12 -keystore certificate.p12 -storepass Nyc1982 out.apk MyAIRApp-app.xml MyAIRApp.swf apktool d -r out.apk air_apk apktool d -r AndroidNative.apk native_apk mkdir native_apk/assets cp -r air_apk/assets/* native_apk/assets cp air_apk/smali/app/AIRApp/AppEntry*.smali native_apk/smali/app/AIRApp apktool b native_apk cd native_apk/dist jarsigner -verbose -keystore ~/.android/debug.keystore -storepass android out.apk androiddebugkey zipalign -v 4 out.apk out-new.apk cd ../../ cp native_apk/dist/out-new.apk MergedAIRApp.apk rm -r native_apk rm -r air_apk rm out.apk adb uninstall app.AIRApp adb install -r MergedAIRApp.apk

如果您熟悉 Linux 或 Unix,清单 6–24 中的命令很简单。如果您更喜欢在 Windows 环境中工作,您可以通过进行以下更改,将清单 6–24 中的命令转换为一组相应的 DOS 命令:

  • 定义APP_HOME变量时使用“set”。
  • 使用 DOS 风格的变量(例如:%abc%而不是$abc)。
  • copy替换cp,用erase替换rm
  • 用反斜杠(" \ ")替换正斜杠("/")。

要记住一个重要的细节:您必须获得正确的自签名证书(在清单 6–24 中称为certificate.p12),以使前面的合并过程正确工作。您可以为基于 Flex 的应用生成证书,如下所示:

  • 在 FlashBuilder 中选择您的项目。
  • 单击导出发布版本。
  • 指定“导出到文件夹:”的位置(或单击“下一步”)。
  • 点击“创建:”按钮。
  • 为必填字段提供值。
  • 在“另存为:”输入栏中指定一个值。
  • 单击确定按钮。
  • 单击“记住此会话的密码”(可选)。
  • 单击“完成”按钮。

生成自签名证书后,将该证书复制到您执行清单 6–24 中显示的 shell 脚本命令的目录中,如果您做的一切都正确,您将生成一个可以部署到基于 Android 的移动设备的合并应用。

本节总结了创建 Adobe AIR 应用并将其与原生 Android 应用合并的多步过程。正如您所看到的,这个集成过程不是微不足道的,也可以说是非直观的,因此在这个过程中您一定会遇到困难(当您遇到困难时不要气馁)。需要记住的是,为 Adobe AIR 应用添加原生 Android 支持可能会使您的应用有别于市场上的类似应用。

总结

在本章中,您学习了如何启动本机浏览器、访问数据库以及将 AIR mobile 应用与本机 Android 代码相结合。更具体地说,您了解了以下内容:

  • 从包含网站实际 HTML 代码的字符串启动本机 web 浏览器
  • 允许用户指定 URL,然后在 AIR 应用中启动该 URL
  • 使用 SQLite 数据库创建、更新和删除特定于用户的数据,以及如何自动刷新更新数据的显示
  • 在 Eclipse 中创建原生 Android 应用
  • 将外部 API、SQLite 数据库和原生 Android 通知的功能集成到一个移动应用中

将 Adobe AIR 应用与原生 Android 应用合并的特定步骤序列。


1**

七、利用硬件输入

在前一章中,您已经了解了如何将您的 Android Flash 应用与 Android 操作系统提供的本地软件服务相集成。在本章中,您将学习如何利用 Android 驱动设备中包含的硬件传感器。本章结束时,你将能够捕捉声音、图像和视频;接入地理定位服务以读取设备的位置;读取加速度计数据以确定设备的方向,所有这些都在您的 Flash 应用中完成。

现代移动设备有一系列令人惊叹的硬件传感器,从加速度计到摄像头再到 GPS 接收器。有效的移动应用应该能够在需要时利用这些特性。AIR 运行时提供了允许您访问这些本机硬件资源的类。这些类中的一些,比如MicrophoneCamera,对于有经验的 Flash 开发者来说可能很熟悉。其他的,如CameraUICameraRoll,是新增加的,允许 AIR 应用利用 Android 设备上常见的功能。

麦克风

如果没有麦克风,电话就没什么用了,所以我们从最基本的输入开始。Flash 支持用Microphone类捕获声音已经很久了,这个类在 Android 的 AIR 上也完全支持。正如你将在本章看到的所有硬件支持类一样,第一步是检查类的静态isSupported属性,以确保它在用户的设备上受支持。当然,所有手机都有麦克风,但平板电脑和电视不一定如此。因为你想要支持多种当前和未来的设备,所以最好总是检查MicrophoneisSupported属性和我们将在本章中讨论的其他类。

如果支持Microphone,那么您可以继续检索一个Microphone实例,设置您的捕获参数,并附加一个事件监听器,使您能够接收来自设备麦克风的声音数据。

清单 7–1 展示了本书示例代码的examples/chapter-07目录中 MicrophoneBasic 示例项目的摘录中的这些步骤。

清单 7–1。 初始化并从麦克风读取样本

`private var activityLevel: uint;

private function onCreationComplete():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, I can hear you...");
  } else {
    showMessage("flash.media.Microphone is unsupported on this device.");
  }
}

private function onSample(event:SampleDataEvent):void {
  if (microphone.activityLevel > activityLevel) {
    activityLevel = Math.min(50, microphone.activityLevel);
  }
}

private function showMessage(msg:String):void {
  messageLabel.text = msg;
}`

麦克风初始化代码位于ViewcreationComplete处理程序中。如果Microphone不被支持,onCreationComplete()函数调用showMessage()函数向用户显示一条消息。showMessage()函数简单地设置位于视图顶部的火花Label的文本属性。然而,如果支持Microphone,那么调用静态函数Microphone.getMicrophone(),它返回一个麦克风对象的实例。然后设置对象的增益和速率属性。设置 100 是麦克风的最大增益设置,速率 22 指定最大采样频率为 22 kHz。这将确保即使是轻柔的声音也能以合理的采样率被捕捉到。你应该注意到Microphone支持高达 44.1 kHz 的采集速率,这与光盘上使用的采样速率相同。然而,记录的质量受限于底层硬件所能支持的。手机麦克风可能会以低得多的速率捕捉音频。虽然 Flash 会将捕获的音频转换为您要求的采样率,但这并不意味着您最终会获得 CD 品质的音频。

最后,我们为SampleDataEvent.SAMPLE_DATA事件添加一个监听器。一旦连接了这个监听器,应用将开始接收声音数据。该事件有两个特别有趣的特性:

  • position:表示数据在音频流中的位置的Number
  • data:包含自上次SAMPLE_DATA事件以来捕获的音频数据的ByteArray

应用通常会将data字节复制到应用创建的ByteArray中,以保存整个音频剪辑,直到可以播放、存储或发送到服务器。关于采集和回放音频数据的更多细节,请参见第八章。MicrophoneBasic 示例应用通过检查activityLevel属性简单地显示来自麦克风的音频数据的视觉反馈,如清单 7–1 所示。

需要记住的一件重要事情是在应用描述符 XML 文件中设置android.permission.RECORD_AUDIO设置。没有此权限,您将无法在 Android 设备上读取麦克风数据。示例项目的清单部分如下面的代码片段所示。

<android>     <manifestAdditions>         <![CDATA[         <manifest>             <!-- For debugging only -->             <uses-permission android:name="android.permission.INTERNET"/>             <uses-permission android:name="android.permission.RECORD_AUDIO"/>         </manifest>         ]]>     </manifestAdditions> </android>

Flash 对捕获音频样本的支持实际上相当复杂。您甚至可以使用setSilenceLevel()设置“零”电平,或者使用setUseEchoSuppression()启用回声抑制。我们鼓励您查看 Adobe 优秀的在线文档 1


1 [help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/media/Microphone.html](http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/three/flash/media/Microphone.html)

Figure 7–1 展示了 MicrophoneBasic 应用在实际手机上运行时的样子。

images

图 7–1。Android 手机上运行的 MicrophoneBasic 示例应用

照相机和摄像机

你会发现大多数移动设备上都有一个摄像头(有时是两个)。Android Flash 应用可以使用相机捕捉静态图像和动态视频。一些设备甚至能够捕捉高清视频。

有两种不同的方式来访问设备的摄像头。flash.media.Camera类将让你访问来自摄像机的原始视频流。这允许您在从设备的主摄像头捕捉图像时对图像进行实时处理。

注意:从 AIR 2.5.1 开始,flash.media.Camera类不支持在 Android 设备上从多个摄像头进行捕捉的功能。在未来发布的 AIR for Android 中,有望实现在视频拍摄过程中选择相机的功能。

替代方法是使用flash.media.CameraUI来捕捉高质量的图像和视频。CameraUI非常适合只需轻松捕捉图像或视频的应用。它使用原生的 Android 摄像头接口来处理繁重的工作。这意味着您的应用的用户将能够在给定的设备上访问 Android 原生支持的所有功能,包括多个摄像头和调整白平衡、地理标记功能、对焦、曝光和闪光灯设置的能力。

Android 还提供了一个标准界面,用于浏览设备上拍摄的图像和视频。AIR 通过flash.media.CameraRoll类提供对该服务的访问。CameraRoll提供了一种将图像保存到设备的简单方法。它还允许用户浏览以前捕获的图像,如果用户选择了图像或视频文件,它会通知您的应用。和CameraUI一样,CameraRoll是原生 Android 媒体浏览器界面的包装器。用户喜欢感觉熟悉并且看起来像他们使用的其他本机应用的应用。因此,AIR 提供了对相机功能的本地接口的简单访问是一件好事。如果它们满足您的应用需求,应该是您的首选。

在接下来的部分中,我们将更深入地探索这三个类。我们将首先向您介绍基本的Camera类,然后展示一个将一些强大的闪光滤镜效果应用到实时视频流的例子。手机上的实时视频处理!这有多酷?之后,我们将带你参观一下CameraRollCameraUI类,并向你展示如何使用它们通过 Android 的本地界面来捕获、保存和浏览媒体。让乐趣开始吧!

照相机

构成 Flash 和 Flex SDKs 的 API 通常设计良好。视频捕捉功能也不例外。这个复杂过程的职责被划分到两个易于使用的类中。flash.media.Camera类负责底层视频捕捉,flash.media.Video类是一个DisplayObject,用于向用户显示视频流。因此,获取摄像头的视频信息是一个简单的三步过程。

  1. 调用Camera.getCamera()来获得对一个Camera实例的引用。
  2. 创建一个flash.media.Video对象,并将摄像机连接到它。
  3. Video对象添加到DisplayObjectContainer中,如UIComponent,使其在舞台上可见。

清单 7–2 中的代码演示了这些基本步骤。您可以在 Flash Builder 4.5 中创建新的 Flex mobile 项目,并将清单 7–2 中的代码复制到作为项目一部分创建的View类中。或者,如果您已经下载了本书的示例代码,也可以通过查看examples/chapter-07目录中的 CameraBasic 项目来继续学习。

View将其动作栏的可见性设置为false,以最大化视频显示的屏幕空间。所有的初始化工作都在creationComplete处理程序中完成。如前面步骤 3 所述,可以使用一个UIComponent作为视频流的容器,使其在舞台上可见。CameraVideoUIComponent都被设置为与视图本身相同的大小。

清单 7–2。 移动中的基本图像捕捉View

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        actionBarVisible="false"
        creationComplete="onCreationComplete()">

fx:Script
    <![CDATA[
      private var camera:Camera;

private function onCreationComplete():void {
        if (Camera.isSupported) {
          var screenWidth:Number = Screen.mainScreen.bounds.width;
          var screenHeight:Number = Screen.mainScreen.bounds.height;

camera = Camera.getCamera();
          camera.setMode(screenWidth, screenHeight, 15);

var video: Video = new Video(screenWidth, screenHeight);
          video.attachCamera(camera);

videoContainer.addChild(video);
        } else {
          notSupportedLabel.visible = true;
        }
      }
    ]]>
  </fx:Script>

<mx:UIComponent id="videoContainer" width="100%" height="100%"/>
  <s:Label id="messageLabel" visible="false" top="0" right="0"
           text="flash.media.Camera is not supported on this device."/>
</s:View>`

检查摄像头支持

该视图还在前景中包含一个标签组件,如果出于某种原因不支持该相机,该组件会向用户显示一条消息。使用文本组件(如标签)是在移动设备的小屏幕上向用户显示状态和错误消息的一种简单方法。这里你看到了静态属性isSupported的第二次出现,这次是在Camera类上。检查CameraisSupported属性以确保该特性在用户的设备上受支持是一个很好的做法。例如,移动浏览器目前不支持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.widththis.height。这与设备屏幕的分辨率相同,因为应用是在全屏模式下运行的,我们将在下一节中介绍。我们还要求以每秒 15 帧的速度捕捉视频。对于视频来说,这是一个合理的速率,同时不会对性能和电池寿命造成太大的消耗。您可能希望在较慢的设备上降低 FPS 请求。

在屏幕分辨率为 800 × 480 的 Nexus S 手机上,该请求导致相机被设置为以 720 × 432 捕捉帧。在分辨率为 854 × 480 的摩托罗拉 Droid 上,摄像头以 848 × 477 拍摄。在这两种情况下,相机都选择尽可能接近所要求的分辨率的模式,同时保持所要求的宽高比

有关配置和使用的更多详细信息,请参考 Adobe 网站[help.adobe.com/en_US/FlashPlatform/reference/actionscript/3](http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3)/上的flash.media.Cameraflash.media.Video的文档。

应用设置和安卓权限

来自 Flash 的Camera类的视频流采用横向方向。将应用锁定在横向模式下可以获得最佳效果。否则所有的视频看起来都像被旋转了 90 度。控制这种行为的选项可以在与您的项目相关的应用描述符 XML 文件的initialWindow部分找到。在 CameraBasic 项目中,该文件名为CameraBasic-app.xml,位于项目的src文件夹中。您需要将aspectRatio设置为landscape,将autoOrients设置为false。请注意,在 Flash Builder 4.5 中创建移动项目时,取消选中“自动重定向”复选框会在创建应用描述符文件时将autoOrients设置为false

清单 7–3 展示了 CameraBasic 项目的最终应用描述符。为清晰起见,已从生成的文件中删除了注释和未使用的设置。如前所述,在创建项目时,应用也被指定为全屏应用。这导致fullScreeninitialWindow设置被设置为true,并导致应用在运行时占据整个屏幕,隐藏屏幕顶部的 Android 指示条。

清单 7–3。??CameraBasic-app.xml来自 camera basic 项目的应用描述符文件

`

    CameraBasic
    CameraBasic
    CameraBasic
    0.0.1


        [This value will be overwritten by Flash Builder…]
        false
        landscape
        true
        false
    


                         ****                      ]]>
    

`

您需要在 APK 文件的清单中指定 Android 摄像头权限,才能访问设备的摄像头。正如您在清单 7–3 中看到的,应用描述符的 Android manifest 部分包含了android.permission.CAMERA权限。指定这个权限意味着使用了android.hardware.cameraandroid.hardware.camera.autofocus特性。因此,它们没有被明确地列在清单附件中。

操纵摄像机的视频流

使用Camera而不是CameraUI的优势在于,您可以在视频流被捕获时访问它。您可以对视频流应用几种类型的图像滤镜效果:模糊、发光、渐变、颜色变换、置换贴图和卷积。其中一些相对便宜,而另一些,如ConvolutionFilter,可能是处理器密集型的,因此会降低捕获的视频流的帧速率。简单的模糊、发光和斜面滤镜使用起来非常简单,所以这个例子将使用一些更复杂的滤镜:ColorMatrixFilterDisplacementMapFilterConvolutionFilter

清单 7–4 显示了 CameraFilter 示例项目的默认视图的代码。如果您已经下载了该书附带的源代码,那么可以在examples/chapter-07目录中找到它。

清单 7–4。??VideoFilterView.mxml文件来自 CameraFilter 示例项目

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        actionBarVisible="false" creationComplete="onCreationComplete()">

<fx:Script source="VideoFilterViewScript.as"/>

fx:Declarations
    <s:NumberFormatter id="fpsFormatter" fractionalDigits="1"/>
  </fx:Declarations>

<s:viewMenuItems>
    <s:ViewMenuItem label="No Filter" click="onFilterChange(event)"/>
    <s:ViewMenuItem label="Night Vision" click="onFilterChange(event)"/>
    <s:ViewMenuItem label="Pencil" click="onFilterChange(event)"/>
    <s:ViewMenuItem label="Ripples" click="onFilterChange(event)"/>
    <s:ViewMenuItem label="Funny Face" click="onFilterChange(event)"/>
  </s:viewMenuItems>

<mx:UIComponent id="videoContainer" width="100%" height="100%"/>
  <s:Label id="messageLabel" top="0" right="0"/>
</s:View>`

你可以在清单 7–4 中看到,我们已经将 ActionScript 代码分离到它自己的文件中,因为与这个例子相关的代码比前一个例子大得多。我们已经包含了使用<fx:Script>标签的source属性的脚本文件。像这样编写跨越两个文件的代码有点不方便,但是这样做可以使两个文件的大小更易于管理。您还会注意到添加了一个<fx:Declarations>元素,该元素声明了一个用于格式化每秒帧数值的NumberFormatter

您可以想象,如果有多个过滤器可以应用于视频流,那么就需要有一种方法让用户选择哪个过滤器应该是活动的。清单 7–4 中的所示的ViewMenuItem为用户提供了一个简单的方法来完成这个任务。点击一个ViewMenuItem会导致一个对onFilterChange处理器的调用,它将处理设置新选择的滤镜效果。产生的应用如图 7–2 所示,菜单可见。

images

图 7–2。 允许用户选择应用哪种滤镜效果的菜单

现在菜单已经工作了,是时候看看如何创建图像过滤效果并将其附加到视频流中了。

提示:当用户按下“home”按钮时,Android 应用的 AIR 不会被通知,因为这是 Android 自己使用的。然而,你可以通过检查你的KeyboardEvent监听器中的Keyboard.BACKKeyboard.SEARCH来监听 Android 的“返回”和“搜索”按钮。在这两种情况下,调用event.preventDefault()可能是一个好主意,以防止系统响应这些按钮按下而采取任何潜在的默认动作。

创建图像滤镜效果

正如所料,Flash 提供了易于使用的复杂图像处理效果。将滤镜效果应用到视频流不是本章的重点,所以我们将只简要描述滤镜并给出相关代码,不做任何注释。关于flash.filters包中包含的滤镜的详细信息,可以参考 Adobe 优秀的在线文档。如果您已经熟悉 Flash 中的滤镜效果,可以浏览代码并快速进入下一部分。

第一步是创建过滤器。清单 7–5 显示了初始化代码。和前面的例子一样,onCreationComplete()方法是视图的creationComplete处理程序。onCreationComplete()做的第一件事是调用initFilters()方法,它封装了所有的过滤器初始化代码。这个例子使用的三个滤镜效果是ColorMatrixFilterConvolutionFilterDisplacementMapFilter

清单 7–5。 创建图像过滤器实例

`private function onCreationComplete():void {
  var screenWidth:Number = Screen.mainScreen.bounds.width;
  var screenHeight:Number = Screen.mainScreen.bounds.height;

initFilters(screenWidth, screenHeight);

if (Camera.isSupported) {
    // The same Camera and Video initialization as before…
  }  else {
    showNotSupportedMsg();
  }
}

private function initFilters(screenWidth:Number, screenHeight:Number):void {
  var colorMat: Array = [
    .5,  0,  0,  0,  0,
     0, 10,  0,  0,  0,
     0,  0, .5,  0,  0,
     0,  0,  0,  1,  0
  ];

nightVisionFilter = new ColorMatrixFilter(colorMat);

var sharpMat: Array = [
    0, -5,  0,
    -5, 20, -5,
    0, -5,  0
  ];

ultraSharpFilter = new ConvolutionFilter(3, 3, sharpMat);

var bmpData: BitmapData = new BitmapData(screenWidth, screenHeight, false);
  var pt: Point = new Point(0, 0);

displacementFilter = new DisplacementMapFilter(bmpData, pt,
    BitmapDataChannel.RED, BitmapDataChannel.RED, 40, 40);
}`

A ColorMatrixFilter使用 4 × 5 矩阵,其值乘以每个像素的颜色通道。例如,矩阵第一行中的条目乘以未过滤像素的红色、绿色、蓝色和 alpha 分量,将结果与该行中的第五个值相加,并指定为最终过滤像素的红色分量。分别使用矩阵的第二、第三和第四行,类似地计算滤波像素的绿色、蓝色和阿尔法分量。对源图像中的每个像素都这样做,以产生最终的滤波图像。ColorMatrixFilter能够实现许多复杂的颜色处理效果,包括饱和度变化、色调旋转、亮度到 alpha 变换(源图像中的像素越亮,过滤后的图像越透明)等。正如你所看到的,这个示例程序使用ColorMatrixFilter 通过增强绿色通道和减弱红色和蓝色通道来产生一个伪夜视效果。阿尔法通道保持不变。

ConvolutionFilter是图像处理过滤器的主力。它的工作原理是定义一个矩阵,其元素乘以一个像素块。然后将该乘法的结果相加,得到像素的最终值。在本例中,我们使用的是一个 3×3 矩阵,从它的值可以看出,源图像中每个像素的红色、绿色、蓝色和 alpha 分量都乘以了 20 倍,同时,源像素的正北、正南、正东和正西的像素都乘以了-5 倍。然后将这些结果相加,得到滤波像素的最终值。矩阵角上的零意味着源像素西北、东北、西南和东南的像素完全从等式中移除。由于负面因素抵消了正面因素,图像的整体亮度保持不变。我们在这里定义的矩阵实现了一个基本的边缘检测算法。并且倍增因子足够大,使得最终的图像将主要是黑色的,边缘是白色的。过滤后的图像看起来有点像是用白色铅笔在黑色页面上绘制的,因此我们将过滤器命名为:铅笔。

DisplacementFilter使用一个位图中的像素值来偏移源图像中的像素。因此,与原始图像相比,所得到的图像将以某种方式扭曲。这会产生各种有趣的效果。initFilters()方法中的代码简单地用一个空位图初始化置换过滤器。置换贴图的选择实际上是在用户选择滑稽脸滤镜或波纹滤镜时设置的。本例使用的位移图如图图 7–3 所示。为了你自己的理智,不要盯着这些图像太久!

置换过滤器被配置为在计算 x 和 y 置换值时使用置换贴图的红色通道。因此图 7–3 左侧的滑稽脸贴图只是一个位于灰色背景中心的红点。这将保持周围的像素不变,但会扩大图像中心的像素,以产生球根状效果。涟漪贴图只是红色和黑色的交替圆圈,导致正负像素偏移(扩展和收缩),给人的印象是源图像是通过荡漾的水观看的,尽管这种涟漪在时间和空间上似乎是冻结的(嗯,听起来像一部糟糕的星际迷航集)。

images

图 7–3。 示例程序中使用的位移贴图

清单 7–6 显示了在位移过滤器上设置这些位移图的代码,以响应相应的菜单按钮点击。当选择波纹或滑稽脸效果时,适当的位图(在启动时从嵌入的资源中加载)被绘制到置换过滤器的mapBitmap中。

清单 7–6。 选择滤镜并设置置换贴图

`[Embed(source="funny_face.png")]
private var FunnyFaceImage:Class;

[Embed(source="ripples.png")]
private var RippleImage:Class;

private var rippleBmp:Bitmap = new RippleImage() as Bitmap;
private var funnyFaceBmp:Bitmap = new FunnyFaceImage() as Bitmap;

// This function is the click handler for all buttons in the menu.  videoContainer
// is a UIComponent that is displaying the video stream from the camera.
private function onFilterChange(event:Event):void {
  var btn: Button = event.target as Button;
  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();
}

private function showDisplacementFilter(ripples:Boolean):void {
  var bmp: Bitmap = ripples ? rippleBmp : funnyFaceBmp;

var mat: Matrix = new Matrix();
  mat.scale(width / bmp.width, height / bmp.height);

displacementFilter.mapBitmap.draw(bmp, mat);

videoContainer.filters = [displacementFilter];
}`

图 7–4 显示了使用这些滤镜拍摄的一些图像。从左上角逆时针方向,你可以看到一个没有滤镜的图像,滑稽脸滤镜,铅笔滤镜,夜视滤镜。

images

图 7–4。Nexus S 手机拍摄的本例中使用的一些滤镜的输出

应该注意的是,虽然置换贴图和颜色矩阵滤镜在性能方面相对便宜,但卷积滤镜在我们测试的 Android 设备上是一个真正的性能杀手。这是一个重要的提醒,处理器周期不像桌面系统那样充足。始终在目标硬件上测试您的性能假设!

显示一个 FPS 计数器

监控目标硬件性能的一个简单方法是显示每秒帧数。清单 7–7 显示了在Label控件中显示摄像机 FPS 计数的代码。FPS 值由在后台运行的定时器每两秒钟格式化和更新一次。FPS 计数直接来自相机,但会考虑滤镜所用的时间。当您应用过滤器,你会看到帧速率下降,因为这将减慢整个程序,包括视频捕捉过程。通过点击屏幕上的任意位置,可以隐藏和重新显示性能计数器。这是通过向显示视频流的 UIComponent 添加一个 click 处理程序来实现的。

清单 7–7。 屏幕上显示一个性能计数器

`private var timer: Timer;
private var fpsString: String;

private function onCreationComplete(): void {
  var screenWidth:Number = Screen.mainScreen.bounds.width;
  var screenHeight:Number = Screen.mainScreen.bounds.height;

initFilters(screenWidth, screenHeight);

if (Camera.isSupported) {
    // The same Camera and Video initialization as before…

videoContainer.addEventListener(MouseEvent.CLICK, onTouch);
    fpsString = " FPS ("+camera.width+"x"+camera.height+")";

timer = new Timer(2000);
    timer.addEventListener(TimerEvent.TIMER, updateFPS);
    timer.start();
  }  else {
    showNotSupportedMsg();
  }
}

private function updateFPS(event:TimerEvent):void {
  messageLabel.text = fpsFormatter.format(camera.currentFPS) + fpsString;
}

private function onTouch(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。 查看支持图像拍摄和保存的增强功能

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        actionBarVisible="false"
        creationComplete="onCreationComplete()">

<fx:Script source="FunHouseVideoViewScript.as"/>

fx:Declarations
    
  </fx:Declarations>

<s:viewMenuItems>
    
  </s:viewMenuItems>

<mx:UIComponent id="videoContainer" width="100%" height="100%"/>
  <mx:UIComponent id="bitmapContainer" width="100%" height="100%"/>

<s:Button id="captureButton" width="100%" bottom="0" label="Capture Image"
**            alpha="0.75" click="onCaptureImage()"/>**

<s:Button id="saveButton" width="40%" right="0" bottom="0"
**            label="Save Image" alpha="0.75" click="onSaveImage()"/>**
  <s:Button id="discardButton" width="40%" left="0" bottom="0"
**            label="Discard Image" alpha="0.75" click="onDiscardImage()"/>**

<s:Label id="messageLabel" top="0" right="0"/>
</s:View>`

当应用处于捕获模式时,“捕获图像”按钮将始终可见。该按钮是半透明的,因此用户可以看到按钮后面的视频流。一旦用户点击了捕获按钮,应用将抓取并显示图像,隐藏捕获按钮,并显示保存图像和丢弃图像按钮。这个逻辑由添加到 ActionScript 文件中的代码以及三个新的点击处理程序控制:onCaptureImage()onSaveImage()onDiscardImage()。这些增加的内容显示在清单 7–9 中。

清单 7–9。 添加和更改 ActionScript 代码以支持图像捕捉和保存

`private function onCreationComplete():void {
  var screenWidth:Number = Screen.mainScreen.bounds.width;
  var screenHeight:Number = Screen.mainScreen.bounds.height;

initFilters(screenWidth, screenHeight);
  setCaptureMode(true);

// The rest of the method is the same as before…
}

// Determines which controls are visible
private function setCaptureMode(capture: Boolean): void {
  videoContainer.visible = capture;
  bitmapContainer.visible = !capture;

captureButton.visible = capture;
  saveButton.visible = !capture;
  discardButton.visible = !capture;        
}

private function onCaptureImage():void {
  var bmp: BitmapData = new BitmapData(width, height, false, 0xffffff);
  bmp.draw(videoContainer);

bitmapContainer.addChild(new Bitmap(bmp));
  setCaptureMode(false);
}

private function onDiscardImage():void {
  bitmapContainer.removeChildAt(0);
  setCaptureMode(true);
}

private function onSaveImage():void {
  if (CameraRoll.supportsAddBitmapData) {
    var bmp: Bitmap = bitmapContainer.removeChildAt(0) as Bitmap;
    new CameraRoll().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当图像已成功保存或出现阻止保存操作的错误时,将发出事件。下一节涉及的照片收集示例将展示使用这些事件的示例。Figure 7–5 显示了完整的 CameraFunHouse 应用捕捉一只合作犬的图像。

images

图 7–5。 我们的模特欣然同意使用她的肖像作为交换。

CameraRoll类还允许用户浏览和选择保存在设备上的图像以及用户保存在互联网相册中的图像!这个特性将在下一节解释。

服务员

CameraRoll在 Android 设备上浏览照片几乎和保存照片一样简单。我们将在一个名为 PhotoCollage 的新示例程序的上下文中说明这个特性。这个程序让你选择已经存储在设备上的图像,并把它们排列成拼贴画。您可以使用多点触控手势来拖动、缩放和旋转图像。当图像按照您的喜好排列后,您可以将新图像存储回“相机胶卷”。这个例子可以在本书的源代码的examples/chapter-07 目录中找到。清单 7–10 显示了应用主视图的 MXML 文件。

清单 7–10。 照片收藏应用的首页视图— PhotoCollageHome.mxml

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        actionBarVisible="false"
        creationComplete="onCreationComplete()" >

<fx:Script source="PhotoCollageHomeScript.as"/>

<s:viewMenuItems>
    <s:ViewMenuItem label="Browse" click="onBrowse()"/>
    <s:ViewMenuItem label="Clear" click="onClear()"/>
    <s:ViewMenuItem label="Save" click="onSave()"/>
  </s:viewMenuItems>

<mx:UIComponent id="photoContainer" width="100%" height="100%"/>
  <s:Label id="messageLabel" top="0" left="0" mouseEnabled="false"/>
</s:View>`

您可以看到,就像前面的例子一样,已经为这个视图声明了一个菜单。我们再次将视图的相关脚本代码分离到它自己的文件中。该视图有两个子组件:一个UIComponent作为正在排列的图像的容器,另一个Label提供显示来自应用的消息的方式。注意标签的mouseEnabled属性被设置为false。这将防止标签干扰用户的触摸手势。关于如何使用messageLabel对运行中的程序提供反馈的更多细节,请参见标题为“除错”的部分。

将 KeyboardEvent 侦听器添加到舞台的代码与前面示例中的代码相同,因此我们在此不再重复。

图片浏览

正如您在清单 7–10 中看到的,当用户点击浏览按钮时会调用onBrowse处理程序。这引发了一系列动作,允许用户使用原生 Android 图像浏览器浏览图像,并且如果图像被选中,则图像最终出现在应用的视图中。清单 7–11 显示了相关的源代码。

清单 7–11。 启动浏览动作并显示选中图像的代码

`private static const BROWSE_UNSUPPORTED: String = "Browsing with " +
    "flash.media.CameraRoll is unsupported on this device.";

private var cameraRoll: CameraRoll = new CameraRoll();

private function onCreationComplete():void {
  cameraRoll.addEventListener(MediaEvent.SELECT, onSelect);
  cameraRoll.addEventListener(Event.CANCEL, onSelectCanceled);
  cameraRoll.addEventListener(ErrorEvent.ERROR, onCameraRollError);   cameraRoll.addEventListener(Event.COMPLETE, onSaveComplete);

// …  
}

private function onBrowse():void {
  if (CameraRoll.supportsBrowseForImage) {
    cameraRoll.browseForImage();
  } else {
    showMessage(BROWSE_UNSUPPORTED);
  }
  toggleMenu();
}

private function onSelect(event:MediaEvent):void {
  var loader: Loader = new Loader();
  loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaded);
  loader.load(new URLRequest(event.data.file.url));
}

private function onLoaded(event:Event):void {
  var info: LoaderInfo = event.target as LoaderInfo;
  var bmp: Bitmap = info.content as Bitmap;

scaleContainer(bmp.width, bmp.height);

var sprite: Sprite = new Sprite();

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);
}

private function onSelectCanceled(event:Event):void {
  showMessage("Select canceled");
}

private function onCameraRollError(event:ErrorEvent):void {
  showMessage("Error: "+event.text);
}

private function onSaveComplete(event:Event):void {
  showMessage("CameraRoll operation complete");
}`

onCreationComplete()函数为与该应用相关的所有CameraRoll事件附加处理程序。MediaEvent.SELECTEvent.CANCEL是浏览操作的两个可能的成功结果。如果在浏览或保存过程中出现错误,将发送ErrorEvent.ERROR。最后,当保存成功完成时,触发Event.COMPLETE事件。onSelectCanceled()onCameraRollError()onSaveComplete()处理函数简单地调用showMessage函数,在屏幕上显示一条消息。

现在应用正在监听所有必要的事件,它准备好处理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会更容易。在这两种情况下,您只需要将ImageBitmapImage的 source 属性设置为event.data.file.url,然后将其添加到您的 stage 中。其他一切都将自动处理。图 7–6 显示了运行在 Android 设备上的 PhotoCollage 应用。

images

图 7–6。Nexus S 上运行的 PhotoCollage 程序

一旁调试

Flash Builder 4.5 附带的设备上调试器是一个非常棒的工具。但是有时使用好的老式调试输出来了解程序的运行情况会更快。在 Flash 中添加这种输出的传统方法是使用trace()函数。只有当应用在调试模式下运行时,该函数才会打印消息。Flash Builder 的调试器连接到 Flash 播放器,并将在调试器的控制台窗口中显示trace消息。此方法在移动设备上调试时也有效。

有时候,您可能希望将输出直接写到屏幕上,作为调试过程的一部分,或者只是为了向用户提供额外的信息。幸运的是,这是非常容易建立在一个 Android 程序的空气。清单 7–12 展示了在设备屏幕上为您的应用提供可行的输出日志所需的少量代码。这个清单包括了之前显示的 MXML 文件中的声明messageLabel,只是作为一个提示。messageLabel是一个简单的火花Label,位于视图的左上角。它位于所有其他显示对象之上,所以它的鼠标交互必须被禁用,这样它才不会干扰用户输入。

清单 7–12。 为 Android 应用的 AIR 添加调试输出

`// From PhotoCollageHome.mxml
<s:Label id="messageLabel" top="0" left="0" mouseEnabled="false"/>

// From PhotoCollageHomeScript.as
private function onCreationComplete():void {
  // CameraRoll event listener initialization…

// Make sure the text messages stay within the confines
  // of our view's width.
  messageLabel.maxWidth = Screen.mainScreen.bounds.width;
  messageLabel.maxHeight = Screen.mainScreen.bounds.height;

// Multitouch initialization…  
}

private function showMessage(msg:String):void {
  if (messageLabel.text && messageLabel.height < height) {
    messageLabel.text += "\n" + msg;
  } else {
    messageLabel.text = msg;
  }
}`

onCreationComplete()函数中,messageLabelmaxWidthmaxHeight属性被设置为屏幕的宽度和高度。这将防止标签的文本被绘制到屏幕边界之外。最后一部分是一个小的showMessage函数,它将一个消息字符串作为参数。如果messageLabel.text属性当前为空,或者如果messageLabel变得太大,那么文本属性被设置为消息字符串,有效地清除标签。否则,新消息将与换行符一起追加到现有文本中。结果是一个消息缓冲区在屏幕上向下扩展,直到到达底部,此时现有的消息将被删除,新消息将从顶部重新开始。它不是一个全功能的应用日志,并且必须为应用中的每个View重新实现,但是作为一种在屏幕上显示调试消息的简单方式,它是无与伦比的。

您现在应该熟悉使用CameraRoll类在 Android 设备上浏览和保存图像。您可以在 PhotoCollage 示例项目的 PhotoCollageHomeScript.as文件中找到完整的源代码。ActionScript 文件包括之前没有显示的部分,例如多点触摸缩放和旋转手势以及触摸拖动的处理。这段代码应该能让您很好地理解如何处理这类用户输入。如果你需要更多关于这些主题的细节,你也可以参考第二章的“多点触摸和手势”部分。

本章将讨论的 Flash 相机支持的最后一个方面是通过CameraUI类使用原生 Android 媒体捕获接口。这将是下一节的主题。

喀麦隆

CameraUI类使您能够利用原生 Android 媒体捕获接口的能力来捕获高质量、高分辨率的图像和视频。这个类的使用包括现在熟悉的三个步骤:确保功能受支持,调用一个方法来调用本机接口,以及注册一个回调以在图像或视频被捕获时得到通知,以便您可以在应用中显示它。清单 7–13 显示了 CameraUIBasic 示例项目的捕获视图。这个简短的程序演示了刚刚列出的三个步骤。

清单 7–13。 基本图像捕捉使用CameraUI

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
              xmlns:s="library://ns.adobe.com/flex/spark"
              actionBarVisible="false" creationComplete="onCreationComplete()">

fx:Script
    <![CDATA[
      private var cameraUI:CameraUI;

private function onCreationComplete():void {
        if (CameraUI.isSupported) {
          cameraUI = new CameraUI();
          cameraUI.addEventListener(MediaEvent.COMPLETE, onCaptureComplete);
        }

captureButton.visible = CameraUI.isSupported;
        notSupportedLabel.visible = !CameraUI.isSupported;
      }

private function onCaptureImage():void {
        cameraUI.launch(MediaType.IMAGE);
      }

private function onCaptureComplete(event:MediaEvent):void {
        image.source = event.data.file.url;
      }
    ]]>
  </fx:Script>

<s:Label id="notSupportedLabel" width="100%" height="100%"                  verticalAlign="middle" textAlign="center"
                 text="CameraUI is not supported on this device."/>

<s:Image id="image" width="100%" height="100%"/>
  <s:Button id="captureButton" width="100%" bottom="0" label="Capture Image"
                  alpha="0.75" click="onCaptureImage()"/>
</s:View>`

onCreationComplete()方法检查CameraUI支持,如果存在的话,创建一个新的cameraUI实例。然后向实例对象添加一个事件监听器,以便应用在捕获完成时通过其onCaptureComplete回调函数得到通知。当用户点击捕获图像按钮时,onCaptureImage回调函数调用CameraUI的 launch 方法来显示 Android 捕获界面。当捕获完成时,我们附加到CameraUI的回调函数被调用。它接收一个MediaEvent参数,与前面讨论的CameraRoll使用的事件类相同。和以前一样,事件在其data属性中包含一个MediaPromise实例。因此,您可以使用event.data.file.url加载捕获的图像,就像使用CameraRoll的浏览功能选择图像时所做的一样。

CameraUIBasic 示例程序显示了一个视图,其中有一个带有“Capture Image”标签的半透明按钮。当用户点击按钮时,Android 的原生相机界面通过CameraUI类启动。捕获图像后,图像数据将返回到原始视图进行显示,如清单 7–13 所示。图 7–7 是一系列图像,显示了原生 Android 摄像头界面捕捉机器人的图像并将其返回给应用进行显示。参加第一届技术挑战机器人竞赛的一组中学生制造了如图图 7–7 所示的机器人。

注意:CameraUI拍摄的任何照片也会自动保存到设备的照片库中,因此以后可以使用CameraRoll类检索。

以上就是我们对 Android 版 AIR 中摄像头功能的介绍。在本章的剩余部分,你将了解到flash.sensors包的内容:加速度计和地理定位。我们希望你会发现它又快又切题。

images

图 7–7。 用原生安卓相机界面和CameraUI 拍摄的机器人图像

加速度计

加速度传感器允许您通过测量重力沿 x、y 和 z 轴产生的加速度来检测设备的方向。静止时,地球上的任何物体都会由于重力而经历大约 9.8 米/秒/秒(米/秒/秒)的加速度。9.8 米/秒/秒也被称为 1 重力或 1 重力,或者简单地称为 1 克加速度。因此,10g 的加速度是重力的 10 倍,即 98 米/秒/秒。这是一个非常大的重力,通常只有战斗机飞行员在极端机动中才会感受到!

由于手机是一个三维物体,它的方向可以通过观察 1g 的力如何沿其三个轴分布来确定。加速度计还可以告诉你手机是否受到震动或以其他快速方式移动,因为在这种情况下,手机在三个轴上的加速度明显大于或小于 1g。您需要知道手机的轴是如何布置的,以便从加速度计值中收集有用的信息。图 7–8 显示了手机加速轴相对于手机机身的方向。

images

图 7–8。 安卓手机的加速度计轴

如果您对齐图 7–8 中标记的轴之一,使其与重力正对,您将在该轴上读取 1g 的加速度。如果一个轴垂直于重力,那么它的读数将是 0g。例如,如果你把手机放下,让面朝上,放在一个平面上,那么加速度计在 z 轴上的读数大约为+1g,在 x 轴和 y 轴上的读数大约为 0g。如果你将手机翻转过来,让它面朝下,加速度计将在 z 轴上显示-1g。

加速度计和加速度计事件类别

Adobe AIR 团队的优秀人员继续他们的趋势,通过提供一个简单的类来使我们的生活变得更容易,通过这个类,您可以与加速度传感器进行交互。毫不奇怪,它被命名为Accelerometer,并配备了通常的静态isSupported属性。该类只声明了一个新的实例方法setRequestedUpdateInterval()。此方法采用单个参数,该参数指定两次更新之间等待的毫秒数。零值意味着使用支持的最小更新间隔。加速度计将使用AccelerometerEvent将其更新发送到您的应用(当然!)的类型AccelerometerEvent.UPDATE。一个AccelerometerEvent实例包含四个属性,告诉您沿着三个轴中的每一个轴检测到的当前加速度,以及测量值的时间戳。这四个属性被命名为accelerationXaccelerationYaccelerationZtimestamp。三个加速度以重力为单位,时间戳以毫秒为单位,从传感器开始向应用发送事件时算起。时间戳可以让你检测到震动和其他运动,告诉你是否在短时间内经历了大的正负重力。

Accelerometer类还包含一个名为muted的属性。如果用户拒绝应用访问加速度传感器,则该属性为true。如果希望在muted属性的值改变时得到通知,应用可以注册一个回调来监听类型StatusEvent.STATUS的事件。清单 7–14 显示了 AccelerometerBasic 示例应用中与初始化和接收来自加速度计的更新相关的代码。

清单 7–14。 读取加速度传感器

`private var accelerometer: Accelerometer;

private function onCreationComplete():void {
  if (Accelerometer.isSupported) {
    showMessage("Accelerometer supported");

accelerometer = new Accelerometer();
    accelerometer.addEventListener(AccelerometerEvent.UPDATE, onUpdate);
    accelerometer.addEventListener(StatusEvent.STATUS, onStatus);
    accelerometer.setRequestedUpdateInterval(100);

if (accelerometer.muted) {
      showMessage("Accelerometer muted, access denied!");
    }
  } else {
    showMessage(UNSUPPORTED);
  }
} private function onStatus(event:StatusEvent):void {
  showMessage("Muted status has changed, is now: "+accelerometer.muted);
}

private function onUpdate(event:AccelerometerEvent):void {
  updateAccel(xAxis, event.accelerationX, 0);
  updateAccel(yAxis, event.accelerationY, 1);
  updateAccel(zAxis, event.accelerationZ, 2);

time.text = "Ellapsed Time: " + event.timestamp + "ms";
}

private function updateAccel(l: Label, val: Number, idx: int):void {
  var item: 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 +
      "\n  Current Value: " + formatter.format(val) + "g" +
      "\n  Minimum Value: " + item.min + "g" +
      "\n  Maximum Value: " + item.max + "g";
}`

在检查以确保加速度计在当前设备上受支持之后,onCreationComplete()方法创建了一个新的Accelerometer类实例,并为更新和状态事件附加了监听器。调用setRequestedUpdateInterval方法来请求每 100 毫秒更新一次。在为移动设备编程时,您应该始终注意电池消耗。始终设置仍能满足应用要求的最长更新间隔。一旦连接了更新监听器,程序将开始从传感器接收事件。这些事件中包含的数据以一系列标签的形式显示在屏幕上:每个轴一个,时间戳一个。该程序还跟踪并显示传感器报告的最小和最大值。图 7–9 显示了 Android 设备上 AccelerometerBasic 程序的输出。

images

图 7–9。 运行在安卓设备上的加速度计基础程序

该程序在其操作栏中包括一个按钮,允许用户清除到目前为止已经记录的最小值和最大值。这有助于在规划自己的应用时试验加速度计读数。最小值和最大值被初始化为正负 10g,因为手机不太可能经历比这更大的加速度,除非你碰巧是战斗机飞行员。最后要注意的是,为了使用加速度计,您的应用不需要在应用描述符中指定任何特殊的 Android 权限。

从本节的材料中可以看出,在 AIR 应用中读取加速度计很容易,并且允许您以新的和创造性的方式接受用户的输入。接下来,我们将看看如何读取移动应用中广泛使用的另一种数据形式:地理位置数据。

地理位置

近年来,移动设备中地理定位服务的流行导致位置感知应用的数量迅速增加。位置数据可以来自蜂窝塔三角测量,一个已知 Wi-Fi 接入点的数据库,当然,还有 GPS 卫星。基于 Wi-Fi 和手机信号塔的位置数据不如 GPS 数据准确,但与使用设备的 GPS 接收器相比,它可以更快地获得并消耗更少的电池电量。由于这种复杂性,获得准确的位置可能比您想象的更复杂。

AIR 为开发人员提供了一种轻松访问位置数据的方式,而不必担心获取准确读数所涉及的大部分细节。GeolocationGeolocationEvent是这个过程中涉及的两个主要类。如果您刚刚阅读完上一节关于加速度计的内容,现在应该对这些类的用法很熟悉了。像往常一样,首先检查Geolocation类的静态isSupported属性。如果支持可用,您可以选择使用setRequestedUpdateInterval方法以某个速率请求更新。该方法采用单个参数,该参数是以毫秒表示的更新之间的请求时间间隔。请务必记住,这只是一个请求,是对设备的一个提示,告诉您希望接收更新的频率。这不是一个保证。实际更新速率可能大于或小于请求的速率。事实上,虽然您可以请求不到一秒钟的时间间隔,但在我们测试的设备上,我们还没有看到一个 AIR 应用可以间隔一秒钟以上接收更新。

对于地理定位数据来说,电池使用是一个更大的问题,因为 GPS 接收机可能会消耗电池寿命。如果设备位于信号较弱的地方,这一点尤其明显。因此,您应该仔细考虑您的应用真正需要位置更新的频率。在位置感知应用中,一分钟(60,000 毫秒)或更长的更新间隔并不少见。并且由于方位和速度也与位置数据一起提供,所以可以基于先前的数据进行一定量的推断。这可以显著地平滑您提供给应用用户的位置更新。

您将从类型为GeolocationEvent.UPDATEGeolocationEvent对象接收位置数据,该数据将被传递给您将向Geolocation实例注册的事件处理程序。GeolocationEvent类包含几个感兴趣的属性:

  • latitude:设备的纬度,单位为度;范围将在-90°和+90°之间。
  • longitude:设备的经度,单位为度;范围将在-180°和+180°之间,其中负数表示本初子午线(也称为格林威治子午线或国际子午线)以西的位置,而正经度表示以东的位置。
  • horizontalAccuracy:估计位置数据在水平面上的精度,单位为米
  • verticalAccuracy:估计位置数据在垂直方向上的精确程度,单位为米
  • speed:使用相对于时间的最近位置读数之间的距离测量的速度;该值以米/秒为单位。
  • altitude:设备当前海拔高度,单位为米
  • timestamp:自应用开始接收位置更新以来,发送事件时的毫秒数

注意:虽然 AIR 2.5.1 支持GeolocationEvent中的一个heading属性,但是这个属性目前在 Android 设备上还不支持,会一直返回NaN

Geolocation类还包含一个名为muted的属性,如果用户禁用了地理定位(或者如果您忘记在应用描述符 XML 文件的 manifest 部分指定android.permission.ACCESS_FINE_LOCATION权限),该属性将被设置为true!).当muted属性的值改变时,Geolocation类将发送一个类型为StatusChange.STATUSStatusChange事件给你的监听器,如果你已经添加了一个的话。清单 7–15 显示了 GeolocationBasic 示例项目的源代码。这段代码演示了在应用中接收和显示地理位置数据的步骤。

清单 7–15。GeolocationBasicHome 视图的源代码

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        creationComplete="onCreationComplete()" title="Geolocation Data">

fx:Declarations
    <s:NumberFormatter id="f" fractionalDigits="4"/>
  </fx:Declarations>

fx:Script
    <![CDATA[
      import flash.sensors.Geolocation;

private static const UNSUPPORTED: String = "flash.sensors.Geolocation "+
        "is not supported on this device.";

private var loc: Geolocation;

private function onCreationComplete():void {
        if (Geolocation.isSupported) {
          showMessage("Geolocation supported");

loc = new Geolocation();           if (!loc.muted) {
            loc.addEventListener(GeolocationEvent.UPDATE, onUpdate);
            loc.addEventListener(StatusEvent.STATUS, onStatus);
            loc.setRequestedUpdateInterval(1000);
          } else {
            showMessage("Geolocation muted");
          }
        } else {
          showMessage(UNSUPPORTED);
        }
      }

private function onStatus(event:StatusEvent):void {
        showMessage("Geolocation status changed, muted is now " + loc.muted);
      }

private function onUpdate(event:GeolocationEvent):void {
        geoDataLabel.text = "Geolocation" +
          "\n  Latitude: " + f.format(event.latitude) + "\u00B0" +
          "\n  Longitude: " + f.format(event.longitude) + "\u00B0" +
          "\n  Horz Accuracy: " + f.format(event.horizontalAccuracy) + " m" +
          "\n  Vert Accuracy: " + f.format(event.verticalAccuracy) + " m" +
          "\n  Speed: " + f.format(event.speed) + " m/s" +
          "\n  Altitude: " + f.format(event.altitude) + " m" +
          "\n  Timestamp: " + f.format(event.timestamp) + " ms";
      }

private function showMessage(msg:String):void {
        if (messageLabel.text && messageLabel.height < height) {
          messageLabel.text += "\n" + msg;
        } else {
          messageLabel.text = msg;
        }
      }
    ]]>
  </fx:Script>

<s:Label id="geoDataLabel" width="100%"/>
  <s:Label id="messageLabel" top="0" left="0" mouseEnabled="false" alpha="0.5"/>
</s:View>`

正如我们顺便提到的,除非您在应用描述符的 manifest 部分指定了正确的 Android 权限,否则这段代码将无法运行。该应用的应用描述符如清单 7–16 所示。

清单 7–16。geolocation basic 的应用描述符,显示了精确定位许可的正确使用

`
<application xmlns="http://ns.adobe.com/air/application/2.5">
    GeolocationBasic
    GeolocationBasic
    GeolocationBasic
    0.0.1


        [This value will be overwritten by Flash Builder in the output app.xml]         false
        false
        false
    


        
                                                              ****                          ]]>
        

    

`

Android 使用ACCESS_COARSE_LOCATION将地理定位数据限制为仅使用 Wi-Fi 和手机信号塔获取位置信息。先前使用的ACCESS_FINE_LOCATION权限包括粗略定位权限,还增加了访问 GPS 接收器以获得更精确读数的能力。图 7–10 显示了运行中的 GeolocationBasic 程序的屏幕截图。

images

图 7–10。Android 设备上运行的 GeolocationBasic 程序

总结

本章介绍了各种传感器和硬件功能。从麦克风和摄像头到媒体存储、加速度计和地理定位数据,您现在已经掌握了将您的应用与 AIR 应用可用的各种硬件服务相集成所需的所有知识。在本章的学习过程中,您已经了解了以下内容:

  • 如何使用麦克风接收音频输入
  • 如何访问和查看实时视频流
  • 如何对这些视频流应用各种滤镜效果
  • 如何在移动设备上存储和浏览媒体
  • 如何在自己的应用中使用 Android 的原生图像和视频拍摄接口
  • 如何读取和解释来自加速度计的加速度数据
  • 如何从设备上的地理位置传感器检索位置数据,包括纬度、经度、速度和高度

在下一章中,您将通过探索 Flash 的媒体播放功能,继续探索 AIR 和 Android 探索之路。

八、富媒体集成

如果你的用户没有使用他们的 Android 设备打电话,那么他们很可能在玩游戏、听音乐或看视频。说到 it,对于现代消费者来说,音频和视频的消费可能比他们的移动设备的通信能力更重要。幸运的是,出色的音频和视频支持是 Flash 平台的真正优势之一。事实上,这是 Flash Player 在我们的计算机和移动设备上变得如此普遍的主要原因之一。

前一章向您展示了如何在 Android 设备上捕捉音频和视频。本章以这些概念为基础,将教您如何使用 Flash 平台的力量来释放 Android 移动设备的富媒体潜力。

播放音效

音效通常是响应各种应用事件(如弹出警告或按下按钮)而播放的简短声音。声音效果的音频数据应该在 MP3 文件中,可以嵌入到应用的 SWF 文件中,也可以从互联网上下载。您通过使用Embed元数据标签来标识素材,将 MP3 素材嵌入到您的应用中,如清单 8–1 所示。

清单 8–1。 嵌入带有Embed元数据标签的声音文件

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        title="SoundAssets">

fx:Script
    <![CDATA[
      import mx.core.SoundAsset;

[Embed(source="mySound.mp3")]
      private var MySound:Class;
      private var sound:SoundAsset = new MySound(); ]]>
    </fx:Script>

<s:Button label="Play SoundAsset" click="sound.play()"/>
</s:View>`

Embed元数据标签将使编译器对 MP3 文件进行代码转换,并将其嵌入到应用的 SWF 文件中。source属性指定 MP3 文件的路径和文件名。在这种情况下,我们将文件放在与源文件相同的包中。您可以通过创建与Embed标签相关联的类的实例来访问嵌入的声音,在清单 8–1 中,该实例是一个名为MySound的类。MySound类由编译器生成,将是mx.core.SoundAsset的子类。因此,它为音频素材的基本回放提供了所有必要的支持。在清单 8–1 中,我们通过创建一个名为sound的实例变量并调用其play方法来响应按钮点击,从而利用了这种支持。

音效类

虽然知道幕后发生的事情很好,但是通常不需要在 Flex 程序中创建和实例化SoundAsset。您选择的工具通常是SoundEffect类,因为它能够在回放样本时轻松创建有趣的效果。它在回放过程中提供了对循环、平移和音量效果的简单控制。因为它扩展了基本的mx.effect.Effect类,所以它可以在任何可以使用常规效果的地方使用。例如,你可以将一个SoundEffect实例设置为一个ButtonmouseDownEffect或者一个Alert对话框的creationCompleteEffect。清单 8–2 展示了如何做到这一点,以及如何手动弹奏一个SoundEffect

清单 8–2。 创建并播放一个循环SoundEffect

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        title="SoundEffects">

fx:Declarations
**    <mx:SoundEffect id="mySound" source="{MySound}" useDuration="false"**
**                    loops="2"/>**
  </fx:Declarations>

fx:Script
    <![CDATA[
      [Bindable]
      [Embed(source="mySound.mp3")]
      private var MySound:Class;

private function playEffect(event:MouseEvent):void {
        mySound.end();
        mySound.play([event.target]);
      }
    ]]>
</fx:Script> <s:VGroup horizontalCenter="0" horizontalAlign="contentJustify">
    <s:Button label="Play mouseDownEffect" mouseDownEffect="{mySound}"/>
    <s:Button label="End & Play SoundEffect" click="playEffect(event)"/>
  </s:VGroup>
</s:View>`

在清单 8–2 中突出显示的SoundEffect声明创建了一个每次播放时循环两次的声音效果。注意设置为falseuseDuration属性。一个SoundEffectduration默认设置为 500 毫秒,如果useDuration保持默认值true,那么只会播放你声音的前半秒。因此,您几乎总是希望将此属性设置为false,除非您也设置了duration属性,以便只播放部分音效。SoundEffectsource属性被赋予嵌入声音素材的类名。

然后我们创建两个按钮来说明玩SoundEffect的两种不同方式。第一个按钮只是将SoundEffect的实例id设置为它的mouseDownEffect。每次在按钮上按下鼠标按钮时,都会播放我们的音频样本。每次按下鼠标按钮,都会创建并播放一个新的效果。如果您点按的速度足够快,并且您的声音样本足够长,就有可能听到它们同时播放。

单击第二个按钮将调用playEffect方法,该方法做两件事。首先,它将通过调用end方法来停止当前正在播放的任何效果实例。这确保声音不会与自身的任何其他实例重叠。第二,使用按钮作为目标对象来播放新的声音效果。MouseEventtarget属性提供了一种便捷的方式来引用我们将用作效果目标的按钮。注意,play方法的参数实际上是一个目标数组。这就是为什么我们需要在event.target参数周围加一组方括号。

您可以看到,以这种方式嵌入的每个声音都需要三行代码:两个元数据标记和声明声音素材类名的代码行。有一种方法可以避免这种情况,直接将声音嵌入到音效中。

嵌入式音效示例

您可以在SoundEffect声明的source属性中使用@Embed指令。这种技术用于 SoundEffectBasic 示例应用,可以在本书示例代码的examples/chapter-08目录中找到。这个示例应用还演示了如何在播放时调整声音效果的音量和声相。清单 8–3 显示了应用的主View

清单 8–3。??【声效基础范例计划】之家View

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"         xmlns:s="library://ns.adobe.com/flex/spark"         xmlns:mx="library://ns.adobe.com/flex/mx"         title="Code Monkey To-Do List"> `fx:Declarations
    <mx:SoundEffect id="coffee" source="@Embed('coffee.mp3')"
                    useDuration="false" volumeFrom="1.0" volumeTo="0.0"/>
    <mx:SoundEffect id="job" source="@Embed('job.mp3')"
                    useDuration="false" panFrom="-1.0" panTo="1.0"/>
    <mx:SoundEffect id="meeting" source="@Embed('meeting.mp3')"
                    useDuration="false" volumeFrom="1.0" volumeTo="0.0"
                    volumeEasingFunction="Back.easeOut"/>
  </fx:Declarations>

fx:Script
    <![CDATA[
      import flash.net.navigateToURL;
      import mx.effects.easing.Back;

private static const CM_URL_STR:String = "http://www.jonathancoulton.com"+
          "/2006/04/14/thing-a-week-29-code-monkey/";

private static const CM_URL:URLRequest = new URLRequest(CM_URL_STR);

private function play(event:MouseEvent, effect:SoundEffect):void {
        effect.end();
        effect.play([event.target]);
      }
    ]]>
  </fx:Script>

<s:VGroup horizontalCenter="0" horizontalAlign="contentJustify" top="15" >
    <s:Button label="1. Get Coffee" click="play(event, coffee)"/>
    <s:Button label="2. Go to Job"  click="play(event, job)"/>
    <s:Button label="3. Have Meeting" mouseDownEffect="{meeting}"/>
  </s:VGroup>

<s:Button horizontalCenter="0" bottom="5" width="90%"
            label="About Code Monkey..." click="navigateToURL(CM_URL)"/>
</s:View>`

在清单 8–3 中首先要注意的是在每个SoundEffect声明的source属性中使用了@Embed语句。这允许您在一个步骤中嵌入声音素材并将其与SoundEffect相关联。就像以前一样,如果你的声音文件和你的源文件在不同的包中,那么你必须在@Embed语句中包含声音文件的路径,这样编译器才能找到它。

每个音效将播放乔纳森·科尔顿的歌曲《代码猴子》的一小段摘录。我们使用了SoundEffect类的volumeFromvolumeTo属性,在音频样本播放时将音量从 1.0(最大音量)渐变到 0.0(最小音量)。由于我们没有指定一个volumeEasingFunction,这将是一个线性渐变。同样,第二个声音效果将在样本播放时线性地将音频样本从-1.0(左扬声器)移动到 1.0(右扬声器)。如果你想为你的平移效果使用不同的缓动函数,你可以使用SoundEffect类的panEasingFunction属性来指定它。最后的SoundEffect声明展示了如何使用 Flex 的一个内置 easers 来改变样本播放时的音量。通过使用Back easer 的fadeOut方法,我们将使音量下降到 0.0 的目标值,稍微超过它,并在最终固定在最终值之前再次反弹超过 0.0。这会在音频样本的结尾产生一个有趣的小音量波动。

这个例子再次演示了播放音效的两种不同方法。在屏幕的底部还有第四个按钮,当点击它时,会启动 Android 的原生网络浏览器,并通过使用第六章中的方法将你带到“代码猴子”网页。结果应用如图 8–1 中的所示。

images

图 8–1。 运行在 Android 设备上的代码猴子音效示例

SoundEffect类非常适合播放小的声音效果来响应应用事件。如果您需要对应用中的声音进行更高级的控制,那么是时候深入挖掘 Flash 平台必须提供的功能了。

复杂的声音解决方案

对于大多数应用来说,SoundEffect类是一个方便的抽象,这些应用的需求不会超出偶尔提示或通知用户的能力。在一些应用中,声音是主要成分之一。如果你想录制语音备忘录或播放音乐,那么你需要更深入地了解 Flash 声音 API。我们将首先看一看Sound类和它的伙伴:SoundChannelSoundTransform。所有这三个类都可以在flash.media包中找到。

Sound类充当音频文件的数据容器。它的主要职责是提供将数据加载到其缓冲区的机制,并开始回放该数据。加载到Sound类中的音频数据通常来自 MP3 文件或应用本身动态生成的数据。不出所料,这个类中需要注意的关键方法是loadplay方法。您使用load方法来提供应该加载到Sound中的 MP3 文件的 URL。数据一旦加载到Sound中,就不能更改。如果您稍后想要加载另一个 MP3 文件,您必须创建一个新的Sound对象。向Sound对象的构造函数传递一个 URL 相当于调用load方法。Sound类在加载音频数据的过程中调度几个事件,如 Table 8–1 所示。

images

加载完数据后,调用Sound类的play方法将导致声音开始播放。play方法返回一个SoundChannel对象,该对象可用于跟踪声音播放的进度并提前停止播放。SoundChannel还有一个与之关联的SoundTransform对象,可以用来改变声音播放时的音量和声相。有三个可选参数可以传递给play方法。首先是startTime参数,它将导致声音在样本中指定的毫秒数开始播放。如果您希望声音播放一定的次数,也可以传递循环计数。最后,如果您想在声音开始播放时设置声音的初始转换,也可以提供一个SoundTransform对象作为play方法的参数。您传递的变换将被设置为SoundChannelSoundTransform

每次调用Sound.play方法时,都会创建并返回一个新的SoundChannel对象。SoundChannel在声音播放时充当你与声音互动的主要点。它允许你跟踪当前的位置和音量。它包含一个stop方法,该方法中断和终止声音的回放。当一个声音到达其数据的末尾时,SoundChannel类将通过分派类型为flash.events.Event.SOUND_COMPLETEsoundComplete事件来通知您。最后,您还可以使用它的soundTransform属性来操纵声音的音量,并将声音移动到左右扬声器。图 8–2 说明了这三个协作类之间的关系。

images

图 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类用来通知我们样本可用的事件类型相同。我们之前问题的答案很简单:您只需为SoundsampleData事件附加一个处理程序,并开始将字节复制到事件的data属性中。

因此,我们增强的应用将为sampleData事件提供两个独立的处理程序。当麦克风处于活动状态时,第一个会将数据复制到一个ByteArray,当我们回放时,第二个会将数据从同一个ByteArray复制到Sound对象。新应用的源代码可以在位于examples/chapter-08目录下的 SoundRecorder 应用中找到。清单 8–4 显示了麦克风数据的sampleData事件处理程序。

清单 8–4。 麦克风数据通知的设置代码和事件处理程序

`private staticconst SOUND_RATE:uint = 44;
private staticconst MICROPHONE_RATE:uint = 22;

// Handles the View’s creationComplete event
private function onCreationComplete():void {
  if (Microphone.isSupported) {
    microphone = Microphone.getMicrophone();
    microphone.setSilenceLevel(0)
    microphone.gain = 75;
    microphone.rate = MICROPHONE_RATE;

sound = new Sound();
    recordedBytes = new ByteArray();  
  } else {
    showMessage("microphone unsupported");
  }
}

// This handler is called when the microphone has data to give us private function onMicSample(event:SampleDataEvent):void {
  if (microphone.activityLevel > activityLevel) {
    activityLevel = Math.min(50, microphone.activityLevel);
  }

if (event.data.bytesAvailable) {
    recordedBytes.writeBytes(event.data);
  }
}`

onCreationComplete处理程序负责检测麦克风,初始化它,并创建应用用来存储和播放声音的ByteArraySound对象。请注意,麦克风的rate设置为 22 kHz。这对于捕获语音记录来说是足够的质量,并且比以全 44 kHz 记录占用更少的空间。

这个处理程序很简单。与之前一样,Microphone对象的activityLevel属性用于计算一个数字,该数字随后用于确定在显示器上绘制的动画曲线的幅度,以指示声音级别。然后事件的data属性,也就是一个ByteArray,被用来确定是否有麦克风数据可用。如果bytesAvailable属性大于零,那么字节从data数组复制到recordedBytes数组。这对于正常的录音来说效果很好。如果您需要记录数小时的音频数据,那么您应该将数据流式传输到服务器,或者将其写入设备上的文件中。

因为我们处理的是原始音频数据,所以由程序来记录声音的格式。在这种情况下,我们有一个麦克风,为我们提供 22 kHz 单声道(单声道)声音样本。Sound对象期望 44 kHz 立体声(左右声道)声音。这意味着每个麦克风样本必须写入Sound数据两次,以将其从单声道转换为立体声,然后再写入两次,以从 22 kHz 转换为 44 kHz。因此,每个麦克风样本名义上将被复制到Sound对象的数据数组中四次,以便使用与捕获时相同的速率回放录音。清单 8–5 显示了执行复制的SoundsampleData处理程序。

清单 8–5。??Sound对象的数据请求的事件处理程序

`// This handler is called when the Sound needs more data
private function onSoundSample(event:SampleDataEvent):void {
  if (soundChannel) {
    var avgPeak:Number = (soundChannel.leftPeak + soundChannel.rightPeak) / 2;
    activityLevel = avgPeak * 50;
  }

// Calculate the number of stereo samples to write for each microphone sample
  var sample:Number = 0;
  var sampleCount:int = 0;
  var overSample:Number = SOUND_RATE / MICROPHONE_RATE * freqMultiplier;

while (recordedBytes.bytesAvailable && sampleCount < 2048/overSample) {
    sample = recordedBytes.readFloat();
    for (var i:int=0; i<overSample; ++i) {
      // Write the data twice to convert from mono to stereo
      event.data.writeFloat(sample); event.data.writeFloat(sample);
    }
    ++sampleCount;
  }
}`

由于在回放和记录期间,显示器上的曲线应该是动画的,所以在处理程序中做的第一件事是计算用于绘制曲线的activityLevel。从上一节对声音相关类的概述中,我们知道SoundChannel类是我们需要查找正在播放的声音的信息的地方。这个类有一个leftPeak和一个rightPeak属性来指示声音的振幅。这两个值的范围都是从 0.0 到 1.0,其中 0.0 是静音,1.0 是最大音量。这两个值被平均并乘以 50 以计算出一个activityLevel,该值可用于激活波形显示。

现在我们到了有趣的部分:将记录的数据传输到声音的数据数组。首先计算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 个样本。

我们现在有能力记录和回放声音样本。应用所缺少的是在两种模式之间转换的方法。

处理状态转换

应用有四种状态:stoppedrecordingreadyToPlayplaying。点击屏幕上的某个地方将使应用从一种状态转换到下一种状态。图 8–3 说明了这一过程。

images

图 8–3。 录音机应用的四种状态

应用在stopped状态下启动。当用户点击屏幕时,应用转换到recording状态,并开始录制他或她的声音。另一次点击停止记录并转换到readyToPlay状态。当用户准备好收听录音时,另一次点击在playing状态下开始回放。然后,用户可以第四次点击以停止播放并返回到stopped状态,准备再次录制。如果播放自行结束,应用也应自动转换到stopped状态。清单 8–6 显示了这个应用唯一的View的 MXML。

清单 8–6。??【录音笔应用的首页】??

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        actionBarVisible="false"
        creationComplete="onCreationComplete()">

<fx:Script source="SoundRecorderHomeScript.as"/>

<s:states>
    <s:State name="stopped"/>
    <s:State name="recording"/>
    <s:State name="readyToPlay"/>
    <s:State name="playing"/>
  </s:states> <s:transitions>
    <s:Transition toState="stopped">
      <s:Parallel>
        <s:Scale target="{stopLabel}" scaleXBy="4" scaleYBy="4"/>
        <s:Fade target="{stopLabel}" alphaFrom="1" alphaTo="0"/>
        <s:Scale target="{tapLabel}" scaleXFrom="0" scaleXTo="1"
                 scaleYFrom="0" scaleYTo="1"/>
        <s:Fade target="{tapLabel}" alphaFrom="0" alphaTo="1"/>
      </s:Parallel>
    </s:Transition>

<s:Transition toState="readyToPlay">
      <s:Parallel>
        <s:Scale target="{stopLabel}" scaleXBy="4" scaleYBy="4"/>
        <s:Fade target="{stopLabel}" alphaFrom="1" alphaTo="0"/>
        <s:Scale target="{tapLabel}" scaleXFrom="0" scaleXTo="1"
                 scaleYFrom="0" scaleYTo="1"/>
        <s:Fade target="{tapLabel}" alphaFrom="0" alphaTo="1"/>
      </s:Parallel>
    </s:Transition>

<s:Transition toState="*">
      <s:Parallel>
        <s:Scale target="{tapLabel}" scaleXBy="4" scaleYBy="4"/>
        <s:Fade  target="{tapLabel}" alphaFrom="1" alphaTo="0"/>
        <s:Scale target="{stopLabel}" scaleXFrom="0" scaleXTo="1"
                 scaleYFrom="0" scaleYTo="1"/>
        <s:Fade  target="{stopLabel}" alphaFrom="0" alphaTo="1"/>
      </s:Parallel>
    </s:Transition>
  </s:transitions>

<s:Group id="canvas" width="100%" height="100%" touchTap="onTouchTap(event)"/>
  <s:Label id="messageLabel" top="0" left="0" mouseEnabled="false" alpha="0.5"
           styleName="label"/>

<s:Label id="tapLabel" bottom="100" horizontalCenter="0" mouseEnabled="false"
           text="Tap to Record" includeIn="readyToPlay, stopped"
           styleName="label"/>
  <s:Label id="stopLabel" bottom="100" horizontalCenter="0" mouseEnabled="false"
           text="Tap to Stop" includeIn="playing, recording"
           styleName="label"/>

<s:Label id="speedLabel" top="100" horizontalCenter="0" mouseEnabled="false"
           text="{1/freqMultiplier}x" fontSize="48" includeIn="playing"
           styleName="label"/>
</s:View>`

这段代码包含了包含这个View的 ActionScript 代码的源文件,声明了View的四个状态以及它们之间的转换,最后声明了显示在View中的 UI 组件。UI 组件包括一个Group,它既是动画波形的绘图画布,也是触发状态转换的点击事件的处理程序。还有一个向用户显示错误消息的Label,两个向用户显示状态消息的Label,以及一个指示播放频率的Label

现在桌子已经摆好了;定义了我们的用户界面和应用状态。下一步将是查看控制状态更改和 UI 组件的代码。清单 8–7 展示了控制从一个状态到下一个状态的转换的 ActionScript 代码。

清单 8–7。 控制录音机应用的状态转换顺序

`private function onTouchTap(event:TouchEvent):void {
  if (currentState == "playing" && isDrag) {
    return;
  }

incrementProgramState();
}

private function onSoundComplete(event:Event):void {
  incrementProgramState();
}

private function incrementProgramState():void {
  switch (currentState) {
    case"stopped":
      transitionToRecordingState();
      break;
    case"recording":
      transitionToReadyToPlayState();
      break;
    case"readyToPlay":
      transitionToPlayingState();
      break;
    case"playing":
      transitionToStoppedState();
      break;
  }
}`

您可以看到,当用户点击屏幕或录制的声音播放完毕时,应用的状态会发生变化。onTouchTap函数还执行检查,以确保点击事件不是作为拖动的一部分生成的(用于控制回放频率)。incrementProgramState函数简单地使用currentState变量的值来确定接下来应该进入哪个状态,并调用适当的函数来执行与进入该状态相关的内务处理。这些函数如清单 8–8 所示。

清单 8–8。 录音机应用的状态转换功能

`private function transitionToRecordingState():void {
  recordedBytes.clear();
  microphone.addEventListener(SampleDataEvent.SAMPLE_DATA, onMicSample);
  currentState = "recording";
}

private function transitionToReadyToPlayState():void {
  microphone.removeEventListener(SampleDataEvent.SAMPLE_DATA, onMicSample);
  tapLabel.text = "Tap to Play";
  currentState = "readyToPlay";
} private function transitionToPlayingState():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";  
}

private function transitionToStoppedState():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 = "Tap to Record";
  currentState = "stopped";
}`

transitionToRecordingState函数从recordedBytes数组中清除任何现有的数据,将sampleData监听器添加到麦克风,以便它开始发送数据样本,最后设置currentState变量来触发动画状态转换。类似地,当记录完成时,调用transitionToReadyToPlayState。它负责从麦克风上移除sampleData监听器,将 UI 中的Label更改为“点击播放”,并再次设置currentState变量来触发动画过渡。

当用户点击屏幕开始回放录制的样本时,会调用transitionToPlayingState功能。它首先将回放频率重置为 1,并将recordedBytes数组的读取位置重置为数组的开头。接下来,它将触摸事件监听器添加到画布Group中,以便在回放期间监听控制倍频器的手势。它还为SoundsampleData事件安装了一个处理程序,这样应用就可以在回放期间为Sound提供数据。然后调用play方法开始播放声音。一旦我们有了对控制回放的soundChannel的引用,我们就可以为soundComplete事件添加一个处理程序,这样我们就可以知道声音是否播放完毕,这样我们就可以自动转换回stopped状态。最后,改变ViewcurrentState变量的值来触发动画状态转换。

最后一个转换是将应用带回到stopped状态。transitionToStoppedState函数负责停止播放(如果声音已经播放完毕,这没有任何作用),并删除所有由transitionToPlayingState函数添加的监听器。它最终重置Labeltext属性,并更改currentState变量的值来触发状态转换动画。

剩下的功能是倍频器。清单 8–9 显示了处理控制这个变量的触摸事件的代码。

清单 8–9。 用触摸手势控制播放的频率

`private function onTouchBegin(event:TouchEvent):void {
  touchAnchor = event.localY;
  isDrag = false;
}

private function onTouchMove(event:TouchEvent):void {
  var delta:Number = event.localY - touchAnchor;
  if (Math.abs(delta) > 75) {
    isDrag = true;
    touchAnchor = event.localY;
    freqMultiplier *= (delta > 0 ? 2 : 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状态的动画转换。

images

图 8–4。 运行在安卓设备上的录音笔应用

我们现在已经向您展示了如何播放和操作存储在ByteArray中的数据。应该注意的是,如果您需要操作存储在Sound对象而不是ByteArray中的数据,这种技术也是可行的。您可以使用Sound类的extract方法来访问原始声音数据,以某种方式操纵它,然后在它的sampleData处理程序中将它写回另一个Sound对象。

声音功能的另一个常见用途是播放音乐,无论是通过互联网还是以 MP3 文件的形式存储在设备上。如果您认为 Flash 平台非常适合这种类型的应用,那么您是对的!下一节将向您展示如何用 Flash 编写移动音乐播放器。

一个 Flash 音乐播放器

在设备上播放 MP3 文件的声音并不复杂。然而,音乐播放器不仅仅是播放声音。本节将首先向您展示如何使用 Flash 的声音 API 来播放 MP3 文件。一旦解决了这个问题,我们将看看你在创建移动应用时需要考虑的其他因素。

播放 MP3 文件

将 MP3 文件加载到Sound对象中就像使用以file协议开头的 URL 一样简单。清单 8–10 展示了这是如何实现的。

清单 8–10。 从文件系统加载并播放 MP3 文件

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        creationComplete="onCreationComplete()"
        title="Sound Loading">

fx:Script
    <![CDATA[
      private var sound:Sound;

private function onCreationComplete():void {
**        var path:String = "file:///absolute/path/to/the/file.mp3";**
**        sound = new Sound(new URLRequest(path));**
**        sound.play();**
      }
    ]]>
  </fx:Script>
</s:View>`

粗体显示的三行是播放 MP3 文件所需的全部内容。注意file://后面的第三个正斜杠,它用来表示这是 MP3 文件的绝对路径。在实际应用中,你显然不希望使用这样的常量路径。在本章的后面,当我们讨论制作实际应用的注意事项时,我们将会看到以更优雅的方式处理文件系统路径的策略。

读取 ID3 元数据

播放音乐文件是一个好的开始;毕竟这是音乐播放器的本质。所有音乐播放器做的另一件事是读取嵌入在文件的 ID3tags 中的元数据。 1 这些元数据包括艺术家和专辑的名字、录制年份,甚至歌曲的流派和曲目号。Sound类为读取这些标签提供了内置支持。清单 8–11 展示了如何将这一功能添加到我们刚刚起步的音乐播放器中。粗体行表示从清单 8–10 中新增的源代码。


1 [www.id3.org/](http://www.id3.org/)

清单 8–11。 从 MP3 文件中读取 ID3 元数据

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        creationComplete="onCreationComplete()"
        title="Sound Loading">

fx:Script
    <![CDATA
      private var sound:Sound;

private function onCreationComplete():void {
        var path:String = "file:///absolute/path/to/the/file.mp3";
        sound = new Sound(new URLRequest(path));
**        sound.addEventListener(Event.ID3, onID3);**
        sound.play()
      }

**      private function onID3(event:Event):void {**
**        metaData.text = "Artist: "+sound.id3.artist+"\n"+**
**                        "Year: "+sound.id3.year+"\n";**
**      }**
  </fx:Script>

<s:Label id="metaData" width="100%" textAlign="center"/>
</s:View>`

添加了onID3处理程序作为Event.ID3事件的监听器。当从 MP3 文件中读取元数据并准备好使用时,调用此处理程序。在ID3Info类中有几个预定义的属性,对应于更常用的 ID3 标签。像专辑名、艺术家名、歌曲名、流派、年份和曲目号都有在类中定义的属性。此外,您还可以访问 ID3 规范 2.3 版定义的任何其他文本信息框架。 [2 例如,要访问包含出版商名称的 TPUB 帧,您可以使用sound.id3.TPUB

不支持的一件事是从 ID3 标签读取图像,如专辑封面。在本章的后面,你将学习如何使用开源的 ActionScript 库来完成这个任务。

实施暂停功能

SoundChannel类不直接支持暂停声音数据的回放。然而,通过结合使用类的position属性和它的stop方法,很容易实现暂停特性。清单 8–12 展示了一种实现播放/暂停切换的可能技术。新添加的代码再次以粗体显示。


2

清单 8–12。?? 实现播放/暂停切换

`
<s:View … >

fx:Script
    <![CDATA[
**      private var sound:Sound;**
**      private var channel:SoundChannel;**
**      private var pausePosition:Number = 0;**

**      [Bindable] private var isPlaying:Boolean = false;**

private function onCreationComplete():void {
        var path:String = "file:///absolute/path/to/the/file.mp3";
        sound = new Sound(new URLRequest(path));
        sound.addEventListener(Event.ID3, onID3);
      }

private function onID3(event:Event):void { /* same as before */ }

**      private function onClick():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;**
**        }**
**      }**

**      private function onSoundComplete(event:Event):void {**
**        isPlaying = false;**
**        pausePosition = 0;**
**      }**
    ]]>
  </fx:Script>

<s:VGroup top="5" width="100%" horizontalAlign="center" gap="20">
    <s:Label id="metaData" width="100%" textAlign="center"/>
    <s:Button label="{isPlaying ? 'Pause' : 'Play'}" click="onClick()"/>
  </s:VGroup>
</s:View>`

onCreationComplete处理程序中不再调用Soundplay方法。取而代之的是,界面上增加了一个按钮,它的Label根据isPlaying标志的值是“播放”还是“暂停”。点击按钮触发对onClick处理器的调用。如果声音当前正在播放,通道的position保存在pausePosition实例变量中,声音停止,并且soundComplete事件监听器从通道中移除。下次播放声音时,将创建一个新的SoundChannel对象。因此,未能从旧的SoundChannel中移除我们的侦听器将导致内存泄漏。

如果声音当前没有播放,它是通过调用Soundplay方法启动的。将pausePosition作为参数传递给play方法,这样声音将从上次停止的位置开始播放。一个soundComplete事件的监听器被附加到由play方法返回的新的SoundChannel对象上。当声音播放完毕时,将调用此事件的处理程序。当这种情况发生时,处理程序会将isPlaying标志的值重置为false并将pausePosition重置为零。这样,下次点击播放按钮时,歌曲将从头开始播放。

调节音量

我们的音乐播放器也必须具备在播放歌曲时调节音量的功能。这是与歌曲播放时的SoundChannel相关联的SoundTransform对象的工作。清单 8–13 展示了如何使用SoundTransform来改变声音播放时的音量和声相。

清单 8–13。 实现音量和声相调整

`
<s:View …>
  fx:Script
    <![CDATA[
      /* All other code is unchanged… */

private function onClick():void {
        if (isPlaying) {
           /* Same as before /
        } else {
          channel = sound.play(pausePosition);
          channel.addEventListener(Event.SOUND_COMPLETE, onSoundComplete);
**          onVolumeChange();
*
**          onPanChange();**
          isPlaying = true;
        }
      }

**      private function onVolumeChange():void {**
**        if (channel) {**
**          var xform:SoundTransform = channel.soundTransform;**
**          xform.volume = volume.value / 100;**
**          channel.soundTransform = xform;**
**        }**
**      }**

**      private function onPanChange():void {**
**        if (channel) {**
**          var xform:SoundTransform = channel.soundTransform;**
**          xform.pan = pan.value / 100;**
**          channel.soundTransform = xform;**
**        }**
    ]]>
  </fx:Script> <s:VGroup top="5" width="100%" horizontalAlign="center" gap="20">
  <s:Label id="metaData" width="100%" textAlign="center"/>
  <s:Button label="{isPlaying ? 'Pause' : 'Play'}" click="onClick()"/>
**  <s:HSlider id="volume" minimum="0" maximum="100" value="100"**
**             change="onVolumeChange()"/>**
**  <s:HSlider id="pan" minimum="-100" maximum="100" value="0"**
**             change="onPanChange()"/>**
  </s:VGroup>
</s:View>`

我们添加了两个水平滑块,可以用来调整音量和声音播放时的平移。对于移动设备上的音乐播放器来说,担心声相可能不是一个很好的理由,但是为了完整起见,这里给出了一个例子。也许这个音乐播放器有一天会成长为一个迷你移动混音工作室。如果发生这种情况,您将在这个功能上有一个良好的开端!

当滑块移动时,调用change事件处理程序。注意调整SoundTransform设置所需的模式。您首先获得一个对现有转换的引用,以便从所有当前设置开始。然后更改您感兴趣的设置,并再次在通道上设置变换对象。设置soundTransform属性会触发频道更新其设置。这样,您可以将多个变换更改一起批处理,并且只需支付一次还原通道变换的成本。

SoundTransformvolume属性需要一个介于 0.0(静音)和 1.0(最大音量)之间的值。类似地,pan属性期望一个介于-1.0(左)和 1.0(右)之间的值。change处理程序负责将滑块的值调整到合适的范围。最后要注意的是onVolumeChangeonPanChange在声音开始播放时也会被调用。同样,这是必要的,因为每次调用Soundplay方法都会创建一个新的通道。这个新的通道对象在调用onVolumeChangeonPanChange之前不会有新的设置。

这就结束了我们对基本音乐播放器功能的快速概述。如果这就是你需要知道的全部信息,就没有必要再往下读了,所以你可以直接跳到“播放视频”部分。然而,如果你有兴趣了解把这个简约的音乐播放器变成一个真正的 Android 应用的所有考虑因素,那么下一节就是为你准备的。

从原型到应用

我们已经介绍了在 Flash 中播放音乐所需的基本技术,但是创建一个真正的音乐播放器应用还需要更多的努力。本节将讨论一些需要完成的事情,包括以下内容:

  • 创建可测试、可维护和可重用的代码
  • 处理不同的屏幕密度
  • 整合第三方库以提供 Flash 中缺少的功能
  • 创建一个自定义控件来增加一点视觉效果
  • 处理应用和View的激活和停用事件
  • 停用应用时保持数据

我们将从一种架构模式开始,这种模式可以帮助您将View的逻辑从它的表示中分离出来,从而创建更具可重用性和可测试性的代码。您可以通过参考在本书源代码的examples/chapter-08目录中找到的 MusicPlayer 示例应用来跟踪这个讨论。

更好的模式:展示模型

当我们以前想要将View的逻辑从它的表示中分离出来时,我们依赖于简单地将 ActionScript 代码移动到一个单独的文件中。然后使用<fx:Script>标签的source属性将该文件包含在 MXML View中。这是可行的,但是最终你会得到与View紧密耦合的脚本逻辑,因此不太容易重用。在用户界面中实现职责分离有更好的选择。

2004 年,Martin Fowler 发表了一篇文章,详细介绍了一种称为表示模型的设计模式。 3 这种模式是对流行的 MVC 模式 4 的一个小小的修改,特别适合现代框架,比如 Flash、Silverlight、WPF 和 JavaFX,它们包含了数据绑定等特性。实现这种模式通常需要三个类协同工作:数据模型、表示模型和View。值得注意的是,数据模型通常只是被称为“模型”或者有时是“领域模型”每个表示模型都可以访问一个或多个数据模型,并将其内容呈现给View进行显示。虽然不是原始模式描述的一部分,但是在富互联网应用中,服务类作为第四个组件包含进来是非常常见的。服务类封装了访问 web 服务(或任何其他类型的服务)所需的逻辑。服务类和表示模型通常会来回传递数据模型对象。

这种常见的应用结构在 Figure 8–5 中进行了说明,我们稍后将在音乐播放器应用中实现该设计。SongListView是我们的 MXML 文件,它声明了一个View来显示对象列表。SongListView只知道它的表示模型SongListViewModel。表示模型不知道使用它的ViewView。它的工作是与MusicService协作来呈现一个用于显示的MusicEntry对象列表。有明确的责任划分,每个班级对系统的其余部分都了解有限。用软件工程术语来说,设计具有低耦合性和高内聚性。这应该是你设计的任何应用的目标。


3 马丁·福勒,《演示模型》,[martinfowler.com/eaaDev/PresentationModel.html](http://martinfowler.com/eaaDev/PresentationModel.html), July 19, 2004

4 马丁·福勒,《模型视图控制器》,[martinfowler.com/eaaCatalog/modelViewController.html](http://martinfowler.com/eaaCatalog/modelViewController.html)

images

图 8–5。 演示模型模式的一种常见实现

总之,使用表示模型模式有两个主要好处:

  1. View知道表示模型,但是表示模型对View一无所知。这使得多个View共享同一个表示模型变得很容易。这是表示模型模式使重用代码变得更容易的一种方式。
  2. 大多数逻辑从View中移出,进入表示模型。View可以绑定到呈现模型的属性,以便向用户呈现数据。像按钮按下这样的动作最好直接传递给表示模型,而不是在View中处理。这意味着大部分值得测试的代码都在表示模型中,您不必担心测试 UI 代码。
创建视图导航应用

既然已经了解了应用设计的基本构建模块,那么是时候创建一个新的 Flex 移动项目了。这个应用将是一个ViewNavigatorApplication,因为我们需要在两个不同的View之间导航:一个View包含歌曲、艺术家或专辑的列表,一个View包含播放歌曲的控件。一旦创建了项目,我们就可以设置应用的包结构。assetsviewsviewmodelsmodelsservices各有一个包。这使得按职责组织应用中的各种类变得很容易。这个assets包是应用的所有图形素材,比如图标和闪屏,将被放置在其中。

ViewNavigatorApplication的主要工作是创建和显示第一个View。这通常通过设置<s:ViewNavigatorApplication>标签的firstView属性来完成。在这个应用中会有一点不同,因为每个View的表示模型都会在它的data属性中传递给它。为了完成这个任务,一个处理程序被分配给ViewNavigatorApplicationinitialize事件。在这个onInitialize处理程序中,MusicService和初始的表示模型将被创建并传递给第一个View。清单 8–14 显示了应用的 MXML。

清单 8–14。??【MXML】主ViewNavigatorApplication

`
<s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                     xmlns:s="library://ns.adobe.com/flex/spark"
                     splashScreenImage="@Embed('assets/splash.png')"
                     initialize="onInitialize()"
                     applicationDPI="160">

fx:Script
    <![CDATA[
      importservices.LocalMusicService;
      importservices.MusicService;
      import views.SongListView;
      import viewmodels.SongListViewModel;

**      private function onInitialize():void {**
**        var service:MusicService = new LocalMusicService();**
**        navigator.pushView(SongListView, new SongListViewModel(service));**
**      }**
    ]]>
  </fx:Script>
</s:ViewNavigatorApplication>`

这个应用中使用的MusicService接口的具体实现是一个名为LocalMusicService的类,它从设备的本地文件系统中读取文件。这个服务实例然后被用来构建表示模型,在这个例子中是SongListViewModel的一个实例。像这样将服务传递给表示模型比让表示模型在内部构造服务更可取。这使得在测试期间,或者如果程序的功能集被扩展到包括其他类型的音乐服务时,很容易向展示模型提供不同版本的服务。但是我们太超前了。我们将在下一节更详细地讨论这些类。

注意:有些人更喜欢让View类创建自己的表示模型,而不是像我们在这里使用data属性传递它。我们更喜欢将表示模型传递给View,因为在其他条件相同的情况下,您应该总是喜欢类之间的耦合更少。然而,这两种方式在实践中都很有效。

在清单 8–14 中需要注意的最后一件事是ViewNavigatorApplicationapplicationDPI属性的声明。我们将它设置为 160,表示应用的 UI 将为 160 dpi 的屏幕设计。如果应用在更高 dpi 的屏幕上运行,UI 将相应地缩放。更多详情,请参考第二章的的“Flex 应用中的密度”一节。

实现音乐服务

将您的服务类定义为一个interface是一个好主意。那么您的表示模型只依赖于interface类,而不依赖于任何一个具体的服务实现。这使得在您的表示模型中使用不同的服务实现成为可能。例如,您可以创建音乐服务的一个实现,从设备的本地存储中读取音乐文件,而另一个实现可以用于通过互联网传输音乐。

然而,使用服务接口还有一个更好的理由;这使得对你的表示模型进行单元测试变得很容易。假设您通常使用从互联网 web 服务读取音乐文件的MusicService实现来运行您的应用。如果您的表示模型硬连线使用这个版本,那么您不能孤立地测试表示模型。您需要确保您有一个活动的互联网连接,并且 web 服务已经启动并且正在运行,否则您的测试将会失败。使表示模型仅依赖于接口使得交换一个模拟服务变得很简单,该模拟服务向表示模型返回一个预定义的MusicEntry对象列表。这使得你的单元测试可靠且可重复。这也使它们运行得更快,因为您不必在每次测试中都从 web 服务下载数据!

给定一个 URL 路径,MusicService的工作只是提供一个MusicEntry对象的列表。因此,interface类将包含一个方法,如清单 8–15 所示。

清单 8–15。MusicService界面

`package services
{
  import mx.collections.ArrayCollection;

public interface MusicService {
    /**
     * A MusicService implementation knows how to use the rootPath to find
     * the list of MusicEntry objects that reside at that path.
     *
     * @return An ArrayCollection of MusicEntry objects.
     * @see models.MusicEntry
     */
    function getMusicEntries(rootPath:String = null):ArrayCollection;
  }
}`

一个MusicEntry对象可以代表一首歌曲,也可以代表一个保存一首或多首其他歌曲的容器。这样,我们可以使用多个MusicEntry对象列表来浏览艺术家、专辑和歌曲的分层列表。与大多数数据模型一样,这个类是一个属性集合,几乎没有逻辑。MusicEntry对象如清单 8–16 所示。

清单 8–16。??MusicEntry数据模型

package models {   import flash.utils.IDataInput;
`/**
   * This class represents an object that can be either a song or a container
   * of other songs.
   */  
  public class MusicEntry {
    private var _name:String;
    private var _url:String;
    private var _streamFunc:Function;

public function MusicEntry(name:String, url:String, streamFunc:Function) {
      _name = name;
      _url = url;
      _streamFunc = streamFunc;
    }

public function get name():String {
      return _name;
    }

public function get url():String {
      return _url;
    }

/**
     * @return A stream object if this is a valid song.  Null otherwise.
     */
    public function get stream():IDataInput {
      return _streamFunc == null ? null : _streamFunc();
    }

public function get isSong():Boolean {
      return _streamFunc != null;
    }
  }
}`

MusicEntry包含条目name的属性,一个url标识条目的位置,一个stream可用于读取条目(如果是一首歌),一个isSong属性可用于区分代表一首歌的条目和代表一个歌曲容器的条目。由于我们事先不知道阅读歌曲需要什么样的流,所以我们依赖 ActionScript 的函数式编程功能。这允许一个MusicEntry对象的创建者将一个函数对象传递给该类的构造器,当被调用时,该构造器负责创建适当类型的流。

这个应用将从设备的本地存储中播放音乐文件,所以我们的服务将提供从设备的文件系统中读取的MusicEntry对象。清单 8–17 展示了LocalMusicService的实现。

清单 8–17。 从本地文件系统中读取歌曲的MusicService的实现

package services {   import flash.filesystem.File;   import flash.filesystem.FileMode;   import flash.filesystem.FileStream;
`import flash.utils.IDataInput;
  import mx.collections.ArrayCollection;
  import models.MusicEntry;

public class LocalMusicService implements MusicService {
    private static const DEFAULT_DIR:File = File.userDirectory.resolvePath("Music");

/**
     * Finds all of the files in the directory indicated by the path variable
     * and adds them to the collection if they are a directory or an MP3 file.
     *
     * @return A collection of MusicEntry objects.
     */
    public function getMusicEntries(rootPath:String=null):ArrayCollection {
      var rootDir:File = rootPath ? new File(rootPath) : DEFAULT_DIR;
      var songList:ArrayCollection = new ArrayCollection();

if (rootDir.isDirectory) {
        var dirListing:Array = rootDir.getDirectoryListing();

for (var i:int = 0; i < dirListing.length; i++) {
          var file:File = dirListing[i];

if (!shouldBeListed(file))
            continue;

songList.addItem(createMusicEntryForFile(file));
        }
      }

return songList;
    }

/**
     * @return The appropriate type of MusicEntry for the given file.
     */
    private function createMusicEntryForFile(file:File):MusicEntry {
      var name:String = stripFileExtension(file.name);
      var url:String = "file://" + file.nativePath;
      var stream:Function = null;

if (!file.isDirectory) {
        stream = function():IDataInput {
          var stream:FileStream = new FileStream();
          stream.openAsync(file, FileMode.READ);
          return stream;
        }
      }

return new MusicEntry(name, url, stream);
    }

// Other utility functions removed for brevity…
  }
}`

毫不奇怪,这种类型的服务严重依赖于flash.filesystem包中的类。当使用文件系统路径时,您应该总是尝试使用在File类中定义的路径属性。DEFAULT_DIR常量使用File.userDirectory作为其默认路径的基础,在 Android 上它指向/mnt/sdcard目录。因此,该服务将默认在/mnt/sdcard/Music目录中查找其文件。这是 Android 设备上音乐文件的一个相当标准的位置。

注意: File.userDirectoryFile.desktopDirectoryFile.documentsDirectory都指向安卓设备上的/mnt/sdcardFile.applicationStorageDirectory指向一个特定于您的应用的“本地存储”目录。File.applicationDirectory空。

LocalMusicPlayer中的getMusicEntries实现将提供的rootPath字符串转换为File,或者如果没有提供rootPath则使用默认目录,然后继续遍历位于该路径的文件。它为任何一个目录(其他歌曲的容器)或 MP3 文件(一首歌曲)的File创建一个MusicEntry对象。如果File是一首歌而不是一个目录,那么createMusicEntryForFile函数创建一个函数闭包,当被调用时,打开一个异步FileStream进行读取。然后,这个函数闭包被传递给播放歌曲时要使用的MusicEntry对象的构造函数。您可能还记得清单 8–16 中,这个闭包对象的值——不管它是否为空——被用来确定对象所代表的MusicEntry的类型。

歌曲列表视图

清单 8–14 显示应用创建的第一个ViewSongListView。应用的onInitialize处理程序实例化适当类型的MusicService,并使用它为View构建SongListViewModel。然后将SongListViewModel作为navigator.pushView函数的第二个参数传递给View。这将在Viewdata属性中放置一个对模型实例的引用。

SongListViewModel的工作非常简单。它使用给定的MusicService来检索SongListView要显示的MusicEntry对象列表。清单 8–18 显示了这个表示模型的源代码。

清单 8–18。 的演示模式为SongListView

`package viewmodels
{
  import models.MusicEntry;
  import mx.collections.ArrayCollection;
  import services.LocalMusicService;
  import services.MusicService;

[Bindable]
  public class SongListViewModel { private var _entries:ArrayCollection = new ArrayCollection();
    private var _musicEntry:MusicEntry;
    private var _musicService:MusicService;

public function SongListViewModel(service:MusicService = null,
                                      entry:MusicEntry = null ) {
      _musicEntry = entry;
      _musicService = service;

if (_musicService) {
        var url:String = _musicEntry ? _musicEntry.url : null;
        entries = _musicService.getMusicEntries(url);
      }
   }

public function get entries():ArrayCollection {
      return _entries;
    }

public function set entries(value:ArrayCollection):void {
      _entries = value;
    }

public function cloneModelForEntry(entry:MusicEntry):SongListViewModel {
      return new SongListViewModel(_musicService, entry);
    }

public function createSongViewModel(selectedIndex:int):SongViewModel {
      return new SongViewModel(entries, selectedIndex);
    }
  }
}`

该类用Bindable进行了注释,因此entries属性可以绑定到View类中的 UI 组件。

构造函数将存储对传入的MusicServiceMusicEntry实例的引用。如果服务引用不为空,则从MusicService中检索条目集合。如果服务为空,那么entries集合将保持为空。

该类中还有两个额外的公共函数。cloneModelForEntry函数将通过传递给它的MusicService引用来创建一个新的SongListViewModelcreateSongViewModel将使用这个模型的entries集合和所选条目的索引为SongView创建一个新的表示模型。这是这些函数的逻辑位置,因为这个表示模型引用了创建新表示模型所需的数据。因此,一个表示模型创建另一个表示模型是很常见的。

考虑到这一点,是时候看看View如何使用它的表示模型了。SongListView的源代码如清单 8–19 所示。

清单 8–19。SongListView

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        initialize="onInitialize()"
        title="Music Player">

fx:Script
    <![CDATA[
      import spark.events.IndexChangeEvent;
      import models.MusicEntry;
      import viewmodels.SongListViewModel;

[Bindable]
      private var model:SongListViewModel;

private function onInitialize():void {
        model = data as SongListViewModel;
      }

private function onChange(event:IndexChangeEvent):void {
        var list:List = List(event.target);
        var selObj:MusicEntry = list.selectedItem as MusicEntry;

if (selObj.isSong) {
          var index:int = list.selectedIndex;
          navigator.pushView(SongView, model.createSongViewModel(index));
        } else {
          navigator.pushView(SongListView, model.cloneModelForEntry(selObj));
        }
      }
    ]]>
  </fx:Script>

<s:List width="100%" height="100%" change="onChange(event)"
          dataProvider="{model.entries}">
    <s:itemRenderer>
      fx:Component
        <s:IconItemRenderer labelField="name" decorator="{chevron}">
          fx:Declarations
            <s:MultiDPIBitmapSource id="chevron"
                                source160dpi="@Embed('assets/chevron160.png')"
                                source240dpi="@Embed('assets/chevron240.png')"
                                source320dpi="@Embed('assets/chevron320.png')"/>
          </fx:Declarations>
        </s:IconItemRenderer>
      </fx:Component>
    </s:itemRenderer>
  </s:List>
</s:View>`

onInitialize处理程序从data属性初始化View的模型引用。然后model被用来访问作为ListdataProviderentries。它也用于ListonChange处理程序中。如果选择的MusicEntry是一首歌曲,则用model创建一首新的SongViewModel,用navigator.pushView功能显示一首SongView。否则,创建一个新的SongListViewModel并使用选择的MusicEntry作为新的MusicEntry对象集合的路径显示一个新的 ??。

还为List组件声明了一个自定义的IconItemRenderer。这样做是为了给项目渲染器添加一个 v 形符号,以表明选择一个项目会导致一个新的View。一个MultiDPIBitmapSource用于参考三个预缩放版本的人字形图像。注意,人字形位图源必须包含在<fx:Declaration>标签中,该标签是<s:IconItemRenderer>标签的子元素。如果位图源被声明为View<fx:Declaration>标签的子标签,那么它对IconItemRenderer是不可见的。

chevron160.png文件是基本大小,而chevron240.png大 50%,chevron320.png大两倍。人字形位图的最佳尺寸将根据运行程序的设备的屏幕属性来选择。图 8–6 显示了在中低 dpi 设备上运行的SongListView。请注意,人字形没有因缩放而产生的像素化伪像,如果我们在两个屏幕上使用相同的位图,就会出现这种情况。

images

图 8–6。??SongListView运行在不同 dpi 分类的设备上

注意:你也可以使用一个 FXG 图形作为一个IconItemRenderer的图标或装饰,方法是以与前面的MultiDPIBitmapSource相同的方式声明它。不幸的是,由于图标和装饰将被转换成位图,然后缩放,您将失去使用矢量图形的好处。出于这个原因,我们建议您将MultiDPIBitmapSource对象与您的自定义IconItemRenderers一起使用。

宋观

这就把我们带到了应用的真正核心:让用户播放音乐的视图!我们希望这个界面具有与大多数其他音乐播放器相同的功能。我们将显示歌名和专辑封面。它应该有控件,允许用户跳到下一首或上一首歌曲,播放和暂停当前歌曲,调整当前歌曲的位置以及音量和平移(只是为了好玩)。产生的界面如图 8–7 所示。

images

图 8–7。SongView界面运行在两种不同的 dpi 设置下

从 Figure 8–7 可以看出,这个界面比列表视图稍微复杂一点。它甚至包括一个自定义控件,不仅可以作为播放/暂停按钮,还可以作为当前歌曲播放位置的进度指示器。此外,你可以通过在按钮上来回滑动手指来控制歌曲的位置。编写这个自定义控件只是本节将要讨论的主题之一。

清单 8–20 显示了定义这个View的 MXML 文件的一部分。由于这是一个更大的接口声明,我们将把它分解成更小、更容易理解的部分。

清单 8–20。??【美国】和SongView MXML 文件的剧本章节

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:assets="assets."
        xmlns:views="views.
"
        initialize="onInitialize()"
        viewDeactivate="onViewDeactivate()"
        title="{model.songTitle}" >

<s:states>
    <s:State name="portrait"/>
    <s:State name="landscape"/>
  </s:states>

fx:Script
    <![CDATA[
      import viewmodels.SongViewModel;

[Bindable]
      private var model:SongViewModel;

private function onInitialize():void {
        model = data as SongViewModel;
        model.addEventListener(SongViewModel.SONG_ENDED, onSongEnded);
      }

private function onViewDeactivate():void {
        model.removeEventListener(SongViewModel.SONG_ENDED, onSongEnded);
        if (model.isPlaying)
          model.onPlayPause();
      }

private function onSongEnded(event:Event):void {
        progressButton.stop();
      }
    ]]>
  </fx:Script>
  
</s:View>`

文件的<s:states>部分声明了界面的portraitlandscape方向的状态。请记住第二章中的,通过在View中明确声明这些状态的名称,Flex 将在设备方向改变时适当地设置我们的View的状态。完成这些后,当方向改变时,您可以利用这些状态名来调整界面的布局。

与在SongListView中一样,onInitialize处理程序从data属性初始化表示模型引用。它还为模型的SONG_ENDED 事件附加了一个处理程序,以便onSongEnded处理程序可以在歌曲结束播放时适当地调整界面。还声明了一个用于ViewviewDeactivate事件的处理程序。这允许当用户离开ViewView停止播放歌曲。

我们现在将一次一个片段地检查这个View的 UI 组件。

<s:Rect width="100%" height="100%">   <s:fill>     <s:LinearGradient rotation="90">       <s:GradientEntry color="0xFFFFFF" ratio="0.40"/>       <s:GradientEntry color="0xe2e5f4" ratio="1.00"/>     </s:LinearGradient>   </s:fill> </s:Rect>

MXML 的第一部分在屏幕底部声明了从白色到浅蓝色的背景渐变。矩形的widthheight被设置为 100%,这样无论设备处于什么方向,它都会自动填充屏幕。

<s:Group width="100%" height="100%">   <s:layout.landscape>     <s:HorizontalLayout verticalAlign="middle" paddingLeft="10"/>   </s:layout.landscape>   <s:layout.portrait>     <s:VerticalLayout horizontalAlign="center" paddingTop="10"/>   </s:layout.portrait>

前面的代码片段创建了作为接口其余部分的容器的Group。再一次,它的widthheight被设置为总是充满屏幕。Group在风景模式下使用一个HorizontalLayout,在肖像模式下使用一个VerticalLayout。状态语法确保在设备重定向时使用正确的布局。图 8–8 显示了横向放置的设备上的SongView界面。

images

图 8–8。 横向音乐播放器界面

下一段代码中的Group是专辑封面图像的容器。Group的大小根据方向动态调整,但是宽度和高度总是保持相等——它总是形成一个正方形。

`<s:Group width.portrait="{height0.4}" height.portrait="{height0.4}"
         width.landscape="{width0.4}" height.landscape="{width0.4}">
  <s:BitmapImage id="albumCover" width="100%" height="100%"
                 source="{model.albumCover}"
                 visible="{model.albumCover != null}"/>

<assets:DefaultAlbum id="placeHolder" width="100%" height="100%"
                       visible="{!model.albumCover}" />
</s:Group>`

albumCover位图的源被绑定到模型的albumCover属性。只有当模型中确实有一个albumCover图像时,该位图才可见。如果没有,则显示占位符图形。占位符是一个 FXG 图像,位于应用的assets包中。您可以看到在您的 MXML 声明中使用 FXG 图形是微不足道的。由于它们是矢量图形,因此对于不同的屏幕密度也能很好地缩放。

在专辑封面之后,我们到达包含这个View控件的VGroup。这个VGroup实际上是由三个独立的HGroup集装箱组成的。第一个包含上一首歌按钮、自定义的ProgressButton控件和下一首歌按钮。下一个HGroup容器包含水平音量滑块,以及它的 FXG 图标,以指示滑块两侧的低音量和高音量。最后的HGroup包含水平平移滑块,以及显示左右方向的Label。注意,模型的volumepanpercentComplete属性通过双向绑定被绑定到接口组件。这意味着绑定的任何一端都可以设置属性的值,而另一端将被更新。

`<s:VGroup id="controls" horizontalAlign="center" width="100%"
          paddingTop="20" gap="40">
  <s:HGroup width="90%">
    <s:Button label="<<" height="40" click="model.previousSong()"/>
    <views:ProgressButton id="progressButton" width="100%" height="40"
                          click="model.onPlayPause()"
                          percentComplete="@{model.percentComplete}"
                          skinClass="views.ProgressButtonSkin"/>
    <s:Button label=">>" height="40" click="model.nextSong()"/>
  </s:HGroup>

<s:HGroup verticalAlign="middle" width="90%">
    <assets:VolLow id="volLow" width="32" height="32"/>
    <s:HSlider width="100%" maximum="1.0" minimum="0.0" stepSize="0.01"
             snapInterval="0.01" value="@{model.volume}" showDataTip="false"/>
    <assets:VolHigh id="volHigh" width="32" height="32"/>
  </s:HGroup>

<s:HGroup verticalAlign="middle" width="90%" >
    <s:Label text="L" width="32" height="32" verticalAlign="middle"
             textAlign="center"/>
    <s:HSlider width="100%" maximum="1.0" minimum="-1.0" stepSize="0.01"
             snapInterval="0.01" value="@{model.pan}" showDataTip="false"/>
    <s:Label text="R" width="32" height="32" verticalAlign="middle" textAlign="center"/>
      </s:HGroup>
    </s:VGroup>
  </s:Group>
</s:View>`

请注意,View中几乎没有逻辑。它都是声明性的表示代码,就像它应该的那样。所有的艰苦工作都委托给了表示模型。

不幸的是,SongViewModel类太大了,无法完整列出,所以我们将限制自己只查看该类的几个精选部分。请记住,播放音乐文件所需的基本功能在本章前面已经介绍过了,如果您想查看该类的完整源代码,可以参考本书示例代码中包含的 MusicPlayer 项目。清单 8–21 显示了SongViewModel类的声明和构造函数。

清单 8–21。SongViewModel的宣言

`package viewmodels
{
  // import statements…

[Event(name="songEnded", type="flash.events.Event")]

[Bindable]
  public class SongViewModel extends EventDispatcher {
    public static const SONG_ENDED:String = "songEnded";

public var albumCover:BitmapData;
    public var albumTitle:String = "";
    public var songTitle:String = "";
    public var artistName:String = "";
    public var isPlaying:Boolean = false;

private var timer:Timer;

public function SongViewModel(songList:ArrayCollection, index:Number) {
      this.songList = songList;
      this.currentIndex = index;

timer = new Timer(500, 0);
      timer.addEventListener(TimerEvent.TIMER, onTimer);

loadCurrentSong();
    }
  }
}`

该类扩展了EventDispatcher以便它可以在歌曲结束时通知任何可能正在收听的View。当这种情况发生时,模型会调度SONG_ENDED事件。这个模型还用Bindable进行了注释,以确保View可以轻松绑定到属性,如albumCover位图、albumTitlesongTitleartistNameisPlaying标志。构造函数获取一个集合MusicEntries和该集合中应该播放的歌曲的索引。这些参数被保存到实例变量中以供以后参考,因为当用户想要跳到集合中的上一首或下一首歌曲时会用到它们。构造函数还初始化一个每 500 毫秒计时一次的计时器。这个定时器读取歌曲的当前位置,并更新类的percentComplete变量。最后,构造函数加载当前歌曲。接下来的两节介绍了关于处理percentComplete更新和loadCurrentSong方法的更多细节。

双向装订的特殊考虑

当查看SongView的 MXML 声明时,我们注意到双向绑定被用于模型的volumepanpercentComplete变量。这意味着它们的值可以在模型类之外设置。这种额外的复杂性需要在模型类中进行一些特殊的处理。清单 8–22 显示了与SongViewModel中的这些属性相关的代码。

清单 8–22。 在展示模型中处理双向绑定

`private var _volume:Number = 0.5;
  private var _pan:Number = 0.0;
  private var _percentComplete:int = 0;

public function get volume():Number {return _volume; }
  public function set volume(val:Number):void {
    _volume = val;
    updateChannelVolume();
  }

public function get pan():Number {return _pan; }
  public function set pan(val:Number):void {
    _pan = val;
    updateChannelPan();
  }

public function get percentComplete():int {return _percentComplete;}

/**
   * Setting this value causes the song's play position to be updated.
   */
  public function set percentComplete(value:int):void {
    _percentComplete = clipToPercentageBounds(value)
    updateSongPosition();
  }

/**
   * Clips the value to ensure it remains between 0 and 100 inclusive.
   */
  private function clipToPercentageBounds(value:int):int {
    return Math.max(0, Math.min(100, value));
  }

/**
   * Set the position of the song based on the percentComplete value.
   */
  private function updateSongPosition():void {
    var newPos:Number = _percentComplete / 100.0 * song.length;
    if (isPlaying) { pauseSong()
    playSong(newPos);
  } else {
    pausePosition = newPos;
  }
}`

volumepanpercentComplete属性的public getset函数保证了它们可以在View中绑定。简单地将变量声明为 public 在这里是行不通的,因为当它们是从类外部设置时,我们需要做一些额外的工作。当设置了volumepan属性时,我们只需要调用更新SoundTransform中的值的函数,如本章前面所示。处理percentageComplete更新有点复杂:如果歌曲正在播放,我们需要停止它,然后在新的位置重新开始。我们使用私有的pauseSongplaySong实用程序方法来处理细节。如果歌曲当前没有播放,我们只需更新私有的pausePosition变量,这样下次歌曲开始播放时,它就从更新的位置开始播放。

这涵盖了对来自类外的percentComplete更新的处理,但是来自类内的更新呢?回想一下,有一个定时器每半秒钟读取一次歌曲的位置,然后更新percentComplete的值。在这种情况下,我们仍然需要通知绑定的另一方,percentComplete的值已经更改,但是我们不能使用set方法来这样做,因为我们不想每隔半秒钟就停止并重新启动歌曲。我们需要一个替代的更新路径,如清单 8–23 所示。

清单 8–23。 在定时器滴答期间更新percentComplete

`/*
 * Update the song's percentComplete value on each timer tick.
 */
private function onTimer(event:TimerEvent):void {
  var oldValue:int = _percentComplete;

var percent:Number = channel.position / song.length * 100;
  updatePercentComplete(Math.round(percent));
}

/**
 * Updates the value of _percentComplete without affecting the playback
 * of the current song (i.e. updateSongPosition is NOT called).  This
 * function will dispatch a property change event to inform any clients
 * that are bound to the percentComplete property of the update.
 */
private function updatePercentComplete(value:int):void {
  var oldValue:int = _percentComplete;
  _percentComplete = clipToPercentageBounds(value);

var pce: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读取完元数据,输入流将被关闭。最后一步是调用ID3Readerread函数,将通过访问MusicEntrystream属性创建的输入流作为参数传入。

清单 8–24。 加载 MP3 文件并读取其元数据

`/**
 * Loads the song data for the entry in the songList indicated by
 * the value of currentSongIndex.
 */
private function loadCurrentSong():void {
  try {
    var songFile:MusicEntry = songList[currentIndex];

song = new Sound(new URLRequest(songFile.url));

var id3Reader:ID3Reader = new ID3Reader();
    id3Reader.onMetaData = onMetaData;
    id3Reader.autoLimit = -1;
    id3Reader.autoClose = true;

id3Reader.read(songFile.stream);
  } catch (err:Error) {
    trace("Error while reading song or metadata: "+err.message);
  } }

/**
 * Called when the song's metadata has been loaded by the Metaphile
 * library.
 */
private function onMetaData(metaData:IMetaData):void {
  var songFile:MusicEntry = songList[currentIndex];
  var id3:ID3Data = ID3Data(metaData);

artistName = id3.performer ? id3.performer.text : "Unknown";
  albumTitle = id3.albumTitle ? id3.albumTitle.text : "Unknown";
  songTitle = id3.songTitle ? id3.songTitle.text : songFile.name;

if (id3.image) {
    var loader:Loader = new Loader();
    loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
                                              onLoadComplete)
    loader.loadBytes(id3.image);
  } else {
    albumCover = null;
  }
}

/**
 * Called when the album image is finished loading from the metadata.
 */
private function onLoadComplete(e:Event):void{
  albumCover = Bitmap(e.target.content).bitmapData
}`


5 [code.google.com/p/metaphile/](http://code.google.com/p/metaphile/)

onMetaData处理程序传递一个符合中期库IMetaData接口的参数。由于这个处理程序被附加到一个ID3Reader对象,我们知道将传入的metaData对象强制转换为一个ID3Data对象的实例是安全的。这样做可以让我们轻松访问ID3Data类的属性,比如performeralbumTitlesongTitle。如果在ID3Data类的 image 属性中存在图像数据,则创建一个新的flash.display.Loader实例,将字节加载到DisplayObject中。当加载图像字节时,onLoadComplete处理程序使用存储在Loader的内容属性中的DisplayObject来初始化albumCover BitmapData对象。由于View被绑定到了albumCover属性,所以一旦相册封面图像被更新,它就会显示出来。

创建定制组件

创建自定义移动组件与在 Flex 4 中创建任何其他自定义 Spark 组件非常相似。你创建了一个扩展了SkinnableComponentcomponent类和一个Skin。只要你的图形不是太复杂,你可以使用普通的 MXML Skin。如果您遇到性能问题,您可能需要用 ActionScript 编写您的Skin。参见第十一章了解有关移动应用性能调整的更多信息。

我们将编写的定制组件是ProgressButton。为了节省用户界面的空间,我们希望将播放/暂停按钮的功能与指示歌曲当前播放位置的进度监视器的功能结合起来。如果需要的话,控制器还将允许用户调整回放位置。因此,如果用户点击控件,我们将把它视为按钮的切换。如果用户触摸控件,然后水平拖动,将被视为位置调整。

因此,该控件将有两个图形元素:一个指示播放/暂停功能状态的图标和一个显示歌曲播放位置的进度条。图 8–9 显示了各种状态下的控制。

images

图 8–9。 自定义ProgressButton控制

当创建自定义 Spark 控件时,您可以将Skin视为您的View并将SkinnableComponent视为您的模型。清单 8–25 显示了ProgressButton类,它扩展了SkinnableComponent,因此充当控件的模型。

清单 8–25。ProgressButton的申报组成部分

`package views
{
  // imports removed…

[SkinState("pause")]
  public class ProgressButton extends SkinnableComponent
  {
    [SkinPart(required="true")]
    public var playIcon:DisplayObject;

[SkinPart(required="true")]
    public var pauseIcon:DisplayObject;

[SkinPart(required="true")]
    public var background:Group;

[Bindable]
    public var percentComplete:Number = 0;

private var mouseDownTime:Number;
    private var isMouseDown:Boolean;

public function ProgressButton() { // Make sure the mouse doesn't interact with any of the skin parts
      mouseChildren = false;

addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
      addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
      addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
      addEventListener(MouseEvent.CLICK, onMouseClick);
    }

override protected function getCurrentSkinState():String {
      if (isPlaying()) {
        return "play";
      } else {
        return "pause";
      }
    }

override protected function partAdded(partName:String, instance:Object):void {
      super.partAdded(partName, instance);

if (instance == pauseIcon) {
        pauseIcon.visible = false;
      }
    }

override protected function partRemoved(partName:String, instance:Object):void {
      super.partRemoved(partName, instance);
    }

// Consult Listing 8–26 for the rest of this class
  }
}`

组件有两种状态,每个Skin都必须支持:playpause。用SkinState(“pause”)component类进行注释,将其Skin的默认状态设置为pause状态。虽然一个Skin可以根据需要声明尽可能多的部分,但是组件要求每个Skin至少定义一个playIconpauseIcon和一个background。组件和Skin之间接口契约的最后一个组件是Skin用来绘制进度条的可绑定percentComplete属性。组件的构造函数禁止鼠标与包含在Skin中的任何子组件交互,并为它需要处理的鼠标事件附加监听器。

大多数组件需要实现三种方法来确保自定义控件的正确行为:getCurrentSkinStatepartAddedpartRemoved。当Skin需要更新显示时,它调用getCurrentSkinState函数。ProgressButton组件覆盖这个函数,根据isPlaying标志的当前值返回状态名。当添加和移除Skin部件时,partAddedpartRemoved功能使组件有机会执行初始化和清理任务。在这种情况下,这两个函数都确保在超类中调用它们对应的函数,并且为ProgressButton所做的惟一特殊化是确保pauseIcon在被添加时是不可见的。

清单 8–26 显示了ProgressButton类中定义的其余函数。它显示了构成该类的公共接口、鼠标事件处理程序和私有实用函数的其他函数。例如,SongView在被通知当前歌曲已经播放完毕时,调用stop函数。

清单 8–26。ProgressButton组件类的剩余功能

`/**
 * If in "play" state, stops the progress and changes the control's
 * state from "play" to "pause".
 */
public function stop():void {
  if (isPlaying()) {
    togglePlayPause();
  }
}

/**
 * @return True if the control is in "play" state.
 */
public function isPlaying():Boolean {
  return pauseIcon && pauseIcon.visible;
}

private function onMouseDown(event:MouseEvent):void {
  mouseDownTime = getTimer();
  isMouseDown = true;
}

private function onMouseMove(event:MouseEvent):void {
  if (isMouseDown && getTimer() - mouseDownTime > 250) {
    percentComplete = event.localX / width * 100;
  }
}

private function onMouseUp(event:MouseEvent):void {
  isMouseDown = false;
}

private function onMouseClick(event:MouseEvent):void {
  if (getTimer() - mouseDownTime < 250) {
    togglePlayPause();
  } else {
    event.stopImmediatePropagation();
  }
}

private function togglePlayPause():void {
  if (playIcon.visible) {
    playIcon.visible = false;
    pauseIcon.visible = true;
  } else {
    playIcon.visible = true;
    pauseIcon.visible = false;
  }
}`

处理程序负责区分点击和拖动手势。如果按下控件的时间少于 250 毫秒,手势将被解释为按钮按下,不会发生拖动。任何持续时间超过 250 毫秒的触摸将被解释为拖动而不是触摸,并且percentComplete值将根据鼠标相对于控件原点的位置进行调整。这个类中的其他一些函数使用togglePlayPause函数来切换图标的可见性,这决定了控件的状态。

创建自定义控件的最后一步是定义一个Skin类。这只是创建一个新的 MXML 组件的问题。用于 MusicPlayer 应用中的ProgressButtonSkin如清单 8–27 所示。每个Skin都必须包含一个元数据标签,该标签指定了Skin的设计目标HostComponent。对元数据标签中指定的HostComponent的引用可以通过SkinhostComponent属性获得。另一个要求是Skin必须声明它感兴趣的所有状态。此外,状态名称必须与主机组件定义的名称一致,以便Skin正确运行。

清单 8–27。??ProgressButtonSkin宣言

`
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:assets="assets.*"
        minWidth="20" minHeight="20">

fx:Metadata
    [HostComponent("views.ProgressButton")]
  </fx:Metadata>

<s:states>
    <s:State name="play"/>
    <s:State name="pause"/>
  </s:states>

<s:Group id="background" width="{hostComponent.width}"
           height="{hostComponent.height}">

<s:Rect top="0" right="0" bottom="0" left="0" radiusX="5" radiusY="5">
      <s:fill>
        <s:SolidColor color="0x1A253C" />
      </s:fill>
    </s:Rect>

<s:Rect top="1" right="1" bottom="1" left="1" radiusX="5" radiusY="5">
      <s:fill>
        <s:LinearGradient rotation="90">
          <s:GradientEntry color="0xa0b8f0" ratio="0.00"/>
          <s:GradientEntry color="0x81A1E0" ratio="0.48"/>
          <s:GradientEntry color="0x6098c0" ratio="0.85"/>
        </s:LinearGradient>
      </s:fill>
    </s:Rect>

<s:Rect  top="1" bottom="1" left="1" right="1" radiusX="5" radiusY="5"> <s:stroke>
        <s:SolidColorStroke color="0xa0b8f0" weight="1"/>
      </s:stroke>
    </s:Rect>

<s:Rect radiusX="5" radiusY="5" top="1" bottom="1" x="1"
          width="{(hostComponent.width-2)*hostComponent.percentComplete/100.0}">
      <s:fill>
        <s:LinearGradient rotation="90">
          <s:GradientEntry color="0xFFE080" ratio="0.00"/>
          <s:GradientEntry color="0xFFc860" ratio="0.48"/>
          <s:GradientEntry color="0xE0a020" ratio="0.85"/>
        </s:LinearGradient>
      </s:fill>
    </s:Rect>

<assets:Play id="playIcon" verticalCenter="0" horizontalCenter="0"
                    width="{hostComponent.height-4}"
                    height="{hostComponent.height-4}"/>
    <assets:Pause id="pauseIcon" verticalCenter="0" horizontalCenter="0"
                   width="{hostComponent.height-4}"
                   height="{hostComponent.height-4}"/>

</s:Group>
</s:Skin>`

背景Group作为Skin其余图形的容器。它被束缚在hostComponent的宽度和高度上。由Skin声明的下三个矩形充当组件的边界和背景填充。第四个矩形绘制进度条。它的宽度是基于对hostComponent及其percentComplete属性的宽度的计算。它是在三个背景和边框矩形之后声明的,因此它将被绘制在它们的顶部。添加到Skin的最后部分是playIconpauseIcon的 FXG 图形。FXG 文件在Skin类中就像在任何其他 MXML 文件中一样容易使用。FXG 文件被编译为优化的格式,并绘制为矢量图形。因此,它们不仅渲染速度快,而且伸缩性也很好。你不必担心它们在不同的分辨率和屏幕密度下看起来很糟糕(除非在IconItemRenderers中使用,如前所述!).

这就结束了我们对在 Flash 中播放声音和创建一个音乐播放器的研究,通过探索在编写真正的 Android 应用时必须处理的问题,这个音乐播放器在某种程度上超越了一个简单的示例应用。在本章的其余部分,我们将探索视频回放,这一功能使 Flash 成为一个家喻户晓的词。

播放视频

最近的一些估计表明,Flash 对多达 75%的网络视频负有责任。 6 无论视频是 On2 VP6 格式还是广泛使用的 H.264 格式,都可以放心地在您的移动 Flash 和 Flex 应用中播放。然而,在处理移动设备时,必须考虑一些事情。尽管移动设备的 CPU 和图形处理能力正以令人难以置信的速度增长,但它们仍然比普通的台式机或笔记本电脑慢得多。最近的高端移动设备支持 H.264 视频的硬件加速解码和渲染,但许多设备不支持。Flash 中的新功能,如 Stage Video,使您的 Flash 应用可以在桌面和电视上访问硬件加速的视频渲染,在 Android 设备上还不可用,尽管这只是时间问题。在此之前,你必须在移动设备上播放视频时做出一些妥协。这从编码开始,这是我们研究移动 Flash 视频的起点。

为移动设备优化视频

视频编码一半是科学,一半是黑色艺术。有一些很好的资源可以探索这个主题的所有精彩细节。因此,我们将只总结一些最近推荐的最佳实践,同时建议您查看本页脚注中引用的资源,以深入了解该主题。当您为移动设备编码视频时,要记住的主要事情是,您正在处理更有限的硬件,并且您将不得不应对 3G、4G 和 Wi-Fi 网络之间波动的带宽。

Adobe 建议在对新视频进行编码时,最好使用最大帧速率为 24 fps(每秒帧数)的 H.264 格式,并使用 44.1 kHz AAC 编码的立体声音频。如果您必须使用 On2 VP6 格式,那么同样的建议也适用于帧速率和音频采样,只适用于 MP3 格式而不是 AAC 格式的音频。如果您正在使用 H.264 进行编码,并且希望在最大数量的设备上保持良好的性能,那么您应该坚持使用基线配置文件。如果源素材的帧速率高于 24,您可能要考虑将其减半,直到低于该目标值。例如,如果您的素材是 30 fps,那么您将通过以 15 fps 编码它来获得最佳结果,因为编码器不必内插任何视频数据。


6 Adobe 公司,“在移动设备上为 Flash Player 10.1 提供视频”,www . Adobe . com/devnet/devices/articles/Delivering _ video _ fp10-1 . html,2010 年 2 月 15 日

7 Adobe 公司,“Android 移动设备视频编码指南”,www . Adobe . com/devnet/devices/articles/encoding-guidelines-Android . html,2010 年 12 月 22 日

表 8–2 显示了从 Adobe 最近的出版物以及 Adobe Max 和 360|Flex 的会议中收集的编码建议。所有这些数字都假定在基线配置文件中使用 H.264 编码。请记住,这些只是建议,它们会随着更快的硬件的出现而快速变化,可能不适用于您的特定情况。此外,这些建议针对尽可能多的设备。如果您的应用专门针对运行最新版本 Android 的高端设备,那么这些数字对于您的需求来说可能有点过于保守。

images

您还可以在应用中采取几个步骤来确保获得最佳性能。您应该避免使用变换:旋转、透视投影和颜色变换。避免阴影、滤镜效果和像素弯曲效果。您应该尽可能避免透明度和视频对象与其他图形的混合。

最好也尽量避免过多的 ActionScript 处理。例如,如果您有一个正在更新播放头的计时器,如果真的没有必要,就不要让它每秒更新多次。目标是在播放视频时,始终将尽可能多的处理时间用于渲染,并将程序逻辑所需的时间量降至最低。出于同样的原因,你也应该尽可能避免拉伸或压缩视频。使用Capabilities类或者View的大小来确定显示区域的大小,然后选择最接近的匹配,这是一个更好的主意。假设你有多种格式的视频可供选择。如果没有,那么最好在应用中包含一些选项,让用户决定是以自然分辨率播放视频,还是拉伸视频以填满屏幕(记住,对于视频,拉伸时几乎总是希望保持纵横比)。

Spark 视频播放器

播放视频这个话题太大了,一本书的一个章节甚至一章都容不下。我们不会安装或连接到流媒体服务器,如 Red5 Media Server 或 Adobe 的 Flash Media Server。我们将不涉及 DRM (数字版权管理) 8 或 CDNs(内容交付网络)等主题。相反,我们将介绍在您的应用中播放视频的基本选项。所有这些选项都适用于渐进式下载或流媒体服务器。我们的目的是让你朝着正确的方向开始,这样你就知道从哪里开始。如果您需要更高级的功能,比如前面提到的那些,Adobe 的文档已经足够了。

我们要看的第一个选项是 Flex 4 中引入的 Spark VideoPlayer组件。该组件构建在开源媒体框架(OSMF)之上,这是一个旨在处理全功能视频播放器所需的所有“幕后”任务的库。这个想法是,你写一个很酷的视频播放器 GUI,连接到 OSMF 提供的功能,你就可以开始了。我们将在本章后面更深入地研究 OSMF。

因此,Spark VideoPlayer是一个预打包的视频播放器 UI,建立在预打包的 OSMF 库之上。这是最方便的(也是最懒惰的),因为你只需要几行代码就可以给你的应用添加视频播放功能。清单 8–28 展示了如何在View MXML 文件中实例化一个VideoPlayer

清单 8–28。 在手机应用中使用 SparkVideoPlayer

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        viewDeactivate="onViewDeactivate()"
        actionBarVisible="false">

fx:Script/
    <![CDATA[
      privatestaticconst sourceURL:String = "http://ia600408.us.archive.org"+
          "/26/items/BigBuckBunny_328/BigBuckBunny_512kb.mp4";

private function onViewDeactivate():void {
        player.stop();
      }
    ]]>
  </fx:Script>

<s:VideoPlayer id="player" width="100%" height="100%" source="{sourceURL}"
                 skinClass="views.MobileVideoPlayerSkin"/>
</s:View>`

这个应用被设置为全屏,ViewActionBar已经被禁用,以允许VideoPlayer占据设备的整个屏幕。组件需要的只是一个源 URL,一旦缓冲了足够的数据,它就会自动开始回放。这真的不会变得更容易。当View被禁用时,我们会小心地停止播放。这是一件小事,但是没有理由继续缓冲和播放超过严格必要的时间。


8help . adobe . com/en _ us/as3/dev/ws 5b 3 CCC 516 D4 fbf 351 e63 e3d 118676 a5 be 7-8000 . html

如果你使用 Flash Builder 或者查阅关于VideoPlayer类的文档,你可能会看到一个不祥的警告,关于VideoPlayer没有“为移动优化”,但是在这种情况下,他们真正的意思是“警告:还没有定义移动皮肤!”你可以直接使用VideoPlayer,但是当你在中等或高 dpi 的设备上运行你的应用时,视频控件将会非常小(是的,这是一个技术术语),很难使用。解决方案是像我们在这个例子中所做的那样,创建自己的MobileVideoPlayerSkin

在这种情况下,我们刚刚使用 Flash Builder 在原来的VideoPlayerSkin的基础上创建了一个新的Skin,然后对它进行了一点修改。我们去掉了阴影,稍微缩放了控件,并调整了间距。修改后的Skin可以在本书源代码的examples/chapter-08目录下的 VideoPlayers 示例项目中找到。结果可以在图 8–10 中看到,我们正在播放视频剪辑中著名的老黄牛:大巴克兔子。这些图片来自 Nexus S,其中的控件现在已经足够大,可以使用了。

images

图 8–10。Nexus S 在常规(上图)和全屏(下图)模式下运行的火花VideoPlayer

这只是当前VideoPlayerSkin的一个快速修改,但是当然,由于 Flex 4 中引入的 Spark 组件的皮肤架构,你可以随心所欲地使用你的新手机Skin。请记住您在移动环境中将面临的一些性能限制。

【NetStream 视频

拥有一个方便的预打包解决方案,比如VideoPlayer是很好的,但是有时候你真的需要一些定制的东西。或者,也许你不想要像 OSMF 那样“一切都包括在内”的图书馆带来的所有包袱。这就是NetConnectionNetStreamVideo类出现的原因。这些类允许你构建一个轻量级的或者全功能的完全定制的视频播放器。

简而言之,NetConnection处理联网;NetStream提供控制视频流、缓冲和回放的编程接口;而Video提供解码视频最终出现的显示对象。在这种情况下,您负责为视频播放器提供用户界面。清单 8–29 展示了一个基于NetStream的视频播放器的极简 MXML 声明。

清单 8–29。MXML 文件为NetStreamVideoView

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        initialize="onInitialize()"
        viewDeactivate="onViewDeactivate()"
        actionBarVisible="false"
        backgroundColor="black">

<fx:Script source="NetStreamVideoViewScript.as"/>

<mx:UIComponent id="videoContainer" width="100%" height="100%"/>

<s:Label id="logger" width="100%" color="gray"/>

<s:HGroup bottom="2" left="30" right="30" height="36" verticalAlign="middle">
    <s:ToggleButton id="playBtn" click="onPlayPause()" selected="true"
      skinClass="spark.skins.spark.mediaClasses.normal.PlayPauseButtonSkin"/>
    <s:Label id="timeDisplay" color="gray" width="100%" textAlign="right"/>
  </s:HGroup>
</s:View>`

我们已经声明了一个UIComponent作为Video显示对象的最终容器。除此之外,只有另外两个可见控件。第一个是从 Spark VideoPlayer组件“借用”了PlayPauseButtonSkinToggleButton(好吧,我们承认,我们彻头彻尾地偷了Skin,我们甚至没有一点点抱歉)。这给了我们一个简单的方法来显示一个带有传统的三角形播放图标和双条暂停图标的按钮。另一个控件只是一个Label,它将显示视频剪辑的持续时间和当前播放位置。

MXML 宣言中提到了各种 ActionScript 函数作为ViewinitializeviewDeactivate事件以及Buttonclick事件的事件处理程序。ActionScript 代码已被移到一个单独的文件中,并包含了一个<fx:Script>标签。清单 8–30 显示了ViewonInitializeonViewDeactivate处理程序的代码。

清单 8–30。View事件处理程序为NetStreamVideoView

`private static const SOURCE:String = "http://ia600408.us.archive.org/"+
  "26/items/BigBuckBunny_328/BigBuckBunny_512kb.mp4";

private var video:Video;
private var ns:NetStream;
private var isPlaying:Boolean;
private var timer:Timer;
private var duration:String = "";

private function onInitialize():void {
  video = new Video();
  videoContainer.addChild(video);

var nc:NetConnection = new NetConnection();
  nc.connect(null);

ns = new NetStream(nc);
  ns.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus);
  ns.client = {
    onMetaData: onMetaData,
    onCuePoint: onCuePoint,
    onPlayStatus: onPlayStatus
  };

ns.play(SOURCE);
  video.attachNetStream(ns);

timer = new Timer(1000);
  timer.addEventListener(TimerEvent.TIMER, onTimer);
  timer.start();
}

private function onViewDeactivate():void {
  if (ns) {
    ns.close();
  }
}`

onInitialize处理程序负责所有的设置代码。Video显示对象被创建并添加到它的UIComponent容器中。接下来,创建一个NetConnection,用一个null值调用它的connect方法。这告诉NetConnection它将播放来自本地文件系统或 web 服务器的 MP3 或视频文件。如果不同的参数被传递给它的connect方法,那么NetConnection也可以用于 Flash Remoting 或者连接到 Flash 媒体服务器。

下一步是通过在构造函数中传递对NetConnection的引用来创建NetStream对象。根据玩家的复杂程度,你可能会对从NetStream物体接收到的几个事件感兴趣。 NET_STATUS事件将通知您缓冲状态、回放状态和错误情况。还有附加到NetStream的客户端属性的metaDatacuePointplayStatus事件。客户端只是一个定义某些属性的Object;它不必是任何特定的类型。在前面的清单中,我们只是使用了一个对象文字来声明一个具有所需属性的匿名对象。

metaData事件将为您提供重要信息,如视频的宽度、高度和持续时间。当视频中嵌入的提示点到达时,cuePoint事件会通知您。处理playStatus甚至会让你知道视频什么时候结束。这些事件处理程序如清单 8–31 所示。

最后的步骤是开始播放NetStream,将其附加到Video显示对象,并创建和启动计时器,该计时器将每秒更新一次时间显示。

清单 8–31。??NetStream事件处理者

`private function onMetaData(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);
}

private function onCuePoint(item:Object):void {
  // Item has four properties: name, time, parameters, type
  log("cue point "+item.name+" reached");
}

private function onPlayStatus(item:Object):void {
  if (item.code == "NetStream.Play.Complete") {
    timer.stop();
    updateTimeDisplay(duration);
  }
}

private function onNetStatus(event:NetStatusEvent):void {
  var msg:String = "";

if (event.info.code)
    msg += event.info.code;

if (event.info.level)
    msg += ", level: "+event.info.level;

log(msg);
}

private function log(msg:String, showUser:Boolean=true):void {
  trace(msg);
  if (showUser)
    logger.text += msg + "\n";
}`

onMetaData处理器使用视频的widthheight使其在View中居中。它还保存视频的duration,以便在时间显示Label中使用。在onPlayStatus处理程序中,我们检查这是否是一个NetStream.Play.Complete通知,如果是,停止更新时间显示的计时器。onCuePointonNetStatus处理程序仅用于演示目的,它们的输出被简单地记录到调试控制台和可选的屏幕上。

清单 8–32 显示了与NetStreamVideoView相关的剩余代码。onPlayPause函数作为ToggleButton的点击处理程序。根据ToggleButtonselected状态,它将暂停或恢复NetStream并启动或停止更新timeDisplayLabel的定时器。onTimer函数是那个Timer的处理程序。它将使用NetStreamtime属性,格式化为minutes:seconds字符串,来更新Label

清单 8–32。 播放,暂停,NetStream 读取属性

`private function onPlayPause():void {
  if (playBtn.selected) {
    ns.resume();
    timer.start();
  } else {
    ns.pause();
    timer.stop();
  }
}

private function onTimer(event:TimerEvent):void {
  updateTimeDisplay(formatSeconds(ns.time));
}

private function updateTimeDisplay(time:String):void {
  if (duration)
    time += " / "+duration;

timeDisplay.text = time;
}

private function formatSeconds(time:Number):String {
  var minutes:int = time / 60;
  var seconds:int = int(time) % 60;

return String(minutes+":"+(seconds<10 ? "0" : "")+seconds);
}`

Figure 8–11 显示了所有这些代码在低 dpi Android 设备上运行的结果。像这样的小型播放器更适合这种类型的屏幕。

images

图 8-11。 运行在低 dpi 设备上的基于NetStream的最小视频播放器

正如你所看到的,在创建我们基于极简NetStream的视频播放器的过程中,涉及了更多的代码。但是,如果您需要轻量级视频播放器实现的终极灵活性,NetStreamVideo类的组合将提供您需要的所有功能。

在播放视频这一节的开始,我们简单地提到了 Stage Video。一旦在 Android 上得到支持,它将允许您基于NetStream的视频播放器利用 H.264 视频的硬件加速解码和渲染。Adobe 提供了一个非常有用的“入门”指南来帮助你转换你的NetStream代码以使用 StageVideo 而不是Video显示对象。如果你喜欢不费吹灰之力就让自己适应未来,你可以利用第三个选项在 Android 上编写视频播放器:OSMF 库。这是我们下一节的主题,当它在 Android 上可用时,它将自动利用 StageVideo。

与 OSMF 玩视频

开源媒体框架是 Adobe 发起的一个项目,旨在创建一个库,收集编写基于 Flash 的媒体播放器的最佳实践。它是一个全功能的媒体播放器,被抽象成一些易于使用的类。该库允许您快速创建用于 Flex 和 Flash 应用的高质量视频播放器。OSMF 包含在 Flex 4 SDK 中,但是您也可以从项目网站下载最新版本。 10 清单 8–33 显示了OSMFVideoView的 MXML 代码。这里显示的用户界面代码与NetStreamVideoView的清单 8–29 中的代码几乎完全相同。本质上,我们只是用基于 OSMF 的MediaPlayer实现替换了基于NetStream的后端。


9 Adobe 公司,“舞台视频入门”,www.adobe.com/devnet/flashplayer/articles/stage_video.html,2011 年 2 月 8 日

10http://sourceforge.net/projects/osmf.adobe/files/10

清单 8–33。《MXML 宣言》为OSMFVideoView

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        initialize="onInitialize()"
        viewDeactivate="onViewDeactivate()"
        actionBarVisible="false"
        backgroundColor="black">

<fx:Script source="OSMFVideoViewScript.as"/>

<mx:UIComponent id="videoContainer" width="100%" height="100%"/>
  <s:HGroup bottom="2" left="30" right="30" height="36" verticalAlign="middle">
    <s:ToggleButton id="playBtn" click="onPlayPause()" selected="true"

skinClass="spark.skins.spark.mediaClasses.normal.PlayPauseButtonSkin"/>
    <s:Label id="timeDisplay" color="gray" width="100%" textAlign="right"/>
  </s:HGroup>
</s:View>`

清单 8–34 显示了将用于实现视频播放器的 OSMF 类的初始化代码。我们将包含电影 URL 的实例URLResource传递给LightweightVideoElement构造函数。OSMF MediaElement是正在播放的媒体类型的接口。LightweightVideoElement是一个代表视频的专门化,支持渐进式下载和简单的 RTMP 流。还有一个名为VideoElement的类支持更多的流协议,但是对于我们的目的来说,LightweightVideoElement拥有所有需要的功能。

一旦LightweightVideoElement被创建,它就被传递给 OSMF MediaPlayer类的构造函数。MediaPlayer是一个类,通过它你可以控制视频的播放。它能够调度许多不同的事件,这些事件可以用来获取关于MediaPlayer的状态和状况的信息。在接下来显示的示例代码中,我们处理了mediaSizeChange事件以使视频显示在View上居中,处理了timeChangedurationChange事件以更新timeDisplayLabel,处理了complete事件以通知我们视频何时结束播放。

MediaPlayer本身不是显示对象。相反,它提供了一个可以添加到显示列表中的displayObject属性。在本例中,它被添加为videoContainerUIComponent的子节点。我们做的最后一点初始化工作是使用currentTimeUpdateInterval属性请求我们每秒只更新一次视频播放器的currentTime,而不是默认值的每 250 毫秒。视频将自动开始播放,因为MediaPlayerautoPlay属性的默认值是true

清单 8–34。 初始化代码为MediaPlayer

`import org.osmf.elements.VideoElement;
import org.osmf.events.DisplayObjectEvent;
import org.osmf.events.MediaElementEvent;
import org.osmf.events.TimeEvent;
import org.osmf.media.MediaPlayer;
import org.osmf.media.URLResource;
import org.osmf.net.NetLoader;

privatestaticconst sourceURL:String = "http://ia600408.us.archive.org"+
  "/26/items/BigBuckBunny_328/BigBuckBunny_512kb.mp4";

privatevar player:MediaPlayer;
privatevar duration:String;

privatefunction onInitialize():void {
  var element:LightweightVideoElement;
  element = new LightweightVideoElement(new URLResource(sourceURL));

player = new MediaPlayer(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;
}

privatefunction onViewDeactivate():void {
  if (player)
    player.stop();
}

privatefunction onPlayPause():void {
  if (player.playing) {
    player.play();
  } else {
    player.pause();
  }
}`

在刚刚显示的onViewDeactivate处理程序中,我们确保当View被停用时停止播放器。您还可以看到播放/暂停ToggleButtonclick处理程序。它只是调用了MediaPlayerplaypause方法,这取决于玩家当前是否在玩游戏。

清单 8–35 通过显示MediaPlayer事件处理程序,继续列出OSMFVideoView的脚本代码。每当媒体改变大小时,就会调用onSize处理程序。我们使用这个处理程序将MediaPlayerdisplayObject置于View的中心。当玩家知道正在播放的视频的总时长时,就会调用onDurationChange处理程序。我们使用这个处理程序将持续时间存储为格式化字符串,供timeDisplayLabel使用。每秒调用一次onTimeChange处理程序——正如我们在初始化期间所请求的那样——这样我们就可以更新timeDisplayLabel。最后,onVideoComplete用于演示目的。我们的实现只是将一条消息打印到调试控制台。

清单 8–35。OSMF 事件处理者

`privatefunction onSize(event:DisplayObjectEvent):void {
  player.displayObject.x = (width - event.newWidth) / 2;
  player.displayObject.y = (height - event.newHeight) / 2;
}

privatefunction onDurationChange(event:TimeEvent):void {
  duration = formatSeconds(player.duration);
}

privatefunction onTimeChange(event:TimeEvent):void {
  updateTimeDisplay(formatSeconds(player.currentTime));
}

privatefunction onVideoComplete(event:TimeEvent):void{
  trace("The video played all the way through!");
}

privatefunction updateTimeDisplay(time:String):void {
  if (duration)
    time += " / "+ duration;

timeDisplay.text = time;
}

privatefunction formatSeconds(time:Number):String {
  var minutes:int = time / 60;
  var seconds:int = int(time) % 60;

return String(minutes+":"+(seconds<10 ? "0" : "")+seconds);
}`

与滚动你自己的基于NetStream的视频播放器相比,有了 OSMF,你可以用更少的代码获得所有的功能。您还可以利用视频专家编写的代码。如果你需要它提供的所有功能,在 OSMF 上构建你的视频播放器是不会错的。运行时,这个基于 OSMF 的视频播放器的外观和行为与图 8–11 中所示的一模一样。

录像机示例

本章的最后一个例子是前面提到的录音机的视频模拟。VideoRecorder 应用将使用 Android 摄像头接口来捕获视频文件,然后允许用户立即在 Flex 应用中播放它。本例的源代码可以在本书源代码的examples/chapter-08目录下的 VideoRecorder 示例应用中找到。

你可能还记得第七章中的提到过,CameraUI类可以用来通过原生的 Android 摄像头接口捕捉视频和图像。

这个例子将使用一个 OSMF MediaPlayer来播放捕获的视频。清单 8–36 显示了CameraUI类和MediaPlayer类的初始化代码。

清单 8–36。 初始化CameraUIMediaPlayer

`import flash.media.CameraUI;
import org.osmf.elements.VideoElement;
import org.osmf.events.DisplayObjectEvent;
import org.osmf.events.MediaElementEvent;
import org.osmf.events.TimeEvent;
import org.osmf.media.MediaPlayer;
import org.osmf.media.URLResource;
import org.osmf.net.NetLoader;

privatevar cameraUI:CameraUI;
privatevar player:MediaPlayer;
privatevar duration:String;

privatefunction onInitialize():void {
  if (CameraUI.isSupported) {
    cameraUI = new CameraUI();
    cameraUI.addEventListener(MediaEvent.COMPLETE, onCaptureComplete);

player = new MediaPlayer();

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。 开始并完成视频捕捉

`privatefunction onCaptureImage():void {
  cameraUI.launch(MediaType.VIDEO);
}

privatefunction onCaptureComplete(event:MediaEvent):void {
  player.media = new VideoElement(new URLResource(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,并将其分配给MediaPlayermedia属性。然后,我们可以开始播放视频,并调整playBtn的属性,使其与应用的状态保持一致。如果videoContainer已经添加了一个displayObject,我们删除它,然后添加玩家新的displayObject

大多数事件处理代码与上一节给出的OSMFVideoView代码相同。清单 8–38 中显示了两个不同之处。

清单 8–38。 MediaPlayer事件的处理略有不同

`privatefunction onSize(event:DisplayObjectEvent):void {
  if (player.displayObject == null)
    return;

var scaleX:int = Math.floor(width / event.newWidth);
  var scaleY:int = Math.floor(height / event.newHeight);
  var scale: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;
}

privatefunction onVideoComplete(event:TimeEvent):void{
  player.seek(0);
  playBtn.selected = false;
}`

在这种情况下,onSize处理程序将尝试缩放视频尺寸,使其更接近显示器的尺寸。注意检查player.displayObject是否是null。当从一个捕获的视频切换到下一个视频时,可能会发生这种情况。因此,我们必须小心不要试图在displayObject不存在时对其进行缩放。另一个区别在于onVideoComplete处理程序。由于用户可能希望多次观看他们捕获的视频剪辑,我们通过将播放头重新定位到开头并重置播放/暂停按钮的状态来重置视频流。Figure 8–12 显示了在 Android 设备上运行的应用。

images

图 8–12。 抓拍短视频后的录像机示例应用

总结

随着移动设备变得越来越强大,在移动设备上欣赏媒体的能力将变得越来越普遍。现在,您已经掌握了在自己的移动应用中利用 Flash media APIs 的能力所需的知识。本章涵盖了与在 Flash 平台上播放各种类型的媒体相关的各种主题。特别是,您现在知道了以下内容:

  • 如何使用SoundEffect类嵌入和播放音效
  • 如何使用Sound类加载 MP3 文件
  • 如何使用SoundChannelSoundTransform类控制声音的回放、音量和平移
  • 如何播放动态生成或录制的声音
  • 如何编写可维护和可测试的 Flex 移动应用
  • 如何为 Flex 4 移动应用编写自定义控件
  • 如何使用 Spark VideoPlayer组件、NetStream类和 OSMF 库播放视频
  • 如何与CameraUI类接口以捕获视频,然后在 Android 应用的 AIR 中播放捕获的视频

在下一章中,我们将继续编写真实的 Flex 移动应用的主题,看看在团队中工作和利用设计师-开发人员工作流的一些方面。

九、设计人员——开发人员工作流

无论您是设计师还是开发人员,现在都是进入移动开发的激动人心的时刻,这是一个充满潜力和机会的年轻行业。但是移动开发行业确实面临着其他软件开发项目所面临的同样的挑战,那些通信和工作流的挑战。图 9–1 取笑软件项目中的沟通和解释问题。这幅漫画与许多公司的做法相差无几。一个项目可能有许多实际的需求,但是大多数参与的人只会表达那些他们关心或感兴趣的需求。

images

图 9–1。 【项目如何真正工作】出自[www.projectcartoon.com](http://www.projectcartoon.com) 1 知识共享署名下许可 3.0 未许可许可:[creativecommons.org/licenses/by/3.0/](http://creativecommons.org/licenses/by/3.0/)


2006 年 7 月 24 日, 1 “项目如何真正运作”,【http://www.projectcartoon.com】??

项目可能会在很多地方失败。一个智能的工作流可以真正帮助缓解这些痛点,这样客户要求的就是设计师设计的,开发者执行的。但是首先必须理解设计者和开发者的角色,以及他们使用的工具。

视觉设计师的角色

设计师的角色是理解客户的需求,将这些转化为应用,并为其创造视觉设计。设计师与客户讨论应用应该如何工作,GUI 如何完成用户故事,以及为什么它会这样工作。这是一条双行道,因为客户的输入也被考虑在内。设计师也根据开发者的需求调整视觉设计。有时候,开发人员可以预见设计人员没有意识到的技术挑战,在这种情况下,他们可以也应该合作解决问题。有时候合作只是澄清事情是如何运作的。其他时候,它可能导致设计和技术限制之间的折衷。

“设计是一种有意识的、直觉的努力,旨在建立有意义的秩序。”

–维克多·帕帕内克,设计师兼教育家

从 Adobe Device Central 开始

Adobe Device Central 简化了移动电话、平板电脑和消费电子设备的内容创建。它允许设计人员和开发人员计划、预览和测试移动体验。您可以通过动态更新的在线设备库访问最新的设备配置文件,并在设备外观环境中模拟背光超时和阳光反射等显示条件,以针对现实条件调整设计。

提示欲了解更多信息,请参见[www.adobe.com/products/devicecentral.html](http://www.adobe.com/products/devicecentral.html)

使用设备中心

Adobe Device Central CS5.5 与大多数设计程序集成,包括 Photoshop、Illustrator、Fireworks 和 Flash,使您能够利用手机数据,从移动项目的开始到最终启动提高工作效率。

从 Device Central 创建新文档

开始新的移动项目时,Device Central CS5.5 是一个不错的起点。当您启动 Device Central 时,会出现一个欢迎屏幕(参见 Figure 9–2)。

images

图 9–2。 从 CS5 中部设备启动新的 Fireworks 文档;从 Device Central CS4 开始,添加了 Captivate 和 Fireworks 文件格式。

  1. 从右边的栏中点击您想要创建的文件类型(参见 Figure 9–3)开始一个新项目。中间的消息指出,“要开始,请在‘文本设备’面板中选择设备。”

  2. Click the Browse button, located in the upper right corner. This will display a list of devices to create and test against (see Figure 9–3). images

    图 9–3。 浏览设备库中的设备,按名称排序,显示创建者评级或搜索特定设备。

  3. 选择要测试的设备,并将其拖至测试设备面板。

  4. Double-click the device name to view the device details (see Figure 9–4). images

    图 9–4。 点击并拖动设备到测试设备面板,将其添加到要测试的设备

  5. 准备好基于所选配置文件创建新文件后,双击左侧列出的测试设备中的配置文件。然后,点击右上角的创建(参见图 9–5)。

images

图 9–5。 从测试设备面板双击设备配置文件。单击右上角的 Create,基于该配置文件创建一个新文档。

新文档会自动设置为适合目标设备的正确显示尺寸和屏幕方向。现在,您已经准备好创建自己的手机设计了。在分析设备和模拟内容外观方面,Device Central 的帮助再大也不为过。这有助于加快设计工作流程,肯定比购买许多不同的设备要好。

就组织和生产力而言,Fireworks 的一个流行功能是能够在单个文件中创建具有不同尺寸、屏幕方向甚至文档分辨率的多个页面。这意味着您可以轻松地同时处理纵向和横向布局,这在针对多点触摸设备和使用加速度计时非常方便。你甚至可以将应用图标和你的主要内容保存在同一个文件中。除了 Fireworks,没有任何 Adobe 产品能做到这一点。

在 Device Central 中预览内容

为移动设备进行设计时,在设计过程中有些时候,您可能希望在实际的手机环境中预览您的作品。最快、最简单的方法是从 Photoshop、Illustrator、Flash 或 Fireworks 中启动预览。

  1. 在 Photoshop 中,选择文件images保存为网络&设备…
  2. 在左下角,选择“设备中心…”
  3. 现在,您将能够看到您的设计在各种设备上的外观。
  4. 当您在 Device Central 中查看您的工作时,您可以通过在“测试设备”面板中双击不同的设备配置文件来更改设备外观。
  5. 您还可以使用 Device Central 中的显示面板来调整照明或反射,以便在不同的照明条件下测试您的内容(请参见 Figure 9–6)。

images

图 9–6。 在 Device Central 中预览三星 Galaxy S 上的设计,选择室内反射

创建自定义配置文件

您可能想要创建自定设备描述文件有几个原因:

  • 您会注意到桌面上仿真工作区中显示的内容与您在实际设备上看到的内容之间存在差异。
  • 出于演示目的,您希望修改设备外观(例如,删除或添加运营商徽标)。
  • 您制造设备并需要创建新的配置文件(一旦设备发货,自定义配置文件就可以分发给社区)。

创建自定描述文件的第一步是制作现有设备描述文件的副本,用作模板。我建议选择尽可能与您想要的自定义配置文件相似的内容。原始配置文件和您的自定义配置文件之间的相似之处越多,您以后在编辑单个数据点时需要做的工作就越少。

  1. 在 Device Central 中,单击“浏览”(位于右上角),以便进入“浏览”工作区。
  2. 如果您移动了面板,您可以随时通过选择窗口 images 工作区 images 重置浏览来恢复默认设置。
  3. 右键单击 Flash Player 10.1 32 320×480 多点触控配置文件,并选择创建可编辑副本(参见图 9–7)。
  4. 为配置文件键入新名称,例如,键入“My_Multitouch_320×480”,然后点按“好”。

images

图 9–7。 创建概要文件的可编辑副本

请注意,如果您计划与他人共享您的自定义配置文件,您应该给他们起一个既独特又有描述性的名称。此外,尽可能完整地填写所有字段。这是一个显而易见的最佳实践,有助于为整个社区的共同利益发展一个准确和完整的数据集。

在右边,您现在应该会看到一个圆形,在设备皮肤的正上方有一个铅笔,表示该配置文件现在是可编辑的。类似地,当您将指针悬停在任何属性上时,比如输入控件或语言,会出现相同的铅笔图标。如果悬停时属性不显示铅笔图标,则该属性不可编辑。

接下来,您可以直接从 CS5 设备中心编辑设备配置文件:

  1. 将指针悬停在语言上;出现铅笔图标,表示该属性是可编辑的。
  2. 单击语言并选择要显示的语言。
  3. 单击复选标记确认您的选择。

您选择的语言现在应该显示在您的自定义配置文件中。

重复这些步骤,从 Device Central 中编辑所有自定义设备配置文件信息。这种从界面编辑配置文件数据的简单而直接的方法是一种真正的时间节省,是对早期版本的巨大改进。

Adobe Photoshop

Adobe Photoshop CS5 非常注重摄影,但也用于创建应用设计,因为它在设计创建和图像编辑方面非常灵活。Adobe Photoshop CS5 拥有卓越的图像选择、图像润饰和逼真绘画的突破性功能,以及广泛的工作流程和性能增强。

提示:更多信息见[www.adobe.com/products/photoshop.html](http://www.adobe.com/products/photoshop.html)

一旦在 Photoshop 中创建了设计(图 9–8),工作流程中的下一步将是将这些图形引入 Flash Professional 或 Flash Builder 进行进一步开发。这可以通过分别导出每个图像,或将 Photoshop 文件(.psd)直接导入 Flash Professional 来完成。

images

图 9–8。 一个在 CS5 Adobe Photoshop 中创建的应用设计,包括形状层、文本和智能对象,仅举几个例子

Photoshop 到 Flash Professional 的工作流程

Flash Professional CS3 中引入的一个令人兴奋的功能是导入 PSD 文件的能力(图 9–9)。在导入时,Flash Professional 使您能够确定如何导入每个层。例如,您可以在 Flash Professional 中将文本层作为可编辑文本导入。在 Flash Professional 中,形状层也可以转换为可编辑的形状。甚至电影剪辑也可以从光栅图形创建,包括实例名称。Photoshop 中的图层在 Flash Professional 中可以显示为图层,并带有仍可编辑的图层效果。甚至物品的位置都可以保持。最终结果是 Flash Professional 中的完整设计,可以制作动画并进一步开发用于移动设备。

images

图 9–9。 将原 Photoshop 文件,导入到 Flash Professional 中;每个层可以不同方式导入,保持文本、形状层,甚至层效果。

尽管导入 Photoshop 文件非常容易而且非常有用,但是您必须注意一些事情。请注意,当导入许多层时,文件会变大,请考虑合并它们。例如,如果在构成背景的不同图层上有多个图形,请考虑在导入之前在 Photoshop 中合并这些图层。此外,考虑在 Flash 中绘制矢量元素,而不是导入它们。这将使您在编辑时有更多的控制权。如果有帮助的话,您甚至可以从 Photoshop 中导入一个图形作为向导,同时在 Flash Professional 中将所有部分创建为矢量元素。

如果 Photoshop 文件相当复杂,由多个图层组成背景,请考虑将这些图层合并为一个背景图层。一般的规则是,如果图形不动,看看能不能和其他图形合并。

Photoshop 到 Flash Builder 的工作流程

Flash Builder 不像 Flash Professional 那样导入 Photoshop 文件。相反,需要从 Photoshop 中导出单独的图像。最有效的方法是将每个元素分离成自己的 Photoshop 文件,并导出适当的文件类型(图 9–10)。

images

图 9–10。 单独 PSD 文件中的单个图形准备导出为 PNG、JPG 或 GIF。请务必保留原始 PSD 文件,以防以后需要进行更改。

在 Photoshop 中,导出图形的最佳方式是使用“文件”菜单下的“存储为 Web 和设备所用格式”选项。这使您能够选择想要导出的格式并查看其质量(Figure 9–11)。在 Flash Builder 中,您可以导入适当的文件类型,无论它是 JPG、GID、PNG、SWF 还是 FXG。

images

图 9–11。 使用文件从 Photoshop 中导出图形images保存为 Web 和设备格式

图形文件格式

基本上有四种不同的文件类型可以在 Flash 应用中使用。您选择哪一个取决于图形的内容。

PNG-24(便携式网络图形,24 位深度)

PNG-24 可能是富图形最流行的图形文件类型之一,因为它允许不同级别的透明度和 24 位颜色。还有一个 PNG-8,它不允许透明,但文件大小更小,因为颜色深度是 8 位(256 色)。

GIF(图形交换格式)

GIF 是一种 8 位文件格式,允许多达 256 种颜色,这使文件大小保持较小。由于颜色数量有限,gif 适用于边缘锐利的线条艺术和平面颜色,如徽标。相反,该格式不用于摄影或带有渐变的图像。gif 可以用来存储游戏的低颜色精灵数据。gif 也可以用于小动画,因为它们可以包含多个帧。GIF 文件也可以有透明度,但不像 PNG-24 文件那样有不同的透明度。GIF 中的每个像素要么不透明,要么透明。

联合图像专家组

JPG 文件通常用于摄影图像。这种格式具有有损压缩,这意味着图像可以被压缩,导致文件较小,但这可能会导致图像质量的一些损失。将图像压缩到 JPG 是在保持图像质量的同时保持文件大小较小的一个很好的平衡。

FXG (Flash XML 图形)

Adobe Flash Platform 基于 XML 的图形交换格式使设计人员能够为 web、交互式和 RIA 项目的开发人员提供更多可编辑、可工作的内容。FXG 用作跨应用文件支持的图形交换格式。它基于 XML,可以包含图像、文本和矢量数据。Flash Professional、Fireworks 和 Illustrator 都可以创建 FXG 文件。然后,这些文件可以在 Flash Professional 或 Flash Builder 中使用(参见 Figure 9–12)。

images

图 9–12。 在 Flash Builder 中打开的一个 FXG 文件,包含矢量、文本和位图数据

所有这些文件格式都可以从大多数图像编辑程序中创建。所使用的程序在很大程度上取决于设计师最喜欢什么,但是如果我们更客观地看一看,你会注意到每个程序在移动 Flash 开发方面都有自己独特的优势。例如,FXG 格式非常灵活,可以在 Flash Builder 中向开发人员展示各种文本和矢量图形元素。PNG-24 文件格式在设计师需要具有不同透明度的像素级完美图形时非常有用。如果照片有多种颜色和阴影,并且不需要透明度,JPG 格式非常适合。最后,GIF 非常适合平面图形,比如徽标。

Adobe Illustrator

Adobe Illustrator 帮助设计师为几乎所有项目创建矢量作品。Illustrator 拥有复杂的绘图工具、自然笔刷和大量内置的省时工具,可用于矢量图像编辑。Illustrator CS5 允许用户在文件的像素网格上精确地创建和对齐矢量对象,以获得干净、清晰的光栅图形。用户还可以利用光栅效果,如投影、模糊和纹理,并跨媒体保持其外观,因为图像与分辨率无关。这使得 Illustrator 成为一个很好的开始创建图形的地方,不管输出是什么。

提示:更多信息见[www.adobe.com/products/illustrator.html](http://www.adobe.com/products/illustrator.html)

Illustrator 转 Flash 专业工作流程

使用 Illustrator,您可以创建移动设计,并将单个图形转换为电影剪辑元件。元件的每个实例都可以有一个实例名,就像在 Flash 中一样。可以将影片剪辑元件实例复制并粘贴到 Flash Professional 中。Flash 维护电影剪辑甚至实例名称(参见图 9–13)。

images

图 9–13。 在 Illustrator(左)中,您可以创建可以直接复制并粘贴到 Flash Professional(右)中的电影剪辑元件。符号和实例名称保持不变。

土坯烟花

Adobe Fireworks CS5 软件提供了为 Web 或几乎任何设备创建高度优化的图形所需的工具。Fireworks 允许您创建、导入和编辑矢量和位图图像。

Fireworks 到 Flash Builder 工作流

在 Fireworks 中创建图形后(参见 Figure 9–14),可以将其导出为最流行的图形格式,包括 FXG 和 MXML,专门用于 Flash Builder。以基于 XML 的 FXG 格式导出有助于确保为 Adobe Flash Builder 精确转换丰富的应用设计。FXG 和 MXML 都是基于 XML 的格式,可以包含可以在 Flash Builder 中打开和编辑的矢量图形和文本(参见图 9–15)。基于位图的图像被外部引用。

images

图 9–14。 烟花中的画面设计

images

图 9–15。 FXG-和 MXML-创建的文件在 Flash Builder 中打开;请注意第 12 行的文本引用以及第 5–7 行的文本标签属性。位图图像在文件外部。

提示:更多信息见[www.adobe.com/products/fireworks.html](http://www.adobe.com/products/fireworks.html)

开发者的角色

从技术角度来看,开发人员应该能够将最基本的设计和技术规范转化为实际的应用。优秀的开发人员在许多方面不同于他们更普通的同事。一些重要的例子如下:

  • 培养理解能力:几乎任何人都可以盲目地遵循为他们制定的指令,但是优秀的开发人员会重视理解他们正在做的事情,这样他们就可以随时发现潜在的问题和改进的机会。
  • 掌握结构和应用架构:在软件开发中,没有任何一种“正确的”做事方式,因为同一个问题通常可以用许多种方式来解决。然而,通常有一些方法比其他方法“更正确”。掌握众所周知的数据结构和应用架构意味着以最直接的方式解决问题,并以最灵活和有效的方式构建应用。
  • 专业化:专业化展示了不断学习和成长的意愿,这有助于开发人员与众不同。对移动开发技术的特殊掌握使开发人员成为任何公司的宝贵员工和资源。

开发者的工具箱

开发人员的工具箱有限。他们应该知道他们所选择的一种或多种语言的开发环境(包括编译器和调试器),以及开发团队的每个成员都需要使用的一些常用工具。这些工具通常被集成到一个平台中,该平台既充当编译器又充当调试器。这通常是用于学习语言的相同工具,因此学习开发环境通常不是一个大挑战。以下开发环境通常用于移动 Flash 开发。

Adobe Flash Professional

Adobe Flash Professional CS5.5 是制作富有表现力的交互式内容的领先创作环境,是设计人员和开发人员共享的工具。ActionScript 是使用的编码语言,可以在二进制 FLA 文件格式中编写,其中可以包含图形、声音、字体、动画,有时还可以包含设计人员添加的视频。代码片段是在 Flash Professional CS5 中引入的,可用于加速开发。ActionScript 也可以在外部 ActionScript 文件(.as)中编写,这是为Document类和其他对象类例行完成的。通常这取决于项目类型来决定动作脚本将被写在哪里。对于较小的项目,在 FLA 中编写 ActionScript 就可以了。对于较大的项目,许多开发人员更喜欢外部 ActionScript 文件来帮助组织他们的代码。

Flash Professional CS5.5 包括舞台元件栅格化,以提高移动设备上复杂矢量对象的渲染性能。此外,还添加了 20 多个新的代码片段,包括用于创建移动和 AIR 应用的代码片段。在通过 USB 电缆连接的支持 Adobe AIR 的设备上可以进行源代码级调试,直接在设备上运行内容。

提示:更多信息见[www.adobe.com/products/flash.html](http://www.adobe.com/products/flash.html)

Flash 专业工作流程

设计师通常要么在 Flash Professional 中创建图形,要么从其他来源导入图形。FLA 可以在会议中用作原型,向客户展示最终的应用将如何工作,和/或它可以用作最终的源。最终结果如下(参见图 9–16)。

  • 客户端请求应用。
  • 设计师创建一个初始设计。
  • 设计者给开发者一个 FLA 或图形文件。
  • 开发者对设计进行编程,并从设计者的 FLA 中合并图形,或者导入设计者的图形文件。
  • 客户请求更改。
  • 如果设计发生变化,设计师会发送新的图形文件。
  • 开发者用新的图形/动画更新应用。

images

图 9–16。 典型的客户、设计师、开发人员工作流程

Flash Builder 4.5

Adobe Flash Builder 4.5(以前称为 Adobe Flex Builder)是一个基于 Eclipse 的开发工具,用于使用 ActionScript 和开源 Flex 框架快速构建富有表现力的移动、web 和桌面应用。Flash Builder 4.5 允许开发人员为一个或多个移动平台(Android、BlackBerry 或 iOS)构建独立的 Flex/ActionScript 应用。设计和代码视图支持使用移动就绪组件进行移动开发。使用移动 Adobe AIR 运行时仿真器在桌面上测试移动应用,或者使用一键式打包、部署和启动流程在本地连接的移动设备上进行测试。开发人员可以将所需资源部署、打包和签名为特定于平台的安装程序文件,以便上传到移动应用分发站点或商店。

Flash Builder 工作流程

Flash Builder 可以导入许多流行的图形文件格式(参见 Figure 9–17)。内容应该决定将使用什么类型的文件。对于摄影,可以使用 JPG。如果有动画,将需要一个 SWF 文件。大概最灵活的文件格式是 FXG。它是一种基于 XML 的格式,公开了大量内容,使开发人员能够进一步编辑或在需要时进行动态更改。

images

图 9–17。 将图形文件导入 Flash Builder

在 Flex 框架中使用 Flash Builder 时,情况与 FLA 工作流略有不同。首先,没有 Fla。Flex 就像传统的 web 开发一样。你所有的文件都在一个文件夹中,由开发人员来组织它们并将它们全部签入到源代码控制中(如果正在使用的话)。代码也在适当的文件夹中公开和组织,或者作为 MXML (Flex 框架)文件或者作为(ActionScript)文件。因此,设计师目前无法轻松地在他们自己的设计“沙盒”中游戏,就像他们可以在 Flash Professional 中使用自己的 FLA 一样。这有利有弊。好处是没有设计师可以编辑开发者的作品。缺点是设计师不能检验他或她的设计。决定使用 Flash Builder 还是 Flash Professional 工作流取决于开发人员的技能和偏好。

总结

一个好的工作流程真的可以决定一个项目成功还是失败。你可以在一个项目中拥有最好的设计师和开发人员,但是如果他们不能有效地一起工作,交换想法和素材,所有这些都很容易失去。设计师设想的可能不是开发人员执行的,项目经理解释的可能不是最初要求的。你可以很容易地看到一个项目在很多地方可能会失败。一个好的工作流可以缓解过程中的许多痛点,并且可以很容易地确定一个项目是否失败。

十、性能调整

业界认为闪存技术发展缓慢。媒体上的负面言论进一步强化了这一点,例如苹果公司首席执行官史蒂夫·乔布斯在他的《关于 Flash 的思考》中称“Flash 在移动设备上的表现不佳”

虽然可以用 Flash 或任何其他移动技术编写运行缓慢的应用,但使用正确的工具和技术,您也可以创建快速、响应迅速的应用。就像本机应用一样,Flash 技术让您可以利用硬件加速的视频播放和 GPU 加速的渲染。正确使用这些技术可以显著提高应用的性能,减少电池消耗。

还很容易陷入这样一个陷阱:将针对桌面应用优化的现有内容用于移动应用。移动设备的屏幕更小,处理器更慢,内存更少,网络连接通常更慢或不可靠。如果您在构建应用时考虑到这些约束,并经常在目标移动设备上进行测试,那么您将会获得更好的结果。

在本章中,您将详细了解 Flash 运行时是如何工作的,从而理解限制您的应用性能的关键因素。然后,我们将深入研究几个不同的性能敏感领域,包括图像、媒体和文本。在此过程中,我们将回顾 ActionScript 和 Flex 中专门为优化移动内容而引入的新 API,您应该充分利用这些 API。

总会有一些写得很差的 Flash 应用让批评者指出 Flash 不适合移动设备的原因。但是,通过遵循本章中的建议和准则,您将确保您的应用不是其中之一。


2011 年 4 月,www.apple.com/hotnews/thoughts-on-flash/,苹果公司

移动性能调优基础知识

性能调优移动应用与桌面应用并没有太大的不同,并分为相同的三个基本考虑事项:

  • 执行时间
  • 内存使用
  • 应用大小

执行时间是在显示每一帧之前,应用在处理上花费的 CPU 周期。这可能是您编写的用于准备或更新内容的应用逻辑、您的应用等待外部服务器响应的网络 I/O,或者在底层 Flash 运行时中用于验证或渲染图形的时间。

内存是应用运行时使用的设备 RAM 的数量。这通常会随着应用的执行而增长,直到达到一个稳定状态,不再创建其他对象,或者新对象的数量大致等于被释放对象的数量。内存的持续增长可能表明存在内存泄漏,即没有释放资源或者没有取消对不可见/屏幕外对象的引用。

移动设备增加了额外的复杂性,主系统和 GPU 上都有内存限制。垃圾收集也是一个因素,因为当收集器复制活动对象以释放未使用的内存时,使用的内存通常是实际需要的两倍。

应用的大小也是一个重要的考虑因素,因为它会影响应用从 Android Market 的初始下载及其启动性能。编译后的 ActionScript 实际上非常紧凑,因此静态素材(如嵌入到项目中的图像和视频)通常会决定应用的大小。

所有这些因素对于决定应用的整体性能都很重要。然而,比执行时间、内存和应用大小的绝对度量更重要的是最终用户感受到的性能。

感知绩效与实际绩效

如果您编写了一个广泛使用的应用,您可能会遇到用户对性能不满意的情况。对于每一个抱怨性能缓慢的用户,都有数十或数百人放弃或停止使用该应用,而不是报告问题。

科罗拉多州立大学的 John Hoxmeier 和 Chris DiCesare 的研究证实了应用性能低、使用率低和用户满意度低之间的相关性。 2 通过用 100 名学生组成的控制组进行测试,他们证明了以下假设:

  • 响应时间越长,满意度越低
  • 不满意导致停止使用
  • 易用性随着满意度的下降而下降

虽然他们是在基于 web 的应用上进行测试,但是这些发现与您在基于 Flash 平台的富客户端应用上的体验非常相似。在这项研究中,花了 6 秒或更短时间的反应被认为是强有力的和足够快的,而花了 9 秒或更长时间的反应被评为非常不利。

注意:在这项研究中,他们还反驳了专家用户更有可能容忍较慢响应时间的假设,所以不要认为这项研究不适用于您的应用。

那么你的应用需要多快才能让用户满意呢?根据本·施奈德曼的说法, 3 你应该保持在以下界限内:

  • 打字、光标移动、鼠标选择:50-150 毫秒
  • 简单频繁的任务 : 1 秒
  • 常见任务:2-4 秒
  • 复杂任务:8-12 秒

此外,通过进度条或旋转的等待图标向用户提供关于长时间运行任务的反馈会极大地改变他们等待的意愿。除了 15 秒之外,这对于确保用户在上下文切换后等待或回来是绝对重要的。

那么这对您的 Flash 应用意味着什么呢?

Flash 应用通常利用动画和过渡来改善用户体验。如果你打算利用这些,它们需要有相对较高的帧速率,以便给用户留下应用响应迅速的印象。这些的目标应该是大约每秒 24 帧或大约 42 毫秒,这是用户感觉动画流畅的最小帧速率。在下一节中,我们将更多地讨论如何调整渲染性能来达到这个目标。


John A. Hoxmeier 和 Chris DiCesare,“系统响应时间和用户满意度:基于浏览器的应用的实验研究。” AMCIS 2000 年会议录 (2000 年)。347 号文件。

Ben Shneiderman,“计算机对人类行为的反应时间和显示率。”计算调查 16 (1984),第 265–285 页。

对于频繁的任务,例如显示细节、提交表单或拖放,您应该将响应时间控制在一秒以内。在执行这些操作时,Flash 应用比 web 应用有明显的优势,因为它们可以在后台执行任务以检索或发送数据时给用户即时反馈。

一些常见的任务,比如加载一个新的页面,或者通过一个标签或者链接来导航,可能会花费更长的时间,但是应该有一个不确定的进度指示器来让用户知道活动正在发生。此外,合理使用过渡动画可以使加载看起来比实际发生得更快。

复杂的任务,如搜索或填充大型数据列表,可能需要更长的时间,但应该限制在 12 秒内完成,或者提供一个进度条,指示任务需要多长时间才能完成。通常可以显示中间结果,如部分搜索结果或前几页数据。这将允许用户在后台加载额外数据的同时继续使用应用,从而显著改变感知的等待时间。

调整图形性能

在其核心,Flash 运行时是一个基于帧的动画引擎,处理保留模式图形。即使您正在使用 Flex 之类的高级框架构建应用,了解 Flash Player 的渲染基础也会有所帮助,这样您就可以优化处理和内容以获得最佳性能。

Flash 引擎的心跳是每秒帧数设置,它控制每秒在屏幕上绘制的帧数。虽然性能瓶颈可能会导致每秒帧数减少,但处理的帧数永远不会超过这个数字。

许多图形工具包使用所谓的即时模式渲染来绘制到屏幕上。在即时模式呈现中,应用实现了一个回调,它必须在每个时钟周期重新绘制屏幕内容。虽然这在概念上很简单,并且接近硬件实现的内容,但是它将保存状态和为动画提供连续性的工作留给了应用开发人员。

Flash 使用保留模式图形,您可以创建一个将在屏幕上渲染的所有对象的显示列表,并让框架负责在每个时钟周期渲染和传输最终图形。这更适合动画和图形应用,但基于显示列表的大小和复杂性,可能会耗费更多资源。

弹性跑道

Ted Patrick 为 Flash 播放器如何处理渲染提出了一个非常有用的概念模型,他称之为弹性跑道。 4 如图图 10–1 所示,该模型将每一帧中的工作在代码执行和渲染之间进行拆分。

images

图 10–1。 闪光播放器弹性跑道

代码执行是运行与该帧相关联的任何 ActionScript 所花费的时间,包括因用户输入而触发的事件处理程序、计时器和ENTER_FRAME事件。渲染包括 Flash Player 为准备显示列表、合成图像和将图形传输到屏幕上而进行的处理。为了保持稳定的帧速率,这两个活动的总持续时间不能超过为该帧分配的时间片。

那么你有多少时间来执行你所有的逻辑呢?Table 10–1 列出了一些常见的帧速率,以及处理代码执行和渲染需要多少毫秒。

images


4 特德·帕特里克,“Flash Player 心智模型——弹性跑道”,[ted.onflash.org/2005/07/flash-player-mental-model-elastic.php](http://ted.onflash.org/2005/07/flash-player-mental-model-elastic.php),2005 年 7 月

Flash 播放器的默认帧速率是 24fps 低于这个值的任何值对用户来说都是明显不稳定或滞后的。然而,用户可以轻松感知高达 60fps 的帧速率差异,特别是在有大量运动或滚动的任务中。拍摄高于 60fps 的帧速率通常是不值得的,尤其是考虑到大多数液晶显示器的刷新率上限为 60hz,一些设备的最大帧速率上限为 60。

当尝试诊断低帧速率时,第一步是确定您是否受到长时间代码执行或缓慢渲染的限制。代码执行是两者中比较容易分析的,因为它在您的控制之下,如果它接近或超过您的目标帧速率的总帧长度,这就是您想要开始优化的地方。

减少代码执行时间

如果您的代码执行时间比单个帧周期稍长,您可以通过优化代码来获得足够的性能。这将根据您是使用纯 ActionScript 还是基于 Flex 构建而有所不同。此外,如果您正在进行复杂或长时间运行的操作,可能需要不同的方法。

值得研究的一些常见 ActionScript 代码性能最佳实践包括:

  • 更喜欢向量而不是数组:向量数据类型经过了高度优化,比使用数组进行基本的列表操作要快得多。在某些情况下,比如大型稀疏列表,数组的性能会更好,但这是一个罕见的例外。
  • 尽可能指定强类型 : ActionScript 是动态类型的,允许你省略显式类型信息。但是,如果提供了类型信息,编译器可以生成更高效的代码。
  • 保持构造函数轻便:实时(JIT)编译器不优化变量初始化器或构造函数中的代码,强制代码以解释模式运行。一般来说,对象构造是很昂贵的,应该推迟到元素在屏幕上可见的时候。
  • 明智地使用绑定:绑定引入了额外的开销,这对更新 UI 是有意义的,但在其他地方应该避免。
  • 正则表达式开销很大:尽量少用正则表达式来验证数据。如果需要搜索,String.index 的速度要快一个数量级。

如果您正在编写一个 Flex 应用,您还需要了解以下内容:

  • 最小化组和容器的嵌套:大型对象图的测量和布局可能非常昂贵。通过保持容器尽可能的平坦,你将加速你的应用。这在构建可重复使用的网格或列表呈现器时尤为重要。
  • 更喜欢组而不是容器:新的 Spark 图形库在重新设计时考虑到了性能。因此,与容器相比,组是非常轻量级的,应该用于布局。

如果在调优代码后,代码执行仍然是瓶颈,那么您可能希望将工作负载分散到多个框架上。例如,如果您正在执行命中检测算法,可能无法检查单个帧中的所有对象。但是,如果您可以按区域对对象进行分组,并对它们进行增量处理,则工作可以分布在多个帧上,从而提高应用的渲染速度。

加速渲染

在 CPU 上运行时,Flash 使用高度优化的保留模式软件渲染器将图形绘制到屏幕上。为了渲染每一帧,它遍历 DisplayList 中所有对象的列表,以确定哪些是可见的,哪些需要绘制。

软件渲染器逐行扫描更新区域,通过查看显示列表中每个元素的顺序、位置和不透明度来计算每个像素的值。Figure 10–2 包含一个在 Flash 中创建的示例图形,该图形由几层文本和图形复合而成。

images

图 10–2。 内脏的样本闪现图示 5

当放置在舞台中时,该场景将具有类似于图 10–3 所示的显示列表。

images

图 10–3。 显示样本闪光器官图形列表


5 基于公共领域器官库的图形:米凯尔·海格斯特伦,《内脏》,【http://commons.wikimedia.org/wiki/File:Internal_organs.png

在渲染阶段,Flash 将使用这个显示列表来决定如何在屏幕上绘制每个像素。由于图形是不透明的,并且嵌套只有三层深,这将在屏幕上非常快地呈现。随着显示列表的复杂性增加,您需要特别注意应用中使用的对象类型,以及应用于它们的效果。

提高应用呈现性能的一些方法包括:

  • 保持你的显示列表很小:一个修剪良好的显示列表将帮助 Flash 渲染器节省内存和扫描层次结构的执行时间。如果不再使用对象,请确保为其父对象移除它们。否则,您可以通过动态更改单个元素的可见性来隐藏和显示它们。
  • 使用合适的对象类型:形状或Bitmap是显示列表中最小的对象,仅占用 236 字节。Sprites更重,具有交互和事件处理功能,占用 414 字节。MovieClips是场景中最昂贵的对象,440 字节和额外的开销来支持动画。为了加快渲染速度,您应该选择满足您需求的最简单的对象类型。
  • 避免 alpha、遮罩、滤镜和混合:如果使用这些功能,Flash 渲染引擎无法进行某些优化,这会降低渲染性能。与其使用 alpha 来隐藏和显示对象,不如使用 visibility 标志。遮罩非常昂贵,通常可以用简单的剪切或场景分层来代替。混合模式特别昂贵,应尽可能避免。

如果您正在开发一个基于 Flex 的应用,那么您将需要特别注意 UIComponents、GraphicElements 和 FXG 的使用。Table 10–2 列出了使用这些不同对象类型的利弊。

images

UIComponents是 Flex 中最复杂的对象类型,会显著影响渲染性能,尤其是在表格或列表渲染器中广泛使用时。和 FXG 都是非常轻量级的组件,渲染器可以对它们进行显著的优化。FXG 有轻微的性能优势,因为它在应用构建时被编译成图形,而GraphicsElements则需要在运行时处理。

移动开发中的一个常见错误是专门在桌面模拟器中开发,并等到应用几乎完成时才开始在设备上测试。如果等到有了极其复杂的显示列表,就很难找出哪些元素导致了速度的下降。另一方面,如果您在构建应用时定期进行测试,那么就很容易诊断出哪些更改对性能影响最大。

场景位图缓存

另一种可以用来以牺牲内存为代价提高渲染性能的技术是场景位图缓存。Flash 通过cacheAsBitmapcacheAsBitmapMatrix属性提供内置支持,可以轻松捕捉和替换静态图像,以代替完整的场景层次。这在移动设备上尤其重要,因为在移动设备上,矢量图形操作要慢得多,并且会显著影响您的性能。

cacheAsBitmap

cacheAsBitmapDisplayObjectboolean属性,通过扩展,你在 Flash 和 Flex 中使用的所有视觉元素包括SpriteUIComponent都可以访问这个变量。当设置为 true 时,每次DisplayObject或它的一个子节点改变时,它将获取当前状态的快照并保存到屏幕外缓冲区。然后,对于未来的渲染操作,它将重新绘制保存的屏幕外缓冲区,这对于场景的复杂部分可以快几个数量级。

要在DisplayObject上启用cacheAsBitmap,您需要执行以下操作:

cacheAsBitmap = true;

Flex UIComponent有一个缓存策略,它会根据启发自动启用cacheAsBitmap。您可以通过执行以下操作来覆盖此行为并强制启用cacheAsBitmap:

cachePolicy = UIComponentCachePolicy.ON;

当您有不经常改变的复杂图形时,打开cacheAsBitmap是一项重要的技术,例如矢量渲染的背景。尽管背景是静态的,但是当围绕它移动的其他元素重叠并遮挡了部分背景时,也会触发更新。此外,简单的翻译,如滚动背景,将导致一个昂贵的重绘操作。

为了弄清楚应用重绘的每一帧中屏幕的哪些部分被重绘,可以使用下面的代码启用showRedrawRegions:

flash.profiler.showRedrawRegions(true);

这将在正在更新的屏幕区域周围绘制红色矩形,并且可以通过编程打开和关闭。图 10–4 显示了一个CheckBox控件的例子,它可以让你打开和关闭重绘区域。该控件最近被单击过,所以它周围有一个红色的矩形。

images

图 10–4。 重绘区域调试功能的例子

此选项仅在调试播放器中可用,因此在测试应用时,它将在 AIR 调试启动程序中工作,但在运行时播放器(如移动设备)中部署时,它将不起作用。Figure 10–4 还展示了一个非常简单的每秒帧数监视器,可用于在开发过程中对您的 Flex 应用性能进行基准测试。这两者的完整代码将在下一节构建 Flash Mobile Bench 应用中显示。

虽然cacheAsBitmap是一个非常强大的优化应用重绘的工具,但如果使用不当,它也是一把双刃剑。在cacheAsBitmap设置为 true 的情况下,为每个DisplayObject保留并刷新一个全尺寸的屏幕缓冲区,如果在图形加速模式下运行,这会消耗大量设备内存或耗尽有限的 GPU 内存。

此外,如果你有一个频繁更新的对象或者应用了一个转换,那么cacheAsBitmap只会用不必要的缓冲操作来降低你的应用的速度。幸运的是,对于转换的情况,有一个改进版本的cacheAsBitmap,叫做cacheAsBitmapMatrix,你可以利用它。

cacheasbitmap 矩阵

cacheAsBitmapMatrix也是DisplayObject上的一个属性,和cacheAsBitmap一起工作。为了使cacheAsBitmapMatrix生效,cacheAsBitmap也必须开启。

如前所述,cacheAsBitmap在对对象应用旋转或倾斜等变换时不起作用。这样做的原因是将这样的变换应用到保存的Bitmap会产生缩放伪像,这会降低最终图像的外观。因此,如果您希望将缓存应用于应用了变换的对象,Flash 要求您还为存储在cacheAsBitmapMatrix属性中的Bitmap指定一个变换矩阵。

在大多数情况下,将cacheAsBitmapMatrix设置为识别矩阵将会达到预期效果。屏幕外的Bitmap将被保存在未变换的位置,并且DisplayObject上的任何后续变换将被应用到那个Bitmap。以下代码显示了如何将cacheAsBitmapMatrix设置为识别转换:

cacheAsBitmap = true; cacheAsBitmapMatrix = new Matrix();

如果您利用一个cachePolicy在一个 Flex UIComponent上做同样的事情,您将做以下事情:

cachePolicy = UIComponentCachePolicy.ON; cacheAsBitmapMatrix = new Matrix();

注意:如果你计划在多个对象上设置cacheAsBitmapMatrix,你可以重用同一个矩阵来消除矩阵创建的开销。

这样做的缺点是,最终的图像可能会出现一些轻微的锯齿,尤其是在图像被放大或直线被旋转的情况下。为此,您可以指定一个变换矩阵,在缓冲图像之前放大图像。同样,如果您知道最终的图形将总是以缩小的尺寸呈现,您可以指定一个变换矩阵来缩小缓冲的图像以节省内存使用。

如果你使用cacheAsBitmapMatrix来缩小图像尺寸,你需要注意不要以原始尺寸显示DisplayObject。Figure 10–5 显示了一个例子,如果你设置一个先缩小并旋转图像的缓存矩阵,然后尝试以其原始大小渲染对象,会发生什么。

images

图 10–5。 演示误用cacheAsBitmapMatrix对图像质量的影响

请注意,由于放大,最终图像有很多锯齿。即使您使用原始图像的一对一转换来显示它,Flash 也会放大缓存的版本,从而产生低保真度的图像。

cacheAsBitmapMatrix的最佳用途是将其设置为比预期的变换稍大,这样您就有足够的像素信息来生成高质量的变换图像。

闪光移动工作台

Flash Mobile Bench 是一个简单的应用,可让您测试不同设置对您部署的移动应用的性能的影响。

它允许您测试的功能包括:

  • 向显示列表添加大量形状
  • 简单 x/y 平移的动画速度
  • 简单顺时针旋转的动画速度
  • cacheAsBitmap对性能的影响
  • cacheAsBitmapMatrix对性能的影响
  • 自动 Flex 缓存启发式算法对性能的影响

它还包括一个简单的 FPS 监控小部件,您可以在自己的应用中重用它。

为了强调运行该应用的设备的能力,我们必须做的第一件事是将帧速率从默认的 24fps 提高到更高的水平。根据对一些设备的测试,我们发现 240fps 是许多平台达到的上限,并选择它作为目标帧速率设置。请记住,这是一个测试理论性能的基准应用,但在大多数情况下,您不会希望将帧速率设置得这么高,因为您可能会处理比硬件所能显示的更多的帧。

为了改变帧速率,Application类中有一个名为frameRate的属性。清单 10–1 展示了如何在 Flex 移动应用中设置这一点。

清单 10–1。 闪光移动板凳ViewNavigatorApplication ( MobileBench.mxml )

<?xml version="1.0" encoding="utf-8"?> <s:ViewNavigatorApplication xmlns:fx="http://ns.adobe.com/mxml/2009"   xmlns:s="library://ns.adobe.com/flex/spark"   firstView="views.MobileBenchHomeView"   **frameRate="240"**> </s:ViewNavigatorApplication>

这遵循了用一个叫做MobileBenchHomeViewView构建 Flex 移动应用的ViewNavigatorApplication模式。这个View的布局在 MXML 完成,如清单 10–2 所示。

清单 10–2。 Flash 移动工作台View布局代码(MobileBenchHomeView.mxml )

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark"     title="Flash Mobile Bench" initialize="init()">
  <fx:Script>     <![CDATA[       …     ]]>   </fx:Script>   <s:VGroup top="10" left="10" right="10">     <s:Label id="fps"/>     <s:CheckBox id="redraw" label="show redraw"                 click="{flash.profiler.showRedrawRegions(redraw.selected)}"/>     <s:HGroup verticalAlign="middle" gap="20">       <s:Label text="Cache:"/>       <s:VGroup>         <s:RadioButton label="Off" click="cacheOff()"/>         <s:RadioButton label="Auto" click="cacheAuto()"/>       </s:VGroup>       <s:VGroup>         <s:RadioButton label="Bitmap" click="cacheAsBitmapX()"/>         <s:RadioButton label="Matrix" click="cacheAsBitmapMatrixX()"/>       </s:VGroup>     </s:HGroup>     <s:TileGroup id="tiles" width="100%">       <s:Button label="Generate Rects" click="generateSquares()"/>       <s:Button label="Generate Circles" click="generateCircles()"/>       <s:Button label="Start Moving" click="moving = true"/>       <s:Button label="Stop Moving" click="moving = false"/>       <s:Button label="Start Rotating" click="rotating = true"/>       <s:Button id="stop" label="Stop Rotating" click="rotating=false"/>     </s:TileGroup>   </s:VGroup>   <s:Group id="bounds" left="20" top="{stop.y + tiles.y + stop.height + 20}">     <s:Group id="shapeGroup" transformX="{tiles.width/2 - 10}"              transformY="{(height - bounds.y)/2 - 10}"/>   </s:Group> </s:View>

这为应用创建了基本的 UI,包括一个填充 FPS 设置的地方,用于选择缓存策略的单选按钮,以及用于添加GraphicsElement以及开始和停止动画的按钮。

还有一个额外的复选框来显示重绘区域。该控件可以按原样放入您自己的应用中,并可以帮助您最小化重绘区域的大小,以优化渲染性能。请记住,此功能仅在 AIR 调试启动程序中有效,因此您不能在设备运行时使用它。

除了 UI 标签,FPS 监视器的代码是相当独立的。它由一个绑定到ENTER_FRAME事件的事件监听器和一些簿记变量组成,以跟踪平均帧速率。代码如清单 10–3 所示。

清单 10–3。 动作脚本导入,初始化,以及 FPS 处理程序的代码

import flash.profiler.showRedrawRegions; import flash.utils.getTimer; import mx.core.UIComponentCachePolicy; import mx.graphics.SolidColor; import mx.graphics.SolidColorStroke; import spark.components.Group; import spark.primitives.Ellipse; `import spark.primitives.Rect;
import spark.primitives.supportClasses.FilledElement;

privatefunction init():void {
  addEventListener(Event.ENTER_FRAME, calculateFPS);
  addEventListener(Event.ENTER_FRAME, animateShapes);
}

// FPS handler

privatevar lastTime:int = getTimer();
privatevar frameAvg:Number = 0;
privatevar lastFPSUpdate:int = getTimer();

privatefunction calculateFPS(e:Event):void {
  var currentTime:int = getTimer();
  var duration:int = currentTime - lastTime;
  var weight: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;
  }
}`

用于计算帧速率的算法针对以下特征进行了调整:

  • 每秒刷新不超过五次:过于频繁地刷新计数器会使其难以读取,并会对您的性能产生负面影响。该条件通过lastFPSUpdate与 200 毫秒阈值的比较来强制执行。
  • 提高慢速帧的权重:随着帧速率的降低,事件的数量也会减少。这要求每一帧的权重更高,以避免读数滞后。权重变量在 1000 毫秒(1 秒)的阈值内完成此操作。
  • 给快速帧赋予最小权重:随着帧速率的上升,权重趋近于零。因此,分配了 1%的最小权重,以防止读数滞后于另一个极端。

该算法中需要注意的另一点是整数和浮点运算的使用。前者更快,在可能的情况下更受欢迎(例如计算持续时间),而后者更准确,并且需要保持精确的平均值(frameAvg)。

代码的下一个关键部分是场景中GraphicsElement的填充。清单 10–4 中的代码实现了这一点。

清单 10–4。 创作GraphicsElement s 的动作脚本代码

`[Bindable]
private var shapes:Vector. = new Vector.();

private function populateRandomShape(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 = new SolidColor(0xFFFFFF * Math.random());
  shape.stroke = new SolidColorStroke(0xFFFFFF * Math.random());
  shapes.push(shape);
  shapeGroup.addElement(shape);
}

private function generateCircles():void {
  for (var i:int=0; i<100; i++) {
    populateRandomShape(new Ellipse());
  }
}

private function generateSquares():void {
  for (var i:int=0; i<100; i++) {
    populateRandomShape(new Rect());
  }
}`

形状的所有属性都是随机的,从填充和描边的颜色到大小和位置。RectEllipse创建之间的重叠逻辑也被抽象成一个公共函数,以最大化代码重用。

为了制作形状的动画,我们使用了在清单 10–5 中找到的代码。

清单 10–5。RectEllipse形状动画的 ActionScript 代码

`privatevar moving:Boolean;
privatevar rotating:Boolean;
privatevar directionCounter:int;

privatefunction animateShapes(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 的应用描述符标签(加粗)

`privatevar identityMatrix:Matrix = new Matrix();

privatefunction cacheOff():void {
  shapeGroup.cachePolicy = UIComponentCachePolicy.OFF;
}

privatefunction cacheAuto():void {
  shapeGroup.cachePolicy = UIComponentCachePolicy.AUTO;
}

privatefunction cacheAsBitmapX():void {
  shapeGroup.cachePolicy = UIComponentCachePolicy.ON;
  shapeGroup.cacheAsBitmapMatrix = null;
}

privatefunction cacheAsBitmapMatrixX():void {
  shapeGroup.cachePolicy = UIComponentCachePolicy.ON;
  shapeGroup.cacheAsBitmapMatrix = identityMatrix;
}`

在阅读了上一节之后,这段代码应该看起来非常熟悉。尽管我们只有一个对象实例来应用cacheAsBitmapMatrix,我们还是遵循了重用公共单位矩阵的最佳实践,以避免额外的内存和垃圾收集开销。

运行 Flash Mobile Bench 后,您将立即看到指定设备上的 FPS 计数器最大值。单击按钮将一些形状添加到场景中,将缓存设置为所需的设置,并查看设备的性能。图 10–6 显示了在摩托罗拉 Droid 2 上运行的 Flash 移动工作台应用,使用cacheAsBitmapMatrix渲染了 300 个圆。

images

图 10–6。 运行在摩托罗拉 Droid 2 上的 Flash 移动工作台

你的设备性能如何?

GPU 渲染

另一种目前仅适用于移动设备的技术是将渲染卸载到图形处理单元(GPU)。虽然 GPU 是一个高度受限的芯片,不能做普通 CPU 能够做的一切,但它擅长做图形和渲染计算,这些计算在 CPU 上需要几个数量级的时间。同时,GPU 产生更少的电池消耗,允许移动设备循环降低 CPU 以节省电池寿命。

Flash 移动项目的默认设置是 renderMode 为“auto”,目前默认为cpu。您可以显式地将其更改为gpu渲染,看看您的应用是否获得了显著的性能提升。要在 Flash Professional 中更改渲染模式,请打开 AIR for Android 设置对话框,并从渲染模式下拉列表中选择 GPU,如图 Figure 10–7 所示。

images

图 10–7。Flash Professional 中的 GPU 渲染模式设置

要更改 Flash Builder 项目中的 renderMode,您需要编辑应用描述符文件,并在initialWindow下添加一个额外的 renderMode 标签,如清单 10–7 所示。

清单 10–7。 用于设置 renderMode 的应用描述符标签(加粗)

<application>   …   <initialWindow>     **<renderMode>gpu</renderMode>**     …   </initialWindow> </application>

gpu模式获得的结果会因您使用的应用特性和运行的硬件而有很大差异。在某些情况下,你会发现你的应用在gpu模式下比在cpu模式下运行得更慢。Table 10–3 列出了在 Motorola Droid 2 上运行 Flash Mobile Bench 的一些实验结果,在不同的缓存和 gpu 模式下运行 100 个圆和 100 个正方形。

images

正如您在这个特定设备上的这个场景的结果中所看到的,GPU 没有提供任何优势,并且在没有矩阵集的情况下启用cacheAsBitmap的情况下速度明显较慢。

这强调了在应用中进行设计决策之前,使用不同设备进行测试的重要性。在这个特定的例子中,性能下降很可能是由于 GPU 将数据发送回 CPU 的回写开销。大多数 GPU 设备都针对从 CPU 接收数据进行了优化,以便快速将其写入屏幕。在某些设备上,从另一个方向发回数据进行处理的成本高得惊人。

然而,随着摩托罗拉 ATRIX 和 XOOM 上的英特尔 Integra 特性等新芯片组的推出,这种情况正在迅速改变,这些芯片组为双向通信优化了管道。此外,Flash 团队正在开发一个优化的渲染管道,通过在处理器上做更多的工作来减少对 CPU 的写回需求。有关闪存团队正在进行的性能改进的更多信息,请参见本章后面的“闪存性能的未来”一节。

表演项目渲染器

性能在关键应用领域的环境中得到最佳调整,这将被用户注意到。对于 Flex 移动应用来说,通过列表组织内容是非常常见的,但也带来了巨大的性能挑战。

由于滚动列表涉及动画,如果在交互过程中帧速率下降,这是非常明显的。同时,项目渲染器代码中的任何性能问题都会因渲染器在每个单独的列表单元格中重用而被放大。

为了演示这些概念,我们将构建一个简单的示例,显示所有 Adobe 用户组的列表,并在单击某个项目时导航到用户组网站。

清单 10–8 展示了基本的View代码,用于创建一个 Flex 列表和连接一个将打开浏览器页面的 click 事件处理程序。我们还利用之前开发的FPSComponent来跟踪我们开发应用的速度。

清单 10–8。 Adobe 用户组应用View

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark"     xmlns:renderers="renderers.*" xmlns:profile="profile.*"     title="Adobe User Groups (Original)">   <fx:Script>     <![CDATA[       import flash.net.navigateToURL;       privatefunction clickHandler(event:MouseEvent):void {         navigateToURL(new URLRequest(event.currentTarget.selectedItem.url));         }     ]]>   </fx:Script>   <s:VGroup width="100%" height="100%">     <profile:FPSDisplay/>     <s:List width="100%" height="100%" dataProvider="{data}"             click="clickHandler(event)">       <s:itemRenderer>         <fx:Component>           <renderers:UserGroupRendererOriginal/>         </fx:Component>       </s:itemRenderer>     </s:List>   </s:VGroup> </s:View>

提示:对于移动应用,总是使用itemRenderer属性而不是itemRendererFunction属性。后者会导致创建项目渲染器的多个实例,并会对性能产生负面影响。

这个类引用了一个显示列表项的UserGroupRenderer。该渲染器的创建包括组合以下组件:

  • 用户组徽标的图像组件
  • 用于显示用户组名称和描述的两个文本字段
  • 分隔不同视觉元素的水平线

清单 10–9 展示了满足这些需求的ItemRenderer的简单实现。

清单 10–9。 未优化的ItemRenderer代码

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark"     xmlns:renderers="renderers.*" xmlns:profile="profile.*"     title="Adobe User Groups (Original)">   <fx:Script>     <![CDATA[       import flash.net.navigateToURL;       privatefunction clickHandler(event:MouseEvent):void {         navigateToURL(new URLRequest(event.currentTarget.selectedItem.url));       }     ]]>   </fx:Script>   <s:VGroup width="100%" height="100%">     <profile:FPSDisplay/>     <s:List width="100%" height="100%" dataProvider="{data}"             click="clickHandler(event)">       <s:itemRenderer>         <fx:Component>           <renderers:UserGroupRendererOriginal/>         </fx:Component>       </s:itemRenderer>     </s:List>   </s:VGroup> </s:View>

在运行这个例子时,我们有一个非常实用的滚动列表,如图 Figure 10–8 所示。

images

图 10-8。 使用自定义的 Adobe 用户组列表ItemRenderer

虽然功能和外观都很好,但这种实现的性能并不理想。对于正常的滚动,帧速率下降到大约 18fps,当通过在屏幕上滑动来长时间滚动列表时,你只能获得 7fps。在这样的速度下,滚动在视觉上会分散注意力,给人一种整个应用都很慢的印象。

灵活形象类

Flash 提供了几个不同的映像类,这些映像类提供不同的功能,并且具有非常不同的性能特征。根据您的应用需求使用正确的图像类可以带来巨大的性能差异。

可用的图像类别按性能升序排列如下:

  • mx.controls.Image:这是原始的 Flex 图像组件。它现在已经过时,不应该用于移动应用。
  • 这取代了以前的 image 类,应该在任何需要样式、进度指示器或其他高级功能的地方使用。
  • flash.display.Bitmap:这是核心的 Flash 镜像组件。它的功能有限,是在屏幕上显示图像的最高性能方式。

对于最初版本的ItemRenderer,我们使用了 Flex Image类。虽然这是一个不错的选择,但是我们也没有使用这个类的高级特性,所以我们可以通过使用Bitmap来提高性能。

此外,Flex 4.5 中添加的一个新特性是ContentCache类。当在一个Bitmap上设置为 contentLoader 时,它缓存远程获取的图像,在同一图像多次显示的情况下显著提高滚动性能。

清单 10–10 显示了项目渲染器类的更新版本,该类包含了这些改进以提高性能。

清单 10–10。 ItemRenderer【代码对图像进行了优化(粗体变化)

<?xml version="1.0" encoding="utf-8"?> <s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"         xmlns:s="library://ns.adobe.com/flex/spark">   <fx:Style>     .descriptionStyle {       fontSize: 15;       color: #606060;     }   </fx:Style>   **<fx:Script>**     **<![CDATA[**       **import spark.core.ContentCache;**       **static private const cache:ContentCache = new ContentCache();**     **]]>**   **</fx:Script>**   <s:Line left="0" right="0" bottom="0">     <s:stroke><s:SolidColorStroke color="gray"/></s:stroke>   </s:Line>   <s:HGroup left="15" right="15" top="12" bottom="12" gap="10" verticalAlign="middle">     **<s:BitmapImage source="{data.logo}" contentLoader="{cache}"/>**     <s:VGroup width="100%" gap="5">       <s:RichText width="100%" text="{data.groupName}"/>       <s:RichText width="100%" text="{data.description}" styleName="descriptionStyle"/>     </s:VGroup>   </s:HGroup> </s:ItemRenderer>

通过这些额外的改进,我们将滚动的帧速率提高到了 19fps,投掷的帧速率提高到了 12fps。后者仅用几行代码就提高了 70%以上,而且没有损失任何功能。

文本组件性能

您会注意到桌面和移动设备之间最显著的性能差异之一是文本的性能。当您能够使用映射到设备字体的文本组件和样式时,您将获得最佳性能。但是,使用自定义字体或组件来提供精确的文本控制和反走样会有很大的性能损失。

随着 Flash Player 10 的发布,Adobe 推出了一个新的低级文本引擎,称为 Flash 文本引擎(FTE)和一个建立在它之上的框架,称为文本布局框架(TLF)。与以前的文本引擎(通常称为经典文本)相比,TLF 具有显著的优势,例如支持双向文本和印刷质量的排版。然而,这对移动应用来说是一个巨大的性能损失。

Flash Player 获得高性能文本显示的最佳设置是将文本引擎设置为“经典文本”,并通过在文本属性窗格中选择“使用设备字体”来关闭抗锯齿,如 Figure 10–9 所示。

images

图 10–9。 Flash 专业优化手机文本设置

对于 Flex 应用,您有大量不同的文本组件,它们利用了从经典文本到 TLF 的所有内容,因此具有不同的性能特征。

表 10–4 中显示了可用的文本组件,以及它们所基于的文本框架和移动性能特征。

images

对于移动应用,使用LabelTextInputTextArea组件可以获得最佳性能,应该尽可能使用它们。由于它们不支持双向文本和其他高级特性和样式,在某些情况下,您可能仍然需要使用RichEditableTextRichText

由于用户组列表应用不需要任何高级文本特性,我们可以用Label代替RichText。更新后的代码如清单 10–11 所示。

清单 10–11。 ItemRenderer【代码对文本进行了优化(更改以粗体显示)

<?xml version="1.0" encoding="utf-8"?> <s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"         xmlns:s="library://ns.adobe.com/flex/spark">   <fx:Style>     .descriptionStyle {       fontSize: 15;       color: #606060;     }   </fx:Style>   <fx:Script>     <![CDATA[       import spark.core.ContentCache;       staticprivateconst cache:ContentCache = new ContentCache();     ]]>   </fx:Script>   <s:Line left="0" right="0" bottom="0">     <s:stroke<<s:SolidColorStroke color="gray"/></s:stroke>   </s:Line>   <s:HGroup left="15" right="15" top="12" bottom="12" gap="10" verticalAlign="middle">     <s:BitmapImage source="{data.logo}" contentLoader="{cache}"/>     <s:VGroup width="100%" gap="5">       **<s:Label width="100%" text="{data.groupName}"/>**       **<s:Label width="100%" text="{data.description}" styleName="descriptionStyle"/>**     </s:VGroup>   </s:HGroup> </s:ItemRenderer>

这次改动后,滚动速度为 20fps,投掷速度为 18fps,有了显著的提升。我们可以通过使用StyleableTextField来实现更高的速度,这正是 Flash 团队为他们的内置组件所做的。

内置项目渲染器

在过去的几节中,我们在测试设备上将自定义项目渲染器的性能从低于 10fps 的完全不可接受的速度提升到大约 20fps。我们可以通过进行以下一些额外的更改来继续优化渲染器:

  • 使用cacheAsBitmap保存最近的细胞图像。
  • 在 ActionScript 中重写以利用StyleableTextField
  • 移除组并使用绝对布局。

但是,已经有一个组件包含了这些优化,并且可以开箱即用。

Flex 团队提供了一个LabelItemRendererIconItemRenderer的默认实现,您可以使用和扩展。这些类中已经包含了很多您可以利用的功能,包括对样式、图标和装饰器的支持。它们也是高度优化的,利用了本章讨论的所有最佳实践。

清单 10–12 展示了您将使用内置IconItemRenderer替换我们的自定义项目渲染器的代码更改。

清单 10–12。 View代码利用内置的IconItemRenderer

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"     xmlns:s="library://ns.adobe.com/flex/spark"     xmlns:views="views.*"     title="Adobe User Groups (Built-in)" xmlns:profile="profile.*">   <fx:Script>     <![CDATA[       import flash.net.navigateToURL;       privatefunction clickHandler(event:MouseEvent):void {         navigateToURL(new URLRequest(event.currentTarget.selectedItem.url));       }     ]]>   </fx:Script>   **<fx:Style>**     **.descriptionStyle {**       **fontSize: 15;**       **color: #606060;**     **}**   **</fx:Style>**   <s:VGroup width="100%" height="100%">     <profile:FPSDisplay/>     <s:List width="100%" height="100%" dataProvider="{data}"               click="clickHandler(event)">       <s:itemRenderer>         <fx:Component>           **<s:IconItemRenderer labelField="groupName"**                     **fontSize="25"**                     **messageField="description"**                     **messageStyleName="descriptionStyle"**                     **iconField="logo"/>**         </fx:Component>       </s:itemRenderer>     </s:List>   </s:VGroup> </s:View>

运行这段代码的结果非常接近我们最初的项目渲染器,如图图 10–10 所示。如果您并排比较这些图像,您会注意到由于使用了StyleableTextComponent,文本中有细微的差异,但是没有显著的差异会影响应用的可用性。

images

图 10-10。 Adobe 用户组列表使用内置的IconItemRenderer

在摩托罗拉 Droid 2 上,使用内置组件的最终性能是滚动 24fps,投掷 27fps。这超过了 Flex 应用的默认帧速率,表明您可以用很少的代码在 Flash 中构建功能丰富、性能卓越的应用。

性能监控 API 和工具

构建高性能移动应用的最佳秘诀是尽早并经常测试性能。通过在构建应用时识别性能问题,您将能够快速识别代码中对性能至关重要的部分,并在开发过程中对它们进行调优。

拥有正确的工具来获得绩效反馈会使这项工作变得更加容易。本节重点介绍了几个免费提供的工具,或者您的系统中可能已经有了这些工具,您可以从今天开始利用它们。

高分辨率!统计

获得关于应用的帧速率、内存使用和整体性能的实时反馈对于确保在开发过程中不会出现性能倒退至关重要。虽然您可以滚动您自己的性能度量,但是如果您不小心的话,您可能会因为使用您自己的工具降低应用的速度而扭曲您的结果。

幸运的是,一个臭名昭著的网络黑客,化名为 Doob 先生,创建了一个开源统计小部件,您可以很容易地将它合并到您的项目中。您可以从以下网址下载源代码:[github.com/mrdoob/Hi-ReS-Stats](https://github.com/mrdoob/Hi-ReS-Stats)

杜布先生的高分辨率!Stats 为您提供了以下工具:

  • 每秒帧数:显示当前 FPS 加上播放器中设置的目标 FPS(越高越好)。
  • 帧持续时间:每秒帧数的倒数,这让你知道渲染一帧需要多少毫秒(越低越好)。
  • 内存使用量:应用当前使用的内存量(以兆字节为单位)
  • 内存使用峰值:该应用达到的最高内存使用阈值(也以兆字节为单位)

添加高分辨率!Stats 添加到 ActionScript 项目中,可以使用以下代码:

import net.hires.debug.Stats; addChild(newStats());

因为它是一个纯 ActionScript 组件,所以您需要做更多的工作来将其添加到 Flex 项目中,具体操作如下:

import mx.core.IVisualElementContainer; import mx.core.UIComponent; import net.hires.debug.Stats; private function addStats(parent:IVisualElementContainer):void {   var comp:UIComponent = new UIComponent();   parent.addElement(comp);   comp.addChild(new Stats()); }

然后,要将它附加到一个View,只需用一个自引用从initialize方法调用它:

<s:View … initialize="addStats(this)"> … </View>

在统计数据下面,绘制了这些值的图表,让您了解应用的趋势。您还可以通过单击读数的顶部或底部来增加或减少应用帧速率。图 10–11 显示了高分辨率的放大版本!统计用户界面。

images

图 10–11。 高清放大截图!统计数据

效能测试 v2 Beta

一旦您确定了您的性能问题,追踪根本原因并确保一旦您修复了它,行为不会随着将来的变化而倒退是非常棘手的。

Grant Skinner 采用了一种科学的方法来解决 PerformanceTest 的问题,为您提供纯 ActionScript APIs 来计时方法、分析内存使用情况,并创建可重复的性能测试场景。运行 PerformanceTest 工具的示例输出如图图 10–12 所示。

images

图 10–12。 运行性能测试工具的输出

由于输出是 XML 格式的,您可以轻松地将其与其他工具或报告集成,包括在编写代码时进行性能测试的 TDD 框架。有关 PerformanceTest v2 的更多信息,请参见以下 URL:

http://gskinner.com/blog/archives/2010/02/performancetest.html.

Flash Builder 分析器

对于堆和内存分析,最好的可用工具之一是内置于 Flash Professional 中的探查器。Flash Builder profiler 为您提供了内存使用情况的实时图表,允许您拍摄堆快照并将其与基准进行比较,还可以捕获应用的方法级性能计时。

虽然这在直接运行在移动设备上时目前不工作,但它可以用于在 AIR Debug Launcher 中运行时分析您的移动应用。要在 profiler 中启动应用,请从运行菜单中选择 Profile。执行时,您将看到应用的实时视图,如图 Figure 10–13 所示。

images

图 10–13。 在调试模式下针对 Flash 移动项目运行的 Flash Builder 探查器

闪存性能的未来

Adobe 的 Flash 运行时团队一直在寻找新的方法来提高 Flash 应用在桌面和移动设备上的性能。这包括对您的应用透明的 Flash 和 AIR 运行时的性能增强,以及可让您在应用内更高效地工作的新 API 和功能。

注意:本节中的所有改进和更改都是针对闪存路线图提出的,但不是承诺的功能。最终的实现可能与所讨论的有很大的不同。

更快的垃圾收集

随着应用规模的增长,垃圾收集暂停会对应用的响应能力产生越来越大的影响。虽然垃圾收集的摊余成本非常低,但由于它提供了所有的好处,所以由全内存清理引起的偶然命中可能会对应用的感知性能造成毁灭性的影响。

从 Flash Player 8 开始,Flash 运行时就使用了标记和清除垃圾收集器。标记和清除垃圾收集器的工作方式是在从根对象遍历所有活动引用之前暂停应用,标记活动对象,如图 Figure 10–14 所示。在该阶段未被标记的对象在算法的扫描阶段被标记为删除。最后一步是释放已释放的内存,这并不保证会立即发生。

images

图 10–14。 标记和清扫垃圾收集算法的可视化表示

标记和清除算法的好处是很少涉及簿记,而且执行起来相当快。然而,随着堆大小的增长,垃圾收集暂停的持续时间也会增长。这可能会对动画或其他对时间要求严格的操作造成严重破坏,这些操作在收集过程中似乎会挂起。

Flash runtime 团队正在考虑对垃圾收集算法进行多项改进,以提高性能:

  • 增量 GC
  • GC 提示 API
  • 分代垃圾收集

增量垃圾收集将允许垃圾收集器将标记和清扫工作拆分到几个帧上。在这种情况下,垃圾收集的总成本会稍微高一些;然而,对任何特定帧持续时间的影响被最小化,允许应用在收集期间维持高帧速率。

垃圾收集器对于何时触发收集是相当天真的,并且总是会选择最糟糕的时间进行标记和清扫。GC 提示 API 可以让开发人员向垃圾收集器提示不需要垃圾收集的性能关键时刻。如果内存足够低,垃圾收集可能仍然会被触发,但这将有助于防止虚假的垃圾收集在错误的时刻降低应用的速度。

虽然还不太为人所知,但反过来已经是可能的了。Flash 已经有了手动触发垃圾收集的机制。要立即触发垃圾收集循环,需要调用两次System.gc()方法,一次强制标记,第二次强制清扫,如清单 10–13 所示。

清单 10–13。 代码强制垃圾回收(有意重复调用)

flash.system.System.gc(); flash.system.System.gc();

提示:以前这个 API 只能从 AIR 获得,并且只能在调试模式下运行,但是现在它完全支持所有模式。

虽然标记和清扫收集器相当有效且易于实现,但它们不太适合交互式应用,并且倾向于破坏新创建的对象。实际上,长寿命对象很少需要收集,而新创建的对象经常被丢弃。分代垃圾收集器认识到了这种趋势,并根据对象的年龄将它们分成不同的代。这使得更频繁地在年轻一代上触发收集成为可能,允许以更少的工作量回收更大量的内存。

拥有一个高效的分代式垃圾收集器将极大地改变 ActionScript 的使用模式,不再需要过多的对象池和缓存策略,而这些策略目前通常用于提高性能。

更快的 ActionScript 性能

您编写的 Flash 应用甚至平台本身中的库都是使用 ActionScript 编写的,因此 ActionScript 性能的增量改进可以对实际性能产生巨大影响。

闪存团队正在研究的一些将惠及所有应用的改进包括:

  • 实时(JIT)编译器优化
  • Float数字型

Flash 利用所谓的实时(JIT)编译器来动态优化 Flash 字节码。JIT 编译器将性能关键的代码段翻译成可以直接在设备上运行的机器代码,以获得更高的性能。同时,它拥有关于代码执行路径的信息,可以利用这些信息来执行优化,从而加速应用。

计划中的一些新的 JIT 优化包括:

  • 基于类型的优化 : ActionScript 是一种动态语言,因此类型信息是可选的。在显式指定类型或者可以通过检查调用链隐式发现类型的地方,可以生成更高效的机器码。
  • 数值优化:目前在 Flash 运行时中,所有的数值运算,包括像加法和乘法这样的重载运算符,都是针对数值对象而不是原始数字和整数的。因此,生成的代码包含额外的指令来检查数字的类型,并从对象中取出值,这在紧循环中是非常昂贵的。通过检查代码来确定原始值可以被替换的位置,这些操作的性能可以得到显著提高。
  • 可空性 : ActionScript 是一种空安全的语言,这对于 UI 编程来说非常方便,但是这意味着产生了许多额外的检查来缩短调用,否则这些调用会取消对空指针的引用。对于在创建时初始化并且从不设置为 null 的变量也是如此。在这些情况下,JIT 有足够的信息来安全地跳过空检查,减少生成代码中的分支数量。

这些 JIT 优化的最终结果是,在不改变应用代码的情况下,您将受益于更快的性能。一般来说,您的应用受 CPU 限制越多,您获得的好处就越大。

此外,Flash 团队还提议增加一个显式的float数字类型和匹配的Vector.<float>。根据定义,Flash 中的数字类型是 64 位精度值,改变它的语义会破坏与现有应用的向后兼容性。然而,许多移动设备已经对硬件进行了优化,可以对 32 位值进行浮点运算。通过让程序员选择指定数值的精度,他们可以决定在有意义的地方牺牲精度来换取性能。

并发

现代计算机拥有多个处理器和内核,可用于并行执行操作以提高效率。这一趋势还扩展到了移动应用,摩托罗拉 ATRIX 等现代设备能够将双核处理器封装在一个非常小的封装中。这意味着为了充分利用硬件,您的应用需要能够在多个线程上并行执行代码。

即使在多个处理器不可用的情况下,考虑在多个线程上并行执行的代码仍然是一个有用的抽象。这使您可以增量处理长期运行的任务,而不会影响需要频繁更新的操作,如渲染管道。

许多内置闪存操作已经在幕后多线程化,可以有效利用多个内核。这包括在后台执行 I/O 操作的网络代码,以及利用在不同线程中运行的本机代码的 Stage Video。通过使用这些 API,您可以隐式地利用并行性。

为了让您能够利用显式线程,Flash 团队正在考虑两种不同的机制向开发人员公开这一点:

  • SWF 委托:代码被编译成两个不同的独立的 SWF 文件。要生成一个新线程,您可以使用主 SWF 文件中的 worker API 来创建子 SWF 的新实例。
  • 入口点类:多线程代码被分成一个不同的类,使用代码注释来指定它是一个唯一的应用入口点。

在这两种场景中,都使用了无共享并发模型。这意味着您不能访问变量或在不同线程中执行的代码之间更改状态,除非使用显式消息传递。无共享模型的优点是它可以防止竞争情况、死锁和其他难以诊断的线程问题。

通过在平台中内置显式并发机制,您的应用将受益于多核处理器的更高效使用,并可以在执行 CPU 密集型操作时避免动画和渲染暂停。

线程渲染流水线

如今,Flash 渲染管道是单线程的,这意味着它不能在较新的移动设备上利用多核,如摩托罗拉 ATRIX。这在渲染图形和视频时尤其成问题,因为它们最终会被顺序处理,如图 Figure 10–15 所示。

images

图 10–15。 单线程渲染流水线

当 ActionScript 代码的执行时间比预期长时,这可能会导致视频帧被丢弃。Flash 将通过跳过舞台渲染并优先处理后续帧上的视频来进行补偿。结果是,当其中一个处理器处于空闲状态时,您的视频和动画性能都会显著下降。

线程渲染管道将视频处理卸载到第二个 CPU,从而使视频能够流畅运行,而不管 ActionScript 执行或舞台渲染中的延迟。这使得多核系统上的可用资源得到了最佳利用,如图 Figure 10–16 所示。

images

图 10–16。 多线程渲染流水线

我们可以更进一步,利用 Stage Video 将视频解码和合成卸载到图形处理器,这为您提供了优化的渲染管道,如图图 10–17 所示。

images

图 10–17。 多线程渲染流水线配合舞台视频

最终结果是,您能够在 ActionScript 代码中进行更多处理,而不会影响帧速率或视频回放。

3d 阶段

闪存路线图中另一个备受关注的项目是 Stage3D。这项技术的代号是 Molehill,对于需要一个非常接近底层图形硬件的跨平台 3D 库的游戏开发者来说,这是特别感兴趣的。Stage3D 使一些应用成为可能,如图 10–18 所示。

images

图 10–18。 来自 Away3D 的 Molehill 演示(右上和右下)和 Adobe Max(左下)

这些示例是使用名为 Away3D 的第三方 3D 工具包在 Stage3D 的预发布版本之上构建的。其他一些可以利用 Stage3D 的工具包包括 Alternative3D、Flare3D、Sophie3D、Unity、Yogurt3D 和 M2D。

除了对游戏开发者有用之外,Stage3D 还开启了拥有高度优化的 2D UI 工具包的可能性。正如前面讨论的 GPU 加速支持,图形处理器可以比 CPU 更快地完成许多操作,同时消耗更少的功率并延长电池寿命。通过将 UI 工具包完全卸载到图形处理器,CPU 可以专用于应用和业务逻辑,而通过现有的 3D 场景图将显示列表管理、合成和渲染留给 GPU。

总结

正如您在本章中了解到的,通过遵循一些移动调优最佳实践,可以构建具有高级图形、高帧速率和流畅动画的高性能 Flex 应用。您已经获得性能调优知识的一些特定领域包括:

  • 加速图形渲染
  • 将场景图的部分缓存为Bitmap s
  • 构建高性能项目渲染器
  • 文本和项目组件的最佳使用

此外,您还了解了 Flash 运行时和图形处理能力的未来改进,您将能够在未来利用这些改进,而无需更改代码。

所有这些性能调整技术也适用于我们的最后一个主题,即将您的 Flash 和 Flex 应用扩展到平板电脑、电视等领域。

十一、超越手机:平板电脑和电视

谷歌和 Adobe 正在努力分别扩展 Android 平台和 AIR 运行时的覆盖范围。Android 已经扩展到摩托罗拉 XOOM 和三星 Galaxy Tab 等平板电脑上,甚至通过谷歌电视进入你的客厅。这为您的 for Android 应用打开了更多潜在的平台!此外,以黑莓手机闻名的 Research In Motion 也发布了自己的平板电脑 PlayBook。该行动手册完全兼容 Flash,因此为您的 Flex 和 Flash 应用赢得新受众提供了又一个机会。

本章将探讨将移动应用转移到平板电脑和电视的大屏幕上时需要考虑的一些特殊因素。

缩放屏幕

屏幕越大,界面设计就越自由。更多的自由带来更多的责任。平板电脑用户希望你的应用能充分利用大屏幕提供的额外空间。图 11–1 显示了来自第八章的 MusicPlayer 应用在一台 10.1 英寸屏幕的摩托罗拉 XOOM 上运行。虽然该应用是可用的,但低像素密度和大屏幕的结合导致了小而长的控件和大量浪费的空间。我们能够并且将会做得更好。

这样做的动机来自这样一个事实,即自 Android 3.0 推出以来,Android 平板电脑领域正在爆炸式增长,Android 3.0 是专为平板电脑和电视的大屏幕而设计的 Android 版本。除了现有的 Android 2.2 平板电脑——戴尔 Streak 和三星 Galaxy Tab——现在还有摩托罗拉 XOOM 和三星 Galaxy Tab 10.1,它们都运行最新版本的 Honeycomb(Android 3 . x 的代号)。此外,东芝、索尼、华硕和亚马逊都有望在 2011 年发布蜂巢平板电脑。

显然,这是一个任何应用开发人员都想认真对待的细分市场。专门为支持这些更大的平板电脑屏幕而修改的应用将比那些不支持的应用有相当大的优势。

images

图 11–1。 运行在摩托罗拉 XOOM 平板电脑上的音乐播放器应用

第一步是让你熟悉硬件。大多数平板电脑拥有比普通智能手机更强大的处理器和更大的内存。Table 11–1 显示了目前市场上流行的 Android 平板电脑的显示屏对比。该表显示,大多数平板电脑的分辨率在 160 dpi 左右,屏幕更大、分辨率更高。随着更强大的处理器和大屏幕的结合,您可能会认为您的应用会比在手机上运行得更快。这不是一个好的假设,尤其是如果你的应用是图形受限的而不是 CPU 受限的。除非他们利用硬件加速,否则图形密集型应用在平板电脑上的运行速度通常会更慢,因为更大的屏幕必须进行大量的像素计算。像往常一样,运行性能测试并根据需要进行优化。

Images

每当您考虑将您的应用迁移到一个新的平台时,您都应该花时间研究现有的应用,以确定正在使用的设计模式和约定。图 11–2 显示了一些现有的 Android 平板电脑应用。从左上方按顺时针方向,我们看到:Locomo Labs 的 Flixster、Newsr 和 TweetComb、??、?? 和谷歌的电影工作室。你看到了哪些常见的模式和惯例?

请注意,尤其是在横向模式下(如图所示),应用都利用额外的屏幕空间来显示多个视图。与类似的手机应用不同,Flixster 和 Newsr 在一个屏幕上同时显示主视图和详细视图,而不必转换到单独的详细视图。TweetComb 利用额外的空间来显示多列推文,而 Movie Studio 为您提供了更大、更易于使用的控件。还要注意标题栏中包含了更多的动作(Flex 应用中的ActionBar)。我们可以对我们的 MusicPlayer 应用进行类似的修改,从而将其转换为一个成熟的平板电脑界面,类似于图 11–2 中的图片。

当考虑对音乐播放器的平板版本进行修改时,立即想到的一件事是使用歌曲视图中的额外空间来显示额外的元数据,这在应用的手机版本中根本没有空间。这种简单的修改是第一种技术的理想选择,我们将研究如何将应用扩展到新的屏幕:基于状态的定制。


从技术上来说,HTC Flyer 运行的是 Android 2.3(代号 Gingerbread)而不是 Android 3.x,但你的 AIR for Android 程序也将运行在 Gingerbread 上。

2http://locomolabs.com/2

images

图 11–2。 运行在安卓平板电脑上的热门应用

基于状态的定制

我们已经展示了如何使用landscapeportraitView状态定制应用的 UI 布局。这种技术采用了这种思想并加以扩展。不仅仅是portraitlandscape,你需要定义四种状态组合来支持手机和平板电脑的每个方向。因此,您假设的 MXML 代码看起来类似于清单 11–1。

清单 11–1。 首次尝试为手机和平板电脑添加独立状态

`<s:states>
  <s:State name="portraitPhone"/>
  <s:State name="landscapePhone"/>
  <s:State name="portraitTablet"/>
  <s:State name="landscapeTablet"/>
</s:states>

<s:Group width="100%" height="100%">
  <s:layout.landscapePhone>
    <s:HorizontalLayout verticalAlign="middle" paddingLeft="10"/>
  </s:layout.landscapePhone>

<s:layout.landscapeTablet>
    <s:HorizontalLayout verticalAlign="middle" paddingLeft="10"/>
  </s:layout.landscapeTablet>

<s:layout.portraitPhone>     <s:VerticalLayout horizontalAlign="center" paddingTop="10"/>
  </s:layout.portraitPhone>

<s:layout.portraitTablet>
  <s:VerticalLayout horizontalAlign="center" paddingTop="10"/>
</s:layout.portraitTablet>

<s:Group width.portraitPhone="{height0.4}" height.portraitPhone="{height0.4}"
               width.landscapePhone="{width0.4}"
               height.landscapePhone="{width
0.4}"
               width.portraitTablet="{height0.3}"
               height.portraitTablet="{height
0.3}"
               width.landscapeTablet="{width0.3}"
               height.landscapeTablet="{width
0.3}">
  
</s:Group>`

我们的View中现在有四种状态:手机和平板电脑的横向和纵向版本。这些都在<s:states>部分使用<s:State>元素进行了枚举。一旦定义了状态,就可以使用 Flex 的特定于状态的属性声明,比如width.portraitPhone,来定制布局、间距,甚至是View用户界面中任何组件的可见性。作为一个例子,在我们假设的代码清单中定义的Group包括一个为我们每个可能的状态定制的widthheight

如您所见,这种技术的主要缺点是特定于状态的属性声明激增。你现在什么都需要四个!幸运的是,有一种方法可以缓解这个问题。

使用状态组

状态组是一种将多个状态(一组状态)分配给一个状态声明的方法。以下面的州声明为例:

<s:State name="portraitPhone" stateGroups="portrait,phone"/>

这意味着当我们将ViewcurrentState设置为portraitPhone时,我们将激活任何被portraitPhoneportraitphone状态修改的属性声明。这允许我们使用这些状态的组合来定义 MXML 属性:

  • attributeName.portraitPhone:这只适用于纵向手机。
  • 这将适用于纵向的手机或平板电脑。
  • 这将适用于横向或纵向手机。

这使您在声明属性时更加灵活,并消除了大量代码重复。既然我们不再定义标准的横向和纵向状态,Flex 将不再自动设置我们的View状态。这是我们将手动处理的事情,通过覆盖getCurrentViewState方法来返回一个基于屏幕大小和当前方向的新状态,如清单 11–2 所示。

清单 11–2。 返回定制View状态

`override public function getCurrentViewState():String {
  var isPortrait:Boolean = height > width;
  var isTablet:Boolean = … // A calculation based on screen size or resolution.

var newState:String = (isPortrait ? "portrait" : "landscape") +
            (isTablet ? "Tablet" : "Phone");

return hasState(newState) ? newState : currentState;
}`

新状态由两个布尔变量决定。通过比较View的宽度和高度,很容易确定isPortrait变量。isTablet这个变量稍微复杂一点。您可以通过测试来使用屏幕的分辨率,看看 x 或 y 维度是否大于 960,这是目前手机上使用的最大分辨率。更可靠的方法是使用屏幕分辨率和像素密度来确定屏幕的物理尺寸。那么你可以假设任何超过 5.5 英寸的都是平板设备。这种计算的一个例子显示在清单 11–4 中的onViewActivate函数中。

现在我们可以回到从歌曲的元数据向 UI 添加更多信息的想法。有四样东西可以添加到平板电脑界面上:专辑名称、艺术家姓名、专辑出版年份以及专辑所属的流派。我们已经将albumTitleartistName定义为SongViewModel类中的属性。这意味着我们只需要添加yeargenres属性。清单 11–3 展示了实现这一点的代码。

清单 11–3。?? 向SongViewModel?? 添加yeargenre属性

`package viewmodels
{
  [Bindable]
  public class SongViewModel extends EventDispatcher {
    public var albumCover:BitmapData;
    public var albumTitle:String = "";
    public var songTitle:String = "";
    public var artistName:String = "";    
    public var year:String = "";
    public var genres:String = "";

// …

/**
     * Called when the song's metadata has been loaded by the Metaphile
     * library.
     */
    privatefunction onMetaData(metaData:IMetaData):void {
      var songFile:MusicEntry = songList[currentIndex];
      var id3:ID3Data = ID3Data(metaData);       artistName = id3.performer ? id3.performer.text : "Unknown";
      albumTitle = id3.albumTitle ? id3.albumTitle.text : "Album by " +
          artistName;
      songTitle = id3.songTitle ? id3.songTitle.text : songFile.name;
      year = id3.year ? id3.year.text : "Unknown";
      genres = id3.genres ? id3.genres.text : "Unknown";

if (id3.image) {
        var loader:Loader = new Loader();
        loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
                                                  onLoadComplete)
        loader.loadBytes(id3.image);
      } else {
        albumCover = null;
      }
    }

// …
  }
}`

粗体突出显示的代码显示了需要进行的更改:声明新的可绑定变量来保存yeargenres字符串,然后从 Metaphile 库返回的ID3Data中加载它们。

我们的注意力现在转向如何将这些信息添加到我们的界面上。图 11–3 显示了新界面的两个模型,一个横向,一个纵向。手机界面将保持不变,但当我们检测到我们正在平板电脑上运行时,我们将进行以下更改:

  • ActionBar中的歌曲名称将被替换为专辑名称。
  • 在纵向模式下,四个新的元数据将被放置在专辑封面和播放控件之间。
  • 在横向模式下,新的元数据将放在屏幕的左侧,专辑封面在中间,播放控制在右侧。

根据设备的方向,新歌曲信息会出现在不同的位置,但这可以使用我们的自定义状态名称和组件的includeIn属性轻松实现。

images

图 11–3。 显示附加信息的设计模型,显示在平板电脑界面上

清单 11–4 中的代码显示了需要对原始View代码进行的第一次修改,以实现如图图 11–3 所示的新设计。

清单 11–4。修改后的开始SongView MXML

`
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:assets="assets."
        xmlns:views="views.
"
        initialize="onInitialize()"
        viewActivate="onViewActivate()"
        viewDeactivate="onViewDeactivate()"
        resize="onResize()"
        title="{isTablet ? model.albumTitle : model.songTitle}">

<s:states>
    <s:State name="portraitPhone" stateGroups="portrait,phone"/>
    <s:State name="landscapePhone" stateGroups="landscape,phone"/>
    <s:State name="portraitTablet" stateGroups="portrait,tablet"/>
    <s:State name="landscapeTablet" stateGroups="landscape,tablet"/>
  </s:states>

fx:Script
    <![CDATA[
      import viewmodels.SongViewModel;

[Bindable]
      private var isTablet:Boolean;

[Bindable]
      private var model:SongViewModel;

override public function getCurrentViewState():String {         var isPortrait:Boolean = height > width;
        var newState:String = (isPortrait ? "portrait" : "landscape") +
            (isTablet ? "Tablet" : "Phone");

return hasState(newState) ? newState : currentState;
      }

private function onViewActivate():void {
        var w:Number = Capabilities.screenResolutionX/Capabilities.screenDPI;
        var h:Number = Capabilities.screenResolutionY/Capabilities.screenDPI;
        isTablet = Math.max(w, h) > 5.5;

setCurrentState(getCurrentViewState());
      }

privatefunction onResize():void {
        setCurrentState(getCurrentViewState());
      }

private function onInitialize():void { /* same as before / }
      private function onViewDeactivate():void { /
same as before / }
      private function onSongEnded(event:Event):void { /
same as before */ }
    ]]>
  </fx:Script>`

View的 title 属性使用一个到isTablet变量的绑定来决定是在ActionBar中显示歌曲标题还是专辑标题。记住,在较小的手机屏幕上,我们在ActionBar的标题区域显示歌曲标题,以避免SongView界面过度拥挤。如果使用更大的平板电脑屏幕,将专辑名称放在ActionBar中更有意义,并且在从一首歌曲转到下一首歌曲时更改歌曲信息。

如本节前面所述,我们的每个状态都定义了相关的状态组。出现在<fx:Script>部分顶部的被覆盖的getCurrentViewState函数负责根据屏幕大小和方向确定View应该处于哪个状态。如果Viewheight大于其width,则设备被标记为纵向。否则我们知道我们处于风景模式。使用这些信息和isTablet标志,该函数构建并返回一个描述View当前状态的字符串。

ViewviewActivate事件的处理程序中设置了isTablet标志。当View激活时,onViewActivate处理器以英寸为单位计算设备屏幕的宽度和高度。如果其中任何一个尺寸超过 5.5 英寸,那么我们可以假设该应用正在平板设备上运行。然后,该函数调用我们被覆盖的getCurrentViewState方法来获取View的初始状态,并将结果传递给setCurrentState函数。

我们还为Viewresize事件附加了一个处理程序来检测方向变化。onResize处理器将通过调用我们的getCurrentViewState函数来设置View的当前状态,并使用返回值来设置当前的View状态。

注意:覆盖getCurrentViewState函数来提供自定义状态确实有一个缺点,那就是它使得 Flash Builder 的设计视图实际上毫无用处。

是时候将这种状态管理代码用于我们的 MXML 宣言了。清单 11–5 显示了根Group容器以及一组标签,它们组成了横向方向的歌曲信息部分。

清单 11–5。View的根容器Group和景观元数据显示

`<s:Group width="100%" height="100%">
  <s:layout.portrait>
    <s:VerticalLayout paddingTop="10" horizontalAlign="center"/>
  </s:layout.portrait>

<s:layout.landscape>
    <s:HorizontalLayout verticalAlign="middle" paddingLeft="10"/>
  </s:layout.landscape>

<s:VGroup width="30%" horizontalCenter="0" gap="20" paddingTop="40"
            paddingBottom="40" includeIn="landscapeTablet">
    <s:VGroup width="100%">
      <s:Label styleName="albumInfoLabel" text="Song"/>
      <s:Label styleName="albumInfo" text="{model.songTitle}"
               maxWidth="{width*.3}" maxDisplayedLines="1"/>
    </s:VGroup>
    
  </s:VGroup>

<s:Group width.portrait="{height0.4}" height.portrait="{height0.4}"
           width.landscape="{width0.4}" height.landscape="{width0.4}">
    <s:BitmapImage width="100%" height="100%" source="{model.albumCover}"
                   visible="{model.albumCover != null}"/>
    <assets:DefaultAlbum id="placeHolder" width="100%" height="100%"                          visible="{!model.albumCover}" />
  </s:Group>`

如同第八章中的一样,我们在纵向模式下使用VerticalLayout作为根Group,在横向模式下使用HorizontalLayout。由于之前声明的状态组,这些布局将用于手机和平板电脑版本的界面。根Group容器的第一个子容器是VGroup,它包含界面风景版本的歌曲信息——回想一下,它在屏幕的最左边。此外,该组应仅出现在平板电脑显示屏上。这就是在其includeIn属性中使用完全指定的landscapeTablet状态的原因。下一个Group是相册封面图片的容器。由于先前的VGroup仅包含在landscapeTablet状态中,相册封面Group将首先出现在任何方向的手机和纵向模式的平板电脑的布局中。

清单 11–6 显示了歌曲信息显示的肖像模式版本以及其余的控件。

清单 11–6。 肖像歌曲信息组和回放控件

`<s:VGroup width="80%" horizontalCenter="0" gap="40" paddingTop="40"
              paddingBottom="40" includeIn="portraitTablet">
      <s:HGroup width="100%">
        <s:VGroup width="50%">
          <s:Label styleName="albumInfoLabel" text="Song"/>
          <s:Label styleName="albumInfo" text="{model.songTitle}"
                   maxWidth="{width.4}" maxDisplayedLines="1"/>
        </s:VGroup>
        <s:VGroup horizontalAlign="right" width="50%">
          <s:Label styleName="albumInfoLabel" text="Artist"/>
          <s:Label styleName="albumInfo" text="{model.artistName}"                    maxWidth="{width
.4}" maxDisplayedLines="1"/>
        </s:VGroup>
      </s:HGroup>
      
    </s:VGroup>

<s:VGroup horizontalAlign="center" paddingTop="20" gap="40"
              width.portrait="100%" width.landscape="50%">
      <s:HGroup width="90%">
        <s:Button label="<<" height="40" click="model.previousSong()"/>
        <views:ProgressButton id="progressButton" width="100%" height="40"
                              click="model.onPlayPause()"
                              percentComplete="@{model.percentComplete}"                               skinClass="views.ProgressButtonSkin"/>
        <s:Button label=">>" height="40" click="model.nextSong()"/>
      </s:HGroup>

<s:HGroup verticalAlign="middle" width="90%">
        <assets:VolLow id="volLow" width="32" height="32"/>
        <s:HSlider width="100%" maximum="1.0" minimum="0.0" stepSize="0.01"
                 snapInterval="0.01" value="@{model.volume}" showDataTip="false"/>
        <assets:VolHigh id="volHigh" width="32" height="32"/>
      </s:HGroup>

<s:HGroup verticalAlign="middle" width="90%" >
        <s:Label text="L" width="32" height="32" verticalAlign="middle"
                 textAlign="center"/>
        <s:HSlider width="100%" maximum="1.0" minimum="-1.0" stepSize="0.01"
                 snapInterval="0.01" value="@{model.pan}" showDataTip="false"/>
        <s:Label text="R" width="32" height="32" verticalAlign="middle"
                 textAlign="center"/>
      </s:HGroup>
    </s:VGroup>
  </s:Group>
</s:View>`

在纵向模式下,歌曲信息VGroup显示在专辑封面和播放控件之间——因此它在 MXML 文件中的位置是这样的,其includeIn属性指定了portraitTablet州。

作为点睛之笔,我们在歌曲信息组件的ViewNavigatorApplication MXML 文件中添加了一点 CSS 样式。我们现在来看看图 11–4 中的应用。我们的应用现在能够适应运行在最小和最大的移动设备上。这是定制的一个简单例子,通过明智地使用状态可以实现这一点。该应用的代码可以在 MusicPlayerWithStates 项目中找到,该项目位于本书示例代码的examples/chapter-11目录中。

images

图 11–4。 音乐播放器支持在小屏幕和大屏幕上运行的应用

这种基于状态的定制技术的主要优点是,它允许您将所有应用代码保存在一个项目中。这使得维护代码更容易,并简化了构建过程。然而,当您考虑当您想要开始支持其他平台时需要做什么时,缺点就变得很明显了。如果你想把你的市场扩大到包括 iPhone、iPad 和 PlayBook,那么你需要开始调整用户界面,以适应这些平台上使用的所有不同的惯例。你将突然面临状态的组合爆炸。如果不同设备类别或平台的接口彼此差异太大,您也会遇到问题。在你拥有一份冗长、难读、难维护的 MXML 档案之前,各州只能带你走这么远。

如果你发现自己处于这个位置,你可以转向界面定制的第二个选择:基于项目的定制。

基于项目的定制

基于项目的定制背后的想法是将应用的所有共享代码放入一个库项目,然后创建单独的项目来实现每个不同平台或设备类别的定制用户界面(例如,手机与平板电脑)。为不同类别的设备或不同平台的应用的每个版本创建单独的项目,可以为您提供配置界面的最大灵活性。这种设置对于跨越两个或多个 web、桌面、电话、平板电脑和电视的项目来说非常常见。为了避免不必要的代码重复,创建了一个库项目来包含所有共享的源文件和图形资源。

让我们假设我们的设计师已经看过了图 11–2 中显示的一些应用,并决定为我们的音乐播放器尝试一种新的外观。他们想出了一种新的横向模式下的平板电脑界面,看起来有点像图 11–5。他们希望将歌曲信息移到屏幕的右侧,将播放控件放在专辑封面下,并将歌曲列表添加到屏幕的左侧。从列表中选择一首歌应该跳到那首歌。列表的选择高亮应该总是反映当前正在播放的歌曲。我们还将假装我们已经开始听到营销部门关于扩展以支持其他移动平台的传言。将所有这些放在一起,我们将决定是时候选择完全定制的能力了,通过将我们的代码库分割成单独的项目,其中一个公共库项目将被其余的共享。

images

图 11–5。 在平板电脑上以风景模式运行的音乐播放器的新界面原型

创建库项目

首先要做的是创建共享库项目。在 Flash Builder 4.5(或更高版本)中,使用应用菜单,点击文件image新建image Flex 库项目。Flash Builder 将显示如图图 11–6 所示的对话框。

images

图 11–6。 在 Flash Builder 4.5 中创建新库项目

您必须为库项目指定一个名称(如 MusicPlayerLib ),正如我们在 Figure 11–6 中所做的那样。因为我们并不关心在这个项目中支持 web 和桌面(还没有!),我们还在配置部分选择了“移动库”选项。

我们知道我们的展示模型将被放入这个项目。我们也知道其中一个依赖于中期库。因此,我们必须将Metaphile.swc文件添加到这个项目中,以便对其进行编译。我们创建了一个libs目录并将Metaphile.swc放在里面。然后,我们通过右键单击项目并选择 Properties,将libs目录添加到构建路径中。将显示项目的属性对话框,它看起来类似于图 11–7 中所示。点按“Flex 库构建路径”,然后点按“添加 SWC 文件夹…”按钮。在出现的对话框的文本字段中键入目录名“libs ”,然后单击 OK。你的对话框现在应该看起来像图 11–7 中的那样,这表明Metaphile.swc文件已经被添加到你的构建路径中。

images

图 11–7。 Metaphile.swc文件添加到我们的库项目

创建我们的库项目的最后一步是从原始的 MusicPlayer 应用中复制必要的包结构,并将源代码和图形素材复制到正确的位置。Table 11–2 显示了已经添加的包以及每个包中的文件。

Images

请注意,我们已经从原来的 MusicPlayer 项目的views包中取出了自定义的ProgressButton控件,并把它放到了共享库项目的一个新的components包中。库项目现在应该可以编译了,我们已经准备好创建新的项目,我们将使用这些项目来构建将在手机和平板电脑上运行的应用版本。

创建手机和平板电脑项目

我们将通过使用应用菜单并点击文件Images 新建 Images ** Flex 移动项目来创建一个新的 Flex 移动项目。**当“新建 Flex Mobile 项目”对话框出现时,将项目命名为 MusicPlayerPhone,单击“下一步”按钮,选择一个基于视图的应用,然后单击“完成”。必须执行以下步骤来填充新项目:

  1. 将原始 MusicPlayer 项目的assets包中的图形资源复制到新项目的assets包中。这包括闪屏、音量图标和默认专辑封面。
  2. 从原 MusicPlayer 项目的views包中复制源代码,并将它们放入新项目的views包中。这将包括SongListView.mxmlSongView.mxml文件。
  3. 修改SongView.mxml中的代码,以考虑到ProgressButton控件的新包。
  4. 将代码从原始项目的默认包中的主ViewNavigatorApplication MXML 文件复制到新项目的主 MXML 文件中。
  5. 通过右键单击项目并选择“属性”,单击“Flex 构建路径”,单击“添加项目...”按钮,然后选择 MusicPlayerLib 项目,将 MusicPlayerLib 项目添加到该项目的构建路径中。

新项目现在应该可以编译和运行了,结果看起来和第八章中的原始音乐播放器一模一样。如果您有任何问题,可以查看本书示例代码的examples/chapter-11目录中的 MusicPlayerPhone 项目中的源代码。通过重复这些步骤来创建一个 MusicPlayerTablet 项目,您就可以开始使用 MusicPlayer 应用的新的自定义平板电脑界面了。

但是在我们开始之前,这是向您介绍 Eclipse 的工作集特性的好时机,如果您还不知道的话。定义一个工作集将允许您将包资源管理器中列出的项目数量限制为您在任何给定时间正在处理的项目。一旦定义了工作集,就可以轻松地在它们之间切换。您可以使用 Package Explorer 选项卡右侧的 View 菜单来访问 Working Sets 特性。视图菜单的图标是倒置的三角形。图 11–8 显示了它的位置。

images

图 11–8。??【包浏览器】的查看菜单图标

通过单击查看菜单图标并选择“选择工作集…”选项,可以定义新的工作集。将显示“选择工作集”对话框。单击“新建”按钮将显示“新建工作集”对话框。选择“资源”作为工作集类型,然后单击“下一步”。在最后一个对话框中,键入工作集的名称,并选择希望成为工作集一部分的项目。然后单击完成。图 11–9 显示了对话框的顺序。

images

图 11–9。 创建新的工作集

要选择一个工作集,点击查看菜单并再次选择工作集。您定义的工作集将出现在列表中。选中要激活的工作集旁边的复选框,然后单击“确定”。一旦您选择了一个工作集,它的名称将直接出现在视图菜单上,使您只需点击两次就可以在工作集之间切换。当您的 Package Explorer 视图开始被您正在处理的所有不同项目塞满时,能够快速定义工作集并在它们之间切换是一个巨大的好处。

实现自定义平板电脑界面

在新的SongView界面中,歌曲列表会出现在屏幕的左侧。列表中的当前选择应该反映当前正在播放的歌曲。点击列表中的新条目应该会切换到该歌曲。我们在这里描述的是两个绑定:一个在模型中的歌曲列表和列表中的项目之间,另一个在列表的当前选择和模型中的当前歌曲索引之间。

我们将从需要对模型进行的修改开始。将创建一个新的songListArrayCollection作为 UI 中List的绑定源。我们还需要使模型的currentIndex变量可绑定,以作为ListselectedIndex属性的来源,以及可设置的,以便新的列表选择将导致模型采取行动播放一首新歌。清单 11–7 展示了模型的第一个变化。

清单 11–7。 改为SongViewModel是因为没有代码的六页实在是太长了!

`[Bindable]
public class SongViewModel extends EventDispatcher {
    // Some variables removed for brevity…

public var year:String = "";
    public var genres:String = "";
    public var songList:ArrayCollection;

private var _currentIndex:Number = 0;

/** A collection of MusicEntry objects. */
    private var musicEntries:ArrayCollection;

public function SongViewModel(entries:ArrayCollection, index:Number) {
      this.musicEntries = entries;
      this.currentIndex = index;

timer = new Timer(500, 0);
      timer.addEventListener(TimerEvent.TIMER, onTimer);

loadCurrentSong();
      filterEntriesBySongs();
    }

/**
     *** Takes all songs in musicEntries and puts them in songList.**
     */     private function filterEntriesBySongs():void {
      songList = new ArrayCollection();

for (var i:int = 0; i<musicEntries.length; ++i) {
        var entry:MusicEntry = MusicEntry(musicEntries.getItemAt(i));
        if (entry.isSong)
          songList.addItem(entry);
      }
    }`

在清单 11–7 中,我们添加了名为songList的新ArrayCollection,并将currentIndex变量重命名为_currentIndex,以表明它现在将具有与其相关联的publicgetset函数。songList集合在filterEntriesBySong函数中初始化,该函数在类的构造函数的末尾被调用。该函数循环遍历musicEntries集合,并将每首歌曲条目复制到songList集合。

清单 11–8 显示了 model 类中的代码,该类提供对currentIndex属性的访问,并处理对应于currentIndex的歌曲的播放。currentIndexget函数为View提供了对素材价值的访问。set函数存储新值并调用playSongAtCurrentIndex函数。

清单 11–8。??SongViewModel与在当前索引播放歌曲相关的代码

`public function get currentIndex():Number {
      return _currentIndex;
    }

public function set currentIndex(value:Number):void {
      _currentIndex = value;
      playSongAtCurrentIndex();
    }

/**
     * Jump to the beginning of the next song in the list.  Will wrap to
     * the beginning of the song list if needed.
     */
    publicfunction nextSong():void {
      incrementCurrentSongIndex();
      playSongAtCurrentIndex();
    }

/**
     * Moves the play position back to the beginning of the current song
     * unless we are within 3 seconds of the beginning already.  In that
     * case, we jump back to the beginning of the previous song.  Will
     * wrap to the end of the song list if needed.
     */
    publicfunction previousSong():void {
      if (channel && channel.position < 3000) {
        decrementCurrentSongIndex();
        playSongAtCurrentIndex();
      } else {
        percentComplete = 0;       }
    }

/**
     *** Will load and play the song indicated by the currentIndex variable.**
     */
    public function playSongAtCurrentIndex():void {
      loadCurrentSong();

if (isPlaying) {
        pauseSong();
        playSong();
      } else {
        percentComplete = 0;
      }
    }`

playSongAtCurrentIndex功能将歌曲加载到内存中,如果模型处于“播放”模式,则停止当前歌曲并播放这首新歌。如果模型被暂停,那么percentComplete变量将被重置,这样下次调用模型的onPlayPause函数时,播放将从歌曲的开始处继续。我们还回到了模型的previousSongnextSong功能,并将其更改为使用新的playSongAtCurrentIndex功能,以消除不必要的代码重复。边走边打扫!

切换到视图,我们知道当我们在横向模式下将歌曲列表添加到屏幕左侧时,纵向模式 UI 应该保持不变。同时,歌曲信息从界面上一次化身的屏幕左侧迁移到最新设计的屏幕右侧。因为我们不再需要额外的状态,现在这是一个特定于平板电脑的用户界面,MXML 文件的开头现在回到了它的原始形式,除了ActionBar显示专辑名称而不是歌曲名称,就像它在电话界面上一样。所有额外的状态声明以及设置和获取View状态的函数都不见了。

我们需要为List添加声明,作为View的根Group容器的第一个子容器,并确保它只包含在景观状态中。现在,我们还将把专辑封面、纵向模式歌曲信息和播放控件放入一个VGroup中,因为这些部分在纵向和横向状态下总是显示为一个垂直组。最后,一个标签的VGroup将被添加到横向状态,以在屏幕的右侧以那个方向显示歌曲信息。清单 11–9 展示了对SongView MXML 文件的这些更改。

清单 11–9。 修改为SongView MXML 支持新景观界面设计

`<s:Group width="100%" height="100%">
    <s:layout.portrait>
      <s:VerticalLayout paddingTop="10" horizontalAlign="center"/>
    </s:layout.portrait>

<s:layout.landscape>
      <s:HorizontalLayout verticalAlign="top"/>
    </s:layout.landscape>     <s:List id="songList" styleName="songList" includeIn="landscape" width="30%"
            height="100%" dataProvider="{model.songList}" labelField="name"
            selectedIndex="{model.currentIndex}"
            change="model.currentIndex = songList.selectedIndex"/>

<s:VGroup horizontalAlign="center" width.portrait="100%"
              width.landscape="40%" paddingTop="20 ">
      <s:Group width.portrait="{height0.4}" height.portrait="{height0.4}"
               width.landscape="{width0.35}" height.landscape="{width0.35}">
        <s:BitmapImage width="100%" height="100%" source="{model.albumCover}"
                       visible="{model.albumCover != null}"/>

<assets:DefaultAlbum id="placeHolder" width="100%" height="100%"
                             visible="{!model.albumCover}" />
      </s:Group>

<!-- The groups defining the portrait mode song info and controls are unchanged --

</s:VGroup>

<s:VGroup width="30%" gap="60" includeIn="landscape" paddingRight="10"
              paddingTop="20">
      <s:VGroup width="100%" horizontalAlign="right">
        <s:Label styleName="albumInfoLabel" text="Song"/>
        <s:Label styleName="albumInfo" text="{model.songTitle}"
                 maxWidth="{width*.3}" maxDisplayedLines="2"/>
      </s:VGroup>
      
    </s:VGroup>`

List使用模型的新songList作为它的dataProvider,并使用它来显示歌曲名称。它的selectedIndex属性被绑定到模型的currentIndex属性,以确保当前播放的歌曲也是列表中高亮显示的那首。每当List的选择改变时,新的selectedIndex用于设置模型的currentIndex属性。这允许用户点击列表中的项目来改变当前正在播放的歌曲。

实现这些更改后,应用现在如图 11–10 所示。该图显示了在摩托罗拉 XOOM 上横向运行的应用,并在屏幕左侧展示了新的歌曲列表。该图右侧的图像显示了在三星 Galaxy Tab 上以纵向模式运行的应用。将平板电脑从纵向旋转到横向,歌曲列表将无缝显示。当然,我们在 MusicPlayerPhone 项目中安全地隐藏了我们最初的手机版本界面,它不受平板电脑版本中这些新功能的影响。当然,对共享库中的SongViewModel的更新将会出现在手机版本中,但是它们在那个应用中没有被使用,因此没有效果。

在某些方面,为每个平台拥有单独的项目简化了构建过程,尤其是当您开始处理多个平台时,因为您可以为每个项目拥有一个应用 XML 描述符文件,而不是在构建时交换它们。

images

图 11–10。 新的平板电脑界面在摩托罗拉 XOOM 上以横向模式运行,在三星 Galaxy Tab 上以纵向模式运行

过渡到电视

成为 Adobe Flash 生态系统的一员,这是一个激动人心的时刻。除了 web、桌面和 Android 平台,AIR 也正在成为 iOS 设备、BlackBerry 平板电脑,甚至电视机、蓝光播放器和机顶盒的可行编程环境!这是一个真正让你在生活中的所有屏幕上利用现有编程和设计技能的环境——甚至是大屏幕。

在 2011 年 5 月的谷歌 I/O 大会上,谷歌宣布将 Android 3.1,即所谓的蜂巢版本,引入其谷歌电视平台。通过此次更新,谷歌电视用户将可以使用 Android market。通过一些限制,您现有的 Android 应用 AIR 应该可以很容易地移植到 Google TV 平台。此外,所有零售的新谷歌电视设备都将包括 Android 调试器,这意味着你应该能够在客厅的谷歌电视上运行和测试你的应用。

另一条通往客厅的道路是 Adobe 的 AIR for TV 平台。这是电视、机顶盒和蓝光播放器的运行时。它目前处于预发布阶段,在 AIR 2.5 上运行。为电视平台开发时要注意的一件事是,它们通常位于 CPU 马力谱的低端。电视中的 CPU 通常比普通智能手机中的 CPU 要慢得多。这并不一定意味着你用于电视应用的 AIR 会很慢,但确实意味着你要注重性能。第十章中给出的许多建议也适用于电视平台。鉴于电视中常见的较慢的 CPU,你应该特别注意那一章“减少代码执行时间”一节中给出的建议。

Adobe AIR for TV 预计将在三星的智能电视平台上首次亮相,在撰写本文时,预计将于 2011 年推出。

如果你决定为这些电视平台中的一个开发,有一些事情你需要记住。第一,一台电视的输入法不一样。即使电视有触摸屏,也没有人愿意经常起身走到电视前触摸屏幕,以便与他们的应用进行交互。因此,电视可能会使用小触摸板或方向按钮板进行导航和交互。其次,正如谷歌提醒我们的,电视实际上是一种“10 英尺的体验”屏幕更大,所以控件和字体也应该更大。处理电视几乎肯定需要一个新的设计通过你的应用。

移植到行动手册

尽管 Research In Motion 是平板电脑市场的新来者,但黑莓 PlayBook 是一个有趣的入口。PlayBook 外形小巧,只有 7.6 英寸宽和 5.4 英寸高,这使它成为一款极其便携的设备。它配备了 7 英寸触摸屏,1 GHz 双核处理器和 1 GB 内存。它与 QNX 中微子实时操作系统配对。这种基于微内核架构的操作系统因其在关键任务系统中的应用而闻名。

PlayBook 的一个优点是它对开发人员非常友好。它为开发人员提供了不少于四种开发应用的环境:native C/C++、Java、HTML5 和相关技术,当然还有 Adobe AIR。再者,AIR 在这个平台上不是二等公民。AIR 应用可以利用视频和图形的硬件加速。虽然存在常见的 Flash 平台组件,但 AIR 程序员可以使用一些特殊的包,这些包使 ActionScript 程序能够在其用户界面中使用本机高性能 QNX 组件。AIR 应用甚至可以访问该平台的原生通知功能。简而言之,AIR 程序得到了很好的支持,并且很好地集成到了平台中。平板电脑唯一真正的缺点是,由于它是一个全新的平台,其市场渗透率相当低。

那么,作为一名 Flash/Flex/AIR 开发人员,你该如何进入这个新市场呢?一个很好的起点是 BlackBerry table t OS SDK for Adobe Air 开发资源网站。 3 从那里你会找到“入门指南” 4 的链接以及安装开发环境的步骤。您首先需要下载并解压缩 SDK 安装程序。安装程序将在您当前安装的 Flash Builder 4.5 的sdks目录中创建一个新的PlayBook目录。这个目录将包含开发剧本应用所需的一切。行动手册模拟器是一个与 VMware 兼容的虚拟机映像,可以在您的 Windows、Mac 或 Linux 桌面上运行行动手册运行时环境。该映像包含在 PlayBook SDK 文件中,这些文件位于您的 Flash Builder 安装目录中。只需在 VMware 中打开这个虚拟机映像,行动手册环境就会启动。启动时,它会要求输入密码。键入“剧本”,您应该会看到剧本 UI 出现。


3

4

您可以在 Flash Builder 4.5 中为您的剧本应用创建一个新项目,方法是从应用的菜单中选择文件 Images 新建 Images ** ActionScript 移动项目**。您使用默认的 SDK,并选择 BlackBerry Tablet OS 作为目标平台。

注意:在撰写本文时,官方的 BlackBerry 平板电脑操作系统支持预计将于 2011 年夏季在 Flash Builder 更新中发布。这可能会改变您为此平台创建移动应用项目的方式。

您可以在桌面上的模拟器中运行和测试 AIR 应用。您只需使用虚拟机中运行的剧本环境的 IP 地址,在 Flash Builder 中为您的项目创建一个运行配置。您可以通过点击位于剧本屏幕右上角的胸前带着装备的人的图标来获得剧本的 IP 地址。刚刚提到的“入门指南”为所有这些步骤提供了简单易懂的说明,这将使您在一个小时内开始在模拟器上进行开发。

这是足够的序言;清单 11–10 显示了一个简单的黑莓 PlayBook Hello World 程序。

清单 11–10。 黑莓 PlayBook 的 Hello World ActionScript 程序

`import flash.display.Bitmap;
  import flash.display.GradientType;
  import flash.display.Graphics;
  import flash.display.SpreadMethod;
  import flash.display.Sprite;
  import flash.events.MouseEvent;
  import flash.geom.Matrix;
  import flash.text.TextFormat;
  import qnx.ui.buttons.LabelButton;
  import qnx.ui.text.Label;

[SWF(width="1024", height="600", frameRate="30")]
  publicclass PlayBookHelloWorld extends Sprite
  {
    [Embed(source="splash.png")]
    privatevar imageClass:Class;

publicfunction PlayBookHelloWorld()
    {
      var bitmap:Bitmap = new imageClass();
      bitmap.x = 10;
      bitmap.y = 10;

var goodByeButton:LabelButton = new LabelButton();
      goodByeButton.label = "Good Bye";       goodByeButton.x = stage.stageWidth - goodByeButton.width;
      goodByeButton.y = stage.stageHeight - goodByeButton.height;
      goodByeButton.addEventListener(MouseEvent.CLICK, onClick);

var myFormat:TextFormat = new TextFormat();
      myFormat.color = 0xf0f0f0;
      myFormat.size = 48;
      myFormat.italic = true;

var label:Label = new Label();
      label.text = "Hello Pro Android Flash!";
      label.x = bitmap.width + 20;
      label.y = 10;
      label.width = stage.stageWidth - bitmap.width - 10;
      label.height = 100;
      label.format = myFormat;

addChild(createBackground());
      addChild(bitmap);
      addChild(goodByeButton);
      addChild(label);

stage.nativeWindow.visible = true;
    }

privatefunction onClick(event:MouseEvent):void{
      stage.nativeWindow.close();
    }

privatefunction createBackground():Sprite {
      var type:String = GradientType.LINEAR;
      var colors:Array = [ 0x808080, 0x404040 ];
      var alphas:Array = [ 1, 1 ];
      var ratios:Array = [ 0, 255 ];
      var spread:String = SpreadMethod.PAD;

var matrix:Matrix = new Matrix();
      matrix.createGradientBox( 100, 100, (90 * Math.PI/180), 0, 0 );

var sprite:Sprite = new Sprite();
      var g:Graphics = sprite.graphics;
      g.beginGradientFill( type, colors, alphas, ratios, matrix, spread );
      g.drawRect( 0, 0, 1024, 600 );

return sprite;      
    }
  }`

正如你所看到的,它看起来很像任何其他的 Flash 程序。我们使用了几个基本的 QNX 控件,只是为了展示在你的程序中包含它们的样子。他们有一个熟悉 Flash 编程的人都非常熟悉的 API。图 11–11 显示了剧本环境和 Hello World 程序在模拟器中运行时的样子。

images

图 11–11。 一个运行在黑莓 PlayBook 模拟器上的简单 Hello World ActionScript 程序

如果您想在实际的 PlayBook 硬件上运行您的应用,您将需要一个“调试令牌”。获得这样的令牌是免费的,但是您需要注册剧本开发计划。如果您希望最终将应用部署到 BlackBerry 应用商店,您还需要申请一个密钥来签署您的应用。

如果你决定将你的 Android 应用移植到 PlayBook,你应该遵循我们之前在从手机移植到平板电脑时使用的建议:了解你的目标平台。比如 PlayBook 没有硬件后退按钮;因此,就像 iPhone 或 iPad 一样,大多数应用屏幕的左上角通常都有一个后退按钮。和往常一样,了解您的目标平台的一个好方法是研究该平台的流行应用。PlayBook 上有一个脸书应用和大量预装的应用供您查看。

调查 iOS

就全球受欢迎程度而言,Android 和 iPhone 设备目前主导着智能手机市场。这使得在将你的应用移植到其他平台时,苹果的 iOS 作为一个潜在的目标很有吸引力。但当你加上苹果的 iPad 是平板电脑市场无可争议的王者这一事实时,这个决定突然变得非常明显。

设置苹果的开发环境有点类似于 PlayBook 所需的过程,尽管苹果没有在实际设备上测试软件的免费选项。你必须加入它的开发者计划 5 (目前每年 99 美元)才能在真正的硬件上运行和测试你的应用。

然而,一旦你获得了会员资格和开发密钥,为 iOS 编写基于 ActionScript 的应用就和为 Android 和 PlayBook 编写应用一样了。

注意:与剧本一样,在撰写本文时,预计将在 2011 年夏天更新对 Flash Builder is 开发的支持。

同样,您需要花一些时间来熟悉这个平台上使用的常见设计模式。例如,就像 PlayBook 一样,iOS 设备没有硬件后退按钮。幸运的是,Adobe 平台团队中的优秀人员在这方面让开发人员的生活变得更加轻松。ActionBardefaultButtonAppearance样式属性可以设置为“斜角”以近似原生 iOS ActionBar按钮的外观。此外,ActionBar中的标题倾向于居中,而不是像在 Android 上那样右对齐。可以将ActionBartitleAlign属性设置为“中心”,以在您的 AIR 应用中实现这种效果。参见第三章中的列表 3-8 ,了解在应用中使用这些样式的示例。

您甚至可以在运行时动态应用这些样式,方法是使用@media (os-platform:“ios”) CSS 选择器,或者确保Capabilities.cpuArchitecture返回字符串“ARM”并且Capabilities.os返回包含术语“iPhone”的字符串。


5

总结

本章向您展示了如何使用您的移动 AIR 应用,并使它们适应 Android 平板电脑、电视,甚至苹果和黑莓设备。您已经了解了以下内容:

  • 如何使用状态和状态组为不同的设备定制您的界面,同时保持单一的代码库
  • 如何将您的应用拆分成多个项目,这些项目包括共享库代码以及针对您想要的每个不同平台的完全定制的用户界面
  • 你有什么选择来扩大你在电视屏幕上的影响力,以及这样做时你需要考虑的一些事情
  • 如何使用 Adobe AIR 的 BlackBerry PlayBook 开发环境
  • 将你的 Android 应用移植到苹果 iOS 平台的一些技巧

你成功了!欢迎来到书的结尾!我们希望你能像我们喜欢写它一样喜欢阅读它。如果你已经学会了一两件事,让你作为 Android 开发者的 AIR 生活变得更轻松,那么你的旅程就值得了。这是科技界激动人心的时刻。这个由智能手机、平板电脑、智能电视和超快的无线带宽组成的现代世界为全世界的软件开发人员带来了新的机遇和新的挑战。祝你好运,祝你编程成功!

posted @   绝不原创的飞龙  阅读(59)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示