ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
#### 1. 触发音量键 音量键被按下后,Android输入系统将该事件一路派发给Activity,如果无人截获并消费这个事件,承载当前Activity的显示的PhoneWindow类的onKeyDown()或onKeyUp()函数将会将其处理,从而开始了通过音量键调整音量的处理流程。输入事件的派发机制以及PhoneWindow类的作用将在后续章节中详细介绍,现在只需要知道,PhoneWindow描述了一片显示区域,用于显示与管理我们所看到的Activity、对话框等内容。同时,它还是输入事件的派发对象,而且只有显示在最上面的PhoneWindow才会收到事件。 **注意** 按照Android的输入事件派发策略,Window对象在事件的派发队列中排在Activity的后面(应该说排在队尾比较合适),所以应用程序可以重写自己的onKeyDown()函数,将音量键用作其他的功能。比如说,在一个相机应用中,按下音量键所执行的动作是拍照而不是调节音量。 PhoneWindow的onKeyDown()函数实现如下: **PhoneWindow.java-->PhoneWindow.onKeyDown()** ``` ......//加省略号, 略过一些内容 switch (keyCode) { caseKeyEvent.KEYCODE_VOLUME_UP: caseKeyEvent.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 voidadjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags) ``` adjustSuggestedStreamVolume()有三个参数,而第三个参数flags的意思就不那么容易猜了。其实AudioManager在handleKeyDown()里设置了两个flags,分别是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(intdirection, int suggestedStreamType, int flags) {格式要调整好 int streamType; // ①从这一小段代码中,可以看出在 AudioService中还有地方可以强行改变音量键控制的流类型 if(mVolumeControlStream != -1) { streamType = mVolumeControlStream; } else { // ②通过getActiveStreamType()函数获取要控制的流类型 // 这里根据建议的流类型与AudioService的实际情况,返回一个值 streamType = getActiveStreamType(suggestedStreamType); } // ③这个啰嗦的if判断的目的,就是只有在特定的流类型下,并且没有处于锁屏状态时才会播放声音 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, intdirection, int flags) { // 首先还是获取streamType映射到的流类型。这个映射的机制确实给我们的分析带来不小的干扰 // 在非必要的情况下忽略它们吧 int streamTypeAlias = mStreamVolumeAlias[streamType]; // 注意VolumeStreamState类 VolumeStreamState streamState = mStreamStates[streamTypeAlias]; final intdevice = getDeviceForStream(streamTypeAlias); // 获取当前音量,注意第二个参数的值,它的目的是如果这个流被静音,则取出它被静音前的音量 final intaliasIndex = streamState.getIndex(device, (streamState.muteCount()!= 0) booleanadjustVolume = 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 intoldIndex = 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 boolean adjustIndex(int deltaIndex, intdevice) { // 将现有的音量值加上变化量,然后调用setIndex设置下去 // 返回值与setIndex一样 return setIndex(getIndex(device, false /* lastAudible */) + deltaIndex, device, true /* lastAudible */); } ``` 这个函数很简单,我们再看一下setIndex()的实现: **AudioService.java-->VolumeStreamState.setIndex()** ``` public synchronized boolean setIndex(int index, intdevice, boolean lastAudible) { intoldIndex = 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)); intnumStreamTypes = AudioSystem.getNumStreamTypes(); for(int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { …… } return true; } else { return false; } } ``` 在这个函数中有三个工作要做: - 首先是保存设置的音量值,这是VolumeStreamState的本职工作,这和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的主线程上。直接看一下在mAudioHandler中负责处理MSG\_SET\_DEVICE\_VOLUME消息的setDeviceVolume()函数: **AudioService.java-->AudioHandler.setIndex()** ``` private void setDeviceVolume(VolumeStreamStatestreamState, int device) { // 调用VolumeStreamState的applyDeviceVolume。 // 这个函数的内容很简单,就是在调用AudioSystem.setStreamVolumeIndex() // 到这里,音量就被设置到底层的AudioFlinger里面去了 streamState.applyDeviceVolume(device); // 和上面一样,需要处理流音量映射的情况。这段代码和上面setIndex的相关代码很像,不是么 intnumStreamTypes = 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, intoldIndex, 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; Intentintent = 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类的一个实例。我们所看到的音量调节通知框就是它了。 至此,从按下音量键开始的整个处理流程就完结了。在继续分析音量调节通知框的工作原李之前,先对之前的分析过程作一个总结,请参考下面的序列图: :-: ![](http://img.blog.csdn.net/20150811132749056?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) 图 3-2 音量键调整音量的处理流程 结合上面分析的结果,由图 3-2可知: - 音量键处理流程的发起者是PhoneWindow。 - AudioManager仅仅起到代理的作用。 - AudioService接受AudioManager的调用请求,操作VolumeStreamState的实例进行音量的设置。 - VolumeStreamState负责保存音量设置,并且提供了将音量设置到底层的方法。 - AudioService负责将设置结果以广播的形式通知外界。 到这里,相信大家对音量量调节的流程已经有了一个比较清晰的认识了。接下来我们将介绍音量调节通知框的工作原理。 #### 4. 音量调节通知框的工作原理 在分析sendVolumeUpdate()函数时曾经注意到它调用了mVolumePanel的postVolumeChanged()函数。mVolumePanel是一个VolumePanel的实例。作为一个Handler的子类,它承接了音量变化的UI/声音的通知工作。在继续上面的讨论之前,先了解一下其工作的基本原理。 VolumePanel为于android.view包下,但是却没有在API中被提供。因为它只能被AudioService使用,所以和AudioService放在一个包下可能更合理一些。从这个类的注释上可以看到,谷歌的开发人员对它被放在android.view下也有极大的不满(What A Mass! 他们这么写道……)。 VolumePanel下定义了两个重要的子类型,分别是StreamResources和StreamControl。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(……); intstreamType; // 流类型 intdescRes; // 描述信息 inticonRes; // 图标 inticonMuteRes;// 静音图标 booleanshow; // 是否显示 StreamResources(intstreamType, 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 { intstreamType; ViewGroupgroup; ImageViewicon; SeekBarseekbarView; inticonRes; inticonMuteRes; } ``` 很简单对不对?StreamControl实例中保存了音量条提示框中所需的所用控件。关于这个类在VolumePanel的使用,我们可能很直观的认为只有一个StreamControl实例,在对话框显示时,使其保存的控件按需加载指定流类型的StreamResources实例中定义的资源。其实不然,应该是出于对运行效率的考虑,StreamControl实例也是每个流类型人手一份,和StreamResources实例形成了一个一一对应的关系。所有的StreamControl 实例被保存在了一个以流类型的值为键的Hashtable中,名为mStreamControls。我们可以在StreamControl的初始化函数createSliders()中一窥其端倪: **VolumePanel-->VolumePanel.createSliders()** ``` private void createSliders() { …… // 遍历STREAM中所有的StreamResources实例 for (inti = 0; i < STREAMS.length; i++) { StreamResources streamRes = STREAMS[i]; intstreamType = streamRes.streamType; …… // 为streamType创建一个StreamControl StreamControl sc = new StreamControl(); // 这里将初始化sc的成员变量 …… // 将初始化好的sc放入mStreamControls中去。 mStreamControls.put(streamType, sc); } } ``` 值得一提的是,这个初始化的工作并没有在构造函数中进行,而是在postVolumeChanged()函数里处理的。 既然已经有了通知框所需要的资源和通知框的控件了,那么接下来就要有一个对话框承载它们。没错,VolumePanel保存了一个名为mDialog的Dialog实例,这就是通知框的本尊了。每当有新的音量变化到来时,mDialog的内容就会被替换为制定流类型对应的StreamControl中所保存的控件,并根据音量变化情况设置其音量条的位置,最后调用mDialog.show()显示出来。同时,发送一个延时消息MSG\_TIMEOUT,这条延时消息生效时,将会关闭提示框。 StreamResource、StreamControl与mDialog的关系就像下面这附图一样,StreamControl可以说是mDialog的配件,随需拆卸。 :-: ![](http://img.blog.csdn.net/20150811132952715?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) 图 3-3 StreamResource、StreamControl与mDialog的关系 接下来具体看一下VolumePanel在收到音量变化通知后都做了什么。我们在上一小节中说到了mVolumePanel.postVolumeChanged()函数。它的内容很简单,直接发送了一条消息MSG\_VOLUME\_CHANGED,然后在handleMessage中调用onVolumeChanged()函数进行真正的处理。 **注意** VolumePanel在MSG\_VOLUME\_CHANGED的消息处理函数中调用onVolumeChanged()函数而不直接在postVolumeChanged()函数中直接调,。这么做是有实际意义的。由于Android要求只能在创建控件的线程中对控件进行操作。postVolumeChanged()作为一个回调性质的函数,不能要求调用者位于哪个线程中。所以必须通过向Handler发送消息的方式,将后续的操作转移到指定的线程中去。在大家设计具有UI Controller功能的类时,VolumePanel的实现方式有很好的参考意义。 看一下onVolumeChanged()函数的实现: **VolumePanel.java-->VolumePanel.onVolumeChanged()** ``` protected void onVolumeChanged(int streamType, intflags) { // 需要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(); } ``` 注意最后一个resetTimeout()的调用。它其实是重新延时发送了MSG\_TIMEOUT消息。当MSG\_TIMEOUT消息生效时,mDialog将会被关闭。 之后就是onShowVolumeChanged了。这个函数负责为通知框的内容填充音量、图表等信息,然后再把通知框显示出来,如果还没有显示的话。以铃声音量为例,省略掉其他的代码。 **VolumePanel.java-->VolumePanel.onShowVolumeChanged()** ``` protectedvoid onShowVolumeChanged(int streamType, int flags) { // 获取音量值 intindex = getStreamVolume(streamType); // 获取音量最大值,这两个将用来设置进度条 intmax = getStreamMaxVolume(streamType); switch (streamType) { // 这个switch语句中,我们要根据每种流类型的特点,进行各种调整。 // 例如Music就有时就需要更新它的图标,因为使用蓝牙耳机时的图标和和平时的不一样 // 所以每一次都需要更新一下 case AudioManager.STREAM_MUSIC: { // Special case for when Bluetooth is active for 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);// 设置蓝牙图标 } 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还有另外一种改变音量的方式。即音量设置函数setStreamVolume(),下面对其进行介绍