Android在替换碎片时,fitsSystemWindows不起作用

22

我有一个SingleFragmentActivity,它的目的仅是在内部容纳和替换片段。

布局看起来像这样:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    tools:context=".SingleFragmentActivity"
    >

    <include layout="@layout/toolbar"/>

    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
</LinearLayout>

我正在替换FrameLayout中的Fragments。当我在Fragment布局上将fitsSystemWindows设置为true时,它没有响应。实际上,它只有在创建Activity时才起作用,但一旦我替换FrameLayout中的Fragment,则会忽略fitsSystemWindows参数,并且布局位于状态栏和导航栏下方。

我在这里找到了一些解决方案,使用了已弃用的方法的自定义FrameLayout,但出于某种原因,它对我不起作用(与普通FrameLayout相同的结果),而且我也不喜欢使用已弃用的方法的想法。


你知道fitsSystemWindows有什么作用吗? - JAAD
希望我理解正确,基本上当它设置为true时,视图及其所有子项都不应在系统窗口下方的状态栏或导航栏中显示。当其设置为false时,则应在系统窗口下方显示。因此,基本上我可以设置我想要在状态栏和导航栏之间呈现整个Fragment布局(fitsSystemWindow = true),并在状态栏和导航栏下方呈现背景图像(fitsSystemWindow = false)。我可以使它完全按照我的意愿进行,直到我不替换片段为止。 - Stepan Sanda
2
我曾经遇到过同样的问题。最终我创建了一个FrameLayout,用于保存窗口插图并将其重新分配给新的子项。我将代码放在了Github gist中。如果这对你有用,请告诉我! - paep3nguin
4个回答

15

你的 FrameLayout 不知道窗口嵌入尺寸,因为它的父级 - LinearLayout 没有传递给它。作为解决方法,你可以继承 LinearLayout 并手动将嵌入传递给子元素:

@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    int childCount = getChildCount();
    for (int index = 0; index < childCount; index++)
        getChildAt(index).dispatchApplyWindowInsets(insets); // let children know about WindowInsets

    return insets;
}
你可以查看我的这个答案,它会详细说明这个是如何工作的,以及如何使用ViewCompat.setOnApplyWindowInsetsListener API。

1
在支持 API 20 以下的情况下,始终使用 ViewCompat.setOnApplyWindowInsetsListener,因为 https://dev59.com/IJPfa4cB1Zd3GeqPDHHe#42380189。 - Eugen Pechanec
@azizbekian,你的解释很有用,但它仍然不能帮助我解决更改片段的问题。当片段首次打开时,fitSystemWindows在片段布局中起作用,但如果片段替换另一个片段,则会被忽略。我谈论的是非根视图上的fitSystemWindows-第二级。在第一级中,它可以工作。在活动和片段中的所有层次结构视图都是Coordinator布局或实现了您答案中的方法。 - lobzik
@lobzik,请检查您的视图层次结构并找出不将插图分派给其子项的父视图。您可以使用ViewCompat.setOnApplyWindowInsetsListener,并检查哪个ViewGroup返回0作为插图大小。 - azizbekian
@azizbekian,如果它在片段被替换之前运行良好,那么如何使一个视图不分派插入?从调试中我看到,在替换另一个片段时,我的根布局在片段中没有接收到onApplyWindowInsets。但是,当它第一次打开时确实会得到这个调用。我不知道该如何解决它。 - lobzik
1
这似乎无法与新的“导航组件”一起使用。使用BottomNavigation和Navigation Controller,如果我重新选择BottomNavigation中的任何项,则会忽略“fitsSystemWindows”。有什么解决方法吗?此处提供一个参考链接 ,其中包含一个演示应用程序。 - musooff
显示剩余5条评论

3

你也可以构建一个自定义的 WindowInsetsFrameLayout,并使用一个 OnHierarchyChangedListener 来请求再次应用插图:

public WindowInsetsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    // Look for replaced fragments and apply the insets again.
    setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
        @Override
        public void onChildViewAdded(View parent, View child) {
            requestApplyInsets();
        }

        @Override
        public void onChildViewRemoved(View parent, View child) {

        }
    });
}

请查看这个详细的答案:https://dev59.com/E10Z5IYBdhLWcg3wrB6H#47349880


就我而言,我也不得不覆盖“onApplyWindowInsets”方法。 - iscariot

1
我认为问题的关键在于 onApplyWindowInsets 在片段视图层次结构附加之前被调用。一个有效的解决方案是在片段视图层次结构中的某个视图上获取以下覆盖。
  @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        // force window insets to get re-applied if we're being attached by a fragment.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
            requestApplyInsets();
        } else {
            //noinspection deprecation
            requestFitSystemWindows();
        }
    }

如果您不必使用CoordinatorLayout,则完整的解决方案如下。确保在更高级别的视图中没有出现fitSystemWindows=true。也许其他地方也没有。我怀疑(但不确定)consumeSystemWindowInsets会吃掉视图树中后面的视图的插页。
package com.twoplay.xcontrols;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowInsets;
import android.widget.FrameLayout;

public class FitSystemWindowsLayout extends FrameLayout {
    private boolean mFit = true;

    public FitSystemWindowsLayout(final Context context) {
        super(context);
        init();
    }

    public FitSystemWindowsLayout(final Context context, final AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public FitSystemWindowsLayout(final Context context, final AttributeSet attrs, final int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setFitsSystemWindows(true);
    }

    public boolean isFit() {
        return mFit;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
            requestApplyInsets();
        } else {
            //noinspection deprecation
            requestFitSystemWindows();
        }

    }

    public void setFit(final boolean fit) {
        if (mFit == fit) {
            return;
        }

        mFit = fit;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
            requestApplyInsets();
        } else {
            //noinspection deprecation
            requestFitSystemWindows();
        }
    }

    @SuppressWarnings("deprecation")
    @Override
    protected boolean fitSystemWindows(final Rect insets) {
        if (mFit) {
            setPadding(
                    insets.left,
                    insets.top,
                    insets.right,
                    insets.bottom
            );
            return true;
        } else {
            setPadding(0, 0, 0, 0);
            return false;
        }
    }

    @TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
    @Override
    public WindowInsets onApplyWindowInsets(final WindowInsets insets) {
        if (mFit) {
            setPadding(
                    insets.getSystemWindowInsetLeft(),
                    insets.getSystemWindowInsetTop(),
                    insets.getSystemWindowInsetRight(),
                    insets.getSystemWindowInsetBottom()
            );
            return insets.consumeSystemWindowInsets();
        } else {
            setPadding(0, 0, 0, 0);
            return insets;
        }
    }
}

怀疑,而非事实:在整个层次结构中只有一个视图有机会使用窗口插入,除非您的层次结构中有CoordinatorLayout,允许多个直接子级具有FitSystemWindow=true。如果您确实有一个CoordinatorLayout,则您的里程可能会有所不同。
整个Android中似乎都存在这种混乱的功能。

我应该补充说明CoordinatorLayout会缓存它接收到的最后一个WindowInsets,如果接收到后续的WindowInsets,例如通过View#requestApplyInsets(),则“新”的插入将与缓存的插入相等,因此它不会将其分派给其子Behavior实例,因此在层次结构中位于其下方的任何内容都不会被通知。在这种情况下,您需要手动分派。 - technicalflaw

0

a) 你可以在片段中使用CoordinatorLayout作为根视图

或者

b) 你可以创建一个自定义的线性布局,调用requestApplyInsets方法,并将其作为片段中的根视图使用

class WindowInsetsLinearLayout : LinearLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        ViewCompat.requestApplyInsets(this)
    }
}

然后在Fragment中,您可以捕获应用插入

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    ViewCompat.setOnApplyWindowInsetsListener(root_layout) { _, insets ->
        //appbar.setPadding(insets.systemWindowInsetLeft, insets.systemWindowInsetTop, 0, 0)
        insets.consumeSystemWindowInsets()
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接