第 13 章 使用 MediaPlayer 播放音频
请参考教材,全面理解和完成本章节内容... ...
接下来的三章,我们先暂停“陋习手记”应用的开发,转而开发另一个应用。使用MediaPlayer类,新应用可支持播放一段历史事件的音频文件,如图13-1所示。
图13-1 你好,月球!
MediaPlayer是一个支持音频及视频文件播放的Android类,可播放不同来源(本地或网络流媒体)、多种格式(如WAV、MP3、Ogg Vorbis、MPEG-4以及3GPP)的多媒体文件。
创建一个名为HelloMoon的项目。在新应用向导对话框中,修改PackageName为你的名字缩写,如图13-2所示。
图13-2 创建HelloMoon应用
单击Next按钮继续。
HelloMoon的项目Minimu SDK为API 16以上,如图13-2-1所示。
图13-2-1 选择Minimu SDK为API 16以上
单击Next按钮继续。最后选择使用 Blank Activity模板,并通过模板创建一个名为HelloMoonActivity的activity。
打开HelloMoonActivity.java文件, 将继承的超类改为FragmentActivity。如下所示:
public class HelloMoonActivity extends FragmentActivity { ... ...}
13.1 添加资源
HelloMoon应用需要一个图片文件以及一个音频文件。这两个文件包含在配套资源文件中,下载Ch13文件包后,找到它们:
- armstrong_on_moon.jpg
- one_small_step.wav
- apollo_17_stroll.mpg
HelloMoon是个简单的小应用,按照Android屏幕密度基准(~160dpi),我们仅创建了一个符合该基准的图片文件armstrong_on_moon.jpg。将其复制至drawable目录下。
音频文件one_small_step将会放置在res\raw目录下,但项目中的res\raw目录并非默认存在,因此必须手工添加它。(右键单击res目录,选择New → Directory菜单项。有时新建目录后,不显示,切换到Project模式后再切换回Android模式)然后将one_small_step.wav文件复制到新建的res\raw目录下。目录raw
负责存放那些不需要Android编译系统特别处理的各类文件。也可顺便将apollo_17_stroll.mpg复制到res\raw目录下。本章末尾的挑战练习将会用到它。
最后,打开res/values/strings.xml,对照代码清单13-1,添加HelloMoon应用所需的字符串资源。
代码清单13-1 添加字符串资源(strings.xml)
(HelloMoon应用为什么在按钮上使用“Play”和“Stop”字符串资源而非图标文件?第15章我们将学习应用本地化的相关内容。使用图标文件会增加本地化的难度。)
准备完必需的资源文件,下面我们来看看HelloMoon应用的整体设计图,如图13-3所示。
图13-3 HelloMoon应用的对象图解
图中可以看出,HelloMoon应用包含一个HelloMoonActivity
及其托管的HelloMoonFragment
。
AudioPlayer
是我们编写的类,用于封装MediaPlayer
类。也可选择不封装MediaPlayer
类,而让HelloMoonFragment
直接与MediaPlayer
进行交互。不过,为保持代码的整洁与独立,我们推荐封装MediaPlayer
类的设计。
创建AudioPlayer
类之前,先来完成应用开发的其他部分。经过前几章的学习,我们应该已经掌握了以下基本的应用开发步骤:
- 定义fragment的布局
- 创建fragment类
- 修改activity及其布局,实现对fragment的托管
13.2 定义 HelloMoonFragment 布局文件
以TableLayout
为根元素,新建一个fragment_hello_moon.xml的布局文件。新建后,参照图13-4,重新设计fragment_hello_moon.xml布局文件的定义。
图13-4 HelloMoon应用的布局图解
TableLayout
使用起来和LinearLayout
差不多。可使用TableRow
,而非嵌套使用的LinearLayout
,来布置组件。联合使用TableLayout
与TableRow
,可更容易地布置形成排列整齐的视图。在HelloMoon应用里,TableLayout
能够协助将两个按钮并排放置到同样大小的两列中。
为什么没有将ImageView
也放入TableRow
中呢?TableRow
子组件的行为方式类似于表里的单元格。这里我们希望让ImageView
占据整个屏幕。如果将ImageView
也定义为TableRow
的子组件,则TableLayout
也会让列中其他单元格占据整个屏幕。而作为TableLayout
的直接子组件,ImageView
可自由地按需配置显示,完全不影响两个按钮以等宽的两列并排显示。
注意,TableRow
组件无需声明高度和宽度的属性定义。实际上,它使用的是TableLayout
的高度和宽度属性定义及其所有其他属性定义。不过,从另一个角度来看,嵌套的LinearLayout可以更灵活地布置并显示组件。
在图形化工具中预览布局,现在应用背景使用浅白主题。但为了让界面酷一些,我们希望使用Holo Dark主题。接下来,我们来看看如何使用Holo Dark主题。
手动重置应用主题
在Android Studio中,我还没有找到合适的方法加入黑酷的“Holo Dark”主题,暂时忽略“手动重置应用主题”这件事!
13.3 创建 HelloMoonFragment
创建名为HelloMoonFragment的类,其超类为android.support.v4.app.Fragment。
覆盖HelloMoonFragment.onCreateView(...)方法,实例化布局文件,并引用按钮,如代码清单13-3所示。
代码清单13-3 初步配置HelloMoonFragment类(HelloMoonFragment.java)
13.4 使用布局 fragment
在CriminalInten应用中,我们一直是通过在activity代码中添加fragment的方式来实现其托管的。而在HelloMoon应用中,我们将使用布局fragment,即在fragment元素节点中指定fragment的类。
打开content_hello_moon.xml文件,以代码清单13-4所示的fragment元素替换原有内容。注意android:name改成你自己的名字。
代码清单13-4 创建布局fragment(content_hello_moon.xml)
接下来,我们看一下HelloMoonActivity类,教材中 HelloMoonActivity继承了FragmentActivity类,我们改为继承AppCompatActivity,AppCompatActivity也是FragmentActivity的子类,没有什么影响。但Activity继承AppCompatActivity类后,应用会显示工具栏,下一章我们会详细介绍。
另外,HelloMoonActivity类中没有托管fragment的代码,但setContentView()函数绑定的布局文件activity_hello_moon中指定了fragment类HelloMoonFragment及其布局文件fragment_hello_moonx.xml(实际是在include layout的content_hello_moon中指定的),如代码清单13-5和13-4所示:
代码清单13-5 HelloMoonActivity继承AppCompatActivity类(HelloMoonActivity.java)
以上便是托管一个布局fragment的全部操作。总结来说就是,在布局中指定fragment类,随后通过布局它被添加给activity并显示在屏幕上。
13.5 音频播放
创建一个名为AudioPlayer
的新类,添加存放MediaPlayer
实例的成员变量,以及停止和播放该实例的方法,如代码清单13-6所示。
代码清单13-6 使用MediaPlayer
类的简单播放代码(AudioPlayer.java)
在play(Context)
方法中,选择调用MediaPlayer.create(Context, int)
方法。MediaPlayer
需利用传入的Context
来寻找音频文件的资源ID。(如果音频来自于其他渠道,如网络或本地URI,则应使用其他MediaPlayer.create(...)
方法。)
在AudioPlayer.stop()
方法中,释放MediaPlayer
实例并将mPlayer
变量设置为null
。调用MediaPlayer.release()
方法,可销毁该实例。
销毁是“停止”的一种具有攻击意味的说法,但我们有充足的理由使用销毁一词。除非调用MediaPlayer.release()
方法,否则MediaPlayer
将一直占用着音频解码硬件及其他系统资源。而这些资源是由所有应用共享的。MediaPlayer
类有一个stop()
方法。该方法可使MediaPlayer
实例进入停止状态,等需要时再重新启动。不过,对于简单的音频播放应用,建议使用release()
方法销毁实例,并在需要时进行重建。
基于以上原因,有一个简单可循的规则:只保留一个MediaPlayer
实例,保留的时长即音频文件播放的时长。
为强化该规则,我们来修改play(Context)
方法。初始调用一个stop()
方法,再设置一个监听器监听音频播放,音频文件播放一完成,就调用stop()
方法。
为了在HelloMoonFragment
类获取当前的播放状态,我们添加isPlaying()
方法获取mPlayer状态,如代码清单13-7所示。
代码清单13-7 避免多MediaPlayer
实例(AudioPlayer.java)
在play(Context)
方法的开头就调用stop()
方法,可避免用户多次单击Play按钮创建多个MediaPlayer
实例的情况发生。音频文件完成播放后,立即调用stop()
方法,可尽可能快地释放MediaPlayer
实例及其占用的资源。
现在我在完善HelloMoonFragment
类,因为fragment被销毁后,还需在HelloMoonFragment
中调用AudioPlayer.stop()
方法,以避免fragment被销毁后MediaPlayer
还在不停播放。
在HelloMoonFragment
中,覆盖onDestroy()
方法,从而实现对AudioPlayer.stop()
方法的调用,如代码清单13-8所示。
代码清单13-8 覆盖onDestroy()
方法(HelloMoonFragment.java)
HelloMoonFragment
被销毁后,MediaPlayer
仍可不停地播放,这是因为MediaPlayer
运行在一个不同的线程上。我们会在其他章中学习到更多有关线程管理的知识,所以这里暂不对HelloMoon应用的多线程使用进行介绍。
关联并设置play和stop按钮
返回HelloMoonFragment.java,创建AudioPlayer
实例并设置play和stop按钮的监听器方法,即可实现音频的播放。另外,还需根据播放状态调整play
按钮状态,我们用updateButtons()方法来更新按钮状态。如代码清单13-9所示。
代码清单13-9 关联并设置Play按钮(HelloMoonFragment.java)
运行HelloMoon应用。单击Play按钮,欣赏一段历史事件记录音频。