创建SoundPool
BeatBox.java
public class BeatBox {
//...
private static final int MAX_SOUNDS = 5;
private AssetManager mAssets;
private List<Sound> mSounds = new ArrayList<>();
private SoundPool mSoundPool;
public BeatBox(Context context) {
mAssets = context.getAssets();
/**
* 创建SoundPool
* @param maxStreams int 同时最大播放音频个数
* @param streamType int 音频流类型,AudioManager.STREAM_MUSIC表示音乐、游戏音频流
* @param srcQuality int 采样率转换品质,现已无效,传入0
*/
mSoundPool = new SoundPool(MAX_SOUNDS, AudioManager.STREAM_MUSIC, 0);
loadSounds();
}
//...
}
加载音频文件
接下来使用 SoundPool 加载音频文件。相比其他音频播放方法,SoundPool 还有个快速响应的优势:指令刚一发出,它就会立即开始播放。但是,它需要在播放前预先加载音频。
SoundPool 加载的音频文件都有自己的 Integer 类型ID。在 Sound 类中添加SoundId 实例变量,并添加相应的获取方法和设置方法管理这些ID。
Sound.java
public class Sound {
private String mAssetPath;
private String mName;
private Integer mSoundId;
//...
public Integer getSoundId() {
return mSoundId;
}
public void setSoundId(Integer soundId) {
mSoundId = soundId;
}
}
mSoundId 用了 Integer 类型而不是 int 。这样,在 Sound 的 mSoundId 没有值时可以设置其为 null 值。
加载音频
BeatBox.java
//加载音频
private void loadSound(Sound sound) throws IOException {
AssetFileDescriptor afd = mAssets.openFd(sound.getAssetPath());
int soundId = mSoundPool.load(afd, 1);
sound.setSoundId(soundId);
}
调用 mSoundPool.load(AssetFileDescriptor, int) 方法可以把文件载入 SoundPool 待播。为方便管理、重播或卸载音频文件,mSoundPool.load(…) 方法会返回一个 int 型ID。这实际就是存储在 mSoundId 中的ID。调用 openFd(String) 方法有可能抛出 IOException,load(Sound)方法也是如此。
载入全部音频文件
BeatBox.java
//获取asset资源,存入列表,载入全部音频文件
private void loadSounds() {
String[] soundNames;
//取得assets中的资源清单
//...
for (String filename : soundNames) {
try {
String assetPath = SOUNDS_FOLDER + "/" + filename;
Sound sound = new Sound(assetPath);
loadSound(sound);
mSounds.add(sound);
} catch (IOException ioe) {
Log.e(TAG, "无法加载声音资源 " + filename, ioe);
}
}
}
播放音频
BeatBox.java
public void playSound(Sound sound) {
Integer soundId = sound.getSoundId();
if (soundId == null) return;
/**
* 播放指定ID的音频文件
* @param soundID int load(...)方法加载音频的返回值
* @param leftVolume float 左音量 (range = 0.0 to 1.0)
* @param rightVolume float 右音量 (range = 0.0 to 1.0)
* @param priority int 优先级,已无效 (0 = lowest priority)
* @param loop int 循环 (0 = no loop, -1 = loop forever)
* @param rate float 播放速率 (1.0 = normal playback, range 0.5 to 2.0)
* @return int 成功返回ID,失败返回0
*/
mSoundPool.play(soundId, 1.0f, 1.0f, 1, 0, 1.0f);
}
添加按钮单击事件
BeatBoxFragment.SoundHolder
private class SoundHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private Button mButton;
private Sound mSound;
public SoundHolder(LayoutInflater layoutInflater, ViewGroup container) {
super(layoutInflater.inflate(R.layout.list_item_sound, container, false));
mButton = itemView.findViewById(R.id.list_item_sound_button);
mButton.setOnClickListener(this);
}
//绑定sound
public void bindSound(Sound sound) {
mSound = sound;
mButton.setText(mSound.getName());
}
@Override
public void onClick(View v) {
mBeatBox.playSound(mSound);
}
}
释放音频
BeatBox.java
public void releasePool() {
mSoundPool.release();
}
覆盖onDesreoy()方法,在其中释放SoundPool:
BeatBoxFragment.java
@Override
public void onDestroy() {
super.onDestroy();
mBeatBox.releasePool();
}
设备旋转和对象保存
在Java世界,要保存对象,要么将其放入 Bundle 中,要么实现 Serializable 接口或者 Parcelable 接口。无论采用哪种方式,对象首先要是可保存对象。
BeatBox 的某一部分可以保存,例如,Sound 类中的一切都可以保存;而 SoundPool 就无法保存了。虽然可以新建包含同样音频文件的 SoundPool ,或者从音频播放中断处继续,然而和电视播放例子一样,播放中断的那段时光是无论如何也找不回了。
保留fragment
savedInstanceState 机制只适用于可保存的对象数据,但 BeatBox 不可保存。在Activity创建和销毁时,我们都需要 BeatBox 实例一直可用。
为应对设备配置变化,fragment有个特殊方法可确保 BeatBox 实例一直存在:retainInstance。覆盖 BeatBoxFragment.onCreate(…) 方法并设置fragment的属性值:
BeatBoxFragment.java
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//保留fragment
//这样,设备旋转时,已保留的fragment不会随activity一起被销毁,它会一直保留,并在需要时原封不动地传递给新的activity。已保留的fragment的全部实例变量也会保持不变。
setRetainInstance(true);
mBeatBox = new BeatBox(getActivity());
}
fragment的 retainInstance 属性值默认为 false ,这表明其不会被保留。如属性值为 true ,则该fragment的视图立即被销毁,但fragment本身不会被销毁。虽然保留的fragment没有被销毁,但它已脱离消亡中的activity并处于保留状态。尽管此时的fragment仍然存在,但已没有任何activity托管它。
必须同时满足两个条件,fragment才能进入保留状态:
- 已调用了fragment的setRetainInstance(true)方法;
- 因设备配置改变(通常为设备旋转),托管activity正在被销毁。
ragment只能短暂处于保留状态,即fragment脱离旧activity到重新附加给立即创建的新activity之间的一段时间。
深入学习
是否要保留
除非万不得已,非常不建议使用这种机制。
- 首先,相比非保留fragment,保留fragment的显示非常复杂。一旦出现问题,排查起来非常耗时。既然使用它会让程序复杂起来,能不用就不用吧。
- 其次,fragment在使用保存实例状态的方式处理设备旋转时,也能够应对所有生命周期场景;但保留的fragment只能处理activity因设备旋转而销毁的情况。如果activity是因操作系统需要回收内存而被销毁,则所有保留的fragment也会随之销毁,数据也就跟着丢失了。
设备旋转处理再探
如果activity或fragment中有需要长久保存的东西,应覆盖 onSaveInstanceState(Bundle) 方法保存其状态。这样,由于和activity记录的生命周期保持了同步,后续就可在需要时恢复。