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;
}
}
完毕!