通过FragmentTransaction添加的片段,fitsSystemWindows效果消失了。

58

我有一个带有导航抽屉和全屏 Fragment(顶部有图片,必须出现在 Lollipop 上半透明系统栏后面)的 Activity。虽然我有一个临时解决方案,即将 Fragment 通过简单地在活动的 XML 中使用 <fragment> 标签进行填充,看起来还不错。

然后我不得不用 <FrameLayout> 替换 <fragment> 并执行片段事务,现在片段不再出现在系统栏后面,尽管在所有所需层次结构中设置了 fitsSystemWindowstrue

我认为可能存在一些区别,在活动布局内部膨胀 <fragment> 与单独膨胀之间。我搜索了一些 KitKat 的解决方案,但这些解决方案都对我(Lollipop)无效。

activity.xml

<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                        xmlns:app="http://schemas.android.com/apk/res-auto"
                                        android:id="@+id/drawer_layout"
                                        android:layout_height="match_parent"
                                        android:layout_width="match_parent"
                                        android:fitsSystemWindows="true">

    <FrameLayout
            android:id="@+id/fragment_host"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true">

    </FrameLayout>

    <android.support.design.widget.NavigationView
            android:id="@+id/nav_view"
            android:layout_height="match_parent"
            android:layout_width="wrap_content"
            android:layout_gravity="start"
            android:fitsSystemWindows="true"/>

</android.support.v4.widget.DrawerLayout>

fragment.xml

<android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="224dp"
            android:fitsSystemWindows="true"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
...

activity.xml 这样设置时,它有效:

<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                        xmlns:app="http://schemas.android.com/apk/res-auto"
                                        android:id="@+id/drawer_layout"
                                        android:layout_height="match_parent"
                                        android:layout_width="match_parent"
                                        android:fitsSystemWindows="true">

    <fragment xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:id="@+id/fragment"
              android:name="com.actinarium.random.ui.home.HomeCardsFragment"
              tools:layout="@layout/fragment_home"
              android:layout_width="match_parent"
              android:layout_height="match_parent"/>

    <android.support.design.widget.NavigationView
            android:id="@+id/nav_view"
            android:layout_height="match_parent"
            android:layout_width="wrap_content"
            android:layout_gravity="start"
            android:fitsSystemWindows="true"/>

</android.support.v4.widget.DrawerLayout>
6个回答

100

当您使用<fragment>时,在Fragment的onCreateView中返回的布局将直接替换<fragment>标记(如果查看视图层次结构,您实际上永远不会看到<fragment>标记)。

因此,在<fragment>情况下,您需要

DrawerLayout
  CoordinatorLayout
    AppBarLayout
    ...
  NavigationView

cheesesquare类似的工作原理。这是因为如此博客文章所解释的那样,DrawerLayoutCoordinatorLayoutfitsSystemWindows应用有不同的规则——它们都使用它来插入其子视图,但还会在每个子视图上调用dispatchApplyWindowInsets()方法,从而允许它们访问fitsSystemWindows="true"属性。

这与默认布局(例如FrameLayout)的行为不同,当您使用fitsSystemWindows="true"时,它会消耗所有插页页边距,而不通知任何子视图(这是博客文章中“深度优先”的部分)。

因此,当您使用FrameLayout和FragmentTransactions替换<fragment>标签时,您的视图层次结构将变为:

DrawerLayout
  FrameLayout
    CoordinatorLayout
      AppBarLayout
      ...
  NavigationView

由于Fragment的视图被插入到FrameLayout中。该视图不知道如何将fitsSystemWindows传递给子视图,因此您的CoordinatorLayout永远无法看到该标志或执行其自定义行为。

修复问题实际上非常简单:用另一个CoordinatorLayout替换您的FrameLayout。这确保了fitsSystemWindows =“true”从Fragment传递到新填充的CoordinatorLayout

替代并同样有效的解决方案是创建FrameLayout的自定义子类并覆盖onApplyWindowInsets()以将调度传递到每个子项(在您的情况下只需一个),或使用ViewCompat.setOnApplyWindowInsetsListener()方法在代码中拦截调用并从那里调度(无需子类)。通常,写更少的代码最容易维护,因此除非您对此有强烈的感觉,否则我不会推荐这些方法来取代CoordinatorLayout的解决方案。


6
有了这些信息,我写了一个FrameLayout的子类来分发WindowInsets给其子View,从而实现了与你建议的CoordinatorLayout类似的效果。对于任何感兴趣的人:https://gist.github.com/Pkmmte/d8e983fb9772d2c91688。这是真正的解决方案。谢谢! - Pkmmte
1
@Pkmmte,我仍然无法让它与FT一起正常工作!有没有使用“WindowInsetsFrameLayout”的示例项目?第二个问题-它是否向下兼容到ICS? - WindRider
1
@WindRider 我没有一个样例工程可以展示,但它应该兼容KitKat及以上版本,因为它所做的只是将WindowInsets委托给它的子视图。我不认为这种行为在KK之前存在。 - Pkmmte
1
@ianhanniballake,一个演示项目将非常受欢迎,同时提供一个要点摘录,从CoordinatorLayout中提取insetlogic,以便应用到另一个布局(线性,相对,约束等)。简单地展示如何在顶部具有全宽度imageView的片段,一直延伸到状态栏,已经足够了 :) - Teovald
1
这似乎与新的“导航组件”不兼容。使用BottomNavigation和Navigation Controller时,如果重新选择BottomNavigation中的任何项目,则会忽略“fitsSystemWindows”。有没有什么解决方法?此处提供演示应用程序的参考链接:https://github.com/musooff/NavigationComponent-with-FitsSystetemWindow - musooff
显示剩余11条评论

