操作Android窗口的几种方式?WindowInsets与其兼容库的使用与踩坑

4,571 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

前言

首先在文章开始之前先抛出几个问题,让我们带着疑问往下走:

什么是窗口控制?

在Android手机中状态栏,导航栏,输入法等这些与app无关,但是需要配合app一起使用的窗口部件。

之前我们都是如何管理窗口的?

在window上添加各种flag,有些flag只适应于指定的版本,而某些flag在高版本不能生效,清除flag也相对麻烦。

WindowInsetsController 又能解决什么问题?

WindowInsetsController 的推出是来取代之前复杂麻烦的窗口控制,之前添加各种Flag不容易理解,而使用Api的方式来管理窗口,更加的语义化,更加的方便理解,可以说看到Api方法就知道是什么意思,使用起来倒是很方便。

WindowInsetsController 就真的没有兼容性问题吗?

虽然flag这不好那不好,那我们直接用 WindowInsetsController 就可以了吗?可是 WindowInsetsController 需要Android 11 (R) API 30 才能使用。虽然谷歌又推出了 ViewCompat 的Api 向下兼容到5.0版本,但是5.0以下的版本怎么办?

可能现在的一些新应用都是5.0以上了,但是这个兼容到哪一个版本也并不是我们开发者说了算,万一要兼容5.0一下怎么办?

就算我们的应用是支持5.0以上,那么我们使用 WindowInsetsController 与 windowInsets 就可以了吗?并不是!

就算是 WindowInsetsController 或它的兼容包 WindowInsetsControllerCompat 也并不是全部就能用的,也会有兼容性问题。部分设备不能用,部分版本不能用等等。

说了这么多,到底如何使用?下面一起来看看吧!

一、WindowInsetsController 与 windowInsets 的使用

WindowInsetsController 能管理的东西不少,但是我们常用的就是状态栏,导航栏,软键盘的一些管理,下面我们就基于这几点来看看到底如何控制

1.1 状态栏

第一种方法:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        window.decorView.setOnApplyWindowInsetsListener { view: View, windowInsets: WindowInsets ->

            //状态栏
            val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())
     
            //状态栏高度
            val statusBarHeight = Math.abs(statusBars.bottom - statusBars.top)

            windowInsets
        }
    }

第二种方法

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        val windowInsets = window.decorView.rootWindowInsets
        //状态栏
        val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())
        //状态栏高度
        val statusBarHeight = Math.abs(statusBars.bottom - statusBars.top)

        YYLogUtils.w("statusBarHeight2:$statusBarHeight")
    }

第三种方法

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
         window.decorView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
            override fun onViewAttachedToWindow(view: View?) {
                val windowInsets = window.decorView.rootWindowInsets
                 //状态栏
                val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())
                //状态栏高度
                val statusBarHeight = Math.abs(statusBars.bottom - statusBars.top)

                YYLogUtils.w("statusBarHeight2:$statusBarHeight")
            }

            override fun onViewDetachedFromWindow(view: View?) {
            }
        })
    }

第一种方法和第三种方法是使用监听回调的方式获取到状态栏高度,第二种方式是使用同步的方式获取状态栏高度,但是第二种方式有坑,它无法在 onCreate 中使用,直接使用会空指针的。

为什么?其实也能理解,onCreate 方法其实就是解析布局添加布局,并没有展示出来,所以我们第三种方式使用了监听,当View已经 OnAttach 之后我们再调用方法才能使用。

1.2 导航栏

第一种方法:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        window.decorView.setOnApplyWindowInsetsListener { view: View, windowInsets: WindowInsets ->

                //导航栏
                val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())

                //导航栏高度
                val navigationHeight = Math.abs(statusBars.bottom - statusBars.top)

                YYLogUtils.w("navigationHeight:$navigationHeight")

            windowInsets
        }
    }

第二种方法

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        val windowInsets = window.decorView.rootWindowInsets
               //导航栏
                val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())

                //导航栏高度
                val navigationHeight = Math.abs(statusBars.bottom - statusBars.top)

                YYLogUtils.w("navigationHeight:$navigationHeight")

        YYLogUtils.w("statusBarHeight2:$statusBarHeight")
    }

