Android系统音量条实例代码

作者:他叫小黑 时间:2022-05-26 15:20:48 

最近在定制Android系统音量条,发现代码还是蛮多的,下面总结一下。

代码是基于5.1.1版本的。

系统音量条的代码是在/frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java

布局文件是在/frameworks/base/packages/SystemUI/res/layout下。

先看看原生的音量条样式:

Android系统音量条实例代码

Android系统音量条实例代码

在代码中可以发现volume_dialog.xml这个文件,这个文件就是承载音量条的布局了,在layout文件夹找到打开会发现这个布局很简单,只是include了一个volume_panel

volume_panel布局包含了一个id叫slider_panel的FrameLayout和include了一个zen_mode_panel,显然slider_panel后面会包含seekbar,看VolumePanel.java也会发现在代码中加载了volume_panel_item.xml这个文件,一看,发现里面就包含了seekbar这个控件啦。另外zen_mode_panel是指勿扰模式。

在看这个布局文件的时候,你会看到android:clipChildren这个属性,它的作用:是否限制子View在其范围内,我们将其值设置为false后那么当子控件的高度高于父控件时也会完全显示,而不会被压缩。默认为true。

若想某个控件不显示,设置属性android:visibility=”gone”就好了。

看完布局,下面就主要看VolumePanel.java这个文件了。

VolumePanel下定义了两个重要的子类型,分别是StreamResources和StreamControl。StreamResources实际上是一个枚举,它的每一个可用元素保存了一个流类型的通知框所需要的各种资源,如图标、提示文字等。StreamResources的定义就像下面这样:


 private enum StreamResources {
   BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO,
       R.string.volume_icon_description_bluetooth,
       IC_AUDIO_BT,
       IC_AUDIO_BT_MUTE,
       false),
   // 这里省略了后面的几个枚举项的构造参数,这些与BluetoothSCOStream的内容是一致的
   RingerStream(...),
   VoiceStream(...),
   AlarmStream(...),
   MediaStream(...),
   NotificationStream(...),
   // for now, use media resources for master volume
   MasterStream(...),
   RemoteStream(...);// will be dynamically updated

int streamType; // 流类型
   int descRes; // 描述信息
   int iconRes; // 图标
   int iconMuteRes; // 静音图标
   // RING, VOICE_CALL & BLUETOOTH_SCO are hidden unless explicitly requested
   boolean show; // 是否显示
   //构造函数
   StreamResources(int streamType, int descRes, int iconRes, int iconMuteRes, boolean show) {
     ...
   }
 }

这几个枚举项组成了一个名为STREAM的数组,如下:


 private static final StreamResources[] STREAMS = {
   StreamResources.BluetoothSCOStream,
   StreamResources.RingerStream,
   StreamResources.VoiceStream,
   StreamResources.MediaStream,
   StreamResources.NotificationStream,
   StreamResources.AlarmStream,
   StreamResources.MasterStream,
   StreamResources.RemoteStream
 };

VolumePanel将从这个STREAMS数组中获取它所支持的流类型的相关资源。

StreamControl类则保存了一个流类型的通知框所需要显示的控件,其定义如下:


 /** Object that contains data for each slider */
 private class StreamControl {
   int streamType;
   MediaController controller;
   ViewGroup group;
   ImageView icon;
   SeekBar seekbarView;
   TextView suppressorView;
   View divider;
   ImageView secondaryIcon;
   int iconRes;
   int iconMuteRes;
   int iconSuppressedRes;
 }

StreamControl实例中保存了音量调节通知框中所需的所有控件。出于对运行效率的考虑,StreamControl实例也是每个流类型人手一份,和StreamResources实例形成一一对应的关系。所有的StreamControl实例被保存在一个以流类型的值为键的SparseArray中,名为mStreamControls。可以在StreamControl的初始化函数createSliders()中看到。


 private void createSliders() {
   ...
   // 遍历STREAM中所有的StreamResources实例
   for (int i = 0; i < STREAMS.length; i++) {
     StreamResources streamRes = STREAMS[i];
     final int streamType = streamRes.streamType;    
     ...
     final StreamControl sc = new StreamControl();// 为streamType创建一个StreamControl
     // 下面将初始化sc的成员变量
     ...
     sc.seekbarView.setOnSeekBarChangeListener(mSeekListener); // 设置监听          
     mStreamControls.put(streamType, sc);// 将初始化好的sc放入mStreamControls中
   }
 }

 private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
     final Object tag = seekBar.getTag();
     if (fromUser && tag instanceof StreamControl) {
       StreamControl sc = (StreamControl) tag;
       //设置音量
       setStreamVolume(sc, progress,AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE);        
     }
     resetTimeout();
   }
   ...
 };

这个初始化的工作并没有在构造函数中进行,而是在postVolumeChanged()、postRemoteVolumeChanged()、postMuteChanged()函数中处理的。

VolumePanel保存了一个名为mDialog的Dialog实例,这就是通知框的本身了。每当有新的音量变化到来时,mDialog的内容就会被替换为指定流类型对应的StreamControl中所保存的控件,并且根据音量变化情况设置其音量条的位置,最后调用mDialog.show()显示出来。同时,发送一个延时消息MSG_TIMEOUT,这条延时消息生效时,将会关闭提示框。

接下来具体看一下VolumePanel在收到音量变化通知后都做了什么。

