继续创造,加快成长!这是我参与「日新计划 10 月更文应战」的第4天,点击检查活动详情

前言

在之前的文章中,大家比较重视宿主侵入的办法,并且有要求适配导航栏的操作。

其实大部分的应用都只需求运用到状态栏,导航栏由体系去办理,为什么不自己办理导航栏,就是导航栏的坑太多。

布景设置的坑,判别是否存在的坑,手动设置躲藏显现导航栏的坑,导航栏高度获取的坑。

假如项目中的确需求用到操作导航栏怎么办?

导航栏的处理

导航栏为什么难处理,由于之前的一些增加Flag的计划有些不实用,有兼容问题,也能够说手机厂商并没有完全适配,导致兼容性有问题。

而咱们经过 WindowInsetsController / WindowInsets 的一些办法则能够相对便利的操作导航栏。

那么是不是 WindowInsetsController / WindowInsets 的办法就完全兼容了呢?也并不是,仅仅相对好一点,重要的功用能用罢了。

下面介绍一下相对稳定的一些操作办法。

判别当时是否显现了导航栏:

    /**
     * 当时是否显现了底部导航栏
     */
    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) {
                }
            });
        }
    }

其实中心代码是相同的,仅仅区分了是否现已onAttach了,防止在onCreate办法中调用的时分会报错。

它的中心思路是和老版别的办法是相似的,仅仅老版别是从window中找到导航栏布局去判别是否躲藏和显现和判别高度。而新版别经过WindowInset 的办法获取导航栏目标相对比较保险。

获取导航栏的高度:

    /**
     * 获取底部导航栏的高度
     */
    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) {
                }
            });
        }
    }
    /**
     * 老的办法获取导航栏的高度
     */
    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;
    }

新版的办法和老版别的办法都界说了,通常咱们运用 WindowInsets 的办法即可获取到导航栏目标,然后去获取它的高度。

而老版别的办法则是经过获取系列内置的一个高度值,而一些手机并不会按这个高度设置导航栏高度,所以获取出来的值则是过错的。

如下图所示:

Android导航栏的处理-HostStatusLayout加入底部的导航栏适配

导航栏的躲藏与沉溺式处理:

在一些应用需求全屏的时分,咱们需求躲藏导航栏(是的,你无法返回了)。

    /**
     * 显现躲藏底部导航栏(留意不是沉溺式作用)
     */
    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);
            }
        }
    }

而在一些常规的页面,咱们假如想像状态栏相同获取沉溺式体会,咱们则是不同的处理逻辑:

    /**
     * 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);
        }
    }

咱们把导航栏常用的一些操作理清之后,咱们再来看 StatusHostLayout 这样的宿主计划怎么帮助咱们办理导航栏。

修正StatusHostLayout计划

前文咱们讲到过状态栏的办理,假如参加导航栏的办理,咱们需求做哪些操作?

先理清一下思路:

  1. 界说一个自界说的ViewGroup,内部顺序排列状态栏,内容容器,导航栏三个布局。
  2. 咱们需求强制设置状态栏和导航栏的沉溺式,让咱们自己的状态栏.导航栏View的布局展示出来。
  3. 自界说状态栏View,与导航栏View,咱们只需求获取到正确的高度,然后测量的时分定死指定的高度即可。
  4. 咱们能够以View的方式来操作自界说导航栏/状态栏的布景,图片,显现躲藏等操作。
  5. 把咱们DecorView中的跟视图替换为咱们自界说的布局。
  6. 暴露一个inject办法注入到指定的Activity中去,并提供自界说布局的目标。

之前状态栏的逻辑现已做好了,现在咱们只需求处理导航栏的逻辑。咱们界说好上面的一些导航栏操作东西类办法。

先界说一个自己的导航栏View,只需求处理高度即可。

/**
 * 自界说底部导航栏的View,用于StatusBarHostLayout中运用
 */
class NavigationView extends View {
    private int mBarSize;
    public NavigationView(Context context) {
        this(context, null, 0);
    }
    public NavigationView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        StatusBarHostUtils.getNavigationBarHeight(this, new HeightValueCallback() {
            @Override
            public void onHeight(int height) {
                mBarSize = height;
            }
        });
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mBarSize);
        } else {
            setMeasuredDimension(0, 0);
        }
    }
    public int getBarSize() {
        return mBarSize;
    }
}

然后在自界说的布局中增加咱们的导航栏View

    //加载自界说的宿主布局
    if (mStatusView == null && mContentLayout == null) {
        setOrientation(LinearLayout.VERTICAL);
        mStatusView = new StatusView(mActivity);
        mStatusView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        addView(mStatusView);
        mContentLayout = new FrameLayout(mActivity);
        mContentLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f));
        addView(mContentLayout);
        mNavigationView = new NavigationView(mActivity);
        mNavigationView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        addView(mNavigationView);
    }

