这章是用来播放音乐和进行单元测试
对于架构来说,MVVM架构很适合于单元测试,对于播放音乐来说,SoundPool能加载一批音乐资源到内存里面,然后能控制同时播放的音频的文件的个数,如下面的代码
(1)在BeatBox里面创建一个SoundPool对象
private static final int MAX_SOUNDS = 5;
private SoundPool mSoundPool;
//在它的构造函数中添加
mSoundPool = new SoundPool(MAX_SOUNDS,AudioManager.STREAM_MUSIC,0)//第一个参数是指定同时播放多少个音频,第二个参数是确定音频流的类型,Android中有不同的音频流,这也是为什么调低音乐的音量而闹钟的音量却没有受到影响,第三个是采样的转换品质,参考文档说这个不起作用,所以直接传入0就好
SoundPool的优势是:指令一发出来,它就能立刻播放,一点都不拖拉,但是它在播放前就必须需要去预加载音频,SoundPool加载的音频文件都有自己的Integer型的Id,所以这边需要在Sound类中添加mSoundId的实例变量,并添加getter和setter方法
private Integer mSoundId;
public Integer getSoundId(){
return mSoundId;
}
public void setSoundId(Integer soundId){
mSoundId = soundId;
}
这边Id使用的是Integer而不是int,这样当mSoundId没有值的时候就可以设置为null了
(2)加载音频,我们在BeatBox中去添加load()方法去载入音频
private void load(Sound sound)throws IOException {
AssetFileDescriptor afd = mAssets.openFd(sound.getAssetPath());
int soundId = mSoundPool.load(afd,1);
sound.setSoundId(soundId);
}
mSoundPool.load()方法可以把文件载入SoundPool待播,为了方便管理,重播,卸载音频文件,那么这个方法就会返回一个int型的Id,这实际上就是放在mSoundId中的Id,
调用openFd()方法有可能抛出IOException
上面的方法写好后就可以在BeatBox中的loadSound()方法中去载入全部的音频
for(String filename : soundName) {
try {
String assetPath = SOUNDS_FOLDER + "/" + filename;
Sound sound = new Sound(assetPath);
load(sound);
mSounds.add(sound);
}catch(IOException e) {
log.e(TAG,"ss");
}
}
(3)播放音频
最后为了播放音频就应该在BeatBox中添加play方法
public void play(Sound sound ) {
Integer soundId = sound.getSoundId();
if(soundId == null) {
return;
}
mSoundPool.Play(sound,1.0f,1.0f,1,0,1,0f);
}
在播放前要确保soundId不是null值,Sound加载失败会出现null的值
下面是用来测试所用的
(1)要写测试代码就首先需要为项目添加两个测试工具:Mockito(一个方便创建虚拟对象的Java框架,有了虚拟对象就可以单独测试SoundViewModel,不用担心会因代码关联关系测试到其他对象)和Hamcret(一个规则的匹配器工具库,可以在代码里模拟匹配条件,如果不能按预期匹配条件就不能通过)两个库
添加方法:
右击app模块,选择Open Module Setting 菜单项目,选择弹出界面的Dependencies选择项,然后点击+弹出依赖库窗口,输入需要的库名
如果,我们只想要在测试的时候去让这个库使用,那么我们应该自己手动修改 build.gradle文件,在里面把依赖库的范围从compile改成testCompile
(2)创建测试类,写单元测试的方式是使用测试框架类,使用测试框架类可以集中编写和运行测试案例,并支持在Android studio中里面去看到测试结果
JUnit是最常见的Android单元测试框架,要使用它首先就需要创建一个用作Junit测试的测试类,打开SoundViewModel类,使用
Ctrl + Shift + T就会打开寻找这个类关联的测试类,如果找不到它会提醒新建
我们选择Creare New Test 创建一个新测试的类,测试库选择JUnit4,勾选setUp/@Before。其他的选择不变,
(3)最后我们应该选择创建哪种测试类,或是说在哪一个目录中去存放测试类
androidTest目录下的都是整合测试类,这样的可以运行在设备和虚拟设备上,这样有优点:可以在运行时动态测试应用行为,缺点:需要编译打包在设备上运行,浪费资源
test目录上的是单元测试类,它可以运行在本地开发机上,可以脱离Android运行时环境,速度会挺快的,它的规模最小,测试单个类,不影响手上的工作,可以快速地反复运行,因此我们可以将它放在这了目录上面去
一.实现测试类
(1)现在就来实现SoundViewModel测试类,测试框架里面只有一个setUp方法
public class SoundViewModelTest {
@Before
public void setUp() throws Exception {
}
}
(这个测试类在于app模块的test目录下)
和大多数对象,测试类也需要创建对象实例和它依赖的其他对象,为了避免每一个测试类都需要写重复的代码,JUnit提供了@Before这个注解,以@Before注解的包含公共代码的方法会在测试之前运行一次,按照约定每一个单元测试类都需要有@Before注解的setUp()方法
在测试类中使用虚拟对象,虚拟对象会继承BeatBox,同样的方法但是这些方法什么都不做,这样依赖BeatBox的SoundViewModel就不会出问题
要用Mockito创建虚拟对象,需要传入虚拟的类,调用mock()静态方法,创建一个虚拟的BeatBox对象,然后存入mBeatBox变量中
public class SoundViewModelTest {
private BeatBox mBeatBox;
@Before
public void setUp() throws Exception {
mBeatBox = mock(BeatBox.class);
}
}
mock()方法会导入一个包,mock()方法会自己创建一个虚拟的BeatBox对象
有了虚拟依赖对象就可以来完成测试类了
public class SoundViewModelTest {
private BeatBox mBeatBox;
private Sound sound;
private SoundViewModel mSubject;//这边的变量命名是一个习惯规定,说明这是一个测试类
@Before
public void setUp() throws Exception {
mBeatBox = mock(BeatBox.class);
mSound = new Sound("assetPath");
mSubject = new SoundViewModel(mBeatBox);
mSubject.setSound(mSound);
}
}
(2)编写测试方法
比如我们可以在测试类判断SoundViewModel的getTitle方法属性和Sound里的getName()属性是有关系的
@Test
public void exposesSoundNameAsTitle(){
assertThat(mSubject.getTitle(),is(mSound.getName()));//判定测试对象获取标题方法和sound的获取文件名返回相同的值,如果不同那么单元测试失败
}
需要导入某些库时,是使用 alt + enter
(3)测试交互
在这边我们可以来整合SoundViewModel和BeatBox.play()的方法,在实践中,我们在写一个新的方法之前,我们会先写一个测试验证这个方法的预期结果,必须我们需要在SoundViewModel类里写onBunttonClicked()去调用BeatBox.play(Sound)方法,那么就需要在测试类中写一个测试方法调用onButtonClicked();
@Test
public void callsBeatBoxPlayOnButtonClicked(){
mSubject.onButtonClicked();
}
将光标移到它那边然后alt enter,然后就创建这个方法,最后我们需要使用verify()方法。确定onButtonClicked()方法调用了BeatBox.play(Sound)
@Test
public void callsBeatBoxPlayOnButtonClicked(){
mSubject.onButtonClicked();
verify(mBeatBox).play(mSound);//验证以mSound作为参数,调用了mBeatBox对象的play()方法
}
如果此时使用ctrl + R 那么就会出现测试失败,因为此时onButtonClicked方法还没有实现,应该按下面去做
public void onButtonClicked(){
mBeatBox.play(mSound);
}
二.数据绑定回调
按钮要响应事件还查查最后一步:关联按钮对象和onButtonClicked()方法
我们可以在布局文件中添加数据绑定lambda表达式子
android:onClick = "@{ ()->viewModel.onButtonClicked()}"
此时如果运行了会听见自己设置的声音
三.释放音频
对于SoundPool.release()方法来说,它需要
public void release(){
mSoundPool.release();
}
最后在onDestroy()方法中,
public void onDestroy(){
super.onDestry();
mBeatBox.release();
}
四设备旋转和对象保存
在这里如果我们点击播放然后旋转,那么此时声音会停止,在前面我们曾经用过onSaveInstanceState()方法来解决问题,但在这里不行,因为上面的方法得首先保存数据,然后才能再使用Bundle中的Parcelable恢复数据,但是在这里SoundPool是不可保存的,而不可保存性具有向外传递性,如果一个对象重度依赖另一个对象,那么这个对象也就无法保存,这里BeatBox也就无法保存了,那么要解决这个问题那么就需要下面的解决方法了
(1)保留fragment
为了应对设备配置的变化,fragment有一个特殊的方法可以确保BeatBox实例不被销毁,那就是retainInstance()
覆盖onCreate()方法,并设置fragment的属性值
在onCreate()方法里面写下面的代码
setRetainInstance(true);//它默认是flalse,表明不会保留,因此设备旋转时,fragment会随着设备旋转时fragment会随着activity一起杯毁灭并重建,true的话,那么就可以保留fragment,已经保留的fragment不会随着activity消失,它会一直保留,并在需要时返回给新的activity,此时fragment保存了,那么全部的实例变量(BeatBox)就会保持不变
下面是上面实现的内在原因:
fragment之所以可以保留,是因为:可以消灭和重建fragment的视图,但是fragment自身可以不被消灭
(1)设备配置发生编号时,FragmentManager首先会销毁队列中的fragment视图,在设备配置发生变化时总会销毁和重建fragment和activity的视图,这是因为新的配置可能需要新的资源来比配,有更适合的资源时,就应该重新构建视图了
(2)接着FragmentManager就会去检查每一个fragment的retainInstance属性值,如果属性值为false那么就会毁灭该fragment的实例,然后配置新的activity就会创建一个新的fragment和其视图,如果是true那么该视图就会立刻被毁灭,但是fragment本身不会被毁灭,但新的activity创建后,新的Fragment创建后新的Fragment就会找到已经保留的fragment,重新创建它的视图
虽然已保留的fragment没有被毁灭,但是它已经脱离activity并处于保留状态了,尽管fragment还在,但是没有activity去托管它了,但是fragment的保留只能保留非常短的时间,就是凑从fragment脱离activity到重新附加给快速的新建activtiy之间的一段时间
上面的方法只适合于因为设备旋转而导致的,如果是系统回收的,那么就没用,