八、富媒体集成
如果你的用户没有使用他们的 Android 设备打电话,那么他们很可能在玩游戏、听音乐或看视频。说到 it,对于现代消费者来说,音频和视频的消费可能比他们的移动设备的通信能力更重要。幸运的是,出色的音频和视频支持是 Flash 平台的真正优势之一。事实上,这是 Flash Player 在我们的计算机和移动设备上变得如此普遍的主要原因之一。
前一章向您展示了如何在 Android 设备上捕捉音频和视频。本章以这些概念为基础,将教您如何使用 Flash 平台的力量来释放 Android 移动设备的富媒体潜力。
播放音效
音效通常是响应各种应用事件(如弹出警告或按下按钮)而播放的简短声音。声音效果的音频数据应该在 MP3 文件中,可以嵌入到应用的 SWF 文件中,也可以从互联网上下载。您通过使用Embed
元数据标签来标识素材,将 MP3 素材嵌入到您的应用中,如清单 8–1所示。
清单 8–1。 嵌入带有Embed
元数据标签的声音文件
`<?xml version="1.0" encoding="utf-8"?>
<![CDATA[ import mx.core.SoundAsset;
[Embed(source="mySound.mp3")]
private var MySound:Class;
private var sound:SoundAsset = new MySound();]]>
`
Embed
元数据标签将使编译器对 MP3 文件进行代码转换,并将其嵌入到应用的 SWF 文件中。source
属性指定 MP3 文件的路径和文件名。在这种情况下,我们将文件放在与源文件相同的包中。您可以通过创建与Embed
标签相关联的类的实例来访问嵌入的声音,在清单 8–1中,该实例是一个名为MySound
的类。MySound
类由编译器生成,将是mx.core.SoundAsset
的子类。因此,它为音频素材的基本回放提供了所有必要的支持。在清单 8–1中,我们通过创建一个名为sound
的实例变量并调用其play
方法来响应按钮点击,从而利用了这种支持。
音效类
虽然知道幕后发生的事情很好,但是通常不需要在 Flex 程序中创建和实例化SoundAsset
。您选择的工具通常是SoundEffect
类,因为它能够在回放样本时轻松创建有趣的效果。它在回放过程中提供了对循环、平移和音量效果的简单控制。因为它扩展了基本的mx.effect.Effect
类,所以它可以在任何可以使用常规效果的地方使用。例如,你可以将一个SoundEffect
实例设置为一个Button
的mouseDownEffect
或者一个Alert
对话框的creationCompleteEffect
。清单 8–2 展示了如何做到这一点,以及如何手动弹奏一个SoundEffect
。
清单 8–2。 创建并播放一个循环SoundEffect
`<?xml version="1.0" encoding="utf-8"?>
<![CDATA[ [Bindable] [Embed(source="mySound.mp3")] private var MySound:Class;
private function playEffect(event:MouseEvent):void {
mySound.end();
mySound.play([event.target]);
}
]]>
`
在清单 8–2中突出显示的SoundEffect
声明创建了一个每次播放时循环两次的声音效果。注意设置为false
的useDuration
属性。一个SoundEffect
的duration
默认设置为 500 毫秒,如果useDuration
保持默认值true
,那么只会播放你声音的前半秒。因此,您几乎总是希望将此属性设置为false
,除非您也设置了duration
属性,以便只播放部分音效。SoundEffect
的source
属性被赋予嵌入声音素材的类名。
然后我们创建两个按钮来说明玩SoundEffect
的两种不同方式。第一个按钮只是将SoundEffect
的实例id
设置为它的mouseDownEffect
。每次在按钮上按下鼠标按钮时,都会播放我们的音频样本。每次按下鼠标按钮,都会创建并播放一个新的效果。如果您点按的速度足够快,并且您的声音样本足够长,就有可能听到它们同时播放。
单击第二个按钮将调用playEffect
方法,该方法做两件事。首先,它将通过调用end
方法来停止当前正在播放的任何效果实例。这确保声音不会与自身的任何其他实例重叠。第二,使用按钮作为目标对象来播放新的声音效果。MouseEvent
的target
属性提供了一种便捷的方式来引用我们将用作效果目标的按钮。注意,play
方法的参数实际上是一个目标数组。这就是为什么我们需要在event.target
参数周围加一组方括号。
您可以看到,以这种方式嵌入的每个声音都需要三行代码:两个元数据标记和声明声音素材类名的代码行。有一种方法可以避免这种情况,直接将声音嵌入到音效中。
嵌入式音效示例
您可以在SoundEffect
声明的source
属性中使用@Embed
指令。这种技术用于 SoundEffectBasic 示例应用,可以在本书示例代码的examples/chapter-08
目录中找到。这个示例应用还演示了如何在播放时调整声音效果的音量和声相。清单 8–3显示了应用的主View
。
清单 8–3。T3【声效基础范例计划】之家View
T4】
<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
title="Code Monkey To-Do List">
`
<![CDATA[ import flash.net.navigateToURL; import mx.effects.easing.Back;
private static const CM_URL_STR:String = "http://www.jonathancoulton.com"+ "/2006/04/14/thing-a-week-29-code-monkey/";
private static const CM_URL:URLRequest = new URLRequest(CM_URL_STR);
private function play(event:MouseEvent, effect:SoundEffect):void { effect.end(); effect.play([event.target]); } ]]>
`
在清单 8–3中首先要注意的是在每个SoundEffect
声明的source
属性中使用了@Embed
语句。这允许您在一个步骤中嵌入声音素材并将其与SoundEffect
相关联。就像以前一样,如果你的声音文件和你的源文件在不同的包中,那么你必须在@Embed
语句中包含声音文件的路径,这样编译器才能找到它。
每个音效将播放乔纳森·科尔顿的歌曲《代码猴子》的一小段摘录。我们使用了SoundEffect
类的volumeFrom
和volumeTo
属性,在音频样本播放时将音量从 1.0(最大音量)渐变到 0.0(最小音量)。由于我们没有指定一个volumeEasingFunction
,这将是一个线性渐变。同样,第二个声音效果将在样本播放时线性地将音频样本从-1.0(左扬声器)移动到 1.0(右扬声器)。如果你想为你的平移效果使用不同的缓动函数,你可以使用SoundEffect
类的panEasingFunction
属性来指定它。最后的SoundEffect
声明展示了如何使用 Flex 的一个内置 easers 来改变样本播放时的音量。通过使用Back
easer 的fadeOut
方法,我们将使音量下降到 0.0 的目标值,稍微超过它,并在最终固定在最终值之前再次反弹超过 0.0。这会在音频样本的结尾产生一个有趣的小音量波动。
这个例子再次演示了播放音效的两种不同方法。在屏幕的底部还有第四个按钮,当点击它时,会启动 Android 的原生网络浏览器,并通过使用第 6 章中的方法将你带到“代码猴子”网页。结果应用如图 8–1中的所示。
图 8–1。 运行在 Android 设备上的代码猴子音效示例
SoundEffect
类非常适合播放小的声音效果来响应应用事件。如果您需要对应用中的声音进行更高级的控制,那么是时候深入挖掘 Flash 平台必须提供的功能了。
复杂的声音解决方案
对于大多数应用来说,SoundEffect
类是一个方便的抽象,这些应用的需求不会超出偶尔提示或通知用户的能力。在一些应用中,声音是主要成分之一。如果你想录制语音备忘录或播放音乐,那么你需要更深入地了解 Flash 声音 API。我们将首先看一看Sound
类和它的伙伴:SoundChannel
和SoundTransform
。所有这三个类都可以在flash.media
包中找到。
Sound
类充当音频文件的数据容器。它的主要职责是提供将数据加载到其缓冲区的机制,并开始回放该数据。加载到Sound
类中的音频数据通常来自 MP3 文件或应用本身动态生成的数据。不出所料,这个类中需要注意的关键方法是load
和play
方法。您使用load
方法来提供应该加载到Sound
中的 MP3 文件的 URL。数据一旦加载到Sound
中,就不能更改。如果您稍后想要加载另一个 MP3 文件,您必须创建一个新的Sound
对象。向Sound
对象的构造函数传递一个 URL 相当于调用load
方法。Sound
类在加载音频数据的过程中调度几个事件,如Table 8–1所示。
加载完数据后,调用Sound
类的play
方法将导致声音开始播放。play
方法返回一个SoundChannel
对象,该对象可用于跟踪声音播放的进度并提前停止播放。SoundChannel
还有一个与之关联的SoundTransform
对象,可以用来改变声音播放时的音量和声相。有三个可选参数可以传递给play
方法。首先是startTime
参数,它将导致声音在样本中指定的毫秒数开始播放。如果您希望声音播放一定的次数,也可以传递循环计数。最后,如果您想在声音开始播放时设置声音的初始转换,也可以提供一个SoundTransform
对象作为play
方法的参数。您传递的变换将被设置为SoundChannel
的SoundTransform
。
每次调用Sound.play
方法时,都会创建并返回一个新的SoundChannel
对象。SoundChannel
在声音播放时充当你与声音互动的主要点。它允许你跟踪当前的位置和音量。它包含一个stop
方法,该方法中断和终止声音的回放。当一个声音到达其数据的末尾时,SoundChannel
类将通过分派类型为flash.events.Event.SOUND_COMPLETE
的soundComplete
事件来通知您。最后,您还可以使用它的soundTransform
属性来操纵声音的音量,并将声音移动到左右扬声器。图 8–2 说明了这三个协作类之间的关系。
图 8–2。Sound``SoundChannel``SoundTransform
的关系
诚然,从SoundChannel
到说话者的路径并不像图 8–2暗示的那样直接。在音频信号到达扬声器之前,存在几个层(包括操作系统驱动程序和数模转换电路)。Flash 在flash.media
包中还提供了另一个名为SoundMixer
的类,它包括几个静态方法,用于在全局级别上操作和收集关于应用正在播放的声音的数据。
这就结束了我们对使用 Flash 在 Android 设备上播放声音所需要熟悉的类的概述。在下一节中,我们将看一些使用这些类来播放来自内存缓冲区和存储在设备上的文件的声音的例子。
播放录制的声音
我们在第 7 章的 MicrophoneBasic 示例应用中向您展示了如何从设备的麦克风录制音频数据。扩展该示例将为更深入地探索 Flash 的音频支持提供一个方便的起点。您可能还记得,我们给Microphone
对象附加了一个事件处理程序来处理它的sampleData
事件。每次麦克风为我们的应用获取数据时,都会调用处理程序。在那个例子中,我们实际上没有对麦克风数据做任何事情,但是将数据复制到一个ByteArray
中用于以后的回放应该是一件简单的事情。问题是:我们如何播放来自ByteArray
的声音数据?
动态生成声音数据
如果你在一个没有加载任何东西的Sound
对象上调用play()
方法,这个对象将被迫寻找声音数据来播放。它通过调度sampleData
事件来请求声音样本。事件的类型是SampleDataEvent.SAMPLE_DATA
,在flash.events
包中找到。这恰好与Microphone
类用来通知我们样本可用的事件类型相同。我们之前问题的答案很简单:您只需为Sound
的sampleData
事件附加一个处理程序,并开始将字节复制到事件的data
属性中。
因此,我们增强的应用将为sampleData
事件提供两个独立的处理程序。当麦克风处于活动状态时,第一个会将数据复制到一个ByteArray
,当我们回放时,第二个会将数据从同一个ByteArray
复制到Sound
对象。新应用的源代码可以在位于examples/chapter-08
目录下的 SoundRecorder 应用中找到。清单 8–4显示了麦克风数据的sampleData
事件处理程序。
清单 8–4。 麦克风数据通知的设置代码和事件处理程序
`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 usprivate function onMicSample(event:SampleDataEvent):void {
if (microphone.activityLevel > activityLevel) {
activityLevel = Math.min(50, microphone.activityLevel);
}
if (event.data.bytesAvailable) { recordedBytes.writeBytes(event.data); } }`
onCreationComplete
处理程序负责检测麦克风,初始化它,并创建应用用来存储和播放声音的ByteArray
和Sound
对象。请注意,麦克风的rate
设置为 22 kHz。这对于捕获语音记录来说是足够的质量,并且比以全 44 kHz 记录占用更少的空间。
这个处理程序很简单。与之前一样,Microphone
对象的activityLevel
属性用于计算一个数字,该数字随后用于确定在显示器上绘制的动画曲线的幅度,以指示声音级别。然后事件的data
属性,也就是一个ByteArray
,被用来确定是否有麦克风数据可用。如果bytesAvailable
属性大于零,那么字节从data
数组复制到recordedBytes
数组。这对于正常的录音来说效果很好。如果您需要记录数小时的音频数据,那么您应该将数据流式传输到服务器,或者将其写入设备上的文件中。
因为我们处理的是原始音频数据,所以由程序来记录声音的格式。在这种情况下,我们有一个麦克风,为我们提供 22 kHz 单声道(单声道)声音样本。Sound
对象期望 44 kHz 立体声(左右声道)声音。这意味着每个麦克风样本必须写入Sound
数据两次,以将其从单声道转换为立体声,然后再写入两次,以从 22 kHz 转换为 44 kHz。因此,每个麦克风样本名义上将被复制到Sound
对象的数据数组中四次,以便使用与捕获时相同的速率回放录音。清单 8–5显示了执行复制的Sound
的sampleData
处理程序。
清单 8–5。T3Sound
对象的数据请求的事件处理程序
`// 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 个样本。
我们现在有能力记录和回放声音样本。应用所缺少的是在两种模式之间转换的方法。
处理状态转换
应用有四种状态:stopped
、recording
、readyToPlay
和playing
。点击屏幕上的某个地方将使应用从一种状态转换到下一种状态。图 8–3说明了这一过程。
图 8–3。 录音机应用的四种状态
应用在stopped
状态下启动。当用户点击屏幕时,应用转换到recording
状态,并开始录制他或她的声音。另一次点击停止记录并转换到readyToPlay
状态。当用户准备好收听录音时,另一次点击在playing
状态下开始回放。然后,用户可以第四次点击以停止播放并返回到stopped
状态,准备再次录制。如果播放自行结束,应用也应自动转换到stopped
状态。清单 8–6显示了这个应用唯一的View
的 MXML。
清单 8–6。T3【录音笔应用的首页】T0
`<?xml version="1.0" encoding="utf-8"?>
`
这段代码包含了包含这个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
中,以便在回放期间监听控制倍频器的手势。它还为Sound
的sampleData
事件安装了一个处理程序,这样应用就可以在回放期间为Sound
提供数据。然后调用play
方法开始播放声音。一旦我们有了对控制回放的soundChannel
的引用,我们就可以为soundComplete
事件添加一个处理程序,这样我们就可以知道声音是否播放完毕,这样我们就可以自动转换回stopped
状态。最后,改变View
的currentState
变量的值来触发动画状态转换。
最后一个转换是将应用带回到stopped
状态。transitionToStoppedState
函数负责停止播放(如果声音已经播放完毕,这没有任何作用),并删除所有由transitionToPlayingState
函数添加的监听器。它最终重置Label
的text
属性,并更改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"?>
<![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(); } ]]> `
粗体显示的三行是播放 MP3 文件所需的全部内容。注意file://
后面的第三个正斜杠,它用来表示这是 MP3 文件的绝对路径。在实际应用中,你显然不希望使用这样的常量路径。在本章的后面,当我们讨论制作实际应用的注意事项时,我们将会看到以更优雅的方式处理文件系统路径的策略。
读取 ID3 元数据
播放音乐文件是一个好的开始;毕竟这是音乐播放器的本质。所有音乐播放器做的另一件事是读取嵌入在文件的 ID3tags 中的元数据。 1 这些元数据包括艺术家和专辑的名字、录制年份,甚至歌曲的流派和曲目号。Sound
类为读取这些标签提供了内置支持。清单 8–11 展示了如何将这一功能添加到我们刚刚起步的音乐播放器中。粗体行表示从清单 8–10中新增的源代码。
1 [www.id3.org/](http://www.id3.org/)
清单 8–11。 从 MP3 文件中读取 ID3 元数据
`<?xml version="1.0" encoding="utf-8"?>
<![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"; }
`
添加了onID3
处理程序作为Event.ID3
事件的监听器。当从 MP3 文件中读取元数据并准备好使用时,调用此处理程序。在ID3Info
类中有几个预定义的属性,对应于更常用的 ID3 标签。像专辑名、艺术家名、歌曲名、流派、年份和曲目号都有在类中定义的属性。此外,您还可以访问 ID3 规范 2.3 版定义的任何其他文本信息框架。 2 例如,要访问包含出版商名称的 TPUB 帧,您可以使用sound.id3.TPUB
。
不支持的一件事是从 ID3 标签读取图像,如专辑封面。在本章的后面,你将学习如何使用开源的 ActionScript 库来完成这个任务。
实施暂停功能
SoundChannel
类不直接支持暂停声音数据的回放。然而,通过结合使用类的position
属性和它的stop
方法,很容易实现暂停特性。清单 8–12展示了一种实现播放/暂停切换的可能技术。新添加的代码再次以粗体显示。
2T0】
清单 8–12。T3 实现播放/暂停切换
`<?xml version="1.0" encoding="utf-8"?>
<![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; } ]]>
`
在onCreationComplete
处理程序中不再调用Sound
的play
方法。取而代之的是,界面上增加了一个按钮,它的Label
根据isPlaying
标志的值是“播放”还是“暂停”。点击按钮触发对onClick
处理器的调用。如果声音当前正在播放,通道的position
保存在pausePosition
实例变量中,声音停止,并且soundComplete
事件监听器从通道中移除。下次播放声音时,将创建一个新的SoundChannel
对象。因此,未能从旧的SoundChannel
中移除我们的侦听器将导致内存泄漏。
如果声音当前没有播放,它是通过调用Sound
的play
方法启动的。将pausePosition
作为参数传递给play
方法,这样声音将从上次停止的位置开始播放。一个soundComplete
事件的监听器被附加到由play
方法返回的新的SoundChannel
对象上。当声音播放完毕时,将调用此事件的处理程序。当这种情况发生时,处理程序会将isPlaying
标志的值重置为false
并将pausePosition
重置为零。这样,下次点击播放按钮时,歌曲将从头开始播放。
调节音量
我们的音乐播放器也必须具备在播放歌曲时调节音量的功能。这是与歌曲播放时的SoundChannel
相关联的SoundTransform
对象的工作。清单 8–13展示了如何使用SoundTransform
来改变声音播放时的音量和声相。
清单 8–13。 实现音量和声相调整
`<?xml version="1.0" encoding="utf-8"?> <![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;
}
]]>
`
我们添加了两个水平滑块,可以用来调整音量和声音播放时的平移。对于移动设备上的音乐播放器来说,担心声相可能不是一个很好的理由,但是为了完整起见,这里给出了一个例子。也许这个音乐播放器有一天会成长为一个迷你移动混音工作室。如果发生这种情况,您将在这个功能上有一个良好的开端!
当滑块移动时,调用change
事件处理程序。注意调整SoundTransform
设置所需的模式。您首先获得一个对现有转换的引用,以便从所有当前设置开始。然后更改您感兴趣的设置,并再次在通道上设置变换对象。设置soundTransform
属性会触发频道更新其设置。这样,您可以将多个变换更改一起批处理,并且只需支付一次还原通道变换的成本。
SoundTransform
的volume
属性需要一个介于 0.0(静音)和 1.0(最大音量)之间的值。类似地,pan
属性期望一个介于-1.0(左)和 1.0(右)之间的值。change
处理程序负责将滑块的值调整到合适的范围。最后要注意的是onVolumeChange
和onPanChange
在声音开始播放时也会被调用。同样,这是必要的,因为每次调用Sound
的play
方法都会创建一个新的通道。这个新的通道对象在调用onVolumeChange
和onPanChange
之前不会有新的设置。
这就结束了我们对基本音乐播放器功能的快速概述。如果这就是你需要知道的全部信息,就没有必要再往下读了,所以你可以直接跳到“播放视频”部分。然而,如果你有兴趣了解把这个简约的音乐播放器变成一个真正的 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
。表示模型不知道使用它的View
或View
。它的工作是与MusicService
协作来呈现一个用于显示的MusicEntry
对象列表。有明确的责任划分,每个班级对系统的其余部分都了解有限。用软件工程术语来说,设计具有低耦合性和高内聚性。这应该是你设计的任何应用的目标。
3 马丁·福勒,《演示模型》,[http://martinfowler.com/eaaDev/PresentationModel.html](http://martinfowler.com/eaaDev/PresentationModel.html), July 19, 2004
4 马丁·福勒,《模型视图控制器》,[http://martinfowler.com/eaaCatalog/modelViewController.html](http://martinfowler.com/eaaCatalog/modelViewController.html)
图 8–5。 演示模型模式的一种常见实现
总之,使用表示模型模式有两个主要好处:
View
知道表示模型,但是表示模型对View
一无所知。这使得多个View
共享同一个表示模型变得很容易。这是表示模型模式使重用代码变得更容易的一种方式。- 大多数逻辑从
View
中移出,进入表示模型。View
可以绑定到呈现模型的属性,以便向用户呈现数据。像按钮按下这样的动作最好直接传递给表示模型,而不是在View
中处理。这意味着大部分值得测试的代码都在表示模型中,您不必担心测试 UI 代码。
创建视图导航应用
既然已经了解了应用设计的基本构建模块,那么是时候创建一个新的 Flex 移动项目了。这个应用将是一个ViewNavigatorApplication
,因为我们需要在两个不同的View
之间导航:一个View
包含歌曲、艺术家或专辑的列表,一个View
包含播放歌曲的控件。一旦创建了项目,我们就可以设置应用的包结构。assets
、views
、viewmodels
、models
和services
各有一个包。这使得按职责组织应用中的各种类变得很容易。这个assets
包是应用的所有图形素材,比如图标和闪屏,将被放置在其中。
ViewNavigatorApplication
的主要工作是创建和显示第一个View
。这通常通过设置<s:ViewNavigatorApplication>
标签的firstView
属性来完成。在这个应用中会有一点不同,因为每个View
的表示模型都会在它的data
属性中传递给它。为了完成这个任务,一个处理程序被分配给ViewNavigatorApplication
的initialize
事件。在这个onInitialize
处理程序中,MusicService
和初始的表示模型将被创建并传递给第一个View
。清单 8–14显示了应用的 MXML。
清单 8–14。T3【MXML】主ViewNavigatorApplication
T4】
`<?xml version="1.0" encoding="utf-8"?>
<![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)); } ]]> `
这个应用中使用的MusicService
接口的具体实现是一个名为LocalMusicService
的类,它从设备的本地文件系统中读取文件。这个服务实例然后被用来构建表示模型,在这个例子中是SongListViewModel
的一个实例。像这样将服务传递给表示模型比让表示模型在内部构造服务更可取。这使得在测试期间,或者如果程序的功能集被扩展到包括其他类型的音乐服务时,很容易向展示模型提供不同版本的服务。但是我们太超前了。我们将在下一节更详细地讨论这些类。
注意:有些人更喜欢让View
类创建自己的表示模型,而不是像我们在这里使用data
属性传递它。我们更喜欢将表示模型传递给View
,因为在其他条件相同的情况下,您应该总是喜欢类之间的耦合更少。然而,这两种方式在实践中都很有效。
在清单 8–14中需要注意的最后一件事是ViewNavigatorApplication
的applicationDPI
属性的声明。我们将它设置为 160,表示应用的 UI 将为 160 dpi 的屏幕设计。如果应用在更高 dpi 的屏幕上运行,UI 将相应地缩放。更多详情,请参考第 2 章的的“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。T3MusicEntry
数据模型
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.userDirectory
、File.desktopDirectory
、File.documentsDirectory
都指向安卓设备上的/mnt/sdcard
。File.applicationStorageDirectory
指向一个特定于您的应用的“本地存储”目录。File.applicationDirectory
空。
LocalMusicPlayer
中的getMusicEntries
实现将提供的rootPath
字符串转换为File
,或者如果没有提供rootPath
则使用默认目录,然后继续遍历位于该路径的文件。它为任何一个目录(其他歌曲的容器)或 MP3 文件(一首歌曲)的File
创建一个MusicEntry
对象。如果File
是一首歌而不是一个目录,那么createMusicEntryForFile
函数创建一个函数闭包,当被调用时,打开一个异步FileStream
进行读取。然后,这个函数闭包被传递给播放歌曲时要使用的MusicEntry
对象的构造函数。您可能还记得清单 8–16中,这个闭包对象的值——不管它是否为空——被用来确定对象所代表的MusicEntry
的类型。
歌曲列表视图
清单 8–14显示应用创建的第一个View
是SongListView
。应用的onInitialize
处理程序实例化适当类型的MusicService
,并使用它为View
构建SongListViewModel
。然后将SongListViewModel
作为navigator.pushView
函数的第二个参数传递给View
。这将在View
的data
属性中放置一个对模型实例的引用。
SongListViewModel
的工作非常简单。它使用给定的MusicService
来检索SongListView
要显示的MusicEntry
对象列表。清单 8–18显示了这个表示模型的源代码。
清单 8–18。 的演示模式为SongListView
`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 组件。
构造函数将存储对传入的MusicService
和MusicEntry
实例的引用。如果服务引用不为空,则从MusicService
中检索条目集合。如果服务为空,那么entries
集合将保持为空。
该类中还有两个额外的公共函数。cloneModelForEntry
函数将通过传递给它的MusicService
引用来创建一个新的SongListViewModel
。createSongViewModel
将使用这个模型的entries
集合和所选条目的索引为SongView
创建一个新的表示模型。这是这些函数的逻辑位置,因为这个表示模型引用了创建新表示模型所需的数据。因此,一个表示模型创建另一个表示模型是很常见的。
考虑到这一点,是时候看看View
如何使用它的表示模型了。SongListView
的源代码如清单 8–19所示。
清单 8–19。SongListView
`<?xml version="1.0" encoding="utf-8"?>
<![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)); } } ]]>
`
onInitialize
处理程序从data
属性初始化View
的模型引用。然后model
被用来访问作为List
的dataProvider
的entries
。它也用于List
的onChange
处理程序中。如果选择的MusicEntry
是一首歌曲,则用model
创建一首新的SongViewModel
,用navigator.pushView
功能显示一首SongView
。否则,创建一个新的SongListViewModel
并使用选择的MusicEntry
作为新的MusicEntry
对象集合的路径显示一个新的 T15。
还为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。T3SongListView
运行在不同 dpi 分类的设备上
注意:你也可以使用一个 FXG 图形作为一个IconItemRenderer
的图标或装饰,方法是以与前面的MultiDPIBitmapSource
相同的方式声明它。不幸的是,由于图标和装饰将被转换成位图,然后缩放,您将失去使用矢量图形的好处。出于这个原因,我们建议您将MultiDPIBitmapSource
对象与您的自定义IconItemRenderers
一起使用。
宋观
这就把我们带到了应用的真正核心:让用户播放音乐的视图!我们希望这个界面具有与大多数其他音乐播放器相同的功能。我们将显示歌名和专辑封面。它应该有控件,允许用户跳到下一首或上一首歌曲,播放和暂停当前歌曲,调整当前歌曲的位置以及音量和平移(只是为了好玩)。产生的界面如图 8–7所示。
图 8–7。SongView
界面运行在两种不同的 dpi 设置下
从Figure 8–7可以看出,这个界面比列表视图稍微复杂一点。它甚至包括一个自定义控件,不仅可以作为播放/暂停按钮,还可以作为当前歌曲播放位置的进度指示器。此外,你可以通过在按钮上来回滑动手指来控制歌曲的位置。编写这个自定义控件只是本节将要讨论的主题之一。
清单 8–20显示了定义这个View
的 MXML 文件的一部分。由于这是一个更大的接口声明,我们将把它分解成更小、更容易理解的部分。
清单 8–20。T3【美国】和SongView
MXML 文件的剧本章节
`<?xml version="1.0" encoding="utf-8"?>
<![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(); } ]]>
`
文件的<s:states>
部分声明了界面的portrait
和landscape
方向的状态。请记住第 2 章中的,通过在View
中明确声明这些状态的名称,Flex 将在设备方向改变时适当地设置我们的View
的状态。完成这些后,当方向改变时,您可以利用这些状态名来调整界面的布局。
与在SongListView
中一样,onInitialize
处理程序从data
属性初始化表示模型引用。它还为模型的SONG_ENDED
事件附加了一个处理程序,以便onSongEnded
处理程序可以在歌曲结束播放时适当地调整界面。还声明了一个用于View
的viewDeactivate
事件的处理程序。这允许当用户离开View
时View
停止播放歌曲。
我们现在将一次一个片段地检查这个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 的第一部分在屏幕底部声明了从白色到浅蓝色的背景渐变。矩形的width
和height
被设置为 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
。再一次,它的width
和height
被设置为总是充满屏幕。Group
在风景模式下使用一个HorizontalLayout
,在肖像模式下使用一个VerticalLayout
。状态语法确保在设备重定向时使用正确的布局。图 8–8显示了横向放置的设备上的SongView
界面。
图 8–8。 横向音乐播放器界面
下一段代码中的Group
是专辑封面图像的容器。Group
的大小根据方向动态调整,但是宽度和高度总是保持相等——它总是形成一个正方形。
`
`
albumCover
位图的源被绑定到模型的albumCover
属性。只有当模型中确实有一个albumCover
图像时,该位图才可见。如果没有,则显示占位符图形。占位符是一个 FXG 图像,位于应用的assets
包中。您可以看到在您的 MXML 声明中使用 FXG 图形是微不足道的。由于它们是矢量图形,因此对于不同的屏幕密度也能很好地缩放。
在专辑封面之后,我们到达包含这个View
控件的VGroup
。这个VGroup
实际上是由三个独立的HGroup
集装箱组成的。第一个包含上一首歌按钮、自定义的ProgressButton
控件和下一首歌按钮。下一个HGroup
容器包含水平音量滑块,以及它的 FXG 图标,以指示滑块两侧的低音量和高音量。最后的HGroup
包含水平平移滑块,以及显示左右方向的Label
。注意,模型的volume
、pan
和percentComplete
属性通过双向绑定被绑定到接口组件。这意味着绑定的任何一端都可以设置属性的值,而另一端将被更新。
`
textAlign="center"/> `
请注意,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
位图、albumTitle
、songTitle
、artistName
和isPlaying
标志。构造函数获取一个集合MusicEntries
和该集合中应该播放的歌曲的索引。这些参数被保存到实例变量中以供以后参考,因为当用户想要跳到集合中的上一首或下一首歌曲时会用到它们。构造函数还初始化一个每 500 毫秒计时一次的计时器。这个定时器读取歌曲的当前位置,并更新类的percentComplete
变量。最后,构造函数加载当前歌曲。接下来的两节介绍了关于处理percentComplete
更新和loadCurrentSong
方法的更多细节。
双向装订的特殊考虑
当查看SongView
的 MXML 声明时,我们注意到双向绑定被用于模型的volume
、pan
和percentComplete
变量。这意味着它们的值可以在模型类之外设置。这种额外的复杂性需要在模型类中进行一些特殊的处理。清单 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;
}
}`
volume
、pan
、percentComplete
属性的public get
和set
函数保证了它们可以在View
中绑定。简单地将变量声明为 public 在这里是行不通的,因为当它们是从类外部设置时,我们需要做一些额外的工作。当设置了volume
和pan
属性时,我们只需要调用更新SoundTransform
中的值的函数,如本章前面所示。处理percentageComplete
更新有点复杂:如果歌曲正在播放,我们需要停止它,然后在新的位置重新开始。我们使用私有的pauseSong
和playSong
实用程序方法来处理细节。如果歌曲当前没有播放,我们只需更新私有的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
读取完元数据,输入流将被关闭。最后一步是调用ID3Reader
的read
函数,将通过访问MusicEntry
的stream
属性创建的输入流作为参数传入。
清单 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 [http://code.google.com/p/metaphile/](http://code.google.com/p/metaphile/)
向onMetaData
处理程序传递一个符合中期库IMetaData
接口的参数。由于这个处理程序被附加到一个ID3Reader
对象,我们知道将传入的metaData
对象强制转换为一个ID3Data
对象的实例是安全的。这样做可以让我们轻松访问ID3Data
类的属性,比如performer
、albumTitle
和songTitle
。如果在ID3Data
类的 image 属性中存在图像数据,则创建一个新的flash.display.Loader
实例,将字节加载到DisplayObject
中。当加载图像字节时,onLoadComplete
处理程序使用存储在Loader
的内容属性中的DisplayObject
来初始化albumCover BitmapData
对象。由于View
被绑定到了albumCover
属性,所以一旦相册封面图像被更新,它就会显示出来。
创建定制组件
创建自定义移动组件与在 Flex 4 中创建任何其他自定义 Spark 组件非常相似。你创建了一个扩展了SkinnableComponent
的component
类和一个Skin
。只要你的图形不是太复杂,你可以使用普通的 MXML Skin
。如果您遇到性能问题,您可能需要用 ActionScript 编写您的Skin
。参见第 11 章了解有关移动应用性能调整的更多信息。
我们将编写的定制组件是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
都必须支持:play
和pause
。用SkinState(“pause”)
对component
类进行注释,将其Skin
的默认状态设置为pause
状态。虽然一个Skin
可以根据需要声明尽可能多的部分,但是组件要求每个Skin
至少定义一个playIcon
、pauseIcon
和一个background
。组件和Skin
之间接口契约的最后一个组件是Skin
用来绘制进度条的可绑定percentComplete
属性。组件的构造函数禁止鼠标与包含在Skin
中的任何子组件交互,并为它需要处理的鼠标事件附加监听器。
大多数组件需要实现三种方法来确保自定义控件的正确行为:getCurrentSkinState
、partAdded
和partRemoved
。当Skin
需要更新显示时,它调用getCurrentSkinState
函数。ProgressButton
组件覆盖这个函数,根据isPlaying
标志的当前值返回状态名。当添加和移除Skin
部件时,partAdded
和partRemoved
功能使组件有机会执行初始化和清理任务。在这种情况下,这两个函数都确保在超类中调用它们对应的函数,并且为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 应用中的ProgressButton
的Skin
如清单 8–27所示。每个Skin
都必须包含一个元数据标签,该标签指定了Skin
的设计目标HostComponent
。对元数据标签中指定的HostComponent
的引用可以通过Skin
的hostComponent
属性获得。另一个要求是Skin
必须声明它感兴趣的所有状态。此外,状态名称必须与主机组件定义的名称一致,以便Skin
正确运行。
清单 8–27。T3ProgressButtonSkin
宣言
`<?xml version="1.0" encoding="utf-8"?>
[HostComponent("views.ProgressButton")]
`
背景Group
作为Skin
其余图形的容器。它被束缚在hostComponent
的宽度和高度上。由Skin
声明的下三个矩形充当组件的边界和背景填充。第四个矩形绘制进度条。它的宽度是基于对hostComponent
及其percentComplete
属性的宽度的计算。它是在三个背景和边框矩形之后声明的,因此它将被绘制在它们的顶部。添加到Skin
的最后部分是playIcon
和pauseIcon
的 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"?>
<![CDATA[ privatestaticconst sourceURL:String = "http://ia600408.us.archive.org"+ "/26/items/BigBuckBunny_328/BigBuckBunny_512kb.mp4";
private function onViewDeactivate():void { player.stop(); } ]]>
`
这个应用被设置为全屏,View
的ActionBar
已经被禁用,以允许VideoPlayer
占据设备的整个屏幕。组件需要的只是一个源 URL,一旦缓冲了足够的数据,它就会自动开始回放。这真的不会变得更容易。当View
被禁用时,我们会小心地停止播放。这是一件小事,但是没有理由继续缓冲和播放超过严格必要的时间。
如果你使用 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 那样“一切都包括在内”的图书馆带来的所有包袱。这就是NetConnection
、NetStream
和Video
类出现的原因。这些类允许你构建一个轻量级的或者全功能的完全定制的视频播放器。
简而言之,NetConnection
处理联网;NetStream
提供控制视频流、缓冲和回放的编程接口;而Video
提供解码视频最终出现的显示对象。在这种情况下,您负责为视频播放器提供用户界面。清单 8–29展示了一个基于NetStream
的视频播放器的极简 MXML 声明。
清单 8–29。MXML 文件为NetStreamVideoView
`<?xml version="1.0" encoding="utf-8"?>
`
我们已经声明了一个UIComponent
作为Video
显示对象的最终容器。除此之外,只有另外两个可见控件。第一个是从 Spark VideoPlayer
组件“借用”了PlayPauseButtonSkin
的ToggleButton
(好吧,我们承认,我们彻头彻尾地偷了Skin
,我们甚至没有一点点抱歉)。这给了我们一个简单的方法来显示一个带有传统的三角形播放图标和双条暂停图标的按钮。另一个控件只是一个Label
,它将显示视频剪辑的持续时间和当前播放位置。
MXML 宣言中提到了各种 ActionScript 函数作为View
的initialize
和viewDeactivate
事件以及Button
的click
事件的事件处理程序。ActionScript 代码已被移到一个单独的文件中,并包含了一个<fx:Script>
标签。清单 8–30显示了View
的onInitialize
和onViewDeactivate
处理程序的代码。
清单 8–30。View
事件处理程序为NetStreamVideoView
`private static const SOURCE:String = "http://ia600408.us.archive.org/"+ "26/items/BigBuckBunny_328/BigBuckBunny_512kb.mp4";
private var video:Video; private var ns:NetStream; private var isPlaying:Boolean; private var timer:Timer; private var duration:String = "";
private function onInitialize():void { video = new Video(); videoContainer.addChild(video);
var nc:NetConnection = new NetConnection(); nc.connect(null);
ns = new NetStream(nc); ns.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); ns.client = { onMetaData: onMetaData, onCuePoint: onCuePoint, onPlayStatus: onPlayStatus };
ns.play(SOURCE); video.attachNetStream(ns);
timer = new Timer(1000); timer.addEventListener(TimerEvent.TIMER, onTimer); timer.start(); }
private function onViewDeactivate():void { if (ns) { ns.close(); } }`
onInitialize
处理程序负责所有的设置代码。Video
显示对象被创建并添加到它的UIComponent
容器中。接下来,创建一个NetConnection
,用一个null
值调用它的connect
方法。这告诉NetConnection
它将播放来自本地文件系统或 web 服务器的 MP3 或视频文件。如果不同的参数被传递给它的connect
方法,那么NetConnection
也可以用于 Flash Remoting 或者连接到 Flash 媒体服务器。
下一步是通过在构造函数中传递对NetConnection
的引用来创建NetStream
对象。根据玩家的复杂程度,你可能会对从NetStream
物体接收到的几个事件感兴趣。 NET_STATUS
事件将通知您缓冲状态、回放状态和错误情况。还有附加到NetStream
的客户端属性的metaData
、cuePoint
和playStatus
事件。客户端只是一个定义某些属性的Object
;它不必是任何特定的类型。在前面的清单中,我们只是使用了一个对象文字来声明一个具有所需属性的匿名对象。
metaData
事件将为您提供重要信息,如视频的宽度、高度和持续时间。当视频中嵌入的提示点到达时,cuePoint
事件会通知您。处理playStatus
甚至会让你知道视频什么时候结束。这些事件处理程序如清单 8–31所示。
最后的步骤是开始播放NetStream
,将其附加到Video
显示对象,并创建和启动计时器,该计时器将每秒更新一次时间显示。
清单 8–31。T3NetStream
事件处理者
`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
处理器使用视频的width
和height
使其在View
中居中。它还保存视频的duration
,以便在时间显示Label
中使用。在onPlayStatus
处理程序中,我们检查这是否是一个NetStream.Play.Complete
通知,如果是,停止更新时间显示的计时器。onCuePoint
和onNetStatus
处理程序仅用于演示目的,它们的输出被简单地记录到调试控制台和可选的屏幕上。
清单 8–32显示了与NetStreamVideoView
相关的剩余代码。onPlayPause
函数作为ToggleButton
的点击处理程序。根据ToggleButton
的selected
状态,它将暂停或恢复NetStream
并启动或停止更新timeDisplayLabel
的定时器。onTimer
函数是那个Timer
的处理程序。它将使用NetStream
的time
属性,格式化为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
的视频播放器的过程中,涉及了更多的代码。但是,如果您需要轻量级视频播放器实现的终极灵活性,NetStream
和Video
类的组合将提供您需要的所有功能。
在播放视频这一节的开始,我们简单地提到了 Stage Video。一旦在 Android 上得到支持,它将允许您基于NetStream
的视频播放器利用 H.264 视频的硬件加速解码和渲染。Adobe 提供了一个非常有用的“入门”指南来帮助你转换你的NetStream
代码以使用 StageVideo 而不是Video
显示对象。如果你喜欢不费吹灰之力就让自己适应未来,你可以利用第三个选项在 Android 上编写视频播放器:OSMF 库。这是我们下一节的主题,当它在 Android 上可用时,它将自动利用 StageVideo。
与 OSMF 玩视频
开源媒体框架是 Adobe 发起的一个项目,旨在创建一个库,收集编写基于 Flash 的媒体播放器的最佳实践。它是一个全功能的媒体播放器,被抽象成一些易于使用的类。该库允许您快速创建用于 Flex 和 Flash 应用的高质量视频播放器。OSMF 包含在 Flex 4 SDK 中,但是您也可以从项目网站下载最新版本。 10 清单 8–33显示了OSMFVideoView
的 MXML 代码。这里显示的用户界面代码与NetStreamVideoView
的清单 8–29中的代码几乎完全相同。本质上,我们只是用基于 OSMF 的MediaPlayer
实现替换了基于NetStream
的后端。
9 Adobe 公司,“舞台视频入门”,www.adobe.com/devnet/flashplayer/articles/stage_video.html,2011 年 2 月 8 日
10http://sourceforge.net/projects/osmf.adobe/files/10
清单 8–33。《MXML 宣言》为OSMFVideoView
`<?xml version="1.0" encoding="utf-8"?>
<s:ToggleButton id="playBtn" click="onPlayPause()" selected="true"
skinClass="spark.skins.spark.mediaClasses.normal.PlayPauseButtonSkin"/> `
清单 8–34 显示了将用于实现视频播放器的 OSMF 类的初始化代码。我们将包含电影 URL 的实例URLResource
传递给LightweightVideoElement
构造函数。OSMF MediaElement
是正在播放的媒体类型的接口。LightweightVideoElement
是一个代表视频的专门化,支持渐进式下载和简单的 RTMP 流。还有一个名为VideoElement
的类支持更多的流协议,但是对于我们的目的来说,LightweightVideoElement
拥有所有需要的功能。
一旦LightweightVideoElement
被创建,它就被传递给 OSMF MediaPlayer
类的构造函数。MediaPlayer
是一个类,通过它你可以控制视频的播放。它能够调度许多不同的事件,这些事件可以用来获取关于MediaPlayer
的状态和状况的信息。在接下来显示的示例代码中,我们处理了mediaSizeChange
事件以使视频显示在View
上居中,处理了timeChange
和durationChange
事件以更新timeDisplayLabel
,处理了complete
事件以通知我们视频何时结束播放。
MediaPlayer
本身不是显示对象。相反,它提供了一个可以添加到显示列表中的displayObject
属性。在本例中,它被添加为videoContainerUIComponent
的子节点。我们做的最后一点初始化工作是使用currentTimeUpdateInterval
属性请求我们每秒只更新一次视频播放器的currentTime
,而不是默认值的每 250 毫秒。视频将自动开始播放,因为MediaPlayer
的autoPlay
属性的默认值是true
。
清单 8–34。 初始化代码为MediaPlayer
`import org.osmf.elements.VideoElement; import org.osmf.events.DisplayObjectEvent; import org.osmf.events.MediaElementEvent; import org.osmf.events.TimeEvent; import org.osmf.media.MediaPlayer; import org.osmf.media.URLResource; import org.osmf.net.NetLoader;
privatestaticconst sourceURL:String = "http://ia600408.us.archive.org"+ "/26/items/BigBuckBunny_328/BigBuckBunny_512kb.mp4";
privatevar player:MediaPlayer; privatevar duration:String;
privatefunction onInitialize():void { var element:LightweightVideoElement; element = new LightweightVideoElement(new URLResource(sourceURL));
player = new MediaPlayer(element); videoContainer.addChild(player.displayObject);
player.addEventListener(DisplayObjectEvent.MEDIA_SIZE_CHANGE, onSize); player.addEventListener(TimeEvent.CURRENT_TIME_CHANGE, onTimeChange); player.addEventListener(TimeEvent.DURATION_CHANGE, onDurationChange); player.addEventListener(TimeEvent.COMPLETE, onVideoComplete); player.currentTimeUpdateInterval = 1000; }
privatefunction onViewDeactivate():void { if (player) player.stop(); }
privatefunction onPlayPause():void { if (player.playing) { player.play(); } else { player.pause(); } }`
在刚刚显示的onViewDeactivate
处理程序中,我们确保当View
被停用时停止播放器。您还可以看到播放/暂停ToggleButton
的click
处理程序。它只是调用了MediaPlayer
的play
和pause
方法,这取决于玩家当前是否在玩游戏。
清单 8–35通过显示MediaPlayer
事件处理程序,继续列出OSMFVideoView
的脚本代码。每当媒体改变大小时,就会调用onSize
处理程序。我们使用这个处理程序将MediaPlayer
的displayObject
置于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。 初始化CameraUI
和MediaPlayer
类
`import flash.media.CameraUI; import org.osmf.elements.VideoElement; import org.osmf.events.DisplayObjectEvent; import org.osmf.events.MediaElementEvent; import org.osmf.events.TimeEvent; import org.osmf.media.MediaPlayer; import org.osmf.media.URLResource; import org.osmf.net.NetLoader;
privatevar cameraUI:CameraUI; privatevar player:MediaPlayer; privatevar duration:String;
privatefunction onInitialize():void { if (CameraUI.isSupported) { cameraUI = new CameraUI(); cameraUI.addEventListener(MediaEvent.COMPLETE, onCaptureComplete);
player = new MediaPlayer();
player.addEventListener(DisplayObjectEvent.MEDIA_SIZE_CHANGE, onSize); player.addEventListener(TimeEvent.CURRENT_TIME_CHANGE, onTimeChange); player.addEventListener(TimeEvent.DURATION_CHANGE, onDurationChange); player.addEventListener(TimeEvent.COMPLETE, onVideoComplete);
player.currentTimeUpdateInterval = 1000; player.autoPlay = false; }
captureButton.visible = CameraUI.isSupported; }`
像往常一样,我们检查以确保设备支持CameraUI
类。如果是这样,就会创建一个新的CameraUI
实例,并为它的complete
事件添加一个处理程序。您在第 7 章中了解到,当图像或视频捕获成功完成时,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
,并将其分配给MediaPlayer
的media
属性。然后,我们可以开始播放视频,并调整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 文件 - 如何使用
SoundChannel
和SoundTransform
类控制声音的回放、音量和平移 - 如何播放动态生成或录制的声音
- 如何编写可维护和可测试的 Flex 移动应用
- 如何为 Flex 4 移动应用编写自定义控件
- 如何使用 Spark
VideoPlayer
组件、NetStream
类和 OSMF 库播放视频 - 如何与
CameraUI
类接口以捕获视频,然后在 Android 应用的 AIR 中播放捕获的视频
在下一章中,我们将继续编写真实的 Flex 移动应用的主题,看看在团队中工作和利用设计师-开发人员工作流的一些方面。
版权属于:月萌API www.moonapi.com,转载请注明出处