Android導航欄的處理-HostStatusLayout加入底部的導航欄適配

語言: CN / TW / HK

theme: smartblue highlight: agate


持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第4天,點擊查看活動詳情

前言

在之前的文章中,大家比較關注宿主侵入的方式,並且有要求適配導航欄的操作。

其實大部分的應用都只需要使用到狀態欄,導航欄由系統去管理,為什麼不自己管理導航欄,就是導航欄的坑太多。

背景設置的坑,判斷是否存在的坑,手動設置隱藏顯示導航欄的坑,導航欄高度獲取的坑。

如果項目中確實需要用到操作導航欄怎麼辦?

導航欄的處理

導航欄為什麼難處理,因為之前的一些添加Flag的方案有些不實用,有兼容問題,也可以説手機廠商並沒有完全適配,導致兼容性有問題。

而我們通過 WindowInsetsController / WindowInsets 的一些方式則可以相對方便的操作導航欄。

那麼是不是 WindowInsetsController / WindowInsets 的方式就完全兼容了呢?也並不是,只是相對好一點,重要的功能能用而已。

下面介紹一下相對穩定的一些操作方法。

判斷當前是否顯示了導航欄: ```java /* * 當前是否顯示了底部導航欄 / 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 的方式獲取導航欄對象相對比較穩妥。

獲取導航欄的高度:

```java /* * 獲取底部導航欄的高度 / 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 的方式即可獲取到導航欄對象,然後去獲取它的高度。

而老版本的方式則是通過獲取系列內置的一個高度值,而一些手機並不會按這個高度設置導航欄高度,所以獲取出來的值則是錯誤的。

如下圖所示:

導航欄的隱藏與沉浸式處理:

在一些應用需要全屏的時候,我們需要隱藏導航欄(是的,你無法返回了)。

```java /* * 顯示隱藏底部導航欄(注意不是沉浸式效果) / 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);
        }
    }
}

```

而在一些常規的頁面,我們如果想像狀態欄一樣獲取沉浸式體驗,我們則是不同的處理邏輯:

```java /* * 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,只需要處理高度即可。

```java /* * 自定義底部導航欄的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

```java //加載自定義的宿主佈局 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: ```java 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);
}

```

然後我們暴露一些公共的方法供外界操作我們自定義的導航欄: ```java /* * 設置導航欄圖片顏色為黑色 / 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;
}

```

使用的時候我們這樣用:

```kotlin 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))
}

```

其中的一些效果如下圖所示,更多的示例代碼可以查看源碼:

狀態欄的操作:

導航欄的操作:

狀態欄與導航欄的沉浸式處理

狀態欄與導航欄圖片背景的設置

全面屏手機與老款的可動態隱藏導航欄的手機都能正確的判斷是否有導航欄:

host_layout_01.gif

Android5.0的老款手機,不帶內置導航欄的:

device-2022-09-30-095520 00_00_00-00_00_30.gif

Android12三星手機滾動的效果:

總結

由於使用了 WindowInsetsController 的Api,所以本方案支持Android5.0+版本。

有關更多的Demo與效果可以查看我的源碼項目,點擊查看,我會持續更新和優化。大家可以點個Star關注一波。

關於本文的Demo我也單獨做了項目與Demo的效果,點擊查看

如果你想直接使用,我也已經上傳到 MavenCentral ,直接依賴即可。

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

慣例,我如有講解不到位或錯漏的地方,希望同學們可以指出交流。

如果感覺本文對你有一點點的啟發,還望你能點贊支持一下,你的支持是我最大的動力。

Ok,這一期就此完結。

「其他文章」