效果图
项目背景:
在一些特殊情况下,我们需要拿到是否开启了导航栏(也称NavgationBar,虚拟导航栏按键),在做布局切换。
比如项目中有类似微信:自定义表情包,功能面板和软键盘进行切换交互时,为了做到无缝切换,就需要拿到NavgationBar是否开启和对应的高度来调整,自定义表情面板的高度。也正是因为项目中有此需求,才进行了一番探索,找到了这个最优方案。网络上找的一些方案,都是一些如下代码:
/**
* 利用反射获取导航栏虚拟键盘的高度
* @param wm
* @return
*/
public static int getNavigationBarHeight(WindowManager wm) {
if (!isNavigationBarShow(wm)) {
return 0;
}
Resources resources = Resources.getSystem();
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
//获取NavigationBar的高度
return resources.getDimensionPixelSize(resourceId);
}
return 0;
}
/**
* 判断底部navigator是否已经显示
* @param windowManager
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private static boolean isNavigationBarShow(WindowManager windowManager) {
Display d = windowManager.getDefaultDisplay();
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
d.getRealMetrics(realDisplayMetrics);
int realHeight = realDisplayMetrics.heightPixels;
int realWidth = realDisplayMetrics.widthPixels;
DisplayMetrics displayMetrics = new DisplayMetrics();
d.getMetrics(displayMetrics);
int displayHeight = displayMetrics.heightPixels;
int displayWidth = displayMetrics.widthPixels;
Log.i(TAG, "hasSoftKeys: " + realWidth + " " + realHeight);
Log.i(TAG, "hasSoftKeys: " + displayWidth + " " + displayHeight);
return (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0;
}
亲测这些方法是无效,只有部分手机可以使用,而且当NavgationBar关闭了在开启,就会出现错误返回。
下面就对这个最优方案进行分析和讲解。
一、最优方案实现方式—分析。
我们知道一个Activity中包含了一个Window,而在安卓中,Window只有一个唯一实现类PhoneWindow
而在PhoneWindow中包含了DecorView,而DecorView又继承至FrameLayout。
而DecorView下只包含了一个LinearLayout+StatusBarView+NavigationBarView。他们的关系层级图如下。关系图一:DecorView下子View关系在视图呈现中LinearLayout是位于StatusBarView之下,NavigationBarView之上。
而DecorView又是一个FrameLayout,那从这个View关系中可以大概的猜测出。NavigationBarView的开启关闭,肯定会影响到LinerLayout中LayoutParams的某一个属性。
二、最优方案实现方式—探索。
利用Android studio 自带工具-> layout inspector获取布局信息在我的demo中获取到的 layout inspector1.开启了NavigationBarView2.关闭了NavigationBarView
经过 layout inspector的结果很清楚的发现了,DecorView下的LinearLayout其LayoutParams的bottomMargin会随着NavigationBarView的开始或关闭自动调整。
在DecorView源码的 1054行 updateColorViews()方法中的的这段代码也证实了这一点,根据是否又显示开启Navigation去修改了mContentRoot的bottomMargin.而这个mContentRoot就是DecorView下的唯一LinearLayout
关于Activity和PhoneWindow和DecorView相关视图层级架构可以参考这个。
外部文章
三、最优方案实现方式—代码实现。
直接上代码
package com.linfc.www.navgationbarutils.utils;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.FitWindowsLinearLayout;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.LinearLayout;
/**
* created by Linfc on 2019/2/12
* NavgationBar 工具类
*/
public class NavgationbarUtils {
/**
* @return false 关闭了NavgationBar ,true 开启了
*/
public static boolean navgationbarIsOpen(Context context) {
ViewGroup rootLinearLayout = findRootLinearLayout(context);
int navigationBarHeight = 0;
if (rootLinearLayout != null) {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) rootLinearLayout.getLayoutParams();
navigationBarHeight = layoutParams.bottomMargin;
}
if (navigationBarHeight == 0) {
return false;
} else {
return true;
}
}
/**
* 导航栏高度,关闭的时候返回0,开启时返回对应值
*
* @param context
* @return
*/
public static int getNavigationBarHeight(Context context) {
ViewGroup rootLinearLayout = findRootLinearLayout(context);
int navigationBarHeight = 0;
if (rootLinearLayout != null) {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) rootLinearLayout.getLayoutParams();
navigationBarHeight = layoutParams.bottomMargin;
}
return navigationBarHeight;
}
/**
* 从R.id.content从上遍历,拿到 DecorView 下的唯一子布局LinearLayout
* 获取对应的bottomMargin 即可得到对应导航栏的高度,0为关闭了或没有导航栏
*/
private static ViewGroup findRootLinearLayout(Context context) {
ViewGroup onlyLinearLayout = null;
try {
Window window = getWindow(context);
if (window != null) {
ViewGroup decorView = (ViewGroup) getWindow(context).getDecorView();
Activity activity = getActivity(context);
View contentView = activity.findViewById(android.R.id.content);
if (contentView != null) {
View tempView = contentView;
while (tempView.getParent() != decorView) {
ViewGroup parent = (ViewGroup) tempView.getParent();
if (parent instanceof LinearLayout) {
//如果style设置了不带toolbar,mContentView上层布局会由原来的 ActionBarOverlayLayout->FitWindowsLinearLayout)
if (parent instanceof FitWindowsLinearLayout) {
tempView = parent;
continue;
} else {
onlyLinearLayout = parent;
break;
}
} else {
tempView = parent;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return onlyLinearLayout;
}
private static Window getWindow(Context context) {
if (getAppCompActivity(context) != null) {
return getAppCompActivity(context).getWindow();
} else {
return scanForActivity(context).getWindow();
}
}
private static Activity getActivity(Context context) {
if (getAppCompActivity(context) != null) {
return getAppCompActivity(context);
} else {
return scanForActivity(context);
}
}
private static AppCompatActivity getAppCompActivity(Context context) {
if (context == null) return null;
if (context instanceof AppCompatActivity) {
return (AppCompatActivity) context;
} else if (context instanceof ContextThemeWrapper) {
return getAppCompActivity(((ContextThemeWrapper) context).getBaseContext());
}
return null;
}
private static Activity scanForActivity(Context context) {
if (context == null) return null;
if (context instanceof Activity) {
return (Activity) context;
} else if (context instanceof ContextWrapper) {
return scanForActivity(((ContextWrapper) context).getBaseContext());
}
return null;
}
}
为什么不是从,从DecorView往下遍历?而是从id为android.R.id.content往上遍历?
1. 因为现在有一些第三方框架,比如SwipeBackLayout 他是直接addView到的DecorView中的,导致LinearLayout成了SwipeBackLayout的子布局。为了准确拿到DecorView下的LinearLayout,所有改用从为android.R.id.content往上遍历。
2. 当我们Activity中的Styles中的AppTheme设置为NoActionBar时,android.R.id.content的第一个ParentView会变成FitWindowsLinearLayout所有在上面代码中做了判断处理2.1.主题设置了NoActionBar layout inspector拿到的如下:2.2 没有设NoActionBar的 layout inspector拿到的如下
最后附上
github地址,如果对你有帮助,点个start,谢谢!!!!