1. 程式人生 > >android 虛擬導航按鈕(NavigationBar)可手動隱藏開發

android 虛擬導航按鈕(NavigationBar)可手動隱藏開發

NavigationBar可以手動隱藏,隨著華為榮耀手機有了這個特點後,目前有很多android手機都有該特性。如下截圖所示:


        上圖的底部虛擬導航按鈕的左、右邊有兩個按鈕點選這個按鈕,虛擬按鈕就會消失,當從螢幕底部向上滑動時候,虛擬按鈕就就會出現,這個消失和出現的過程中整個螢幕的佈局都會從新計算。

         接下來,分下面幾個點來說一下具體實現的效果。

         (一):首先第一個問題是如何讓虛擬按鈕(NavigationBar)消失。通過分析android原始碼可以發現虛擬按鈕(NavigationBar)的加入和實現是由系統app:systemui來完成的。

          在systemui這個app裡面有一個類PhoneStatusBar,在這個類被建立的時候會判斷當前系統是否定義了虛擬按鈕(NavigationBar),如果定義了就新增,否則不新增。原始碼如下。

           判斷是否有定義虛擬按鈕(NavigationBar):

boolean showNav = mWindowManagerService.hasNavigationBar();
           建立虛擬按鈕(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);
          很顯然mNavigationBarView這個view就是顯示虛擬按鈕(NavigationBar)的。

          最後在systemui的這個類PhoneStatusBar裡面通過如下方法把mNavigationBarView顯示出來(新增到系統中):

mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());
          隱藏的方法就可以通過 mWindowManager來removeView方法來實現。實驗結果是:這是一個不錯的選擇。

          具體如何實現呢?首先通過上面的mNavigationBarView的佈局檔案(R.layout.navigation_bar

)可以知道,我們可以重寫這個佈局檔案,在重寫的佈局檔案中新增兩個隱藏按鈕,這個很簡單。其次還有個問題,就是在systemui裡面隱藏了虛擬按鈕(NavigationBar,如何通知WindowManager,這個可以通過定義一個新的KEYCODE,單點選這個隱藏按鈕時候發出這個特殊的KEYCODE的按鍵事件。

        (二)如何實現從底部滑動時候能夠顯示已經隱藏的虛擬按鈕(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;
        }
    }


完畢!