第三种方法

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
         window.decorView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
            override fun onViewAttachedToWindow(view: View?) {
                val windowInsets = window.decorView.rootWindowInsets
                //导航栏
                val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())

                //导航栏高度
                val navigationHeight = Math.abs(statusBars.bottom - statusBars.top)

                YYLogUtils.w("navigationHeight:$navigationHeight")
            }

            override fun onViewDetachedFromWindow(view: View?) {
            }
        })
    }

其实导航栏和状态栏是一样样的,这里打印Log如下:

可以看到其实也更推荐大家使用第三种方式,因为它是在 onAttach 中调用,而其他的方式需要在 onResume 之后调用,相对来说第三种方式更快一些。

1.3 软键盘

同样的我们可以操作软键盘的打开,收起,还能监听软键盘弹起的动画的Value,获取当前的值,这个也是巨方便

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

        //打开键盘
        window?.insetsController?.show(WindowInsets.Type.ime())
//      mBinding.llRoot.windowInsetsController?.show(WindowInsets.Type.ime())


        window.decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {

            override fun onProgress(insets: WindowInsets, runningAnimations: MutableList<WindowInsetsAnimation>): WindowInsets {

                val isVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
                val keyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

                //当前是否展示
                YYLogUtils.w("isVisible = $isVisible")
                //当前的高度进度回调
                YYLogUtils.w("keyboardHeight = $keyboardHeight")

                return insets
            }
        })

    }

我们可以通过 window?.insetsController 或者 window.decorView.windowInsetsController? 来获取 WindowInsetsController 对象,通过 Controller 对象我们就能操作软键盘了。

打印Log如下:

关闭软键盘:

打开软键盘:

1.4 其他

除了软键盘的操作,我们还能进行其他的操作

    window?.insetsController?.apply {

        show(WindowInsetsCompat.Type.ime())
                
        show(WindowInsetsCompat.Type.statusBars())
                
        show(WindowInsetsCompat.Type.navigationBars())
                
        show(WindowInsetsCompat.Type.systemBars())

    }

不过都不是太常用。

除此之外我们还能设置状态栏与导航栏的文本图标颜色

    window?.insetsController?.apply {

       setAppearanceLightNavigationBars(true)
       setAppearanceLightStatusBars(false)
    }

不过也并不好用,内部有兼容性问题。

二、兼容库 WindowInsetsControllerCompat 的使用

为了兼容低版本的Android,我们可以使用 implementation 'androidx.core:core:1.5.0' 以上的版本,内部即可使用 WindowInsetsControllerCompat 兼容库,最多可以支持到5.0以上版本。

这里我使用的是 implementation 'androidx.core:core:1.6.0'版本作为示例。

2.1 状态栏

我们对于前面的版本,同样的我们使用三种方式来获取

方式一:

    ViewCompat.setOnApplyWindowInsetsListener(view, new OnApplyWindowInsetsListener() {
        @Override
        public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {

            Insets statusInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars());

            int top = statusInsets.top;
            int bottom = statusInsets.bottom;
            int height = Math.abs(bottom - top);

            return insets;
        }
    });

方式二:

        WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view);
            assert windowInsets != null;
            int top = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
            int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).bottom;
            int height = Math.abs(bottom - top);

方式三:


    view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(View v) {

                WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
                 assert windowInsets != null;
                int top = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
                int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).bottom;
                int height = Math.abs(bottom - top);
                
                }

            @Override
            public void onViewDetachedFromWindow(View v) {
            }
        });

和R的版本一致,我更推荐使用第三种方式,当View已经 OnAttach 之后我们再调用方法,更快捷一点。

2.2 导航栏

方式一:

    ViewCompat.setOnApplyWindowInsetsListener(view, new OnApplyWindowInsetsListener() {
        @Override
        public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {

            Insets navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars());

            int top = navInsets.top;
            int bottom = navInsets.bottom;
            int height = Math.abs(bottom - top);

            return insets;
        }
    });