中心办法是替换掉 DecorView 中的 ContentView:

    private void replaceContentView() {
        Window window = mActivity.getWindow();
        ViewGroup contentLayout = window.getDecorView().findViewById(Window.ID_ANDROID_CONTENT);
        if (contentLayout.getChildCount() > 0) {
            //先找到DecorView的容器移除去现已设置的ContentView
            View contentView = contentLayout.getChildAt(0);
            contentLayout.removeView(contentView);
            ViewGroup.LayoutParams contentParams = contentView.getLayoutParams();
            //外部设置的ContentView增加到宿主中来
            mContentLayout.addView(contentView, contentParams.width, contentParams.height);
        }
        //再把整个宿主增加到Activity对应的DecorView中去
        contentLayout.addView(this, -1, -1);
    }

然后咱们暴露一些公共的办法供外界操作咱们自界说的导航栏:

 /**
     * 设置导航栏图片色彩为黑色
     */
    public StatusBarHostLayout setNavigatiopnBarIconBlack() {
        StatusBarHostUtils.setNavigationBarDrak(mActivity, true);
        return this;
    }
    /**
     * 设置导航栏图片色彩为白色
     */
    public StatusBarHostLayout setNavigatiopnBarIconWhite() {
        StatusBarHostUtils.setNavigationBarDrak(mActivity, false);
        return this;
    }
      /**
     * 设置自界说状态栏布局的布景色彩
     */
    public StatusBarHostLayout setNavigationBarBackground(int color) {
        if (mNavigationView != null)
            mNavigationView.setBackgroundColor(color);
        return this;
    }
    /**
     * 设置自界说状态栏布局的布景图片
     */
    public StatusBarHostLayout setNavigationBarBackground(Drawable drawable) {
        if (mNavigationView != null)
            mNavigationView.setBackground(drawable);
        return this;
    }
    /**
     * 设置自界说状态栏布局的透明度
     */
    public StatusBarHostLayout setNavigationBarBackgroundAlpha(int alpha) {
        if (mNavigationView != null) {
            Drawable background = mNavigationView.getBackground();
            if (background != null) {
                background.mutate().setAlpha(alpha);
            }
        }
        return this;
    }
    /**
     * 设置自界说导航栏的沉溺式
     */
    public StatusBarHostLayout setNavigationBarImmersive(boolean needImmersive, int color) {
        if (mNavigationView != null) {
            if (needImmersive) {
                mNavigationView.setVisibility(GONE);
            } else {
                mNavigationView.setVisibility(VISIBLE);
                mNavigationView.setBackgroundColor(color);
            }
        }
        return this;
    }

运用的时分咱们这样用:

   val hostLayout = StatusBarHost.inject(this)
            .setStatusBarBackground(startColor)
            .setStatusBarBlackText()
            .setNavigationBarBackground(startColor)
    //修正导航栏的图标色彩 - 深色
    fun btn07(view: View) {
        hostLayout.setNavigationBarIconBlack()
    }
    //修正导航栏的图标色彩 - 亮色
    fun btn08(view: View) {
        hostLayout.setNavigationBarIconWhite()
    }
    fun btn06(view: View) {
        hostLayout.setNavigationBarBackground(resources.getColor(R.color.teal_200))
    }          

其间的一些作用如下图所示,更多的示例代码能够检查源码:

状态栏的操作:

Android导航栏的处理-HostStatusLayout加入底部的导航栏适配
Android导航栏的处理-HostStatusLayout加入底部的导航栏适配

导航栏的操作:

Android导航栏的处理-HostStatusLayout加入底部的导航栏适配
Android导航栏的处理-HostStatusLayout加入底部的导航栏适配

状态栏与导航栏的沉溺式处理

Android导航栏的处理-HostStatusLayout加入底部的导航栏适配
Android导航栏的处理-HostStatusLayout加入底部的导航栏适配

状态栏与导航栏图片布景的设置

Android导航栏的处理-HostStatusLayout加入底部的导航栏适配

全面屏手机与老款的可动态躲藏导航栏的手机都能正确的判别是否有导航栏:

Android导航栏的处理-HostStatusLayout加入底部的导航栏适配

Android5.0的老款手机,不带内置导航栏的:

Android导航栏的处理-HostStatusLayout加入底部的导航栏适配

Android12三星手机滚动的作用:

Android导航栏的处理-HostStatusLayout加入底部的导航栏适配

总结

由于运用了 WindowInsetsController 的Api,所以本计划支撑Android5.0+版别。

有关更多的Demo与作用能够检查我的源码项目,点击检查,我会继续更新和优化。大家能够点个Star重视一波。

关于本文的Demo我也独自做了项目与Demo的作用,点击检查。

假如你想直接运用,我也现已上传到 MavenCentral ,直接依靠即可。

implementation "com.gitee.newki123456:status_host_layout:1.0.0"

常规,我如有讲解不到位或错漏的当地,期望同学们能够指出沟通

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

Ok,这一期就此结束。

Android导航栏的处理-HostStatusLayout加入底部的导航栏适配