android 虛擬導航按鈕(NavigationBar)可手動隱藏開發
NavigationBar可以手動隱藏,隨著華為榮耀手機有了這個特點後,目前有很多android手機都有該特性。如下截圖所示:
上圖的底部虛擬導航按鈕的左、右邊有兩個按鈕點選這個按鈕,虛擬按鈕就會消失,當從螢幕底部向上滑動時候,虛擬按鈕就就會出現,這個消失和出現的過程中整個螢幕的佈局都會從新計算。
接下來,分下面幾個點來說一下具體實現的效果。
(一):首先第一個問題是如何讓虛擬按鈕(NavigationBar)消失。通過分析android原始碼可以發現虛擬按鈕(NavigationBar)的加入和實現是由系統app:systemui來完成的。
在systemui這個app裡面有一個類PhoneStatusBar,在這個類被建立的時候會判斷當前系統是否定義了虛擬按鈕(NavigationBar),如果定義了就新增,否則不新增。原始碼如下。
判斷是否有定義虛擬按鈕(NavigationBar):
boolean showNav = mWindowManagerService.hasNavigationBar();
建立虛擬按鈕(NavigationBar)的程式碼:
很顯然mNavigationBarView這個view就是顯示虛擬按鈕(NavigationBar)的。int layoutId = R.layout.navigation_bar; if(RecentsActivity.FLOAT_WINDOW_SUPPORT){ layoutId = R.layout.navigation_bar_float_window; } mNavigationBarView = (NavigationBarView) View.inflate(context, /*R.layout.navigation_bar*/layoutId, null);
最後在systemui的這個類PhoneStatusBar裡面通過如下方法把mNavigationBarView顯示出來(新增到系統中):
mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());
隱藏的方法就可以通過 mWindowManager來removeView方法來實現。實驗結果是:這是一個不錯的選擇。
具體如何實現呢?首先通過上面的mNavigationBarView的佈局檔案(R.layout.navigation_bar
(二):如何實現從底部滑動時候能夠顯示已經隱藏的虛擬按鈕(NavigationBar),當然在橫屏的時候,是從左、右邊緣滑動來顯示已經隱藏的虛擬按(NavigationBar) 通過對原始碼的分析發現,在PhoneWindowManager這個類裡面可以找到蛛絲馬跡。
在包 com.android.internal.policy.impl裡面有PhoneWindowManager類,這是一個主要管理手機顯示系統的所有window的類,我們知道在android中每個顯示的介面其實都是一個window,比如一個activity的顯示介面、一個Dialog彈出框、還有上面提到的 虛擬按鈕(NavigationBar)、包括下拉狀態列、已經鎖屏介面等等,通過hierarchyviewer.bat這個工具就可以看到當前手機正在顯示的所有window.
當然PhoneWindowManager類還處理一些其他的事情,比如按鍵事件的處理(按home回到待機介面)、鎖屏的觸發等等,不過我覺得其實都是管理的是window的切換。
在這個PhoneWindowManager類裡面,你會發現一個有用的物件:mSystemGestures。從名字可以發現這是一個系統手勢的類。沒錯,你能夠從手機螢幕頂部下滑拉出系統狀態列就是靠他了。
我們就需要修改這個類,使得手機可以發現我們從下往上滑動的手勢(從左、右邊邊緣滑動),這樣就可以在 PhoneWindowManager裡面收到這個我們需要的手勢了。
這樣一來,在PhoneWindowManager裡面收到顯示虛擬按鈕(NavigationBar)的手勢,就想辦法去顯示虛擬按鈕(NavigationBar)就可以了。在PhoneWindowManager中要顯示虛擬按鈕(NavigationBar)的辦法和顯示下拉狀態列類似,你需要在systemui裡面定義相應的方法,然後在PhoneWindowManager裡面通過如下程式碼獲取StatusBarService:
IStatusBarService statusbar = getStatusBarService();
最後通過StatusBarService來呼叫在PhoneStatusBar裡面定義的顯示虛擬按鈕(NavigationBar)的方法。
如下是原始碼:
PhoneWindowManager顯示虛擬按鈕(NavigationBar)的函式:
private void showNavigationBar(final int type){
if(isKeyguardLocked()){
return;
}
startToShowNavbar = true;
//Slog.i("yu_PhoneWindowManager", "showNavigationBar: NavigationBarMoveType="+NavigationBarMoveType);
NavigationBarMoveType = type;
if(mNavigationBar == null) {
Slog.i("yu_PhoneWindowManager", "RAMOS showNavigationBar: showNavigationBar");
//Log.v("NavigationGuard", "RAMOS showNavigationBar startToShowNavbar="+startToShowNavbar);
mHandler.post(new Runnable() {
@Override
public void run() {
try {
IStatusBarService statusbar = getStatusBarService();
if (statusbar != null) {
//Slog.i("yu_PhoneWindowManager", "showNavigationBar");
statusbar.showNavigationBar(type);
}
} catch (RemoteException e) {
// re-acquire status bar service next time it is needed.
mStatusBarService = null;
}
}
});
}
}
注意:在PhoneWindowManager類裡面可以通過判斷這個物件mNavigationBar是否為空來確認 虛擬按鈕(NavigationBar)是否被隱藏。
其他的showNavigationBar方法的宣告和定義你只需仿照hideRecentApps來做就可以了。
最後在PhoneStatusBar裡面實現具體的虛擬按鈕(NavigationBar)顯示即可。
比如,下面是我的實現方法:
@Override // CommandQueue
public void showNavigationBar(int type) {
//Log.i("way", TAG + " showNavigationBar...");
Log.i("yu_PhoneStatusBar", "showNavigationBar type="+type);
if (mNavigationBarView != null) {
try {
mWindowManagerService.StartToShowNavbar(type);
} catch (RemoteException ex) {
}
return;
}
if(mTempNavigationBarView == null){
makeNewNavigationBar();
}
if(mTempNavigationBarView != null){
mNavigationBarView = mTempNavigationBarView;
//mNavigationBarView.setVisibility(View.VISIBLE);
mWindowManager.addView(mNavigationBarView, mNavigationBarLayoutParams);
prepareNavigationBarView(true);
mNavigationBarView.setDisabledFlags(mDisabled);
//mNavigationBarView.reorient();
//mNavigationBarView.notifyScreenOn(true);
mTempNavigationBarView = null;
}else{
Log.i("yu_PhoneStatusBar", "showNavigationBar: ERROR");
}
}
重要是這這一個語句:mWindowManager.addView(mNavigationBarView, mNavigationBarLayoutParams);從上面的程式碼你會發現一個特別的變數:mTempNavigationBarView,其實這就是一個NavigationBarView。這是因為:為了顯示NavigationBarView的時候能夠快一點,所以每次在通過mWindowManager來removeView掉NavigationBarView後,我會自動去建立一個新的NavigationBarView等待下次顯示用,這樣一來下次要顯示的時候直接使用即可,就不用建立了。
注意:每次通過mWindowManager來removeView掉NavigationBarView後,這個剛剛被remove的NavigationBarView是不能再次利用的,下次還使用這個NavigationBarView會報錯。
(三)如何實現,在設定裡面去配置虛擬按鈕(NavigationBar)的排序。如下圖:
要解決這個問題,需要修改下面三個地方:
1:在frameworks/base/core/java/android/provider/Settings.java裡面新增如此程式碼:
public static final String RAMOS_NAVBAR_STYLE = "RAMOS_NAVBAR_STYLE";
我們就可以通過RAMOS_NAVBAR_STYLE 來儲存我們虛擬按鈕(NavigationBar)配置的排序了。
2:在設定中app中,新增一個設定的介面重寫SettingsPreferenceFragment來實現,配置自己的Preferences的xml檔案,其中的RadioPreferences需要自己重寫CheckBoxPreference來完成:
如下是我寫的CheckBoxPreference的RamosRadioNavbarStylePreference關鍵程式碼:
public RamosRadioNavbarStylePreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setWidgetLayoutResource(R.layout.preference_widget_radiobutton);
}
void setOnClickListener(OnClickListener listener) {
mListener = listener;
}
@Override
public void onClick() {
if (mListener != null) {
mListener.onRadioButtonClicked(this);
}
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
mViewGroup = view;
TextView title = (TextView) view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
title.setMaxLines(3);
}
UpdateNavbarStyle(view);
}
public void setNavbarStyle(int style){
if(style > -1 && mNavbarStyle != style){
mNavbarStyle = style;
notifyChanged();//UpdateNavbarStyle(mViewGroup);
}
}
private void UpdateNavbarStyle(View view){
if(mNavbarStyle < 0){
return;
}
ImageView tempview;
switch(mNavbarStyle){
case NAV_BAR_STYLE_0:
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_left);
tempview.setVisibility(View.VISIBLE);
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_right);
tempview.setVisibility(View.VISIBLE);
tempview = (ImageView) view.findViewById(R.id.back);
tempview.setImageResource(R.drawable.ic_sysbar_back);//ic_sysbar_back_right
tempview = (ImageView) view.findViewById(R.id.recent_apps);
tempview.setImageResource(R.drawable.ic_sysbar_recent);
break;
case NAV_BAR_STYLE_1:
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_left);
tempview.setVisibility(View.VISIBLE);
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_right);
tempview.setVisibility(View.VISIBLE);
tempview = (ImageView) view.findViewById(R.id.back);
tempview.setImageResource(R.drawable.ic_sysbar_recent); // ic_sysbar_back ic_sysbar_recent
tempview = (ImageView) view.findViewById(R.id.recent_apps);
tempview.setImageResource(R.drawable.ic_sysbar_back_right); //ic_sysbar_back_right ic_sysbar_recent
break;
case NAV_BAR_STYLE_2:
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_left);
tempview.setVisibility(View.INVISIBLE);
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_right);
tempview.setVisibility(View.VISIBLE);
tempview = (ImageView) view.findViewById(R.id.back);
tempview.setImageResource(R.drawable.ic_sysbar_back);//ic_sysbar_back_right
tempview = (ImageView) view.findViewById(R.id.recent_apps);
tempview.setImageResource(R.drawable.ic_sysbar_recent);
break;
case NAV_BAR_STYLE_3:
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_left);
tempview.setVisibility(View.VISIBLE);
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_right);
tempview.setVisibility(View.INVISIBLE);
tempview = (ImageView) view.findViewById(R.id.back);
tempview.setImageResource(R.drawable.ic_sysbar_back);//ic_sysbar_back_right
tempview = (ImageView) view.findViewById(R.id.recent_apps);
tempview.setImageResource(R.drawable.ic_sysbar_recent);
break;
case NAV_BAR_STYLE_4:
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_left);
tempview.setVisibility(View.INVISIBLE);
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_right);
tempview.setVisibility(View.VISIBLE);
tempview = (ImageView) view.findViewById(R.id.back);
tempview.setImageResource(R.drawable.ic_sysbar_recent); // ic_sysbar_back ic_sysbar_recent
tempview = (ImageView) view.findViewById(R.id.recent_apps);
tempview.setImageResource(R.drawable.ic_sysbar_back_right); //ic_sysbar_back_right ic_sysbar_recent
break;
case NAV_BAR_STYLE_5:
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_left);
tempview.setVisibility(View.VISIBLE);
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_right);
tempview.setVisibility(View.INVISIBLE);
tempview = (ImageView) view.findViewById(R.id.back);
tempview.setImageResource(R.drawable.ic_sysbar_recent); // ic_sysbar_back ic_sysbar_recent
tempview = (ImageView) view.findViewById(R.id.recent_apps);
tempview.setImageResource(R.drawable.ic_sysbar_back_right); //ic_sysbar_back_right ic_sysbar_recent
break;
case NAV_BAR_STYLE_6:
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_left);
tempview.setVisibility(View.INVISIBLE);
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_right);
tempview.setVisibility(View.INVISIBLE);
tempview = (ImageView) view.findViewById(R.id.back);
tempview.setImageResource(R.drawable.ic_sysbar_back);//ic_sysbar_back_right
tempview = (ImageView) view.findViewById(R.id.recent_apps);
tempview.setImageResource(R.drawable.ic_sysbar_recent);
break;
case NAV_BAR_STYLE_7:
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_left);
tempview.setVisibility(View.INVISIBLE);
tempview = (ImageView) view.findViewById(R.id.ramos_hide_navbar_right);
tempview.setVisibility(View.INVISIBLE);
tempview = (ImageView) view.findViewById(R.id.back);
tempview.setImageResource(R.drawable.ic_sysbar_recent); // ic_sysbar_back ic_sysbar_recent
tempview = (ImageView) view.findViewById(R.id.recent_apps);
tempview.setImageResource(R.drawable.ic_sysbar_back_right); //ic_sysbar_back_right ic_sysbar_recent
break;
default:
break;
}
}
在設定裡面通過如下程式碼來儲存期配置的值:
Settings.System.putInt(mActivity.getContentResolver(), Settings.System.RAMOS_NAVBAR_STYLE, style);
(3)最後在NavigationBarView裡面來實現其配置的虛擬按鈕(NavigationBar):
在NavigationBarView中通過ContentObserver來監控RAMOS_NAVBAR_STYLE值得變化,一旦變化,就會從新排序NavigationBarView中各個按鈕的顯示。
這裡,我是通過替換他們View的ID、ImageResourc和KeyCode以及他們的長按短按事件ClickLisener。
到這裡就算完結了,不過有兩個問題需要解決:
其一:在輸入法的彈出框出現的時候,來回隱藏和顯示虛擬按鈕(NavigationBar),會看到輸入框下面有黑色背景或者顯示不全等問題,如何解決呢?
其實這是因為輸入法彈出框比較獨立,沒有能夠試試重新整理和佈局造成的。解決辦法是:在包com.android.internal.policy.impl的PhoneWindow類有一個方法:
private void updateNavigationGuard(WindowInsets insets)
在這個方法裡面只需要做到每次虛擬按鈕(NavigationBar)有變化時候呼叫該方法即可:requestFitSystemWindows();
其二:每次要顯示虛擬按鈕(NavigationBar)時候,從底部往上滑動時候會觸發螢幕中其他view的click或者move觸控事件,造成誤點選了某個圖示,滑動了一些選單等問題,如何破解?
首先在PhoneWindow中攔截不需要的觸控操作,在PhoneWindow的DecorView中的onInterceptTouchEvent裡面把不需要的觸控事件return true即可。
注意:DecorView是所有activity的顯示view的父view.
這裡難點就是如何判斷一個觸控事件是不需要的,也就是說如何判斷一個觸控事件是要顯示虛擬按鈕(NavigationBar)的 ,如果一個使用的操作(手勢)是來顯示虛擬按鈕(NavigationBar)的,那麼這個操作就不要用來做其他的,就可以把這次觸控操作當成不需要的操作了。因為通常情況下,你不可能又要顯示虛擬按鈕(NavigationBar),又要點選一個其他介面的按鈕。
如何判斷一個觸控(手勢)是來顯示虛擬按鈕(NavigationBar)的呢? 這裡需要通過上面說到的PhoneWindowManager和SystemGesturesPointerEventListener了。
大致的辦法是:
在SystemGesturesPointerEventListener識別顯示虛擬按鈕(NavigationBar)的手勢,從底部滑動、從左、右邊滑動,這裡有一個要求,就是要儘快的識別出來,希望能在滑動的前3個MotionEvent事件識別出來,原來的從頂部往下滑動的手勢識別需要6個MotionEvent事件以上,這是不夠的。
然後在PhoneWindowManager定義一個正在開始顯示虛擬按鈕(NavigationBar)的boolean startToShowNavbar變數,並定義一個public方法來判斷startToShowNavbar的狀態。
private static boolean startToShowNavbar = false;
//private static final int KEY_CODE_RAMOS_HIDE_NAVBAR = 1994;
@Override
public boolean hasShowingNavbar() {
//Log.v("NavigationGuard", "RAMOS hasShowingNavbar startToShowNavbar="+startToShowNavbar);
return startToShowNavbar;
}
如何復位startToShowNavbar這個變數呢,就是說如何判斷顯示虛擬按鈕(NavigationBar)已經完成呢?在PhoneWindowManager的方法layoutWindowLw被呼叫,並且在layoutWindowLw中出現了TYPE_NAVIGATION_BAR,就復位。如下修改的截圖:
最後在PhoneWindow的DecorView中的onInterceptTouchEvent判斷即可了,如下原始碼:
private boolean getShowIngNavBar() {
try {
return WindowManagerHolder.sWindowManager.hasShowingNavbar();
} catch (RemoteException ex) {
Log.e(TAG, "RAMOS getShowIngNavBar:", ex);
return false;
}
}
完畢!