方式二:

        WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view);
            assert windowInsets != null;
            int top = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).top;
            int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
            int height = Math.abs(bottom - top);

方式三:


    view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(View v) {

                WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
                 assert windowInsets != null;
                int top = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).top;
                int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
                int height = Math.abs(bottom - top);
                
                }

            @Override
            public void onViewDetachedFromWindow(View v) {
            }
        });

和R版本的一致,这样即可正确的获取到底部导航栏的高度

2.3 软键盘

操作软键盘的方式和R的版本差不多,只是调用的类变成了兼容类。

    ViewCompat.setWindowInsetsAnimationCallback(window.decorView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
         override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {

                val isVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
                val keyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

                //当前是否展示
                YYLogUtils.w("isVisible = $isVisible")
                //当前的高度进度回调
                YYLogUtils.w("keyboardHeight = $keyboardHeight")

                return insets
            }
        })

        ViewCompat.getWindowInsetsController(findViewById(android.R.id.content))?.apply {

            show(WindowInsetsCompat.Type.ime())

        }

这样的兼容类,其实并没有完全兼容,低版本的部分手机还是拿不到进度。

那么我们可以在兼容类上再做一个版本的兼容


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

    activity.window.decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
        override fun onProgress(insets: WindowInsets, animations: MutableList<WindowInsetsAnimation>): WindowInsets {

            val imeHeight = insets.getInsets(WindowInsets.Type.ime()).bottom
       
             listener.onKeyboardHeightChanged(imeHeight)

            return insets
        }
    })

} else {
    ViewCompat.setOnApplyWindowInsetsListener(activity.window.decorView) { _, insets ->

        val posBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

        listener.onKeyboardHeightChanged(posBottom)

        insets
    }
}

无赖,兼容类的软键盘监听效果并不好,只能使用以前的方式。

打印的Log如下:

2.4 其他

同样的我们可以使用兼容类来操作状态栏,导航栏,软键盘等

    View decorView = activity.findViewById(android.R.id.content);
    WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(decorView);

    if (controller != null) {
           
        controller.show(WindowInsetsCompat.Type.navigationBars());

        controller.show(WindowInsetsCompat.Type.statusBars());

        controller.show(WindowInsetsCompat.Type.ime());    
    }

注意坑点,如果使用的是Activity对象,这里推荐使用 findViewById(android.R.id.content) 的方式来获取View来操作,如果是通过window.decorView 来获取 Controller 有可能为null。

控制导航栏,状态栏的文本图标颜色

    WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(activity.findViewById(android.R.id.content));
    if (controller != null) {
            
      controller.setAppearanceLightNavigationBars(false);

      controller.setAppearanceLightStatusBars(false);    

    }

注意坑点,看起来很美好,其实底部导航栏只有版本R以上才能控制,而顶部状态栏的颜色控制则有很大的兼容性问题,几乎不可用,我目前测试过的机型只有一款能生效。

三、实战中兼容库的兼容问题

在应用的开发中我们可以用 WindowInsetsControllerCompat 吗?它能解决我们那些痛点呢?

当然可以用,在状态栏高度,导航栏高度,判断状态栏导航栏是否显示,监听软键盘的高度等一系列场景中确实能起到很好的作用。

为什么要用 WindowInsetsControllerCompat ?

看之前的状态栏高度,导航栏高度获取,都是监听的方式获取啊,如果想使用我还需要加个回调才行,这里就引入一个问题,一定要异步使用吗?使用同步行不行?

