1. 触发音量键
在音量键被按下后,Android输入系统将该事件一路派发给Activity,如果无人截获并处理这个事件,承载当前Activity的显示PhoneWindow类的onKeyDown()或onKeyUp()函数将会处理,从而开始通过音量键调整音量的处理流程。输入事件的派发机制及PhoneWindow类的作用将在后续章节中详细介绍,现在只需要知道,PhoneWindow描述了一片显示区域,用于显示与管理我们所看到的Activity和对话框等内容。同时,它还是输入事件的派发对象,而且只有显示在最上面的PhoneWindow才会收到事件。
按照Android的输入事件派发策略,Window对象在事件的派发队列中位于Activity的后面,所以应用程序可以重写自己的Activity.onKeyDown()函数以截获音量键的消息,将其用作其他的功能。比如说,在一个相机应用中,按下音量键所执行的动作是拍照而不是调节音量。
PhoneWindow的onKeyDown()函数实现如下:
[PhoneWindow.java-->PhoneWindow.onKeyDown()]
......
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_MUTE: {
// 直接调用到AudioManager的handleKeyUp里面去,是不是很简单而且直接呢
getAudioManager().handleKeyDown(event, mVolumeControlStreamType);
return true;
}
......
注意handleKeyDown()函数的第二个参数,它的意义是指定音量键将要改变哪一种流类型的音量。在Android中,音量的控制与流类型是密不可分的,每种流类型都独立地拥有自己的音量设置,它们在绝大部分情况下互不干扰,例如音乐音量、通话音量就是相互独立的。所以说,离开流类型谈音量是没有意义的。在Android中,音量这个概念描述的一定是某一种流类型的音量。
这里传入了mVolumeControlStreamType,那么这个变量的值是从哪里来的呢?Activity类中有一个函数名为setVolumeControlStream(int streamType)。应用可以通过调用这个函数来指定显示这个Activity时音量键所控制的流类型。这个函数的内容很简单,就一行,如下:
[Activity.java-->Activity.setVolumeControlStream()]
getWindow().setVolumeControlStream(streamType);
getWindow()的返回值就是用于显示当前Activity的PhoneWindow。从名字就可以看出,这个调用改变了mVolumeControlStreamType,于是也改变了按下音量键后传入AudioManager.handleKeyUp()函数的参数,从而达到setVolumeControlStream的目的。同时,还应该能看出,这个设置被绑定到Activity的Window上,在不同Activity之间切换时,接收按键事件的Window也会随之切换,所以应用不需要去考虑在其生命周期中音量键所控制的流类型的切换问题。
AudioManager的handleKeyDown()的实现很简单,在一个switch中,它调用了AudioService的adjustSuggestedStreamVolume(),所以直接看一下AudioService的这个函数。
2. adjustSuggestedStreamVolume()分析
我们先来看函数原型:
public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType,
int flags)
adjustSuggestedStreamVolume()有三个参数,第一个参数direction指示了音量的调整方向,1为增大,-1为减小;第二个参数suggestedStreamType表示要求调整音量;第三个参数flags的意思就不那么容易理解了。其实AudioManager在handleKeyDown()中设置了两个flag,分别是FLAG_SHOW_UI和FLAG_VIBRATE。从名字上我们就能看出一些端倪。前者告诉AudioService我们需要弹出一个音量控制面板。而在handleKeyUp()里设置了FLAG_PLAY_SOUND,这是为什么当在松开音量键后“有时候”会有一个提示音。注意,handleKeyUp()中设置了FLAG_PLAY_SOUND,但只是有时候这个flag才会生效,在下面的代码中可以看到这是为什么。还需要注意的是,第二个参数名为suggestedStreamType,从其命名来推断,这个参数传入的流类型对AudioService来说只是一个建议,是否采纳这个建议,AudioService有自己的考虑。
看一下它的实现:
[AudioService.java-->AudioService.adjustSuggestedStreamVolume()]
public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType,
int flags) {
int streamType;
// ① 从这一小段代码中可以看出,在 AudioService中还有地方可以强行改变音量键控制的流类型
if (mVolumeControlStream != -1) {
streamType = mVolumeControlStream;
} else {
// ② 通过getActiveStreamType()函数获取要控制的流类型
// 这里根据建议的流类型与AudioService的实际情况,返回一个值
streamType = getActiveStreamType(suggestedStreamType);
}
// ③ 这个冗长条件判断的目的,就是只有在特定的流类型下,并且没有处于锁屏状态时才会播放声音
if ((streamType != STREAM_REMOTE_MUSIC) &&
(flags& AudioManager.FLAG_PLAY_SOUND) != 0 &&
((mStreamVolumeAlias[streamType] != AudioSystem.STREAM_RING)
|| (mKeyguardManager != null && mKeyguardManager.
isKeyguardLocked()))) {
flags&= ~AudioManager.FLAG_PLAY_SOUND;
}
if (streamType == STREAM_REMOTE_MUSIC) {
...... //我们不讨论远程播放的情况
} else {
// ④ 调用adjustStreamVolume
adjustStreamVolume(streamType, direction, flags);
}
}
初看这段代码时,可能有读者对下面这句代码感到疑惑:
VolumeStreamState streamState = mStreamStates[mStreamVolumeAlias[streamType]];
其实,这是为了满足所谓的“将铃声音量用作通知音量”这种需求。这就需要实现在两个有这个需求的流A与B之间建立起一个A→B映射。当我们对A流进行音量操作时,实际上是在操作B流。笔者个人认为这个功能对用户体验的提升并不大,却给AudioService的实现增加了不小的复杂度。直观上来想,我们可以使用一个HashMap解决这个问题,键是源流类型,值是目标流类型。而Android使用了一个更简单却不是那么好理解的方法来完成这件事。AudioService用一个名为mStreamVolumeAlias的整型数组来描述这个映射关系。
要实现“以铃声音量用作音乐音量”,只需要修改相应位置的值为STREAM_RING即可,就像下面这样:
mStreamVolumeAlias[AudioSystem.STREAM_MUSIC] = AudioSystem.STREAM_RING;
之后,因为需要对A流进行音量操作时,实际上是在操作B流,所以就不难理解为什么在很多和流相关的函数里都会先做这样的一个转换:
streamType = mStreamVolumeAlias[streamType];
其具体的工作方式就留给读者思考。在本章的分析过程中,大可忽略这种转换,这并不影响我们对音量控制原理的理解。
简单来说,这个函数做了三件事:
确定要调整音量的流类型。
在某些情况下屏蔽FLAG_PLAY_SOUND。
调用adjustStreamVolume()。
关于这个函数有几点仍需要说明一下。在函数刚开始的时候有一个判断,条件是一个名为mVolumeControlStream的整型变量是否等于-1,从这块代码来看,mVolumeControlStream比参数传入的suggestedStreamType厉害多了,只要它不是-1,要调整音量的流类型就是它。那这么厉害的控制手段的作用是什么?其实,mVolumeControlStream是VolumePanel通过forceVolumeControlStream()函数设置的。什么是VolumePanel呢?就是我们按下音量键后的那个音量调节通知框。VolumePanel在显示时会调用forceVolumeControlStream强制后续的音量键操作固定为促使它显示的那个流类型,并在它关闭时取消这个强制设置,即设置mVolumeControlStream为-1。这个在后面分析VolumePanel时会看到。
接下来我们继续看一下adjustStreamVolume()的实现。
3. adjustStreamVolume()分析
[AudioService.java-->AudioService.adjustStreamVolume()]
public void adjustStreamVolume(int streamType, int direction, int flags) {
// 首先还是获取streamType映射到的流类型。这个映射的机制确实给我们的分析带来不小的干扰
// 在非必要的情况下忽略它们吧
int streamTypeAlias = mStreamVolumeAlias[streamType];
// 注意VolumeStreamState类
VolumeStreamState streamState = mStreamStates[streamTypeAlias];
final int device = getDeviceForStream(streamTypeAlias);
// 获取当前音量,注意第二个参数的值,它的目的是如果这个流被静音,则取出它被静音前的音量
final int aliasIndex = streamState.getIndex(device,
(streamState.muteCount() != 0)
boolean adjustVolume = true;
// rescaleIndex用于将音量值的变化量从源流类型变换到目标流类型下
// 由于不同的流类型的音量调节范围不同,所以这个转换是必需的
int step = rescaleIndex(10, streamType, streamTypeAlias);
//上面准备好了所需的所有信息,接下来要做一些真正有用的动作了
// 比如说checkForRingerModeChange()。调用这个函数可能变更情景模式
// 它的返回值adjustVolume是一个布尔变量,用来表示是否有必要继续设置音量值
// 这是因为在一些情况下,音量键用来改变情景模式,而不是设置音量值
if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
(streamTypeAlias == getMasterStreamType())) {
......
adjustVolume = checkForRingerModeChange(aliasIndex, direction, step);
......
}
int index;
// 取出调整前的音量值。这个值稍后被用在sendVolumeUpdate()的调用中
final int oldIndex = mStreamStates[streamType].getIndex(device,
(mStreamStates[streamType].muteCount() != 0) /* lastAudible */);
// 接下来我们可以看到,只有流没有被静音时,才会设置音量到底层去,否则只调整其静音前的音量
// 为了简单起见,暂不考虑静音时的情况
if (streamState.muteCount() != 0) {
......
} else {
// 为什么还要判断streamState.adjustIndex的返回值呢?
// 因为如果音量值在调整(adjust)之后并没有发生变化,比如说达到了最大值,就不需要
继续后面的操作了
if (adjustVolume && streamState.adjustIndex(direction * step, device)) {
// 发送消息给AudioHandler
// 这个消息在setStreamVolumeInt()函数的分析中已经看到过
// 这个消息将把音量设置到底层去,并将其存储到SettingsProvider中
sendMsg(mAudioHandler,
MSG_SET_DEVICE_VOLUME,
SENDMSG_QUEUE,
device,
0,
streamState,
0);
}
index = mStreamStates[streamType].getIndex(device, false /* lastAudible */);
}
// 最后,调用sendVolumeUpdate函数,通知外界音量值发生了变化
sendVolumeUpdate(streamType, oldIndex, index, flags);
}
在这个函数的实现中,有一个非常重要的类型:VolumeStreamState。前面提到过,Android的音量是依赖于某种流类型的。如果Android定义了N个流类型,AudioService就需要维护N个音量值与之对应。另外每个流类型的音量等级范围不一样,所以还需要为每个流类型维护它们的音量调节范围。VolumeStreamState类的功能就是为了保存与一个流类型所有音量相关的信息。AudioService为每一种流类型都分配了一个VolumeStreamState对象,并且以流类型的值为索引,保存在一个名为mStreamStates的数组中。在这个函数中调用了VolumeStreamState对象的adjustIndex()函数,于是就改变了这个对象中存储的音量值。不过,仅仅是改变了它的存储值,并且没有把这个变化设置到底层。
总结一下这个函数都做了什么。
准备工作。计算按下音量键的音量步进值。细心的读者一定注意到了,这个步进值是10而不是1。原来,在VolumeStreamState中保存的音量值是其实际值的10倍。为什么这么做呢?这是为了在不同流类型之间进行音量转换时能够保证一定精度的一种实现,其转换过程读者可以参考rescaleIndex()函数的实现。我们可以将这种做法理解为在转换过程中保留了小数点后一位的精度。其实,直接使用float类型来保存岂不更简单?
检查是否需要改变情景模式。checkForRingerModeChange()和情景模式有关。读者可以自行研究其实现。
调用adjustIndex()更改VolumeStreamState对象中保存的音量值。
通过sendMsg()发送消息MSG_SET_DEVICE_VOLUME到mAudioHandler。
调用sendVolumeUpdate()函数,通知外界音量发生了变化。
我们将重点分析后面三项内容:adjustIndex()、MSG_SET_DEVICE_VOLUME消息的处理和sendVolumeUpdate()。
4. VolumeStreamState的adjustIndex()分析
我们先看一下这个函数的定义:
[AudioService.java-->VolumeStreamState.adjustIndex()]
public b3oolean adjustIndex(int deltaIndex, int device) {
// 将现有的音量值加上变化量,然后调用setIndex进行设置
// 返回值与setIndex一样
return setIndex(getIndex(device, false /* lastAudible */) + deltaIndex,
device,
true /* lastAudible */);
}
这个函数很简单,下面再看一下setIndex()的实现:
[AudioService.java-->VolumeStreamState.setIndex()]
public synchronized boolean setIndex(int index, int device, boolean lastAudible) {
int oldIndex = getIndex(device, false /* lastAudible */);
index = getValidIndex(index);
// 在VolumeStreamState中保存设置的音量值,注意使用了一个HashMap
mIndex.put(device, getValidIndex(index));
if (oldIndex != index) {
// 保存到lastAudible
if (lastAudible) {
mLastAudibleIndex.put(device, index);
}
// 同时设置所有映射到当前流类型的其他流的音量
boolean currentDevice = (device == getDeviceForStream(mStreamType));
int numStreamTypes = AudioSystem.getNumStreamTypes();
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
......
}
return true;
} else {
return false;
}
}
在这个函数中有三项工作要做:
首先保存设置的音量值。这是VolumeStreamState的本职工作,这和Android 4.1之前的版本不一样,音量值与设备相关联了。因此对同一种流类型来说,在不同的音频设备下将会拥有不同的音量值。
然后根据参数的要求保存音量值到mLastAudibleIndex中。从名字就可以看出,它保存了静音前的音量。当取消静音时,AudioService就会恢复到这里保存的音量。
再就是对流映射的处理。既然A→B,那么在设置B的音量的同时要改变A的音量。这就是后面那个循环的作用。
可以看出,VolumeStreamState.adjustIndex()除了更新自己所保存的音量值外,没有做其他的事情。接下来再看一下MSG_SET_DEVICE_VOLUME的消息处理做了什么。
5. MSG_SET_DEVICE_VOLUME消息的处理
adjustStreamVolume()函数使用sendMsg()函数发送MSG_SET_DEVICE_VOLUME消息给mAudioHandler,这个Handler运行在AudioService的主线程上。直接看一下在mAudio-Handler中负责处理MSG_SET_DEVICE_VOLUME消息的setDeviceVolume()函数:
[AudioService.java-->AudioHandler.setIndex()]
private void setDeviceVolume(VolumeStreamState streamState, int device) {
/* 调用VolumeStreamState的applyDeviceVolume。这个函数的内容很简单,就是在调用Audio-
System.setStreamVolumeIndex()。到这里,音量就被设置到底层的AudioFlinger中 */
streamState.applyDeviceVolume(device);
// 和上面一样,需要处理流音量映射的情况。这段代码和上面setIndex的相关代码很像,不是吗
int numStreamTypes = AudioSystem.getNumStreamTypes();
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
......
}
/* 发送消息给mAudioHandler,其处理函数将会调用persitVolume()函数,这将会把音量的设置信息
存储到SettingsProvider中。AudioService在初始化时,将会从SettingsProvider中将音量
设置读取出来并进行设置 */
sendMsg(mAudioHandler,
MSG_PERSIST_VOLUME,
SENDMSG_QUEUE,
PERSIST_CURRENT|PERSIST_LAST_AUDIBLE,
device,
streamState,
PERSIST_DELAY);
}
sendMsg()是一个异步操作,这就意味着,完成adjustIndex()更新音量信息后adjustStreamVolume()函数就返回了,但是音量并没有立刻被设置到底层。不过由于Handler处理多个消息的过程是串行的,这就隐含着一种风险:如果当Handler正在处理某一个消息时发生了阻塞,那么按下音量键,虽然调用adjustStreamVolume()可以立刻返回,并且从界面上看或用getStreamVolume()获取音量值都是没有问题的,但是手机发出声音时的音量大小并没有改变。
6. sendVolumeUpdate()分析
接下来,分析一下sendVolumeUpdate()函数,它用于通知外界音量发生了变化。
[AudioService.java-->AudioService.sendVolumeUpdate()]
private void sendVolumeUpdate(int streamType, int oldIndex, int index, int flags) {
/* 读者可能会感觉这句代码有点奇怪,mVoiceCapable是从SettingsProvider中取出来的一个常量。
从某种意义上来说,它可以用来判断设备是否拥有通话功能。对没有通话能力的设备来说,RING流类
型自然也就没有意义了。这句话应该算是一种从语义操作上进行的保护 */
if (!mVoiceCapable && (streamType == AudioSystem.STREAM_RING)) {
streamType = AudioSystem.STREAM_NOTIFICATION;
}
// mVolumePanel是一个VolumePanel类的实例,就是它显示了音量提示框
mVolumePanel.postVolumeChanged(streamType, flags);
/*发送广播。可以看到它们都有(x+5)/10的一个操作。为什么除以10可以理解,但是+5的意义是什么
呢?原来是为了实现四舍五入 */
oldIndex = (oldIndex + 5) / 10;
index = (index + 5) / 10;
Intent intent = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
intent.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
mContext.sendBroadcast(intent);
}
这个函数将音量的变化通过广播的形式通知给其他感兴趣的模块。同时,它还特别通知了mVolumePanel。mVolumePanel是VolumePanel类的一个实例。我们所看到的音量调节通知框就是它。
至此,从按下音量键开始的整个处理流程就完结了。在继续分析音量调节通知框的工作原理之前,先对之前的分析过程进行总结,参考图3-2的序列图。
结合上面分析的结果,由图 3-2可知:
音量键处理流程的发起者是PhoneWindow。
AudioManager仅仅起到代理的作用。
AudioService接受AudioManager的调用请求,操作VolumeStreamState的实例进行音量的设置。
VolumeStreamState负责保存音量设置,并且提供了将音量设置到底层的方法。
AudioService负责将设置结果以广播的形式通知外界。
到这里,相信大家对音量调节的流程已经有了一个比较清晰的认识。接下来我们将介绍音量调节通知框的工作原理。
7.音量调节通知框的工作原理
在分析sendVolumeUpdate()函数时曾经注意到,它调用了mVolumePanel的post-VolumeChanged()函数。mVolumePanel是一个VolumePanel的实例,作为一个Handler的子类,它承接了音量变化的UI/声音的通知工作。在继续上面的讨论之前,先了解一下VolumePanel工作的基本原理。
VolumePanel位于android.view包下,却没有在API中提供,因为它只能被AudioService使用,所以和AudioService放在一个包下可能更合理一些。从这个类的注释上可以看到,谷歌的开发人员对它被放在android.view下也有极大不满(What A Mass! 他们这么写道……)。
VolumePanel下定义了两个重要的子类型,分别是StreamResources和StreamControl。StreamResources实际上是一个枚举,它的每一个可用元素保存了一个流类型的通知框所需要的各种资源,如图标、提示文字等。StreamResources的定义就像下面这样:
[VolumePanel.java-->VolumePanel.StreamResources]
private enum StreamResources {
BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO,
R.string.volume_icon_description_bluetooth,
R.drawable.ic_audio_bt,
R.drawable.ic_audio_bt,
false),
// 我们省略了后面的几个枚举项的构造参数,这些与BluetoothSCOStream的内容是一致的
RingerStream(......),
VoiceStream(......),
AlarmStream(......),
MediaStream(......),
NotificationStream(......),
MasterStream(......),
RemoteStream(......);
int streamType; // 流类型
int descRes; // 描述信息
int iconRes; // 图标
int iconMuteRes; // 静音图标
boolean show; // 是否显示
// 构造函数
StreamResources(int streamType, int descRes, int iconRes, int iconMuteRes
, boolean show) {
......
}
};
这几个枚举项组成了一个名为STREAM的数组,如下:
[VolumePanel.java-->VolumePanel.STREAMS]
private static final StreamResources[] STREAMS = {
StreamResources.BluetoothSCOStream,
StreamResources.RingerStream,
StreamResources.VoiceStream,
StreamResources.MediaStream,
StreamResources.NotificationStream,
StreamResources.AlarmStream,
StreamResources.MasterStream,
StreamResources.RemoteStream
};
VolumePanel将从这个STREAMS数组中获取它所支持的流类型的相关资源。这么做是不是有点啰嗦呢?事实上,在这里使用枚举并没有什么特殊的意义,使用一个普通的Java类来定义StreamResources就已经足够了。
StreamControl类则保存了一个流类型的通知框所需要显示的控件,其定义如下:
[VolumePanel.java-->VolumePanel.StreamControl]
private class StreamControl {
int streamType;
ViewGroup group;
ImageView icon;
SeekBar seekbarView;
int iconRes;
int iconMuteRes;
}
很简单对不对?StreamControl实例中保存了音量调节通知框中所需的所有控件。关于这个类在VolumePanel的使用,我们可能很直观地认为只有一个StreamControl实例,在对话框显示时,使其保存的控件按需加载指定流类型的StreamResources实例中定义的资源。其实不然,出于对运行效率的考虑,StreamControl实例也是每个流类型人手一份,和StreamResources实例形成一一对应的关系。所有的StreamControl实例被保存在一个以流类型的值为键的Hashtable中,名为mStreamControls。我们可以在StreamControl的初始化函数createSliders()中一窥端倪。
[VolumePanel-->VolumePanel.createSliders()]
private void createSliders() {
......
// 遍历STREAM中所有的StreamResources实例
for (int i = 0; i < STREAMS.length; i++) {
StreamResources streamRes = STREAMS[i];
int streamType = streamRes.streamType;
......
// 为streamType创建一个StreamControl
StreamControl sc = new StreamControl();
// 这里将初始化sc的成员变量
......
// 将初始化好的sc放入mStreamControls中
mStreamControls.put(streamType, sc);
}
}
值得一提的是,这个初始化的工作并没有在构造函数中进行,而是在postVolume-Changed()函数中处理的。
既然已经有了通知框所需要的资源和通知框的控件,接下来就要有一个对话框承载它们。没错,VolumePanel保存了一个名为mDialog的Dialog实例,这就是通知框的本身了。每当有新的音量变化到来时,mDialog的内容就会被替换为指定流类型对应的StreamControl中所保存的控件,并且根据音量变化情况设置其音量条的位置,最后调用mDialog.show()显示出来。同时,发送一个延时消息MSG_TIMEOUT,这条延时消息生效时,将会关闭提示框。
StreamResource、StreamControl与mDialog的关系就像图3-3所示的那样,StreamControl可以说是mDialog的配件,随需拆卸。
接下来具体看一下VolumePanel在收到音量变化通知后都做了什么。我们在上一小节中说到了mVolumePanel.postVolumeChanged()函数。它的内容很简单,直接发送了一条消息MSG_VOLUME_CHANGED,然后在handleMessage中调用onVolumeChanged()函数进行真正的处理。
VolumePanel在MSG_VOLUME_CHANGED的消息处理函数中调用onVolume-Changed()函数,而不是直接在postVolumeChanged()函数中直接调用。这么做是有实际意义的。由于Android要求只能在创建控件的线程中对控件进行操作。postVolumeChanged()作为一个回调性质的函数,不能要求调用者位于哪个线程中。所以必须通过向Handler发送消息的方式,将后续的操作转移到指定的线程中。在设计具有UI Controller功能的类时,VolumePanel的实现方式有很好的参考意义。
下面看一下onVolumeChanged()函数的实现:
注意最后一个resetTimeout()的调用,其实它重新延时发送了MSG_TIMEOUT消息。当MSG_TIMEOUT消息生效时,mDialog将被关闭。
之后就是onShowVolumeChanged了。这个函数负责为通知框的内容填充音量、图表等信息,然后再显示通知框(如果还没有显示)。以铃声音量为例,省略其他的代码。
[VolumePanel.java-->VolumePanel.onShowVolumeChanged()]
protected void onShowVolumeChanged(int streamType, int flags) {
// 获取音量值
int index = getStreamVolume(streamType);
// 获取音量最大值,这两个将用来设置进度条
int max = getStreamMaxVolume(streamType);
switch (streamType) {
// 在这个switch语句中,我们要根据每种流类型的特点进行各种调整。
// 例如Music有时就需要更新它的图标,因为使用蓝牙耳机时的图标和平时的不一样,
// 所以每一次都需要更新一下
case AudioManager.STREAM_MUSIC: {
if ((mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC) &
(AudioManager.DEVICE_OUT_BLUETOOTH_A2DP |
AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES |
AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) {
setMusicIcon(R.drawable.ic_audio_bt,
R.drawable.ic_audio_bt_mute); // 设置为蓝牙
[VolumePanel.java-->VolumePanel.onVolumeChanged()]
protected void onVolumeChanged(int streamType, int flags) {
// 需要flags中包含AudioManager.FLAG_SHOW_UI 才会显示音量调节通知框
if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
synchronized (this) {
if (mActiveStreamType != streamType) {
reorderSliders(streamType); // 在Dialog里装载需要的StreamControl
}
// 这个函数负责最终的显示
onShowVolumeChanged(streamType, flags);
}
}
// 是否播出Tone音,注意有个小延迟
if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) {
removeMessages(MSG_PLAY_SOUND);
sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags),
PLAY_SOUND_DELAY);
}
// 取消声音与振动的播放
if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) {
removeMessages(MSG_PLAY_SOUND);
removeMessages(MSG_VIBRATE);
onStopSounds();
}
// 开始安排回收资源
removeMessages(MSG_FREE_RESOURCES);
sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY);
// 重置音量框超时关闭的时间
resetTimeout();
}
图标
} else {
setMusicIcon(R.drawable.ic_audio_vol,
R.drawable.ic_audio_vol_mute);// 设置为普通图标
}
break;
......
}
// 取出Music流类型对应的StreamControl,并设置其SeekBar的音量显示
StreamControl sc = mStreamControls.get(streamType);
if (sc != null) {
if (sc.seekbarView.getMax() != max) {
sc.seekbarView.setMax(max);
}
sc.seekbarView.setProgress(index);
......
}
if (!mDialog.isShowing()) { // 如果对话框还没有显示
/*forceVolumeControlStream()的调用在这里,一旦此通知框被显示,之后按下音量键都只能
调节当前流类型的音量。直到通知框关闭时,重新调用forceVolumeControlStream(),并设
置streamType为-1*/
mAudioManager.forceVolumeControlStream(streamType);
// 为Dialog设置显示控件
/ *注意, mView目前已经在reorderSlider()函数中安装好Music流所对应的StreamControl了 */
mDialog.setContentView(mView);
......
// 显示对话框
mDialog.show();
}
}
至此,音量调节通知框就被显示出来了,下面总结一下它的工作过程:
postVolumeChanged() 是VolumePanel显示的入口。
检查flags中是否有FLAG_SHOW_UI。
VolumePanel会在第一次被要求弹出时初始化其控件资源。
mDialog 加载指定流类型对应的StreamControl,也就是控件。
显示对话框并开始超时计时。
超时计时到达,关闭对话框。
到此为止,AudioService对音量键的处理流程介绍完毕。而 Android还有另外一种改变音量的方式,即音量设置函数etStreamVolume(),下面对其进行介绍。