1.NavigationBar定義
咱們使用的大多數android 手機上的Home 鍵,返回鍵以及menu 鍵都是實體觸摸感應按鍵。若是你用Google 的Nexus4 或Nexus5 話,你會發現它們並無實體按鍵或觸摸感應按鍵,取而代之的是在屏幕的下方加了一個小黑條,在這個黑條上有3 個按鈕控件,這種設置無疑使得手機的外觀的設計更加簡約。但我遇到身邊用Nexus 4 手機的人都吐槽這種設計,緣由很簡單:好端端的屏幕,被劃出一塊區域用來顯示3 個按鈕:Back, Home, Recent 。而且它一直用在那裏佔用着。 java
在android 源碼中,那一塊區域被叫作NavigationBar 。同時,google 在代碼中也預留了標誌,用來控制它的顯示與隱藏。NavigationBar 的顯示與隱藏的控制是放在SystemUI 中的,具體的路徑是:\frameworks\base\packages\SystemUI 。對android4.0 以上的手機而言, SystemUi 包含兩部分:StatusBar 和NavigationBar 。在SystemUI 的工程下有一個類PhoneStatusBar.java ,在該類中能夠發現關於控制NavigationBar 的相關代碼: android
在start() 方法裏能夠看到NavigationBar 是在那時候被添加進來,但只是添加,決定它顯示仍是隱藏是在後面控制的。@Override public void start() { mDisplay = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); updateDisplaySize(); /// M: Support Smartbook Feature. if (SIMHelper.isMediatekSmartBookSupport()) { /// M: [ALPS01097705] Query the plug-in state as soon as possible. mIsDisplayDevice = SIMHelper.isSmartBookPluggedIn(mContext); Log.v(TAG, "start, mIsDisplayDevice=" + mIsDisplayDevice); } super.start(); // calls createAndAdd Windows() function。 addNavigationBar(); // Lastly, call to the icon policy to install/update all the icons. mIconPolicy = new PhoneStatusBarPolicy(mContext); mHeadsUpObserver.onChange(true); // set up if (ENABLE_HEADS_UP) { mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(SETTING_HEADS_UP), true, mHeadsUpObserver); } }
其中的addNavigationBar() 具體的實現方法以下:
// For small-screen devices (read: phones) that lack hardware navigation buttons private void addNavigationBar() { if (DEBUG) Slog.v(TAG, "addNavigationBar: about to add " + mNavigationBarView); if (mNavigationBarView == null) return; //表示在Frame沒有時不進行下一步操做。 prepareNavigationBarView(); mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams()); }
能夠看到Navigationbar 其實是windowmanager 向window 窗口裏添加一個view 。在調用addNavigationBar() 方法以前會 回調start() 的父方法super.start() 來判斷是否要添加NavigationBar 。在super.start() 的調用父類方法裏會調用createAndAddWindows() ,該方法內會判斷是否須要添加顯示NavigationBar, 而後決定是否要實例化NavigationBarView.
try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Slog.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { mNavigationBarView = (NavigationBarView) View.inflate(context, R.layout.navigation_bar, null); mNavigationBarView.setDisabledFlags(mDisabled); mNavigationBarView.setBar(this); } } catch (RemoteException ex) { // no window manager? good luck with that }
@Override public boolean hasNavigationBar() { return mPolicy.hasNavigationBar(); }Policy 向下調用實際上調用的是PhoneWindowManager 實現的hasNavigationBar 方法,下面代碼是PhoneWindowManager 中的 hasNavigationBar() 方法。
// Use this instead of checking config_showNavigationBar so that it can be consistently // overridden by qemu.hw.mainkeys in the emulator. public boolean hasNavigationBar() { return mHasNavigationBar; }
if (!mHasSystemNavBar) { mHasNavigationBar = mContext.getResources().getBoolean( com.android.internal.R.bool.config_showNavigationBar); // Allow a system property to override this. Used by the emulator. // See also hasNavigationBar(). String navBarOverride = SystemProperties.get("qemu.hw.mainkeys"); if (! "".equals(navBarOverride)) { if (navBarOverride.equals("1")) mHasNavigationBar = false; else if (navBarOverride.equals("0")) mHasNavigationBar = true; } } else { mHasNavigationBar = false; }
從上面代碼能夠看到 mHasNavigationBar 的值的設定 是由兩處決定的:
1. 首先從系統的資源文件中取設定值config_showNavigationBar, 這個值的設定的文件路徑是 frameworks/base/core/res/res/values/config.xml
<!-- Whether a software navigation bar should be shown. NOTE: in the future this may be
autodetected from the Configuration. -->
<bool name="config_showNavigationBar">false</bool>
2. 而後系統要獲取「 qemu.hw.mainkeys」 的值,這個值可能會覆蓋上面獲取到的mHasNavigationBar 的值。若是 「qemu.hw.mainkeys」 獲取的值不爲空的話,無論值是true 仍是false, 都要依據後面的狀況來設定。
PhoneWindowManager.java中: app
String navBarOverride = SystemProperties.get("qemu.hw.mainkeys"); ide
該配置項所在目錄通常在:/system/ build.prop中。
因此上面的兩處設定共同決定了NavigationBar的顯示與隱藏。 函數
2.顯示與隱藏
如今要說的顯示與隱藏,並非指在開機的時候,這能夠在 xml 中設置,如上,不詳述。NavigationBar能夠在開機後根據須要顯示或隱藏,好比在打開某個應用隱藏,打開另外一應用顯示。 學習
修改步驟: ui
1) ActivityStack.java中的 resumeTopActivityLocked 是全部啓動應用的啓動的入口,因此在這裏添加進入的入口。 this
2) 在PhoneWindowManager.java中的 mHasNavigationBar 是顯示與否的標誌,確定要修改,而這裏的修改應該在WindowManagerService.java中進行,由於,WindowManagerService中的mPolicy是操做PhoneWindowManager的接口,這樣不會破壞封裝,因此1)中要添加調入到WMS中的接口,WMS而後再調入PWM。 google
3) PWM(PhoneWindowManager)中有mStatusBarService,之因此用這個服務,是由於不破壞封裝和同步。 spa
4) StatusBarManagerService 中添加顯示消失的接口,同理在Client端也要添加相應的顯示和消失接口,具體在 CommandQueue和PhoneStatusBar中。
5) PhoneStatusBar中添加顯示和消失的邏輯。
public void showNavigationBar() { Xlog.d(TAG, " showNavigationBar "); if (mNavigationBarView == null) { try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Slog.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { mNavigationBarView = (NavigationBarView) View.inflate(mContext, R.layout.navigation_bar, null); mNavigationBarView.setDisabledFlags(mDisabled); mNavigationBarView.setBar(this); } mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams()); } catch (RemoteException ex) { // no window manager? good luck with that } } } public void hideNavigationBar() { Xlog.d(TAG, " hideNavigationBar "); if ( mNavigationBarView != null) { mWindowManager.removeView(mNavigationBarView); } mNavigationBarView = null; }
3.Launcher與應用之間切換
1
) 對上面的方式進行總結
從AMS--> WMS-->PMS --> StatusbarManagerService--> CommandQueue(callback) -->
PhoneStatusBar
也許有人會說,這樣的調用很繁瑣,爲啥不用廣播呢?
緣由很直接:廣播顯然是在不一樣的線程裏面,這樣作不能保證窗口同步刷新,layout之後的後果未知。
2) 修改固然也會有問題,問題的發生在Launcher和應用切換間。Navigationbar的添加與否會影響Configuration的變化,這裏的Configuration不單包括oritation的變化(其實這裏沒有oritation的變化),在實際中自寫Launcher沒有NV bar,android的原生應用有NV bar,發現一個問題,在從MMs返回自寫Launcher時,會從新調用Launcher的onCreate函數,致使本來進入了GridView的,結果停留在Home的壁紙界面;而其它的應用如計算器就會直接進如gridview頁面。問題的緣由就是因爲應用的fullscreen和Launcher 的Configuration變化引發的。
a. 解釋下MMs和計算器的區別,實際上就是fullscreen的區別,一般來講和透明度相關
詳細參見ActivityRecord()@ActivityRecord.java
MMs: fullscreen == false
Caculator: fullscreen == true
b. 解釋下上面問題的緣由,想象一下這樣的情景,從GridView進入MMS中,
1)Launcher(fullscreen) Paused
2)launch MMs, Resumed
3)因爲MMs不是fullscreen,因此在ensureActivitiesVisibleLocked中會去檢查當前的top應用是否全屏,若是不是全屏,則會把下邊的Acitivity show出來,此刻,下面的Activity爲Launcher,並且Launcher一直是啓動的,因此這裏調用relaunchActivityLocked,這會致使從新的onCreate:
java.lang.Exceptioncom.android.server.am.ActivityStack.relaunchActivityLocked(ActivityStack.java:5374) :at com.android.server.am.ActivityStack.ensureActivityConfigurationLocked(ActivityStack.java:5339) :at com.android.server.am.ActivityStack.ensureActivitiesVisibleLocked(ActivityStack.java:1607) :at com.android.server.am.ActivityStack.ensureActivitiesVisibleLocked(ActivityStack.java:1727) :at com.android.server.am.ActivityStack.completeResumeLocked(ActivityStack.java:1538) :at com.android.server.am.ActivityStack.realStartActivityLocked(ActivityStack.java:865) :at com.android.server.am.ActivityManagerService.attachApplicationLocked(ActivityManagerService.java:4958) :at com.android.server.am.ActivityManagerService.attachApplication(ActivityManagerService.java:5027) :at android.app.ActivityManagerNative.onTransact(ActivityManagerNative.java:387) :at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:1908) :at android.os.Binder.execTransact(Binder.java:351) :at dalvik.system.NativeStart.run(Native Method)
4) 正常狀況下,就算對Launcher調用了ensureActivityConfigurationLocked
也不會刷新屏幕,從而進入launcher的onCeate流程,緣由在下面就返回了:
if (r.configuration == newConfig && !r.forceNewConfig) { if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG, "Configuration unchanged in " + r); return true; }
可是這裏Navigation bar的從有到無必然會致使前面所說的Configuration的變化,Configuration的變化以下:
ensureActivityConfigurationLocked@ActivityStack.java newConfig = {1.0 ?mcc?mnc en_USldltr sw360dp w360dp h567dp 320dpi nrml port finger -keyb/v/h -nav/hskin=/system/framework/framework-res.apk s.9}, r.configuration = {1.0?mcc?mnc en_US ldltr sw360dp w360dp h615dp 320dpi nrml long portfinger -keyb/v/h -nav/h skin=/system/framework/framework-res.apks.8}, r.forceNewConfig = false
細心的讀者可能發現屏幕的高度h567dp 和 h615dp 發生了變化,差異就是NavigationBar的高度,因爲這樣緣由,這下真的就要調用relaunchActivityLocked來刷新後面的Activity了(Launcher)
問題的緣由已經分析出來,修改也就簡單了
final boolean ensureActivityConfigurationLocked(ActivityRecord r, int globalChanges) { if ((changes&(~r.info.getRealConfigChanged())) != 0 || r.forceNewConfig) { if ((r.packageName.contains("com.your.packagename") && (changes == 0x500 || changes == 0x580))) { // cancel relauncher ifyour package r.configChangeFlags = 0; //r.stopFreezingScreenLocked(false); return true; } .... } }
4.系統重啓Launcher界面顯示Nv Bar
在實際的應用中,發現沒有Navigation Bar的橫屏Launcher在從新啓動後的第一次顯示,會把NavigationBar顯示出來,這裏解釋一下爲何會出現此現象,問題發生的緣由其實和PhoneWindowManager(PWM)中的mStatusBarService的值有關,在某些時候,mStatusBarService 可能爲null,StatusBarManagerService 依賴WMS(參見SystemServer.java), WMS中才能調用PWM,而hideNavigation Bar又是從PWM中調入,必然在中間PWM中mStatusBarSerivice有時刻爲空,致使會先顯示navigation bar而後再消失。
解決方案:
1) 取消默認的navigation bar顯示 【在config.xml 中取消】
2) PWM中判斷mStatusBarService是否爲空的邏輯
public void showNavigationBar() { Slog.d(TAG, " PWM showNavigationBar xxxx hasNavigationBar = " + hasNavigationBar()); if (!hasNavigationBar()) { //TODO: need open it if (mStatusBarService != null) { mHasNavigationBar = true; // wait for mStatusBarService prepared try { Slog.d(TAG, " PWM showNavigationBar mStatusBarService xxxx"); mStatusBarService.showNavigationBar(); } catch (RemoteException e) { // oh well } } } }
3) 在第一次啓動的時候在PhoneStatusBar中添加Navigation bar的調用邏輯
public void showNavigationBar() { Xlog.d(TAG, " showNavigationBar "); if (mFirstBoot) { if (mNavigationBarView == null) { try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Slog.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { mNavigationBarView = (NavigationBarView) View.inflate(mContext, R.layout.navigation_bar, null); mNavigationBarView.setDisabledFlags(mDisabled); mNavigationBarView.setBar(this); } } catch (RemoteException ex) { // no window manager? good luck with that Xlog.e(TAG, " RemoteException error happened, [can't find WindowManager]"); return; } } addNavigationBar(); mFirstBoot = false; return; } if (mNavigationBarView == null) { try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Slog.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { mNavigationBarView = (NavigationBarView) View.inflate(mContext, R.layout.navigation_bar, null); mNavigationBarView.setDisabledFlags(mDisabled); mNavigationBarView.setBar(this); } mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams()); } catch (RemoteException ex) { // no window manager? good luck with that } } }
實際中,這樣的修改仍是有問題的,因爲mNavigationBarView不斷的釋放和建立,會發生某些相似:
01-06 18:04:59.024 14261 14261 E AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@425d2d50 -- another window of this type already exist 的錯誤.
5.問題:another window of this type already exists
這個問題的緣由其實和PhoneWindowManager中mNavigationBar相關,在Navigation bar被移除後,沒有及時的把mNavigationBar置爲空,調用 removeWindowLw(mNavigationBar);
這樣就不會出現這個錯誤了,mNavigationBar的從新賦值會在mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());以後
因此Navigation bar 除了在Phonestatus中有添加View外,實際在PhoneWindowManager中也有對應的對象。之因此有這個對象,是由於Statusbar和Navigationbar和其它的Window不同,在PhoneWindowManager中,會對Statusbar和Navigationbar單獨的計算其Frame。
對statusbar和Navbar 的frame的計算是在beginLayoutLw() 中,而對於其它的任何Window frame的計算是layoutWindow() 中,因此在layoutWindow中有以下的語句:
// we've already done the status bar ,return directly cause they will be processed in beginLayoutLw. if (win == mStatusBar || win == mNavigationBar) { return; }
因此這裏mNavigationBar的修改,會對layoutWindow() 是否layout Navbar的判斷產生影響,例如應該加上這樣的判斷,保證再也不對Navbar進行layout,這樣對大大的提升效率和程序的穩定性
if(win.getAttrs().getTitle().toString().contains("NavigationBar")) { return; }
6.問題:setSystemUiVisibility接口異常
Navbar 的隱藏和顯示能夠經過setSystemUiVisibility這個接口來改變,一般狀況下Navbar要麼顯示,要麼不顯示,當Navbar顯示的時候,有一些狀況是須要隱藏Navbar的,最長見的例子就是視頻播放的時候,因此Android提供了這個接口。
在設置和顯示隱藏Navbar的過程當中,出現了一個問題,就是在播放視頻的時候,再也不能全屏顯示,而是所謂的LOW_PROFILE模式(邊框的layout存在,變黑,有3個黑點),這是事先沒有預想到的,
下面說一下解決的辦法:
1) 右邊的邊框的layout還存在,因此說明窗口的Frame還存在,而窗口Frame是在PhoneWindowManager中發起而且計算的,因此可能和PMW這塊有關。
2) 爲何是LOW_PROFILE模式呢,這個能夠在PhoneStatusBar中的同名setSystemUiVisibility的同名函數找到答案。
其實問題的緣由就是和PWM中 mHasNavigationBar有關係,這個變量的改變會影響mCanHideNavigationBar的值,而這個值爲false的時候,Navbar是不會消失的,問題的緣由其實就是mCanHideNavigationBar致使的
遇到問題並不可怕,關鍵是解決問題的思路對不對。
7.問題: Recents啓動應用Frame異常和Navbar閃爍
首先須要說明一下, 修改過程當中遇到問題,不少的時候都是和Recents 這個界面有關係,Recets 界面也是在SystemUI 中,具體沒有詳細的研究。Recents界面是有Navbar的,並且它是透明的,在它下面是WallPaper,當在Recents界面在從新打開一個應用的時候,實際上會調用moveToFront這個函數,先把HomeActvity顯示出來而後再更新應用界面,在Home界面是沒有Actionbar的因此就會致使Navbar先消失再從新再顯現。
知道和home以及moveToFront相關,修改其實也簡單:
final void moveTaskToFrontLocked(TaskRecord tr, ActivityRecord reason, Bundle options) { // frank: Launch recents app (moveToFront) cause Navigation bar flick @{ if (reason != null && reason.isHomeActivity) { mHasMoveHomeToFront = true; Slog.d(TAG, " moveTaskToFrontLocked mHasMoveHomeToFront =" + mHasMoveHomeToFront); } ... }
在須要的使用mHasMoveHomeToFront就能夠了。
8.問題: 修改嘗試值之緣由與總結
1.原本想在WMS中加入下面的代碼,以想經過systemUiVisibility這個接口來控制Navigation bar的layout,結果發現不行,多是android對這種模式不支持所致
relayoutWindow@WMS if (attrs != null && seq == win.mSeq) { win.mSystemUiVisibility = systemUiVisibility; /// frank //if (win.mAttrs.packageName.contains("com.your.pacakgename")) { // Slog.d(TAG" , " hide SYSTEM_UI_FLAG_HIDE_NAVIGATION systemUiVisibility = " // + Integer.toHexString(systemUiVisibility) // + " win.mSystemUiVisibility = " + Integer.toHexString(win.mSystemUiVisibility)); // // systemUiVisibility |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; // if ((win.mSystemUiVisibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { // win.mSystemUiVisibility |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; // //win.mLayoutNeeded = true; // } //} }
setSystemUiVisibility hideNavigationBar@StatusbarManagerService //boolean visible = false; //int mask = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // 400 // | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // 200 // | View.SYSTEM_UI_FLAG_LAYOUT_STABLE //100 // | View.SYSTEM_UI_FLAG_LOW_PROFILE //1 // | View.SYSTEM_UI_FLAG_FULLSCREEN // 4 // | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; //2 //int newVis = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; //if (!visible) { // //newVis |= View.SYSTEM_UI_FLAG_LOW_PROFILE // // | View.SYSTEM_UI_FLAG_FULLSCREEN // // | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; // newVis |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; //} //setSystemUiVisibility(newVis, 0xffffffff);
3. 最終採用的仍是addView和 removeView來實現的,須要注意的是:addView和removeView都會致使窗口的從新layout,因此用起來仍是很方便。
20150514
ANDROID學習筆記系列
--------------------------------------------
聯繫方式
--------------------------------------------
Weibo: ARESXIONG
E-Mail: aresxdy@gmail.com
------------------------------------------------