VolumePanel在MSG_VOLUME_CHANGED的消息处理函数中调用onVolumeChanged()函数,而不是直接在postVolumeChanged()函数中直接调用。这么做是有实际意义的。由于Android要求只能在创建控件的线程中对控件进行操作。postVolumeChanged()作为一个回调性质的函数,不能要求调用者位于哪个线程中。所以必须通过向Handler发送消息的方式,将后续的操作转移到指定的线程中。

下面再看一下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, null);
     }
   }
   // 是否播出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()了。这个函数负责为通知框的内容填充音量、图表等信息,然后再显示通知框(如果还没有显示)。


 protected void onShowVolumeChanged(int streamType, int flags, MediaController controller) {
   int index = getStreamVolume(streamType);// 获取音量值
   int max = getStreamMaxVolume(streamType); // 获取音量最大值,这两个将用来设置进度条
   StreamControl sc = mStreamControls.get(streamType);
   //在这个switch语句中,我们要根据每种流类型的特点进行各种调整。例如Music有时就需要更新它的图标,因为使用蓝牙耳机时的图标和平时的不一样,所以每一次都需要更新一下
   switch (streamType) {
     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(IC_AUDIO_BT, IC_AUDIO_BT_MUTE);
       } else {
         setMusicIcon(IC_AUDIO_VOL, IC_AUDIO_VOL_MUTE);
       }
       break;
     }
     ...
   }

if (sc != null) {
     ...
     updateSliderProgress(sc, index); // 更新Seekbar的显示
     final boolean muted = isMuted(streamType);
     updateSliderEnabled(sc, muted, (flags & AudioManager.FLAG_FIXED_VOLUME) != 0);
     ...
        updateSliderIcon(sc, muted); //更新stream_icon
     }
   }

if (!isShowing()) { // 如果对话框还没有显示
     int stream = (streamType == STREAM_REMOTE_MUSIC) ? -1 : streamType;
     //一旦此通知框被显示,之后按下音量键都只能调节当前流类型的音量。直到通知框关闭时,重新调用forceVolumeControlStream(),并设置streamType为-1
     if (stream != STREAM_MASTER) {
       mAudioManager.forceVolumeControlStream(stream);
     }
     mDialog.show(); // 显示对话框
     ...
   }

// Do a little vibrate if applicable (only when going into vibrate mode)
   if ((streamType != STREAM_REMOTE_MUSIC) &&
       ((flags & AudioManager.FLAG_VIBRATE) != 0) &&
       isNotificationOrRing(streamType) &&
       mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) {
     sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY);//稍微振动(仅当进入振动模式时)
   }
 ...
 }

看到updateSliderProgress()更新Seekbar音量。代码如下:


 private void updateSliderProgress(StreamControl sc, int progress) {
   final boolean isRinger = isNotificationOrRing(sc.streamType);
   if (isRinger && mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) {
     progress = mLastRingerProgress;
   }
   if (progress < 0) {
     progress = getStreamVolume(sc.streamType); // 获取音量值
   }
   sc.seekbarView.setProgress(progress);//设置音量条
   if (isRinger) {
     mLastRingerProgress = progress;
   }
 }

下面总结一下:

postVolumeChanged() 是VolumePanel显示的入口。是通过VolumeUI.java里面调用mPanel.postVolumeChanged()方法进入的。

检查flags中是否有FLAG_SHOW_UI。

VolumePanel会在第一次被要求弹出时初始化其控件资源。

mDialog 加载指定流类型对应的StreamControl,也就是控件。

显示对话框并开始超时计时。

超时计时到达,关闭对话框。

来源:http://blog.csdn.net/u013082948/article/details/59484202

标签:android,系统,音量条
0
投稿

猜你喜欢

  • java实现163邮箱发送邮件到qq邮箱成功案例

    2023-09-18 02:38:09
  • C#使用HttpClient的正确方式你了解吗

    2023-07-04 12:12:40
  • Android可自定义神奇动效的卡片切换视图实例

    2022-11-24 09:07:46
  • Android自定义控件实现时间轴

    2021-07-12 04:13:08
  • Java Struts图片上传至指定文件夹并显示图片功能

    2023-03-15 10:48:51
  • Android编程实现对电池状态的监视功能示例

    2023-11-16 08:40:03
  • Java Web开发过程中登陆模块的验证码的实现方式总结

    2022-01-29 19:33:16
  • C#中数组、ArrayList和List三者的区别详解及实例

    2023-11-07 03:12:47
  • Java 爬虫工具Jsoup详解

    2022-04-11 03:46:16
  • SpringBoot启动yaml报错的解决

    2021-09-09 22:58:14
  • Android仿微信QQ设置图形头像裁剪功能

    2022-06-21 10:12:25
  • Android通过访问网页查看网页源码实例详解

    2023-10-05 03:09:11
  • 详解利用SpringCloud搭建一个最简单的微服务框架

    2023-08-21 04:24:32
  • SpringCloud启动失败问题汇总

    2021-07-28 02:45:51
  • C#实现在PDF文档中应用多种不同字体

    2022-01-18 02:13:28
  • Spring源码解析之BeanPostProcessor知识总结

    2022-04-07 22:13:34
  • Java annotation元注解原理实例解析

    2022-09-08 00:42:45
  • 零基础入门SpringMVC拦截器的配置与使用

    2023-07-17 21:59:28
  • C#简单快速的json组件fastJSON使用介绍

    2022-08-13 01:47:50
  • 详解Android使用@hide的API的方法

    2023-02-16 07:10:31
  • asp之家 软件编程 m.aspxhome.com