博主,你这个太复杂了,我们之前的方式都是直接一个静态方法就行了。


    /**
     * 老的方法获取状态栏高度
     */
    private static int getStatusBarHeight(Context context) {
        int result = 0;
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = context.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    /**
     * 老的方法获取导航栏的高度
     */
    private static int getNavigationBarHeight(Context context) {
        int result = 0;
        int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = context.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

相信大家包括我都是这么用的,确实简单好用有快速又便捷,搞得这些监听啊回调啊有个diao用?

但是但是,这些值只是预设的值,部分手机厂商会修改不使用这些值,而我们使用 WindowInsets 的方式来获取的话,是其真正展示的值。

例如状态栏的高度,早前一些刘海屏的手机,如果刘海做的比较大,比较高,状态栏的高度都显示不下,那么就会加大状态栏高度,那么使用预设值就会有问题,显得比较小。

再比如现在流行的全面屏手机,全面屏手势,由于要兼容各种操作模式,底部的导航栏高度就完全不是预设值,如果还是用老方法就会踩大坑了。

如下图,非常典型的例子,真正的导航栏是黑色,使用老方法获取到的导航栏高度为深灰色。

再比如判断导航栏是否存在,因为部分手机可以手动隐藏导航栏,还能在设置中动态改变交互模式,全面屏手势,底部三大金刚键等。

大家使用的老的方式,大概都是这样判断:

 /**
     * 老方法,并不好用
     */
    public static boolean isNavBarVisible(Context context) {
        boolean isVisible = false;
        if (!(context instanceof Activity)) {
            return false;
        }
        Activity activity = (Activity) context;
        Window window = activity.getWindow();
        ViewGroup decorView = (ViewGroup) window.getDecorView();
        for (int i = 0, count = decorView.getChildCount(); i < count; i++) {
            final View child = decorView.getChildAt(i);
            final int id = child.getId();
            if (id != View.NO_ID) {
                String resourceEntryName = context.getResources().getResourceEntryName(id);
                if ("navigationBarBackground".equals(resourceEntryName) && child.getVisibility() == View.VISIBLE) {
                    isVisible = true;
                    break;
                }
            }
        }
        if (isVisible) {
            // 对于三星手机,android10以下做单独的判断
            if (isSamsung()
                    && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1
                    && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                try {
                    return Settings.Global.getInt(activity.getContentResolver(), "navigationbar_hide_bar_enabled") == 0;
                } catch (Exception ignore) {
                }
            }

            int visibility = decorView.getSystemUiVisibility();
            isVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
        }

        return isVisible;
    }

    private static final String[] ROM_SAMSUNG = {"samsung"};

    private static boolean isSamsung() {
        final String brand = getBrand();
        final String manufacturer = getManufacturer();
        return isRightRom(brand, manufacturer, ROM_SAMSUNG);
    }

    private static String getBrand() {
        try {
            String brand = Build.BRAND;
            if (!TextUtils.isEmpty(brand)) {
                return brand.toLowerCase();
            }
        } catch (Throwable ignore) {/**/}
        return "UNKNOWN";

    }

    private static String getManufacturer() {
        try {
            String manufacturer = Build.MANUFACTURER;
            if (!TextUtils.isEmpty(manufacturer)) {
                return manufacturer.toLowerCase();
            }
        } catch (Throwable ignore) {/**/}
        return "UNKNOWN";
    }

    private static boolean isRightRom(final String brand, final String manufacturer, final String... names) {
        for (String name : names) {
            if (brand.contains(name) || manufacturer.contains(name)) {
                return true;
            }
        }
        return false;
    }

核心思路是直接遍历 decorView 找到导航栏的控件,去判断它是否隐藏还是显示。。。

其实不说全面屏手机了,就是我的老华为 7.0系统的手机都判断的不准确,巨坑!

比如,全面屏手机的导航栏判断:

看到我全面屏手势的小横杠杠的了吗?我明明没有底部导航栏了,居然判断我存在导航栏,还给一个完全不合理的状态栏高度。

我醉了,真的是够了!

而以上方法都是可以通过 WindowInsets 来解决的,也就是为什么推荐部分场景下的一些效果还是使用 WindowInsets 来做为好。

那么我们真的在实战中使用了 WindowInsetsControllerCompat 就完美了吗?就没坑了吗?

no no no, 答案是否定的。你根本不知道会发生什么兼容性的问题。(兼容性可用说是我们安卓人的一生之敌)

WindowInsetsController 的兼容性问题

我们知道 WindowInsetsController 是安卓11以上用的,而 WindowInsetsControllerCompat 是安卓5以上可用的兼容包,那么 WindowInsetsControllerCompat 的兼容包就没有兼容性问题了吗?一样有!

例如一些 WindowInsetsControllerCompat 的获取方式,设置状态栏文本图标的颜色方式,设置导航栏的图标颜色方式。设置状态栏导航栏的背景颜色等。

如果 WindowInsetsController / WindowInsets的方式在某些效果上并没有那么好用,那么我们是不是还是要用flag的方式来实现这些效果,在一些兼容性好的方式上,那么我们就可以用 WindowInsetsController / WindowInsets的方式的方式来实现,这样是不是就能相对完美的实现我们想要的效果了。

所以我封装了这样的工具类。

四、推荐的工具类

此工具类5.0以上可用,记录了一些状态栏与导航栏操作的常用的方法。


public class StatusBarHostUtils {

    // =======================  StatusBar begin ↓ =========================

    /**
     * 5.0以上设置沉浸式状态
     */
    public static void immersiveStatusBar(Activity activity) {
        //方式一
        //false 表示沉浸,true表示不沉浸
//        WindowCompat.setDecorFitsSystemWindows(activity.getWindow(), false);

        //方式二:添加Flag,两种方式都可以,都是5.0以上使用
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            View decorView = window.getDecorView();
            decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.setStatusBarColor(Color.TRANSPARENT);
        }
    }

    /**
     * 设置当前页面的状态栏颜色,使用宿主方案一般不用这个修改颜色,只是用于沉浸式之后修改状态栏颜色为透明
     */
    public static void setStatusBarColor(Activity activity, int statusBarColor) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(statusBarColor);
        }
    }

    /**
     * 6.0版本及以上可以设置黑色的状态栏文本
     *
     * @param activity
     * @param dark     是否需要黑色文本
     */
    public static void setStatusBarDarkFont(Activity activity, boolean dark) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            Window window = activity.getWindow();
            View decorView = window.getDecorView();
            if (dark) {
                decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
            } else {
                decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
            }
        }

    }

    /**
     * 老的方法获取状态栏高度
     */
    private static int getStatusBarHeight(Context context) {
        int result = 0;
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = context.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    /**
     * 新方法获取状态栏高度
     */
    public static void getStatusBarHeight(Activity activity, HeightValueCallback callback) {
        getStatusBarHeight(activity.findViewById(android.R.id.content), callback);
    }

    /**
     * 新方法获取状态栏高度
     */
    public static void getStatusBarHeight(View view, HeightValueCallback callback) {

        boolean attachedToWindow = view.isAttachedToWindow();

        if (attachedToWindow) {

            WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view);
            assert windowInsets != null;
            int top = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
            int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).bottom;
            int height = Math.abs(bottom - top);
            if (height > 0) {
                callback.onHeight(height);
            } else {
                callback.onHeight(getStatusBarHeight(view.getContext()));
            }

        } else {

            view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {

                    WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
                    assert windowInsets != null;
                    int top = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
                    int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).bottom;
                    int height = Math.abs(bottom - top);
                    if (height > 0) {
                        callback.onHeight(height);
                    } else {
                        callback.onHeight(getStatusBarHeight(view.getContext()));
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                }
            });
        }
    }

    // =======================  NavigationBar begin ↓ =========================

    /**
     * 5.0以上-设置NavigationBar底部导航栏的沉浸式
     */
    public static void immersiveNavigationBar(Activity activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            View decorView = window.getDecorView();
            decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);

            window.setNavigationBarColor(Color.TRANSPARENT);
        }
    }

    /**
     * 设置底部导航栏的颜色
     */
    public static void setNavigationBarColor(Activity activity, int navigationBarColor) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setNavigationBarColor(navigationBarColor);
        }
    }

    /**
     * 底部导航栏的Icon颜色白色和灰色切换,高版本系统才会生效
     */
    public static void setNavigationBarDrak(Activity activity, boolean isDarkFont) {
        WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(activity.findViewById(android.R.id.content));
        if (controller != null) {
            if (!isDarkFont) {
                controller.setAppearanceLightNavigationBars(false);
            } else {
                controller.setAppearanceLightNavigationBars(true);
            }
        }
    }

    /**
     * 老的方法获取导航栏的高度
     */
    private static int getNavigationBarHeight(Context context) {
        int result = 0;
        int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = context.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    /**
     * 获取底部导航栏的高度
     */
    public static void getNavigationBarHeight(Activity activity, HeightValueCallback callback) {
        getNavigationBarHeight(activity.findViewById(android.R.id.content), callback);
    }

    /**
     * 获取底部导航栏的高度
     */
    public static void getNavigationBarHeight(View view, HeightValueCallback callback) {

        boolean attachedToWindow = view.isAttachedToWindow();

        if (attachedToWindow) {

            WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view);
            assert windowInsets != null;
            int top = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).top;
            int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
            int height = Math.abs(bottom - top);
            if (height > 0) {
                callback.onHeight(height);
            } else {
                callback.onHeight(getNavigationBarHeight(view.getContext()));
            }

        } else {

            view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {

                    WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
                    assert windowInsets != null;
                    int top = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).top;
                    int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
                    int height = Math.abs(bottom - top);
                    if (height > 0) {
                        callback.onHeight(height);
                    } else {
                        callback.onHeight(getNavigationBarHeight(view.getContext()));
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                }
            });
        }
    }

    // =======================  NavigationBar StatusBar Hide Show begin ↓ =========================

    /**
     * 显示隐藏底部导航栏(注意不是沉浸式效果)
     */
    public static void showHideNavigationBar(Activity activity, boolean isShow) {

        View decorView = activity.findViewById(android.R.id.content);
        WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(decorView);

        if (controller != null) {
            if (isShow) {
                controller.show(WindowInsetsCompat.Type.navigationBars());
                controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH);
            } else {
                controller.hide(WindowInsetsCompat.Type.navigationBars());
                controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
            }
        }
    }

    /**
     * 显示隐藏顶部的状态栏(注意不是沉浸式效果)
     */
    public static void showHideStatusBar(Activity activity, boolean isShow) {

        View decorView = activity.findViewById(android.R.id.content);
        WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(decorView);

        if (controller != null) {
            if (isShow) {
                controller.show(WindowInsetsCompat.Type.statusBars());
            } else {
                controller.hide(WindowInsetsCompat.Type.statusBars());
            }
        }

    }

    /**
     * 当前是否显示了底部导航栏
     */
    public static void hasNavigationBars(Activity activity, BooleanValueCallback callback) {

        View decorView = activity.findViewById(android.R.id.content);
        boolean attachedToWindow = decorView.isAttachedToWindow();

        if (attachedToWindow) {

            WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(decorView);

            if (windowInsets != null) {

                boolean hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
                        windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0;

                callback.onBoolean(hasNavigationBar);
            }

        } else {

            decorView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {

                    WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);

                    if (windowInsets != null) {

                        boolean hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
                                windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0;

                        callback.onBoolean(hasNavigationBar);
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                }
            });
        }
    }

}

关于状态栏的沉浸式两种方式都可以,而导航栏的沉浸式使用的Flag,修改状态栏与导航栏的背景颜色使用flag,修改状态栏文本颜色使用flag,修改导航栏的图片颜色使用的 controller,获取导航栏状态栏的高度使用的 controller ,判断导航栏是否存在使用的 controller。

一些效果如图:

总结

由于使用了 WindowInsetsController与其兼容库,所以我们定义的工具类在5.0版本以上。

如果使用flag的方式,那么我们可以兼容到更低的版本,这一点还请知悉。

在5.0版本以上使用工具类,我们有些兼容性不好的使用的是flag方案,而有些效果比较好的我们使用的是 indowInsetsController 方案。

此方案并非什么权威方案,只是我个人在开发过程中踩坑踩出来的,对我个人来说相对完善的一个方案,在实战开发中我个人觉得还算能用。

当然由于各种原因受限,个人水平也有限,难免有闭门造车的情况,如果你有更好的方案或者觉得有错漏的地方,还望指出来大家一起交流学习进步。

后期我也会针对本文进行一些扩展,会出一些相关的细节文章与一些效果的实现。

好了,本文的全部代码与Demo都已经开源。有兴趣可以看这里。项目会持续更新,大家可以关注一下。

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。