<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
專案中為了適應產品形態需要對Android系統狀態列系統圖示以及時鐘和電池等做客製化,滿足不同使用者群體的視覺特性,那在客製化過程中需要注意哪些事項?圖示icon是否可以任意大小?狀態列多顏色模式下圖示如何適配?複雜狀態圖示如何調整邏輯?
首先來看下狀態列載體是什麼?狀態列本質其實就是一個懸浮窗,在systemui初始化時建立顯示。SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
protected void inflateStatusBarWindow(Context context) { mStatusBarWindow = (StatusBarWindowView) mInjectionInflater.injectable( LayoutInflater.from(context)).inflate(R.layout.super_status_bar, null); }
由上可知狀態列就是使用super_status_bar.xml佈局建立的一個懸浮窗。而這個佈局包含了狀態列所有內容,應用通知,系統圖示,時鐘等。其主體內容如下
<com.android.systemui.statusbar.phone.StatusBarWindowView ... <FrameLayout android:id="@+id/status_bar_container" android:layout_width="match_parent" android:layout_height="wrap_content" /> ... </com.android.systemui.statusbar.phone.StatusBarWindowView>
其中包含status_bar_container 的framelayout的容器即為狀態列的view,在程式碼中通過fragmentmanager替換了了這個container。
protected void makeStatusBarView(@Nullable RegisterStatusBarResult result) { ... FragmentHostManager.get(mStatusBarWindow) .addTagListener(...).getFragmentManager() .beginTransaction() .replace(R.id.status_bar_container, new CollapsedStatusBarFragment(), CollapsedStatusBarFragment.TAG) .commit();
而CollapsedStatusBarFragment的實現就是載入了status_bar.xml 這個佈局。
@Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.status_bar, container, false); }
status_bar.xml 佈局內容就是顯示出來的狀態列布局。這樣狀態列整體佈局就比較清晰,包含了應用通知,系統圖示, 時鐘,電池等。
<com.android.systemui.statusbar.phone.PhoneStatusBarView ... android:layout_height="@dimen/status_bar_height" android:id="@+id/status_bar" ... > ... <LinearLayout android:id="@+id/status_bar_contents" ... <!-- 左側顯示區域 整體權重只佔了1--> <FrameLayout android:layout_height="match_parent" android:layout_width="0dp" android:layout_weight="1"> ... <LinearLayout android:id="@+id/status_bar_left_side" ... > <!-- 時鐘 --> <com.android.systemui.statusbar.policy.Clock android:id="@+id/clock" ... android:textAppearance="@style/TextAppearance.StatusBar.Clock" /> <!-- 應用通知icon區域 --> <com.android.systemui.statusbar.AlphaOptimizedFrameLayout android:id="@+id/notification_icon_area" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:orientation="horizontal" android:clipChildren="false"/> </LinearLayout> </FrameLayout> ... <!-- 中間icon顯示區域 --> <com.android.systemui.statusbar.AlphaOptimizedFrameLayout android:id="@+id/centered_icon_area" android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal" android:clipChildren="false" android:gravity="center_horizontal|center_vertical"/> <!-- 系統icon顯示區域--> <com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/system_icon_area" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:orientation="horizontal" android:gravity="center_vertical|end" > <!-- 系統icon實際顯示佈局 --> <include layout="@layout/system_icons" /> </com.android.keyguard.AlphaOptimizedLinearLayout> </LinearLayout> ... </com.android.systemui.statusbar.phone.PhoneStatusBarView>
系統icon區域 system_icons.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:systemui="http://schemas.android.com/apk/res-auto" android:id="@+id/system_icons" ...> <com.android.systemui.statusbar.phone.StatusIconContainer android:id="@+id/statusIcons" android:layout_width="0dp" android:layout_weight="1" .../> <com.android.systemui.statusbar.phone.seewo.BatteryImageView android:id="@+id/battery" .../> </LinearLayout>
整個狀態列整體佈局示意如下:
其中我們需要客製化的從UI設計稿中可以看出,是三個區域,時鐘, 系統icon,電池, 應用通知在這個專案中不需要,可以直接去掉通知資訊功能,就不會顯示出來。clock和battery都是自定義控制元件,比較好處理。重點看下系統icon實現。
由上客制系統圖示區域包含一個statusIcons 的容器view,還有battery 顯示view。
其佈局也是自定義view, StatusIconContainer.java
<com.android.systemui.statusbar.phone.StatusIconContainer android:id="@+id/statusIcons" android:layout_width="0dp" .../>
其實現是基於AlphaOptimizedLinearLayout佈局實現的一個自定義佈局。AlphaOptimizedLinearLayout是繼承自LinearLayout只是覆蓋了
public boolean hasOverlappingRendering() { return false; }
該方法用來標記當前view是否存在過度繪製,存在返回ture,不存在返回false,預設返回為true。 在android的View裡有透明度的屬性,當設定透明度setAlpha的時候,android裡預設會把當前view繪製到offscreen buffer中,然後再顯示出來。 這個offscreen buffer 可以理解為一個臨時緩衝區,把當前View放進來並做透明度的轉化,然後在顯示到螢幕上。這個過程是消耗資源的,所以應該儘量避免這個過程。而當繼承了hasOverlappingRendering()方法返回false後,android會自動進行合理的優化,避免使用offscreen buffer。
系統icon繪製流程會比較多。 先從頂層view StatusIconContainer的繪製來分析。view的繪製離不開三個步驟,onMeasure, onLayout, onDraw,現在來一一拆解檢視。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 獲取到所有需要展示的view,注意看不可見的,icon處於blocked狀態的, // 還有需要忽略的都不會被加入mMeasureViews中 for (int i = 0; i < count; i++) { StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i); if (icon.isIconVisible() && !icon.isIconBlocked() && !mIgnoredSlots.contains(icon.getSlot())) { mMeasureViews.add((View) icon); } } int visibleCount = mMeasureViews.size(); // 計算最大可見的icon數量,預設為7 int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; int totalWidth = mPaddingLeft + mPaddingRight; boolean trackWidth = true; int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED); mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS; for (int i = 0; i < mMeasureViews.size(); i++) { // Walking backwards View child = mMeasureViews.get(visibleCount - i - 1); //測量每個childview的寬 measureChild(child, childWidthSpec, heightMeasureSpec); if (mShouldRestrictIcons) { // 計算總的寬度 if (i < maxVisible && trackWidth) { totalWidth += getViewTotalMeasuredWidth(child); } else if (trackWidth) { // 超過最大可見數量時 需要給省略點計算空間。 totalWidth += mUnderflowWidth; trackWidth = false; } } else { totalWidth += getViewTotalMeasuredWidth(child); } } // 通過setMeasuredDimension設定view的寬高 if (mode == MeasureSpec.EXACTLY) { ... setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec)); } else { ... setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec)); } }
從上面可以看出來, onMeaure主要時計算每個子view的寬高,並計算出父view的整的寬度,其中會給超過最大數量的情況下 計算省略點的寬度,可以視專案情況來決定這個省略點的數量,其可在程式碼中通過常數來自定義。
protected void onLayout(boolean changed, int l, int t, int r, int b) { float midY = getHeight() / 2.0f; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); int top = (int) (midY - height / 2.0f); child.layout(0, top, width, top + height); } // 重置每個view的狀態。通過StatusIconState重置狀態 resetViewStates(); // 重新依據實際情況計算每個icon的顯示狀態,下面單獨拎出來講。 calculateIconTranslations(); // 應用view的狀態,包含icon顯示的動畫。 applyIconStates(); }
onLayou常規是計算每個view的寬高,並按預定的規則排放,然後計算每個view的位置。calculateIconTranslations顯示邏輯會比較多,單獨拎出來講:
private void calculateIconTranslations() { mLayoutStates.clear(); ... // for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); StatusIconDisplayable iconView = (StatusIconDisplayable) child; StatusIconState childState = getViewStateFromChild(child); if (!iconView.isIconVisible() || iconView.isIconBlocked() || mIgnoredSlots.contains(iconView.getSlot())) { childState.visibleState = STATE_HIDDEN; if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible"); continue; } childState.visibleState = STATE_ICON; // 位置顯示的關鍵點, translationX 初始值是整個view的寬度,這樣計算每個view // 的實際佈局位置 childState.xTranslation = translationX - getViewTotalWidth(child); mLayoutStates.add(0, childState); translationX -= getViewTotalWidth(child); } // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow int totalVisible = mLayoutStates.size(); int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; mUnderflowStart = 0; int visible = 0; int firstUnderflowIndex = -1; for (int i = totalVisible - 1; i >= 0; i--) { StatusIconState state = mLayoutStates.get(i); // Allow room for underflow if we found we need it in onMeasure // 這裡比較關鍵 從列表中逆序獲取到每個view的位置,如果view的xTranslation 下雨 // 小於顯示的內容就停止,後續就從這個index開始繪製 if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))|| (mShouldRestrictIcons && visible >= maxVisible)) { firstUnderflowIndex = i; break; } mUnderflowStart = (int) Math.max(contentStart, state.xTranslation - mUnderflowWidth); visible++; } //後續邏輯就是設定是否顯示icon和顯示多少個dot ... }
onLayout邏輯較多,簡單來說就是通過每個子view的xTranslation和整體的view空間,計算需要顯示多少icon,同時要給省略點預留空間。簡單示意如下。可能超過空間的就用dot來顯示。
這塊沒有客製化處理,只是做了debug的一些資訊繪製.
至此係統icon的頂層view分析完成,其主要是通過子view的狀態以及父view的空間等情況來決定是否需要顯示哪些icon,以及顯示省略點符號。接下來再看每個子view的情況。
子view就是顯示狀態列上icon,但是其封裝了一層繼承自AnimatedImageView,帶動畫效果的ImageView。
子view的具體實現 StatusBarIconView
SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconView.java
挑其中重點解析。圖片的縮放,怎麼把任意圖片的大小適合在狀態列顯示。
private void updateIconScaleForSystemIcons() { float iconHeight = getIconHeight(); if (iconHeight != 0) { mIconScale = mSystemIconDesiredHeight / iconHeight; } else { mIconScale = mSystemIconDefaultScale; } }
先獲取到mIconScale需要縮放的比例,mSystemIconDesiredHeight 是設定的全域性的system icon的大小。
mSystemIconDesiredHeight = res.getDimension( com.android.internal.R.dimen.status_bar_system_icon_size);
存在icon的情況下,通過獲取實際的icon的大小, 計算出 mIconScale.
在onDraw的時候 通過canvas.scale 把畫布以icon的中心點根據mIconScale縮放到system_icon_size. 但是這樣存在一個問題,icon的實際大小還是原大小,只是顯示小了。其它部分包含動畫就不再細講。
@Override protected void onDraw(Canvas canvas) { if (mIconAppearAmount > 0.0f) { canvas.save(); canvas.scale(mIconScale * mIconAppearAmount, mIconScale * mIconAppearAmount, getWidth() / 2, getHeight() / 2); super.onDraw(canvas); canvas.restore(); } ... }
到這裡狀態列布局以及系統圖示的view繪製大體分析完成。 接下來看icon是怎麼控制新增,刪除以及更新的。
狀態列圖示顯示邏輯是通過 StatusBarIconControllerImpl 這個類來實現管理, 在物件構造的時候預設初始化
public StatusBarIconControllerImpl(Context context) { super(context.getResources().getStringArray( com.android.internal.R.array.config_statusBarIcons)); ... }
config_statusBarIcons 這個array中包含了所有支援的icon。如有需要客製化圖示順序可在這個列表中對圖示對應的item進行調整。
<string-array name="config_statusBarIcons"> <item><xliff:g id="id">@string/status_bar_alarm_clock</xliff:g></item> ... <item><xliff:g id="id">@string/status_bar_battery</xliff:g></item> <item><xliff:g id="id">@string/status_bar_sensors_off</xliff:g></item> </string-array>
這些 icon字串資訊當做一個title資訊,儲存在mSlots列表中。而Slot中包含StatusBarIconHolder:
public static class Slot { private final String mName; private StatusBarIconHolder mHolder; ... }
public class StatusBarIconHolder { public static final int TYPE_ICON = 0; public static final int TYPE_WIFI = 1; public static final int TYPE_MOBILE = 2; private StatusBarIcon mIcon; private WifiIconState mWifiState; private MobileIconState mMobileState; ... }
啟動mIcon即為顯示的圖示資源儲存類。其中包含了圖示顯示狀態,標籤資訊以及狀態資訊等。將其都儲存在mSlogs的列表中,方便管理顯示。
總結資料儲存鏈條 Slots--> StatusBarIconHolder --> StatusBarIcon;
在狀態列初始化的時候 CollapsedStatusBarFragment 的view建立中 onViewCreated對系統icon管理進行初始化。
mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons)); mDarkIconManager.setShouldLog(true); Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager);
DarkIconManager 建構函式中傳入了system_icon的容器viewgroup, 負責view的增加和刪除。StatusBarIconController管理DarkIconManager. 這樣顯示圖示區域控制部分與顯示部分關聯起來。
控制管理的實現策略類都在 PhoneStatusBarPolicy 這個裡面實現。 具體實現通過StatusBarIconControllerImpl類實現,可以通過如下介面更新顯示圖示。
mIconController.setIcon(mSlotVolume, volumeIconId, volumeDescription); mIconController.setIconVisibility(mSlotVolume, volumeVisible);
以音量更新為例。setIcon流程分析。
@Override public void setIcon(String slot, int resourceId, CharSequence contentDescription) { // 檢查是否在列表中存在holder,預設初始情況下是都沒有holder的,需要新建 int index = getSlotIndex(slot); StatusBarIconHolder holder = getIcon(index, 0); if (holder == null) { 先通過resoureid和 contentDescription建立一個StatusBarIcon範例 StatusBarIcon icon = new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(), Icon.createWithResource( mContext, resourceId), 0, 0, contentDescription); // 通過icon封裝一個holder。 holder = StatusBarIconHolder.fromIcon(icon); // 將holder賦值給mSlots setIcon(index, holder); } else { holder.getIcon().icon = Icon.createWithResource(mContext, resourceId); holder.getIcon().contentDescription = contentDescription; handleSet(index, holder); } }
傳入slot為icon的title, resourceId為資原始檔,contentDescription為描述字串。如果判斷為沒有holder就會新建一個holder類,並傳入mSlots的列表中。
@Override public void setIcon(int index, @NonNull StatusBarIconHolder holder) { boolean isNew = getIcon(index, holder.getTag()) == null; super.setIcon(index, holder); if (isNew) { // 通過tag判斷如果是新的就加到systemicon中 addSystemIcon(index, holder); } else { //已經存在的直接設定 handleSet(index, holder); } }
private void addSystemIcon(int index, StatusBarIconHolder holder) { String slot = getSlotName(index); int viewIndex = getViewIndex(index, holder.getTag()); boolean blocked = mIconBlacklist.contains(slot); mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, blocked, holder)); }
onIconAdded是在DarkIconManager中實現。
protected void onIconAdded(int index, String slot, boolean blocked, StatusBarIconHolder holder) { addHolder(index, slot, blocked, holder); }
protected StatusIconDisplayable addHolder(int index, String slot, boolean blocked, StatusBarIconHolder holder) { switch (holder.getType()) { case TYPE_ICON: return addIcon(index, slot, blocked, holder.getIcon()); case TYPE_WIFI: return addSignalIcon(index, slot, holder.getWifiState()); case TYPE_MOBILE: return addMobileIcon(index, slot, holder.getMobileState()); } return null; }
protected StatusBarIconView addIcon(int index, String slot, boolean blocked, StatusBarIcon icon) { StatusBarIconView view = onCreateStatusBarIconView(slot, blocked); view.set(icon); //mGroup 即為狀態列系統圖示的容器view。這裡就完成了view的新增 mGroup.addView(view, index, onCreateLayoutParams()); return view; }
這樣就通過setIcon把圖示新增到了系統圖示區,然後再通過setIconVisibility顯示出圖示。顯示的邏輯和setIcon差不多,只是增加了visible狀態,可以自行分析。更新過程中有兩個特殊的圖示,wifi和資料網路,其狀態會包含多個,正常都是隻有顯示與否邏輯,所以這裡邏輯會多一些,但是原理一樣的。
至此就完成了整個系統圖示顯示控制分析。
數量和順序 通過設定 config_statusBarIcons 增刪自己需要的圖示。
狀態列大小客製化 framework/base/core/res/res/values/dimens.xml
設定項 | 說明 |
---|---|
status_bar_height_portrait | 狀態列高度 |
status_bar_system_icon_intrinsic_size | 系統圖示期望大小, 用於icon的縮放,和icon_size設定一樣大小即可 |
status_bar_system_icon_size | 系統圖示大小 |
SystemUI/res/values/dimens.xml
設定項 | 說明 |
---|---|
status_bar_padding_start | 狀態列離左側空間 |
status_bar_padding_end | 狀態列離右側空間 |
signal_cluster_battery_padding | 系統圖示離電池圖示距離 |
圖示顯示客製化 通過上述分析程式碼 setIcon找到對應的icon進行替換自己專案的icon, StatusIconContainer中需要修改MAX_DOTS為0,不顯示省略點。再onLayout的時候需要根據專案設定icon的間距, child.layout中增加r值。
顯示邏輯策略都在PhoneStatusBarPhicy中實現,尤其是系統原生沒有支援的圖示邏輯會客製化較多。
圖示選擇,使用新的svg圖示時, 寬高最好和系統system_icon_size設定為一致,原生邏輯會把圖示縮放到高度和system_icon一致,倒是寬度卻保持了原有圖示寬,導致顯示佈局不對。
以上就是Android系統狀態列客製化圖示顯示邏輯控制的詳細內容,更多關於Android 狀態列圖示的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45