22

我的问题和你的类似:我有一个底部导航栏,它正在替换内容片段。现在一些片段想要在状态栏上绘制(使用CoordinatorLayoutAppBarLayout),而其他片段则不需要(使用ConstraintLayoutToolbar)。

ConstraintLayout
  FrameLayout
    [the ViewGroup of your choice]
  BottomNavigationView

根据 ianhanniballake 的建议添加另一个 CoordinatorLayout 层并不是我想要的,所以我创建了一个自定义的 FrameLayout 来处理插图(就像他建议的那样),经过一段时间后,我找到了这个解决方案,它真的不需要太多代码:

activity_main.xml

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/content"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.app.WindowInsetsFrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</android.support.constraint.ConstraintLayout>

WindowInsetsFrameLayout.java

/**
 * FrameLayout which takes care of applying the window insets to child views.
 */
public class WindowInsetsFrameLayout extends FrameLayout {

    public WindowInsetsFrameLayout(Context context) {
        this(context, null);
    }

    public WindowInsetsFrameLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    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) {

            }
        });
    }

}

4

好的,经过多人指出fitsSystemWindows的使用方式不同,并且不应该在层级结构中的每个视图上使用,我进行了实验并从不同的视图中删除了该属性。

在从activity.xml中的每个节点中删除fitsSystemWindows后,我得到了预期的状态 =\


所以基本上你不知道它是什么,也不知道你不需要它? - BladeCoder
10
是的 :( 我不得不承认,我盲目地跟随了Cheesesquare样例;它在所有布局中都有这个属性。不会再发生了。我愚蠢地以为,通过使用设计库,我可以创建一个漂亮的应用程序,而不必深入Android复杂的布局内部。天哪,我曾经实现过一个自定义表达式语言解释器,它比Android的UI还不那么复杂。 - Actine
1
我不理解你的解决方案。你从activity.xml中删除了fitsSystemWindows,并将其留给了CoordinatorLayout,就像Cheesesquare示例一样?但是,你的导航抽屉是否会在状态栏后面绘制? - Pin
我最终用XML中的<fragment>标签替换了该事务。而且,如果片段中没有fitsSystemWindows属性,则无法按预期显示半透明状态栏。 - WindRider
如果我按照你说的做,由于某种原因,在状态栏下面什么也没有被绘制出来。你的活动主题是什么样的? - Pkmmte

3

另一种用Kotlin编写的方法:

问题:

您正在使用的FrameLayout未将fitsSystemWindows="true"传递给其子项:

<FrameLayout
    android:id="@+id/fragment_host"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true" />

解决方案:

扩展FrameLayout类,并覆盖函数onApplyWindowInsets(),将窗口插图传播到附加的片段:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class BetterFrameLayout : FrameLayout {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

    override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets {
        childCount.let {
            // propagates window insets to children's
            for (index in 0 until it) {
                getChildAt(index).dispatchApplyWindowInsets(windowInsets)
            }
        }
        return windowInsets
    }
}

使用此布局作为片段容器,而不是标准的FrameLayout

<com.foo.bar.BetterFrameLayout
    android:id="@+id/fragment_host"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true" />

额外信息:

如果您想了解更多关于此内容的信息,请查看Chris Banes的博客文章《成为一名优秀的窗户安装师》


一种相当不错的方法。但是,最好覆盖dispatchApplyWindowInsets而不是onApplyWindowInsets。ViewGroup的dispatchApplyWindowInsets的默认实现已经在子视图上调用了dispatchAppyWindowInsets,因此您最终会为每个子视图调用两次dispatchApplyWindowInsets。 - Robin Davies

3
另一个可怕的问题是在分派窗口插入时,第一个深度优先搜索中消耗了窗口插入的视图,防止所有其他视图在层次结构中看到窗口插入。
以下代码片段允许多个子项处理窗口插入。非常有用,如果您正在尝试将窗口插入应用于NavigationView(或CoordinatorLayout)之外的装饰。在所选ViewGroup中覆盖。
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
    if (!insets.isConsumed()) {

        // each child gets a fresh set of window insets
        // to consume.
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            WindowInsets freshInsets = new WindowInsets(insets);
            getChildAt(i).dispatchApplyWindowInsets(freshInsets);
        }
    }
    return insets; // and we don't.
}

还有其他的有用信息:

@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
      return insets.consume(); // consume without adding padding!
}

这允许此视图的普通子视图在没有窗口插入的情况下布局。


2
我去年创建了这个来解决这个问题:https://gist.github.com/cbeyls/ab6903e103475bd4d51b 编辑:请确保您先了解fitsSystemWindows的作用。当您将其设置在View上时,它基本上意味着:“将此View及其所有子项放置在状态栏下方和导航栏上方”。在顶部容器上设置此属性没有意义。

我尝试过这个,但在Lollipop上无法工作。此外,IDE显示super.fitSystemWindows自API 20以来已被弃用。 - Actine
也许问题出在我使用了来自appcompat-design库的CoordinatorLayout?它在底层有点混乱。我再次尝试了您的解决方案,现在当我的工具栏折叠时,我看到了两个状态栏 -_- - Actine
你试过这个吗? @Override protected boolean fitSystemWindows(@NonNull Rect insets) { windowInsets.set(insets); return super.fitSystemWindows(insets); } - BladeCoder
“双系统栏”消失了,但原始问题仍然存在,即视图不在系统栏后面而是在下方。 - Actine
1
是的,但如果您在任何父容器(DrawerLayout或FrameLayout)上应用android:fitsSystemWindows,它将不起作用。具有此属性的层次结构中的第一个视图会为其子项使用它。 - BladeCoder
显示剩余3条评论

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