效果图

Android 虚拟导航导致界面上移 虚拟导航栏:navigation_NavigationBar

项目背景:

在一些特殊情况下,我们需要拿到是否开启了导航栏(也称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关系

Android 虚拟导航导致界面上移 虚拟导航栏:navigation_导航栏高度_02


在视图呈现中LinearLayout是位于StatusBarView之下,NavigationBarView之上。


而DecorView又是一个FrameLayout,那从这个View关系中可以大概的猜测出。NavigationBarView的开启关闭,肯定会影响到LinerLayout中LayoutParams的某一个属性。

二、最优方案实现方式—探索。

利用Android studio 自带工具-> layout inspector获取布局信息
在我的demo中获取到的 layout inspector
1.开启了NavigationBarView

Android 虚拟导航导致界面上移 虚拟导航栏:navigation_导航栏_03


2.关闭了NavigationBarView


Android 虚拟导航导致界面上移 虚拟导航栏:navigation_Android 虚拟导航导致界面上移_04


经过 layout inspector的结果很清楚的发现了,DecorView下的LinearLayout其LayoutParams的bottomMargin会随着NavigationBarView的开始或关闭自动调整。



在DecorView源码的 1054行 updateColorViews()方法中的的这段代码也证实了这一点,根据是否又显示开启Navigation去修改了mContentRoot的bottomMargin.而这个mContentRoot就是DecorView下的唯一LinearLayout


Android 虚拟导航导致界面上移 虚拟导航栏:navigation_导航栏高度_05


关于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拿到的如下:

Android 虚拟导航导致界面上移 虚拟导航栏:navigation_导航栏是否开启_06


2.2 没有设NoActionBar的 layout inspector拿到的如下


Android 虚拟导航导致界面上移 虚拟导航栏:navigation_Android 虚拟导航导致界面上移_07


最后附上

github

地址,如果对你有帮助,点个start,谢谢!!!!