Android導(dǎo)航欄適配的實現(xiàn)

和你一起終身學(xué) 習(xí),這里是程序員Android
經(jīng)典好文推薦,通過閱讀本文,您將收獲以下知識點(diǎn):
一、前言
二、導(dǎo)航欄的處理
三、 修改StatusHostLayout方案
四、總結(jié)
一、前言
在之前的文章中,大家比較關(guān)注宿主侵入的方式,并且有要求適配導(dǎo)航欄的操作。
其實大部分的應(yīng)用都只需要使用到狀態(tài)欄,導(dǎo)航欄由系統(tǒng)去管理,為什么不自己管理導(dǎo)航欄,就是導(dǎo)航欄的坑太多。
背景設(shè)置的坑,判斷是否存在的坑,手動設(shè)置隱藏顯示導(dǎo)航欄的坑,導(dǎo)航欄高度獲取的坑。
如果項目中確實需要用到操作導(dǎo)航欄怎么辦?
二、導(dǎo)航欄的處理
導(dǎo)航欄為什么難處理,因為之前的一些添加Flag的方案有些不實用,有兼容問題,也可以說手機(jī)廠商并沒有完全適配,導(dǎo)致兼容性有問題。
而我們通過 WindowInsetsController / WindowInsets 的一些方式則可以相對方便的操作導(dǎo)航欄。
那么是不是 WindowInsetsController / WindowInsets 的方式就完全兼容了呢?也并不是,只是相對好一點(diǎn),重要的功能能用而已。
下面介紹一下相對穩(wěn)定的一些操作方法。
2.1 判斷當(dāng)前是否顯示了導(dǎo)航欄
/**
* 當(dāng)前是否顯示了底部導(dǎo)航欄
*/
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) {
}
});
}
}
復(fù)制代碼
其實核心代碼是一樣的,只是區(qū)分了是否已經(jīng)onAttach了,防止在onCreate方法中調(diào)用的時候會報錯。
它的核心思路是和老版本的方法是相似的,只是老版本是從window中找到導(dǎo)航欄布局去判斷是否隱藏和顯示和判斷高度。而新版本通過WindowInset 的方式獲取導(dǎo)航欄對象相對比較穩(wěn)妥。
2.2 獲取導(dǎo)航欄的高度
/**
* 獲取底部導(dǎo)航欄的高度
*/
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) {
}
});
}
}
/**
* 老的方法獲取導(dǎo)航欄的高度
*/
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;
}
復(fù)制代碼
新版的方法和老版本的方法都定義了,通常我們使用 WindowInsets 的方式即可獲取到導(dǎo)航欄對象,然后去獲取它的高度。
而老版本的方式則是通過獲取系列內(nèi)置的一個高度值,而一些手機(jī)并不會按這個高度設(shè)置導(dǎo)航欄高度,所以獲取出來的值則是錯誤的。
如下圖所示:

image.png
2.3 導(dǎo)航欄的隱藏與沉浸式處理
在一些應(yīng)用需要全屏的時候,我們需要隱藏導(dǎo)航欄(是的,你無法返回了)。
/**
* 顯示隱藏底部導(dǎo)航欄(注意不是沉浸式效果)
*/
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);
}
}
}
復(fù)制代碼
而在一些常規(guī)的頁面,我們?nèi)绻胂駹顟B(tài)欄一樣獲取沉浸式體驗,我們則是不同的處理邏輯:
/**
* 5.0以上-設(shè)置NavigationBar底部導(dǎo)航欄的沉浸式
*/
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);
}
}
復(fù)制代碼
我們把導(dǎo)航欄常用的一些操作理清之后,我們再來看 StatusHostLayout 這樣的宿主方案如何幫助我們管理導(dǎo)航欄。
三、 修改StatusHostLayout方案
前文我們講到過狀態(tài)欄的管理,如果加入導(dǎo)航欄的管理,我們需要做哪些操作?
先理清一下思路:
-
定義一個自定義的ViewGroup,內(nèi)部順序排列狀態(tài)欄,內(nèi)容容器,導(dǎo)航欄三個布局。
-
我們需要強(qiáng)制設(shè)置狀態(tài)欄和導(dǎo)航欄的沉浸式,讓我們自己的狀態(tài)欄.導(dǎo)航欄View的布局展示出來。
-
自定義狀態(tài)欄View,與導(dǎo)航欄View,我們只需要獲取到正確的高度,然后測量的時候定死指定的高度即可。
-
我們可以以View的形式來操作自定義導(dǎo)航欄/狀態(tài)欄的背景,圖片,顯示隱藏等操作。
-
把我們DecorView中的跟視圖替換為我們自定義的布局。
-
暴露一個inject方法注入到指定的Activity中去,并提供自定義布局的對象。
之前狀態(tài)欄的邏輯已經(jīng)做好了,現(xiàn)在我們只需要處理導(dǎo)航欄的邏輯。我們定義好上面的一些導(dǎo)航欄操作工具類方法。
先定義一個自己的導(dǎo)航欄View,只需要處理高度即可。
/**
* 自定義底部導(dǎo)航欄的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;
}
}
復(fù)制代碼
然后在自定義的布局中添加我們的導(dǎo)航欄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);
}
復(fù)制代碼
核心方法是替換掉 DecorView 中的 ContentView:
private void replaceContentView() {
Window window = mActivity.getWindow();
ViewGroup contentLayout = window.getDecorView().findViewById(Window.ID_ANDROID_CONTENT);
if (contentLayout.getChildCount() > 0) {
//先找到DecorView的容器移除掉已經(jīng)設(shè)置的ContentView
View contentView = contentLayout.getChildAt(0);
contentLayout.removeView(contentView);
ViewGroup.LayoutParams contentParams = contentView.getLayoutParams();
//外部設(shè)置的ContentView添加到宿主中來
mContentLayout.addView(contentView, contentParams.width, contentParams.height);
}
//再把整個宿主添加到Activity對應(yīng)的DecorView中去
contentLayout.addView(this, -1, -1);
}
復(fù)制代碼
然后我們暴露一些公共的方法供外界操作我們自定義的導(dǎo)航欄:
/**
* 設(shè)置導(dǎo)航欄圖片顏色為黑色
*/
public StatusBarHostLayout setNavigatiopnBarIconBlack() {
StatusBarHostUtils.setNavigationBarDrak(mActivity, true);
return this;
}
/**
* 設(shè)置導(dǎo)航欄圖片顏色為白色
*/
public StatusBarHostLayout setNavigatiopnBarIconWhite() {
StatusBarHostUtils.setNavigationBarDrak(mActivity, false);
return this;
}
/**
* 設(shè)置自定義狀態(tài)欄布局的背景顏色
*/
public StatusBarHostLayout setNavigationBarBackground(int color) {
if (mNavigationView != null)
mNavigationView.setBackgroundColor(color);
return this;
}
/**
* 設(shè)置自定義狀態(tài)欄布局的背景圖片
*/
public StatusBarHostLayout setNavigationBarBackground(Drawable drawable) {
if (mNavigationView != null)
mNavigationView.setBackground(drawable);
return this;
}
/**
* 設(shè)置自定義狀態(tài)欄布局的透明度
*/
public StatusBarHostLayout setNavigationBarBackgroundAlpha(int alpha) {
if (mNavigationView != null) {
Drawable background = mNavigationView.getBackground();
if (background != null) {
background.mutate().setAlpha(alpha);
}
}
return this;
}
/**
* 設(shè)置自定義導(dǎo)航欄的沉浸式
*/
public StatusBarHostLayout setNavigationBarImmersive(boolean needImmersive, int color) {
if (mNavigationView != null) {
if (needImmersive) {
mNavigationView.setVisibility(GONE);
} else {
mNavigationView.setVisibility(VISIBLE);
mNavigationView.setBackgroundColor(color);
}
}
return this;
}
復(fù)制代碼
使用的時候我們這樣用:
val hostLayout = StatusBarHost.inject(this)
.setStatusBarBackground(startColor)
.setStatusBarBlackText()
.setNavigationBarBackground(startColor)
//修改導(dǎo)航欄的圖標(biāo)顏色 - 深色
fun btn07(view: View) {
hostLayout.setNavigationBarIconBlack()
}
//修改導(dǎo)航欄的圖標(biāo)顏色 - 亮色
fun btn08(view: View) {
hostLayout.setNavigationBarIconWhite()
}
fun btn06(view: View) {
hostLayout.setNavigationBarBackground(resources.getColor(R.color.teal_200))
}
復(fù)制代碼
其中的一些效果如下圖所示,更多的示例代碼可以查看源碼:
狀態(tài)欄的操作:

image.png

image.png
導(dǎo)航欄的操作:

image.png

image.png
狀態(tài)欄與導(dǎo)航欄的沉浸式處理

image.png

image.png
狀態(tài)欄與導(dǎo)航欄圖片背景的設(shè)置

image.png
全面屏手機(jī)與老款的可動態(tài)隱藏導(dǎo)航欄的手機(jī)都能正確的判斷是否有導(dǎo)航欄:

image.png
Android5.0的老款手機(jī),不帶內(nèi)置導(dǎo)航欄的:

image.png
Android12三星手機(jī)滾動的效果:

image.png
四、總結(jié)
由于使用了 WindowInsetsController 的Api,所以本方案支持Android5.0+版本。
有關(guān)更多的Demo與效果可以查看我的源碼項目,點(diǎn)擊查看,我會持續(xù)更新和優(yōu)化。大家可以點(diǎn)個Star關(guān)注一波。
關(guān)于本文的Demo我也單獨(dú)做了項目與Demo的效果,點(diǎn)擊查看。
如果你想直接使用,我也已經(jīng)上傳到 MavenCentral ,直接依賴即可。
implementation "com.gitee.newki123456:status_host_layout:1.0.0"
復(fù)制代碼
慣例,我如有講解不到位或錯漏的地方,希望同學(xué)們可以指出交流。
作者:newki
鏈接:https://juejin.cn/post/7150088571313979399
友情推薦:
至此,本篇已結(jié)束。轉(zhuǎn)載網(wǎng)絡(luò)的文章,小編覺得很優(yōu)秀,歡迎點(diǎn)擊閱讀原文,支持原創(chuàng)作者,如有侵權(quán),懇請聯(lián)系小編刪除,歡迎您的建議與指正。同時期待您的關(guān)注,感謝您的閱讀,謝謝!
點(diǎn)擊閱讀原文,為大佬點(diǎn)贊!
