沉浸式布局能够使得状态栏和内容区域更加融合,带来更加美好的视觉效果。
未处理的效果:
我们要的效果:
我们要修改状态栏的颜色,安卓5.0的时候已经提供了相关API。但是我们同时也需要将状态栏的字体颜色设置成黑色才可以,否则状态栏文字就看不见。状态栏字体颜色的修改直到6.0才提供了相关API,所以对沉浸式的适配我们基于Android6.0。
适配状态栏的工具方法如下:
package com.znq.demo.utils;
import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
public class StatusBar{
public static void fitSystemBar(Activity activity){
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
return;
}
Window window = activity.getWindow();
View decorView = window.getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LAYOUT_STABLE|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
}
}
适配状态栏,首先判断是否为6.0之上的设备,如果低于6.0就不做操作,直接返回;否则获取decorView,调用decorView的setSystemUiVisibility,为decorView增加对应的flag:
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:能够使得我们的布局页面延伸到状态栏之下,但不会隐藏状态栏。View.SYSTEM_UI_FLAG_LAYOUT_STABLE:无论底部导航键是显示还是隐藏,都能保证页面布局在状态栏执行,同时保证布局宽高匹配屏幕尺寸。View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR:将状态栏设置为白底黑字的样式
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN与View.SYSTEM_UI_FLAG_FULLSCREEN区别?
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:能够使得我们的布局页面延伸到状态栏之下,但不会隐藏状态栏。也就相当于状态栏是遮盖在布局之上。View.SYSTEM_UI_FLAG_FULLSCREEN:能够使得页面的布局延伸到状态栏,但是会隐藏状态栏。它和WindowManager.LayoutParams.FLAG_FULLSCREEN作用一样。
指定了decorView的Flag还需要处理下window。首先为Widnow增加FLag:WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,开启Wondow对状态栏背景进行绘制。开启Window对状态栏绘制之后,就需要指定下状态栏的颜色Color.TRANSPARENT,如果不指定,Widow的状态栏就是灰色的。这样就处理完成了。
在需要适配状态栏的Activity的onCreate方法应用该工具方法之后,页面的布局就能延伸到状态栏之下,但这个时候页面的文字就会和状态栏的文字重叠 ,所以还需要对每个页面的根布局设置属性android:fitSystenWindows="true"。
NavHostFragment适配问题
运行起来发现首页的状态栏展示正常,但是沙发页面的状态栏文字和页面文字还是重叠。
android:fitsSystemWindows="true"这个属性的作用就是给每个页面添加一个上下左右的内间距,使得页面能够适应状态栏。首页和沙发页面都是添加都MainActivity的NavHostFragment里面的,问题也就处在这里:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="56dp"
android:tag="nav_host_fragment"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
...
在NavHostFragnent的onCreateView方法中,会创建一个FrameLayout:
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FrameLayout frameLayout = new FrameLayout(inflater.getContext());
// When added via XML, this has no effect (since this FrameLayout is given the ID
// automatically), but this ensures that the View exists as part of this Fragment's View
// hierarchy in cases where the NavHostFragment is added programmatically as is required
// for child fragment transactions
frameLayout.setId(getId());
return frameLayout;
}
FrameLayout是继承自ViewGroup,问题也就在ViewGroup中。ViewGrop中有个分发适应状态栏的方法dispatchApplyWindowInsets,在这个分发的过程如果WindowInsets没有被消费才回去分发,在分发的过程中,通过dispatchApplyWindowInsets方法将事件分发给子View,判断了ViewGroup的子View 如果将WindowInsets消费了,就直接return,后面的子View就不会收到dispatchApplyWindowInsets事件。也就是只有首页的布局被加入到NavHostFragment创建的FrameLayout可以收到适配状态栏的分发方法dispatchApplyWindowInsets,沙发页面的布局在被加入到NavHostFragment创建的FrameLayout的时候没有调用到dispatchApplyWindowInsets方法,自然无法设置状态栏间隔,导致页面布局文字和系统状态栏文字重叠。
ViewGroup中的dispatchApplyWindowInsets方法:
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
if (!insets.isConsumed()) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
insets = getChildAt(i).dispatchApplyWindowInsets(insets);
if (insets.isConsumed()) {//Android 10之后就没有这个判断了
break;
}
}
}
return insets;
}
ViewGroup中的dispatchApplyWindowInsets的方法是在ViewRootImpl的ViewGroup中的dispatchApplyInsets被调用的。ViewRootImpl的dispatchApplyInsets如下:
void dispatchApplyInsets(View host) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchApplyInsets");
WindowInsets insets = getWindowInsets(true /* forceConstruct */);
final boolean dispatchCutout = (mWindowAttributes.layoutInDisplayCutoutMode
== LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS);
if (!dispatchCutout) {
// Window is either not laid out in cutout or the status bar inset takes care of
// clearing the cutout, so we don't need to dispatch the cutout to the hierarchy.
insets = insets.consumeDisplayCutout();
}
host.dispatchApplyWindowInsets(insets);
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
ViewRootImpl的dispatchApplyInsets这个方法在Activity的setContentView时候会被调用。
明白了问题原因,解决起来就容易了。解决办法:
- 首先自定义View,继承FrameLayout,覆写dispatchApplyWindowInsets方法:在子View消费了WindowInsets后仍要将适配状态栏的事件分发给下面的子View
- 完成上面步骤后,再需要复写addView方法:在addView方法中请求ViewRootImpl重新分发适配状态栏的方法
具体的代码实现如下:
package com.znq.demo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class NavHostFrameLayout extends FrameLayout {
public NavHostFrameLayout(@NonNull Context context) {
super(context);
}
public NavHostFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public NavHostFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public NavHostFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void addView(View child) {
super.addView(child);
requestApplyInsets();
}
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
insets = super.dispatchApplyWindowInsets(insets);
if (!insets.isConsumed()) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
insets = getChildAt(i).dispatchApplyWindowInsets(insets);
}
}
return insets;
}
}
这样无论先添加还是后添加的View都能收到适配状态栏的事件。
重写完的布局文件还需要被应用了才会起作用。这就需要重写NavHostFragment的onCreateView方法,将其创建的FrameLayout替换成上面处理过的NavHostFrameLayout。
package com.znq.demo.view;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
public class FixedNavHostFragment extends NavHostFragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
NavHostFrameLayout frameLayout = new NavHostFrameLayout(inflater.getContext());
// When added via XML, this has no effect (since this FrameLayout is given the ID
// automatically), but this ensures that the View exists as part of this Fragment's View
// hierarchy in cases where the NavHostFragment is added programmatically as is required
// for child fragment transactions
frameLayout.setId(getId());
return frameLayout;
}
}
最后,将MainActivity布局文件之中的NavHostFragment替换成上面的FixedNavHostFragment就大功告成了。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment"
android:name="com.znq.demo.view.FixedNavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="56dp"
android:tag="nav_host_fragment"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />