手機中狀態欄主要用來顯示電池電量信息、時間、信號格數、系統圖標(鬧鐘)、通知圖標,我們先來看看手機statusbar的界面
今天我們先來簡單介紹下這個界面是怎麼顯示出來,考慮到放到一起寫,文章就有點太長了,後續會對信號格圖標顯示、通知圖標、系統圖標這幾個複雜點的一一介紹
從上圖中我們基本可以看出,從左到右基本上是通知圖標顯示區域、系統圖標顯示區域,系統圖標區域裏主要包括wifi、飛行模式、鬧鐘、耳機、信號格顯示、數據業務上下行箭頭、電池電量圖標、時間圖標,具體佈局如下:
//frameworks/base/packages/SystemUI/res/layout/status_bar.xml
<com.android.systemui.statusbar.phone.PhoneStatusBarView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res/com.android.systemui"
android:id="@+id/status_bar"
android:background="@drawable/system_bar_background"
android:orientation="vertical"
android:focusable="false"
android:descendantFocusability="afterDescendants"
>
...
<LinearLayout android:id="@+id/status_bar_contents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="6dp"
android:paddingEnd="8dp"
android:orientation="horizontal"
>
<!-- The alpha of this area is controlled from both PhoneStatusBarTransitions and
PhoneStatusBar (DISABLE_NOTIFICATION_ICONS). -->
<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
android:id="@+id/notification_icon_area"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal" />
<com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/system_icon_area"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
>
<include layout="@layout/system_icons" />
<com.android.systemui.statusbar.policy.Clock
android:id="@+id/clock"
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:singleLine="true"
android:paddingStart="@dimen/status_bar_clock_starting_padding"
android:paddingEnd="@dimen/status_bar_clock_end_padding"
android:gravity="center_vertical|start"
/>
</com.android.keyguard.AlphaOptimizedLinearLayout>
</LinearLayout>
...
</com.android.systemui.statusbar.phone.PhoneStatusBarView>
如果有通知圖標相關問題,基本上就可以根據notification_icon_area這個id去跟蹤就行,而系統圖標區域還有一個比較重要的佈局文件system_icons.xml
//frameworks/base/packages/SystemUI/res/layout/system_icons.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/system_icons"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical">
<com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/statusIcons"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"/>
<include layout="@layout/signal_cluster_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/signal_cluster_margin_start"/>
<!-- battery must be padded below to match assets -->
<com.android.systemui.BatteryMeterView android:id="@+id/battery"
android:layout_height="@dimen/status_bar_battery_icon_height"
android:layout_width="@dimen/status_bar_battery_icon_width"
android:layout_marginBottom="@dimen/battery_margin_bottom"/>
</LinearLayout>
這個裏面的signal_cluster_view.xml主要是和sim卡相關的一些圖標(vpn、信號格數、網絡狀態、數據業務上下行、漫遊等)、以及wifi、飛行模式圖標,BatteryMeterView主要用來顯示電池電量信息的
//frameworks/base/packages/SystemUI/res/layout/signal_cluster_view.xml
<com.android.systemui.statusbar.SignalClusterView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/signal_cluster"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingEnd="@dimen/signal_cluster_battery_padding" >
<ImageView
android:id="@+id/vpn"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:paddingEnd="6dp"
android:src="@drawable/stat_sys_vpn_ic"
/>
<FrameLayout
android:id="@+id/ethernet_combo"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
> <com.android.systemui.statusbar.AlphaOptimizedImageView
android:theme="@style/DualToneLightTheme"
android:id="@+id/ethernet"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
/> <com.android.systemui.statusbar.AlphaOptimizedImageView
android:theme="@style/DualToneDarkTheme"
android:id="@+id/ethernet_dark"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:alpha="0.0"
/>
</FrameLayout>
<FrameLayout
android:id="@+id/wifi_combo"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
> <com.android.systemui.statusbar.AlphaOptimizedImageView
android:theme="@style/DualToneLightTheme"
android:id="@+id/wifi_signal"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
/> <com.android.systemui.statusbar.AlphaOptimizedImageView
android:theme="@style/DualToneDarkTheme"
android:id="@+id/wifi_signal_dark"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:alpha="0.0"
/>
</FrameLayout>
<View
android:id="@+id/wifi_signal_spacer" android:layout_width="@dimen/status_bar_wifi_signal_spacer_width"
android:layout_height="4dp"
android:visibility="gone"
/>
<LinearLayout
android:id="@+id/mobile_signal_group"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
>
</LinearLayout>
<FrameLayout
android:id="@+id/no_sims_combo"
android:layout_height="wrap_content"
android:layout_width="wrap_content" android:contentDescription="@string/accessibility_no_sims">
<com.android.systemui.statusbar.AlphaOptimizedImageView
android:theme="@style/DualToneLightTheme"
android:id="@+id/no_sims"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/stat_sys_no_sims"
/>
<com.android.systemui.statusbar.AlphaOptimizedImageView
android:theme="@style/DualToneDarkTheme"
android:id="@+id/no_sims_dark"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/stat_sys_no_sims"
android:alpha="0.0"
/>
</FrameLayout>
<View
android:id="@+id/wifi_airplane_spacer" android:layout_width="@dimen/status_bar_airplane_spacer_width"
android:layout_height="4dp"
android:visibility="gone"
/>
<ImageView
android:id="@+id/airplane"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
/>
</com.android.systemui.statusbar.SignalClusterView>
基本佈局文件差不多介紹完了,下面畫了個簡單的圖總結下
下面介紹完了佈局,就來看看這些佈局是在那兒被加載的,以及電池電量、和時間圖標的具體顯示流程
status_bar.xml文件是在super_status_bar.xml裏被include的,而super_status_bar.xml的加載是在PhoneStatusBar.java裏的makeStatusBarView中初始化的,如下
//frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
// ================================================================================
// Constructing the view
// ================================================================================
protected PhoneStatusBarView makeStatusBarView() {
...
inflateStatusBarWindow(context);
mStatusBarWindow.setService(this);
mStatusBarWindow.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
checkUserAutohide(v, event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (mExpandedVisible) {
animateCollapsePanels();
}
}
return mStatusBarWindow.onTouchEvent(event);
}
});
mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById(
R.id.notification_panel);
mNotificationPanel.setStatusBar(this);
mNotificationPanel.setGroupManager(mGroupManager);
mStatusBarView = (PhoneStatusBarView) mStatusBarWindow.findViewById(R.id.status_bar);
mStatusBarView.setBar(this);
mStatusBarView.setPanel(mNotificationPanel);
// set the initial view visibility
setAreThereNotifications();
mBatteryController = createBatteryController();
mBatteryController.addStateChangedCallback(new BatteryStateChangeCallback() {
@Override
public void onPowerSaveChanged(boolean isPowerSave) {
mHandler.post(mCheckBarModes);
if (mDozeServiceHost != null) {
mDozeServiceHost.firePowerSaveChanged(isPowerSave);
}
}
@Override
public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
// noop
}
});
mNetworkController = new NetworkControllerImpl(mContext, mHandlerThread.getLooper());
mNetworkController.setUserSetupComplete(mUserSetup);
mHotspotController = new HotspotControllerImpl(mContext);
mBluetoothController = new BluetoothControllerImpl(mContext, mHandlerThread.getLooper());
mSecurityController = new SecurityControllerImpl(mContext);
if (mContext.getResources().getBoolean(R.bool.config_showRotationLock)) {
mRotationLockController = new RotationLockControllerImpl(mContext);
}
initSignalCluster(mStatusBarView);
initSignalCluster(mKeyguardStatusBar);
initEmergencyCryptkeeperText();
mCarrierLabel = (TextView)mStatusBarWindow.findViewById(R.id.carrier_label);
...
((BatteryMeterView) mStatusBarView.findViewById(R.id.battery)).setBatteryController(
mBatteryController);
mKeyguardStatusBar.setBatteryController(mBatteryController);
...
return mStatusBarView;
}
基本上狀態裏顯示的入口就在這,下面主要看看電池電量、和時間圖標,從前面佈局文件可知,顯示時間對應的view爲Clock.java,電池電量的爲BatteryMeterView.java
時間的更新主要是通過監聽‘android.intent.action.TIME_TICK’廣播
//frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
public class Clock extends TextView implements DemoMode, Tunable {
...
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!mAttached) {
mAttached = true;
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIME_TICK);//監聽廣播
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
filter.addAction(Intent.ACTION_USER_SWITCHED);
getContext().registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter,
null, getHandler());
TunerService.get(getContext()).addTunable(this, CLOCK_SECONDS,
StatusBarIconController.ICON_BLACKLIST);
}
// NOTE: It's safe to do these after registering the receiver since the receiver always runs
// in the main thread, therefore the receiver can't run before this method returns.
// The time zone may have changed while the receiver wasn't registered, so update the Time
mCalendar = Calendar.getInstance(TimeZone.getDefault());
// Make sure we update to the current time
updateClock();
updateShowSeconds();
}
...
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_TIMEZONE_CHANGED)) {
String tz = intent.getStringExtra("time-zone");
mCalendar = Calendar.getInstance(TimeZone.getTimeZone(tz));
if (mClockFormat != null) {
mClockFormat.setTimeZone(mCalendar.getTimeZone());
}
} else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
final Locale newLocale = getResources().getConfiguration().locale;
if (! newLocale.equals(mLocale)) {
mLocale = newLocale;
mClockFormatString = ""; // force refresh
}
}
updateClock();//更新時間
}
};
final void updateClock() {
if (mDemoMode) return;
mCalendar.setTimeInMillis(System.currentTimeMillis());
setText(getSmallTime());
setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime()));
}
//格式化時間
private final CharSequence getSmallTime() {
Context context = getContext();
boolean is24 = DateFormat.is24HourFormat(context, ActivityManager.getCurrentUser());
LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
final char MAGIC1 = '\uEF00';
final char MAGIC2 = '\uEF01';
SimpleDateFormat sdf;
String format = mShowSeconds
? is24 ? d.timeFormat_Hms : d.timeFormat_hms
: is24 ? d.timeFormat_Hm : d.timeFormat_hm;
if (!format.equals(mClockFormatString)) {
mContentDescriptionFormat = new SimpleDateFormat(format);
/*
* Search for an unquoted "a" in the format string, so we can
* add dummy characters around it to let us find it again after
* formatting and change its size.
*/
if (mAmPmStyle != AM_PM_STYLE_NORMAL) {
int a = -1;
boolean quoted = false;
for (int i = 0; i < format.length(); i++) {
char c = format.charAt(i);
if (c == '\'') {
quoted = !quoted;
}
if (!quoted && c == 'a') {
a = i;
break;
}
}
if (a >= 0) {
// Move a back so any whitespace before AM/PM is also in the alternate size.
final int b = a;
while (a > 0 && Character.isWhitespace(format.charAt(a-1))) {
a--;
}
format = format.substring(0, a) + MAGIC1 + format.substring(a, b)
+ "a" + MAGIC2 + format.substring(b + 1);
}
}
mClockFormat = sdf = new SimpleDateFormat(format);
mClockFormatString = format;
} else {
sdf = mClockFormat;
}
String result = sdf.format(mCalendar.getTime());
if (mAmPmStyle != AM_PM_STYLE_NORMAL) {
int magic1 = result.indexOf(MAGIC1);
int magic2 = result.indexOf(MAGIC2);
if (magic1 >= 0 && magic2 > magic1) {
SpannableStringBuilder formatted = new SpannableStringBuilder(result);
if (mAmPmStyle == AM_PM_STYLE_GONE) {
formatted.delete(magic1, magic2+1);
} else {
if (mAmPmStyle == AM_PM_STYLE_SMALL) {
CharacterStyle style = new RelativeSizeSpan(0.7f);
formatted.setSpan(style, magic1, magic2,
Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
}
formatted.delete(magic2, magic2 + 1);
formatted.delete(magic1, magic1 + 1);
}
return formatted;
}
}
...
return result;
}
}
電池電量圖標這個和時鐘差不多,主要是BatteryMeterView(ImageView的子類)和BatteryMeterDrawable(Drawable的子類)這兩個類,其中BatteryMeterView負責view顯示,BatteryMeterDrawable負責圖標內容更新,並且這兩個類都實現了BatteryController.BatteryStateChangeCallback這個接口
public interface BatteryController extends DemoMode {
...
/**
* A listener that will be notified whenever a change in battery level or power save mode
* has occurred.
*/
interface BatteryStateChangeCallback {
void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging);
void onPowerSaveChanged(boolean isPowerSave);
}
}
frameworks/base/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java
public class BatteryMeterDrawable extends Drawable implements
BatteryController.BatteryStateChangeCallback {
...
@Override
public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
mLevel = level;
mPluggedIn = pluggedIn;
postInvalidate();//更新圖標
}
@Override
public void draw(Canvas c) {
...
}
}
onBatteryLevelChanged是在BatteryControllerImpl.java裏被調用的
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
public class BatteryControllerImpl extends BroadcastReceiver implements BatteryController {
...
public BatteryControllerImpl(Context context) {
mContext = context;
mHandler = new Handler();
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
registerReceiver();
updatePowerSave();
}
private void registerReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED); //註冊電量改變的廣播
filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING);
filter.addAction(ACTION_LEVEL_TEST);
mContext.registerReceiver(this, filter);
}
@Override
public void onReceive(final Context context, Intent intent) {
final String action = intent.getAction();
if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
if (mTestmode && !intent.getBooleanExtra("testmode", false)) return;
...
fireBatteryLevelChanged();
}
}
...
protected void fireBatteryLevelChanged() {
synchronized (mChangeCallbacks) {
final int N = mChangeCallbacks.size();
for (int i = 0; i < N; i++) {
mChangeCallbacks.get(i).onBatteryLevelChanged(mLevel, mPluggedIn, mCharging);
}
}
}
...
}
後續會對信號格圖標顯示、通知圖標、系統圖標都單獨分析介紹,如有錯誤,麻煩留言指出。