admin管理员组

文章数量:1659575

原文:Pro Android Flash

协议:CC BY-NC-SA 4.0

七、利用硬件输入

在前一章中,您已经了解了如何将您的 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/en_US/FlashPlatform/reference/actionscript/3/flash/media/Microphone.html](http://help.adobe/en_US/FlashPlatform/reference/actionscript/three/flash/media/Microphone.html)

Figure 7–1 展示了 MicrophoneBasic 应用在实际手机上运行时的样子。

**图 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

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/flex/spark”
        xmlns:mx=“library://ns.adobe/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/en_US/FlashPlatform/reference/actionscript/3](http://help.adobe/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 项目的应用描述符文件

`<?xml version="1.0" encoding="utf-8" standalone="no"?>

    CameraBasic
    CameraBasic
    CameraBasic
    0.0.1


        [This value will be overwritten by Flash Builder…]
        false
        landscape
        true
        false
    


        <![CDATA[                              ****                      ]]>
    
`

您需要在 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 示例项目

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/flex/spark”
        xmlns:mx=“library://ns.adobe/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 所示,菜单可见。

图 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 左侧的滑稽脸贴图只是一个位于灰色背景中心的红点。这将保持周围的像素不变,但会扩大图像中心的像素,以产生球根状效果。涟漪贴图只是红色和黑色的交替圆圈,导致正负像素偏移(扩展和收缩),给人的印象是源图像是通过荡漾的水观看的,尽管这种涟漪在时间和空间上似乎是冻结的(嗯,听起来像一部糟糕的星际迷航集)。

图 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 显示了使用这些滤镜拍摄的一些图像。从左上角逆时针方向,你可以看到一个没有滤镜的图像,滑稽脸滤镜,铅笔滤镜,夜视滤镜。

**图 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。 查看支持图像拍摄和保存的增强功能

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/flex/spark”
        xmlns:mx=“library://ns.adobe/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 应用捕捉一只合作犬的图像。

图 7–5。 我们的模特欣然同意使用她的肖像作为交换。

CameraRoll类还允许用户浏览和选择保存在设备上的图像以及用户保存在互联网相册中的图像!这个特性将在下一节解释。

服务员

CameraRoll在 Android 设备上浏览照片几乎和保存照片一样简单。我们将在一个名为 PhotoCollage 的新示例程序的上下文中说明这个特性。这个程序让你选择已经存储在设备上的图像,并把它们排列成拼贴画。您可以使用多点触控手势来拖动、缩放和旋转图像。当图像按照您的喜好排列后,您可以将新图像存储回“相机胶卷”。这个例子可以在本书的源代码的examples/chapter-07 目录中找到。清单 7–10 显示了应用主视图的 MXML 文件。

清单 7–10。 照片收藏应用的首页视图— PhotoCollageHome.mxml

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/flex/spark”
        xmlns:mx=“library://ns.adobe/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 应用。

**图 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

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
              xmlns:s=“library://ns.adobe/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包的内容:加速度计和地理定位。我们希望你会发现它又快又切题。

图 7–7。 用原生安卓相机界面和CameraUI 拍摄的机器人图像

加速度计

加速度传感器允许您通过测量重力沿 x、y 和 z 轴产生的加速度来检测设备的方向。静止时,地球上的任何物体都会由于重力而经历大约 9.8 米/秒/秒(米/秒/秒)的加速度。9.8 米/秒/秒也被称为 1 重力或 1 重力,或者简单地称为 1 克加速度。因此,10g 的加速度是重力的 10 倍,即 98 米/秒/秒。这是一个非常大的重力,通常只有战斗机飞行员在极端机动中才会感受到!

由于手机是一个三维物体,它的方向可以通过观察 1g 的力如何沿其三个轴分布来确定。加速度计还可以告诉你手机是否受到震动或以其他快速方式移动,因为在这种情况下,手机在三个轴上的加速度明显大于或小于 1g。您需要知道手机的轴是如何布置的,以便从加速度计值中收集有用的信息。图 7–8 显示了手机加速轴相对于手机机身的方向。

图 7–8。 安卓手机的加速度计轴

如果您对齐图 7–8 中标记的轴之一,使其与重力正对*,您将在该轴上读取 1g 的加速度。如果一个轴垂直于重力,那么它的读数将是 0g。例如,如果你把手机放下,让面朝上,放在一个平面上,那么加速度计在 z 轴上的读数大约为+1g,在 x 轴和 y 轴上的读数大约为 0g。如果你将手机翻转过来,让它面朝下,加速度计将在 z 轴上显示-1g。*

加速度计和加速度计事件类别

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 程序的输出。

图 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 视图的源代码

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/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 的应用描述符,显示了精确定位许可的正确使用

`<?xml version=*"1.0"* encoding=*"utf-8"* standalone=*"no"*?>
<application xmlns=“http://ns.adobe/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
    


        
            <![CDATA[                                                               ****                          ]]>
        
    
`

Android 使用ACCESS_COARSE_LOCATION将地理定位数据限制为仅使用 Wi-Fi 和手机信号塔获取位置信息。先前使用的ACCESS_FINE_LOCATION权限包括粗略定位权限,还增加了访问 GPS 接收器以获得更精确读数的能力。图 7–10 显示了运行中的 GeolocationBasic 程序的屏幕截图。

**图 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元数据标签的声音文件

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/flex/spark”
        xmlns:mx=“library://ns.adobe/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

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/flex/spark”
        xmlns:mx=“library://ns.adobe/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/mxml/2009"         xmlns:s="library://ns.adobe/flex/spark"         xmlns:mx="library://ns.adobe/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.navigateToURL;
      import mx.effects.easing.Back;

private static const CM_URL_STR:String = “http://www.jonathancoulton”+
          “/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 中的所示。

图 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 所示。

加载完数据后,调用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 说明了这三个协作类之间的关系。

图 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 说明了这一过程。

图 8–3。 录音机应用的四种状态

应用在stopped状态下启动。当用户点击屏幕时,应用转换到recording状态,并开始录制他或她的声音。另一次点击停止记录并转换到readyToPlay状态。当用户准备好收听录音时,另一次点击在playing状态下开始回放。然后,用户可以第四次点击以停止播放并返回到stopped状态,准备再次录制。如果播放自行结束,应用也应自动转换到stopped状态。清单 8–6 显示了这个应用唯一的View的 MXML。

清单 8–6。??【录音笔应用的首页】??

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/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状态的动画转换。

图 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 文件

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/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/](http://www.id3/)

清单 8–11。 从 MP3 文件中读取 ID3 元数据

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/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。?? 实现播放/暂停切换

`<?xml version="1.0" encoding="utf-8"?>
<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。 实现音量和声相调整

`<?xml version="1.0" encoding="utf-8"?>
<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/eaaDev/PresentationModel.html](http://martinfowler/eaaDev/PresentationModel.html), July 19, 2004

4 马丁·福勒,《模型视图控制器》,[martinfowler/eaaCatalog/modelViewController.html](http://martinfowler/eaaCatalog/modelViewController.html)

图 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

`<?xml version="1.0" encoding="utf-8"?>
<s:ViewNavigatorApplication xmlns:fx=“http://ns.adobe/mxml/2009”
                     xmlns:s=“library://ns.adobe/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

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/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。请注意,人字形没有因缩放而产生的像素化伪像,如果我们在两个屏幕上使用相同的位图,就会出现这种情况。

图 8–6。??SongListView运行在不同 dpi 分类的设备上

**注意:**你也可以使用一个 FXG 图形作为一个IconItemRenderer的图标或装饰,方法是以与前面的MultiDPIBitmapSource相同的方式声明它。不幸的是,由于图标和装饰将被转换成位图,然后缩放,您将失去使用矢量图形的好处。出于这个原因,我们建议您将MultiDPIBitmapSource对象与您的自定义IconItemRenderers一起使用。

宋观

这就把我们带到了应用的真正核心:让用户播放音乐的视图!我们希望这个界面具有与大多数其他音乐播放器相同的功能。我们将显示歌名和专辑封面。它应该有控件,允许用户跳到下一首或上一首歌曲,播放和暂停当前歌曲,调整当前歌曲的位置以及音量和平移(只是为了好玩)。产生的界面如图 8–7 所示。

图 8–7。SongView界面运行在两种不同的 dpi 设置下

从 Figure 8–7 可以看出,这个界面比列表视图稍微复杂一点。它甚至包括一个自定义控件,不仅可以作为播放/暂停按钮,还可以作为当前歌曲播放位置的进度指示器。此外,你可以通过在按钮上来回滑动手指来控制歌曲的位置。编写这个自定义控件只是本节将要讨论的主题之一。

清单 8–20 显示了定义这个View的 MXML 文件的一部分。由于这是一个更大的接口声明,我们将把它分解成更小、更容易理解的部分。

清单 8–20。??【美国】和SongView MXML 文件的剧本章节

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/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界面。

图 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/p/metaphile/](http://code.google/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 显示了各种状态下的控制。

图 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宣言

`<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/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 的高端设备,那么这些数字对于您的需求来说可能有点过于保守。

您还可以在应用中采取几个步骤来确保获得最佳性能。您应该避免使用变换:旋转、透视投影和颜色变换。避免阴影、滤镜效果和像素弯曲效果。您应该尽可能避免透明度和视频对象与其他图形的混合。

最好也尽量避免过多的 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

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/flex/spark”
        viewDeactivate=“onViewDeactivate()”
        actionBarVisible=“false”>

fx:Script/
    <![CDATA[
      privatestaticconst sourceURL:String = “http://ia600408.us.archive”+
          “/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,其中的控件现在已经足够大,可以使用了。

**图 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

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/flex/spark”
        xmlns:mx=“library://ns.adobe/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/”+
  “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 设备上运行的结果。像这样的小型播放器更适合这种类型的屏幕。

图 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/devnet/flashplayer/articles/stage_video.html,2011 年 2 月 8 日

10http://sourceforge/projects/osmf.adobe/files/10

清单 8–33。《MXML 宣言》为OSMFVideoView

`<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx=“http://ns.adobe/mxml/2009”
        xmlns:s=“library://ns.adobe/flex/spark”
        xmlns:mx=“library://ns.adobe/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.NetLoader;

privatestaticconst sourceURL:String = “http://ia600408.us.archive”+
  “/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.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 设备上运行的应用。

图 8–12。 抓拍短视频后的录像机示例应用

总结

随着移动设备变得越来越强大,在移动设备上欣赏媒体的能力将变得越来越普遍。现在,您已经掌握了在自己的移动应用中利用 Flash media APIs 的能力所需的知识。本章涵盖了与在 Flash 平台上播放各种类型的媒体相关的各种主题。特别是,您现在知道了以下内容:

  • 如何使用SoundEffect类嵌入和播放音效
  • 如何使用Sound类加载 MP3 文件
  • 如何使用SoundChannelSoundTransform类控制声音的回放、音量和平移
  • 如何播放动态生成或录制的声音
  • 如何编写可维护和可测试的 Flex 移动应用
  • 如何为 Flex 4 移动应用编写自定义控件
  • 如何使用 Spark VideoPlayer组件、NetStream类和 OSMF 库播放视频
  • 如何与CameraUI类接口以捕获视频,然后在 Android 应用的 AIR 中播放捕获的视频

在下一章中,我们将继续编写真实的 Flex 移动应用的主题,看看在团队中工作和利用设计师-开发人员工作流的一些方面。

本文标签: 高级教程flash