注册

重复setContentView后fitsSystemWindows失效

项目中有个沉浸式的activity,在调用setContentView切换布局的时候fitsSystemWindows失效了,效果如图:


demo.gif


Activity代码:



class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
immerse()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

private fun immerse() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val decorView = window.decorView
decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.statusBarColor = Color.TRANSPARENT
}
}

fun reload(view: View) {
setContentView(R.layout.activity_main)
}
}

布局代码:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
android:gravity="center_horizontal"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:scaleType="centerCrop"
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="400dp"
android:src="@drawable/avatar"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:layout_marginTop="20dp"
android:onClick="reload"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="reload"
android:textAllCaps="false"/>
</LinearLayout>

首先看下fitsSystemWindows起到的作用


        <!-- Boolean internal attribute to adjust view layout based on
system windows such as the status bar.
If true, adjusts the padding of this view to leave space for the system windows.
Will only take effect if this view is in a non-embedded activity. -->

<attr name="fitsSystemWindows" format="boolean" />

这个属性用于根据系统窗口(如状态栏)来调整视图的布局。如果为true,则调整此视图的padding来为系统窗口留出空间,也就是说视图布局的内容不会扩展到任务栏中


正常情况下,什么时候会触发fitsSystemWindows的padding调整?


ViewRootImpl首次绘制的时候会调用dispatchApplyInsets方法,将WindowInset(窗口内容的插入,包括状态栏,导航栏,键盘等,可以理解为这些它们所占窗口的大小)分发给decorView,最终会分发到到上述布局中的根布局LinearLayout的fitSystemWindowsInt方法完成padding的设置,LinearLayout没有重写此方法,最终调用的还是View的fitSystemWindowsInt


ViewRootImpl

private void performTraversals() {
......
//首次绘制判断,host为decorView
if (mFirst) {
......
dispatchApplyInsets(host);
}
......
//其他条件触发,这个标记位在下文会用到
if (...... || mApplyInsetsRequested){
dispatchApplyInsets(host)
}
......
}

public void dispatchApplyInsets(View host) {
......
WindowInsets insets = getWindowInsets(true);
host.dispatchApplyWindowInsets(insets);
......
}

View

private boolean fitSystemWindowsInt(Rect insets) {
//判断fitSystemWindows是否为true
if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
Rect localInsets = sThreadLocal.get();
boolean res = computeFitSystemWindows(insets, localInsets);
applyInsets(localInsets);
return res;
}
return false;
}

private void applyInsets(Rect insets) {
mUserPaddingStart = UNDEFINED_PADDING;
mUserPaddingEnd = UNDEFINED_PADDING;
mUserPaddingLeftInitial = insets.left;
mUserPaddingRightInitial = insets.right;
internalSetPadding(insets.left, insets.top, insets.right, insets.bottom);
}

protected void internalSetPadding(int left, int top, int right, int bottom) {
......
//设置padding
......

//如果padding改变了,重新布局
if (changed) {
requestLayout();
invalidateOutline();
}
}

为什么重新setContentView之后没有为新的视图设置padding?


当我们调用setContentView重新设置布局时,activity对应的window已经被添加到WindowManager中了,ViewRootImpl不会重新创建,但是布局是重新加载并实例化视图了。此时ViewRootImpl的首次绘制判断不成立,不会将WindowInset分发给新加载的布局,因此新的视图没有设置顶部的padding,绘制的时候也就跑到了状态栏中去了


ActivityThread

public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
......
if (r.window == null && !a.mFinished && willBeVisible) {
......
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//ViewRootImpl创建的起点
wm.addView(decor, l);
}
......
}
......
}

应该怎样让ViewRootImpl重新分发WindowInset


从上文中ViewRootImpl调用dispatchApplyInsets的地方可以看到,mApplyInsetsRequested也能影响是否调用该方法,可以从这个标志位入手。分析代码发现,调用ViewrequestFitSystemWindowsrequestApplyInsets方法可以向上调用到ViewRootImpl的同名方法中,在这个方法中会将mApplyInsetsRequested设为true,并调用scheduleTraversals触发界面绘制。


View

@Deprecated
public void requestFitSystemWindows() {
//最终会调用到ViewRootImpl中去
if (mParent != null) {
mParent.requestFitSystemWindows();
}
}

public void requestApplyInsets() {
requestFitSystemWindows();
}

ViewRootImpl

public void requestFitSystemWindows() {
checkThread();
mApplyInsetsRequested = true;
scheduleTraversals();
}

demo中的reload方法修改为如下可以解决此问题


    fun reload(view: View) {
val root = layoutInflater.inflate(R.layout.activity_main, null)
setContentView(root)
//使用此方法做版本兼容,最终还是会调用到 View.requestFitSystemWindows()
ViewCompat.requestApplyInsets(root)
}

作者:今天想吃什么
链接:https://juejin.cn/post/7091260989504815112
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册