系統音量進度條定製

需求,全志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 佈局修改。

SystemUI源碼地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章