需求,全志A33平台Android4.4版本进度条定制 定制系统音量条:
解决思路:
- 修改源码 com.android.systemui.volume.VolumePanel.java
- volumePanel是一个类,进度条加载的容器是一个Dialog,修改dialog样式、背景、添加修改图标
按照基本需求得到的样式如下:
这个音量条原始状态啥样的呢,如下:
这里不做音量调节的逻辑步骤分析,只介绍音量调节的UI显示上面的分析。分析之前还是要看一看VolumePanel简单的构成。
1)class VolumePanel extends Handler implements DemoMode 追踪DemoMode 是一个接口
2)public interface DemoMode {
void dispatchDemoCommand(String command, Bundle args);
public static final String ACTION_DEMO = "com.android.systemui.demo";
public static final String COMMAND_ENTER = "enter";
public static final String COMMAND_EXIT = "exit";
public static final String COMMAND_CLOCK = "clock";
public static final String COMMAND_BATTERY = "battery";
public static final String COMMAND_NETWORK = "network";
public static final String COMMAND_BARS = "bars";
public static final String COMMAND_STATUS = "status";
public static final String COMMAND_NOTIFICATIONS = "notifications";
public static final String COMMAND_VOLUME = "volume";
}
那么可以这么理解:volumePanel 其实就是一个Handler
其中两个重要的子类型:
- StreamResources
- StreamControl
就是一系列的音量资源和控制
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) {
this.streamType = streamType; // 流类型
this.descRes = descRes; // 描述信息
this.iconRes = iconRes; // 图标
this.iconMuteRes = iconMuteRes; // 静音图标
this.show = show; // 是否显示
}
}
/** 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;
}
StreamResources是一个enum的资源,定义了不同流类型下的图标;StreamControl是一个控制相关的class,是能够控制数据的
Object that contains data for each slider
前面分析了VolumePanel 就是一个简单的Handler类,那么最直接的就是分析
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_VOLUME_CHANGED: {
onVolumeChanged(msg.arg1, msg.arg2);
break;
}
case MSG_MUTE_CHANGED: {
...
break;
}
case MSG_FREE_RESOURCES: {
...
break;
}
case MSG_STOP_SOUNDS: {
...
break;
}
case MSG_PLAY_SOUND: {
...
break;
}
case MSG_VIBRATE: {
...
break;
}
case MSG_TIMEOUT: {
...
}
case MSG_RINGER_MODE_CHANGED:
case MSG_INTERNAL_RINGER_MODE_CHANGED:
case MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED: {
...
}
case MSG_REMOTE_VOLUME_CHANGED: {
...
break;
}
case MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN:
...
break;
case MSG_SLIDER_VISIBILITY_CHANGED:
...
break;
case MSG_DISPLAY_SAFE_VOLUME_WARNING:
...
break;
case MSG_LAYOUT_DIRECTION:
...
break;
case MSG_ZEN_MODE_AVAILABLE_CHANGED:
...
break;
case MSG_USER_ACTIVITY:
...
break;
}
}
重点关注:定位到onVolumeChanged方法
/**
* Override this if you have other work to do when the volume changes (for
* example, vibrating, playing a sound, etc.). Make sure to call through to
* the superclass implementation.
*/
protected void onVolumeChanged(int streamType, int flags) {
if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
synchronized (this) {
if (mActiveStreamType != streamType) {
reorderSliders(streamType);
}
onShowVolumeChanged(streamType, flags, null);
}
}
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();
}
重点关注:定位到onShowVolumeChanged方法
protected void onShowVolumeChanged(int streamType, int flags, MediaController controller) {
int index = getStreamVolume(streamType); //得到电量值
switch (streamType) { //根据流类型设置图标、音量大小
case AudioManager.STREAM_MUSIC:
...
case AudioManager.STREAM_ALARM
...
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) {
if (streamType == STREAM_REMOTE_MUSIC && controller != sc.controller) {
if (sc.seekbarView.getMax() != max) {
sc.seekbarView.setMax(max); //设置音量条大小
}
updateSliderProgress(sc, index); //更新进度条大小
final boolean muted = isMuted(streamType);
updateSliderEnabled(sc, muted, (flags & AudioManager.FLAG_FIXED_VOLUME) != 0);
if (isNotificationOrRing(streamType)) {
// check for secondary-icon transition completion
if (mSecondaryIconTransition.isRunning()) {
mSecondaryIconTransition.cancel(); // safe to reset
sc.seekbarView.setAlpha(0); sc.seekbarView.animate().alpha(1);
mZenPanel.setAlpha(0); mZenPanel.animate().alpha(1);
}
updateSliderIcon(sc, muted); //更新图标进度条
}
}
if (!isShowing()) {
....
}
}
updateSliderProgress 方法,更新progress
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;
}
}
**
以上是基本流程,只看大进度条的更新,那么背景和进度条样式到底在哪里初始化,怎么更改样式呢?
仔细分析updateSliderProcess((StreamControl sc, int progress)方法,会发现最终设置进度setProcess的是StreamControl
/** All the slider controls mapped by stream type */
private SparseArray<StreamControl> mStreamControls; //声明的类型, 是把所有流类型的控制器都保存在一个Array中了
定位mStreamControls 的代码,源码里面有这么一个方法:createSliders()
private void createSliders() {
final Resources res = mContext.getResources();
final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
mStreamControls = new SparseArray<StreamControl>(STREAMS.length);
final StreamResources notificationStream = StreamResources.NotificationStream;
for (int i = 0; i < STREAMS.length; i++) {
StreamResources streamRes = STREAMS[i];
final int streamType = streamRes.streamType;
final boolean isNotification = isNotificationOrRing(streamType);
final StreamControl sc = new StreamControl();
sc.streamType = streamType;
sc.group = (ViewGroup) inflater.inflate(
com.android.systemui.R.layout.volume_panel_item, null);
sc.group.setTag(sc);
sc.icon = (ImageView) sc.group.findViewById(com.android.systemui.R.id.btn_mul);
sc.icon.setTag(sc);
sc.icon.setContentDescription(res.getString(streamRes.descRes));
sc.iconRes = streamRes.iconRes;
sc.iconMuteRes = streamRes.iconMuteRes;
//sc.icon.setImageResource(sc.iconRes);
sc.icon.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mAudioManager.setStreamVolume(streamType,mAudioManager.getStreamVolume(streamType)-1,AudioManager.FLAG_SHOW_UI); }
});
sc.seekbarView = (SeekBar) sc.group.findViewById(com.android.systemui.R.id.seekbar);
sc.suppressorView =
(TextView) sc.group.findViewById(com.android.systemui.R.id.suppressor);
sc.suppressorView.setVisibility(View.GONE);
final boolean showSecondary = !isNotification && notificationStream.show;
sc.secondaryIcon = (ImageView) sc.group
.findViewById(com.android.systemui.R.id.btn_add);
sc.secondaryIcon.setContentDescription(res.getString(notificationStream.descRes));
sc.secondaryIcon.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mAudioManager.setStreamVolume(streamType,mAudioManager.getStreamVolume(streamType)+1,AudioManager.FLAG_SHOW_UI);
}
});
final int plusOne = (streamType == AudioSystem.STREAM_BLUETOOTH_SCO ||
streamType == AudioSystem.STREAM_VOICE_CALL) ? 1 : 0;
sc.seekbarView.setMax(getStreamMaxVolume(streamType) + plusOne);
sc.seekbarView.setOnSeekBarChangeListener(mSeekListener);
sc.seekbarView.setTag(sc);
mStreamControls.put(streamType, sc);
}
}
mStreamControls 装载了所有类型的UI。
分析构造方法了:
public VolumePanel(Context context, ZenModeController zenController) {
mDialog = new Dialog(context,com.android.systemui.R.style.dialog) { //定义了一个Dialog
@Override
public boolean onTouchEvent(MotionEvent event) {
//if (isShowing() && event.getAction() == MotionEvent.ACTION_OUTSIDE &&
// sSafetyWarning == null) {
// forceTimeout(0);
// return true;
// }
return false;
}
};
final Window window = mDialog.getWindow(); //1、通过dialog得到window
window.requestFeature(Window.FEATURE_NO_TITLE);
mDialog.setCanceledOnTouchOutside(true);
mDialog.setContentView(com.android.systemui.R.layout.volume_dialog);
mDialog.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
mActiveStreamType = -1;
mAudioManager.forceVolumeControlStream(mActiveStreamType);
setZenPanelVisible(false);
mDemoIcon = 0;
mSecondaryIconTransition.cancel();
}
});
mDialog.create();
final LayoutParams lp = window.getAttributes();
lp.token = null;
...
window.setBackgroundDrawable(new ColorDrawable(0x00000000));
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
window.addFlags(LayoutParams.FLAG_NOT_FOCUSABLE
| LayoutParams.FLAG_NOT_TOUCH_MODAL
| LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
| LayoutParams.FLAG_HARDWARE_ACCELERATED);
mView = window.findViewById(R.id.content); //2、通过window得到一个mView
Interaction.register(mView, new Interaction.Callback() {
@Override
public void onInteraction() {
resetTimeout();
}
});
mPanel = (ViewGroup) mView.findViewById(com.android.systemui.R.id.visible_panel); //3、通过mView找到panel【控件】
mSliderPanel = (ViewGroup) mView.findViewById(com.android.systemui.R.id.slider_panel); //3、通过mView找到panel【控件】
mZenPanel = (ZenModePanel) mView.findViewById(com.android.systemui.R.id.zen_mode_panel); //3、通过mView找到panel【控件】
....
registerReceiver();
}
构造方法重1、2、3是重点。
下面追踪一个方法,看看UI组件怎么变化的:updateStates()
private void updateStates() {
final int count = mSliderPanel.getChildCount(); //得到面板重各种流类型的数量
for (int i = 0; i < count; i++) {
StreamControl sc = (StreamControl) mSliderPanel.getChildAt(i).getTag(); //得到流类型的StreamControl
updateSlider(sc, true /*forceReloadIcon*/); //更新slider
}
}
/** Update the mute and progress state of a slider */
private void updateSlider(StreamControl sc, boolean forceReloadIcon) { //方法内不就是update 各个组件UI
updateSliderProgress(sc, -1);
final boolean muted = isMuted(sc.streamType);
if (forceReloadIcon) {
sc.icon.setImageDrawable(null);
}
updateSliderIcon(sc, muted);
updateSliderEnabled(sc, muted, false);
updateSliderSuppressor(sc);
}
以上就是UI更新逻辑。
那么怎么实现UI自定义效果呢?综上思路:
1)Dialog、window定义自己样式
2)更改布局,添加加减图标
3)定义音量加减广播,执行自己的逻辑
- VolumePanel.java 设置style:com.android.systemui.R.style.dialog
- 添加seekbar_volume dialog
- 添加进度条process.bg.xml 背景
- 添加thumb_bg.xml volume_back.xml 背景
- volume_dialog.xml volume_panel.xml volume_panel_item.xml zen_mode_panel.xml 布局修改。