58

I have an Activity with navigation drawer and full-bleed Fragment (with image in the top that must appear behind translucent system bar on Lollipop). While I had an interim solution where the Fragment was inflated by simply having <fragment> tag in Activity's XML, it looked fine.

Then I had to replace <fragment> with <FrameLayout> and perform fragment transactions, and now the fragment does not appear behind the system bar anymore, despite fitsSystemWindows is set to true across all required hierarchy.

I believe there might be some difference between how <fragment> gets inflated within Activity's layout vs on its own. I googled and found some solutions for KitKat, but neither of those worked for me (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">
...

It worked when activity.xml was this way:

<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>
Actine
  • 2,867
  • 2
  • 25
  • 38

6 Answers6

100

When you use <fragment>, the layout returned in your Fragment's onCreateView is directly attached in place of the <fragment> tag (you'll never actually see a <fragment> tag if you look at your View hierarchy.

Therefore in the <fragment> case, you have

DrawerLayout
  CoordinatorLayout
    AppBarLayout
    ...
  NavigationView

Similar to how cheesesquare works. This works because, as explained in this blog post, DrawerLayout and CoordinatorLayout both have different rules on how fitsSystemWindows applies to them - they both use it to inset their child Views, but also call dispatchApplyWindowInsets() on each child, allowing them access to the fitsSystemWindows="true" property.

This is a difference from the default behavior with layouts such as FrameLayout where when you use fitsSystemWindows="true" is consumes all insets, blindly applying padding without informing any child views (that's the 'depth first' part of the blog post).

So when you replace the <fragment> tag with a FrameLayout and FragmentTransactions, your view hierarchy becomes:

DrawerLayout
  FrameLayout
    CoordinatorLayout
      AppBarLayout
      ...
  NavigationView

as the Fragment's view is inserted into the FrameLayout. That View doesn't know anything about passing fitsSystemWindows to child views, so your CoordinatorLayout never gets to see that flag or do its custom behavior.

Fixing the problem is actually fairly simple: replace your FrameLayout with another CoordinatorLayout. This ensures the fitsSystemWindows="true" gets passed onto the newly inflated CoordinatorLayout from the Fragment.

Alternate and equally valid solutions would be to make a custom subclass of FrameLayout and override onApplyWindowInsets() to dispatch to each child (in your case just the one) or use the ViewCompat.setOnApplyWindowInsetsListener() method to intercept the call in code and dispatch from there (no subclass required). Less code is usually the easiest to maintain, so I wouldn't necessarily recommend going these routes over the CoordinatorLayout solution unless you feel strongly about it.

ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • 6
    This is the real solution. Thanks! With this information in mind, I wrote a subclass of FrameLayout that dispatches WindowInsets to its children to achieve a similar effect as CoordinatorLayout as you suggested. For anyone interested: https://gist.github.com/Pkmmte/d8e983fb9772d2c91688 – Pkmmte Dec 17 '15 at 23:05
  • Thank you Ian! I just came here to answer this question with the reference to your yesterday's post, but you did this already (: – Actine Dec 18 '15 at 21:40
  • 1
    @Pkmmte, I still can't get it to work with FT's! Is there some sample project using `WindowInsetsFrameLayout`? And 2nd question - is it compatible down to ICS? – WindRider Jan 23 '16 at 19:37
  • 1
    @WindRider I don't really have a sample project to show but it should be compatible on KitKat and above since all it does is delegate the WindowInsets to its children. I don't think this behavior exists pre-KK. – Pkmmte Feb 08 '16 at 22:42
  • 1
    @ianhanniballake a demo project would be very welcome, along with a gist extracting the insetlogic from CoordinatorLayout so it can be applied to another Layout (Linear, Relative, Constraint, ....) Simply showing how to have a fragment with a full width imageView on top peaking all the way to the statusbar would be more than enough :) – Teovald Jul 03 '16 at 18:26
  • 1
    This seems to not work with new `Navigation Component`. With BottomNavigation and Navigation Controller, if I reselect any item in BottomNavigation, the `fitsSystemWindows` is getting ignored. Any fix for that? For a reference [here](https://github.com/musooff/NavigationComponent-with-FitsSystetemWindow) is an demo application – musooff May 20 '19 at 03:24
  • @musooff did you find a solution? – Pav Sidhu Sep 10 '19 at 17:00
  • @PavSidhu - Navigation 2.2.0 takes care of this for you – ianhanniballake Sep 11 '19 at 13:35
  • @ianhanniballake that's odd, since I'm using 2.2.0-alpha02 – Pav Sidhu Sep 11 '19 at 14:06
  • @PavSidhu - file a bug with a sample project that reproduces your issue and we can take a look. – ianhanniballake Sep 11 '19 at 14:26
  • @ianhanniballake sure will do, appreciate your help – Pav Sidhu Sep 11 '19 at 14:27
  • @PavSidhu regardless of Navigation 2.2.0, I had workaround with calling `requestApplyInsets` on `navHostContainer` everything I resume any `Destination` – musooff Sep 16 '19 at 02:32
  • @musooff - FragmentManager calls that for you as per [the source code](https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java#1372). Please file an issue with a sample project if you're still finding that you need to do something specific for this case. – ianhanniballake Sep 16 '19 at 02:34
  • @ianhanniballake it is called during during creation. So when I onResume it will not be called. That's why I am calling it manually every time I visit `onResume` – musooff Sep 16 '19 at 02:37
  • @musooff - that's called immediately after *any* Fragment gets its view added to the view hierarchy (i.e., in `onViewCreated()`). That always happens before onResume, but after insets are able to be dispatched down. There should be absolutely no need to ever call it again in onResume() - it'll just send the exact same insets down. Like I said, if you're finding differently, please file a bug. – ianhanniballake Sep 16 '19 at 02:47
  • I will check out and if there is something unusual I will report. – musooff Sep 16 '19 at 10:12
22

My problem was similar to yours: I have a Bottom Bar Navigation which is replacing the content fragments. Now some of the fragments want to draw over the status bar (with CoordinatorLayout, AppBarLayout), others not (with ConstraintLayout, Toolbar).

ConstraintLayout
  FrameLayout
    [the ViewGroup of your choice]
  BottomNavigationView

The suggestion of ianhanniballake to add another CoordinatorLayout layer is not what I want, so I created a custom FrameLayout which handles the insets (like he suggested), and after some time I came upon this solution which really is not much code:

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

            }
        });
    }

}
mbo
  • 4,611
  • 2
  • 34
  • 54
4

OK, after several people pointing out that fitsSystemWindows works differently, and it should not be used on every view down the hierarchy, I went on experimenting and removing the property from different views.

I got the expected state after removing fitsSystemWindows from every node in activity.xml =\

Actine
  • 2,867
  • 2
  • 25
  • 38
  • So basically you didn't know what it is and you didn't know you don't need it? – BladeCoder Jul 02 '15 at 22:17
  • 10
    Yes :( I have to admit, I blindly followed the Cheesesquare sample; it had this attribute all over its layouts. Won't happen again. I foolishly thought that with the use of Design library I would be able to create a beautiful app without having to dig deep into Android's tangled layouting internals. Gosh, I implemented a custom expression language interpreter once, and it was less tangled than Android's UI. – Actine Jul 03 '15 at 13:10
  • 1
    I don't get your solution. You removed `fitsSystemWindows` from `activity.xml` and left it for the `CoordinatorLayout` as in the Cheesesquare example? But then, does your navigation drawer paint behind the status bar? – Pin Jul 31 '15 at 09:22
  • I ended up replacing the transaction with `` tag in the XML. And also without a `fitsSystemWindows` attributes in the fragment it doesn't show a translucent status bar as intended. – WindRider Nov 22 '15 at 13:19
  • If I do as you say, nothing gets drawn under the status bar for some reason. What's your activity theme like? – Pkmmte Dec 17 '15 at 21:08
3

Another approach written in Kotlin,

The problem:

The FrameLayout you are using does not propagate fitsSystemWindows="true" to his childs:

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

A solution:

Extend FrameLayout class and override the function onApplyWindowInsets() to propagate the window insets to attached fragments:

@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
    }
}

Use this layout as a fragment container instead of the standard FrameLayout:

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

Extra:

If you want to know more about this checkout Chris Banes blog post Becoming a master window fitter.

Ryan Amaral
  • 4,059
  • 1
  • 41
  • 39
  • A fairly decent approach. But you'd be better to override dispatchApplyWindowInsets instead of onApplyWindowInsets. The default implementation of dispatchApplyWindowInsets for a ViewGroup is already calling dispatchAppyWindowInsets on child view, so you end up calling dispatchApplyWindowInsets twice for each child. – Robin Davies Mar 06 '19 at 21:29
3

The other horrendous problem with dispatching of Window insets is that the first View to consume window insets in a depth-first search prevents all other views in the heirarchy from seeing window insets.

The following code fragment allows more than one child to handle window insets. Extremely useful if you're trying to apply windows insets to decorations outside a NavigationView (or CoordinatorLayout). Override in the ViewGroup of your choice.

@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.
}

Also useful:

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

which allows plain ordinary Views that are children of this view to be laid out without window insets.

Robin Davies
  • 7,547
  • 1
  • 35
  • 50
2

I created this last year to solve this problem: https://gist.github.com/cbeyls/ab6903e103475bd4d51b

Edit: be sure you understand what fitsSystemWindows does first. When you set it on a View it basically means: "put this View and all its children below the status bar and above the navigation bar". It makes no sense to set this attribute on the top container.

BladeCoder
  • 12,779
  • 3
  • 59
  • 51
  • I tried this, it didn't work for Lollipop. Also the IDE says `super.fitSystemWindows` is deprecated since API 20 – Actine Jul 02 '15 at 17:12
  • Thanks for the feedback, last time I tried it in my app under Lollipop it worked but I'll test again and look if I can find a new implementation which doesn't use a deprecated method. – BladeCoder Jul 02 '15 at 17:18
  • Maybe the problem is with me using `CoordinatorLayout` from appcompat-design library? It kind of messes with all those layout things under the hood. I tried your solution again and now I see two status bars when my toolbar is collapsed -_- – Actine Jul 02 '15 at 17:28
  • Did you try this instead?: @Override protected boolean fitSystemWindows(@NonNull Rect insets) { windowInsets.set(insets); return super.fitSystemWindows(insets); } – BladeCoder Jul 02 '15 at 19:29
  • "Double system bar" disappeared, but the original issue is still present, i.e. the view is not behind the system bar but below – Actine Jul 02 '15 at 19:59
  • 1
    Yeah but if you apply android:fitsSystemWindows on any of the parent containers (the DrawerLayout or the FrameLayout), it won't work. First view in the hierachy which has it consumes it for its children. – BladeCoder Jul 02 '15 at 20:03
  • Well, it worked when the fragment was included as ``, and it didn't work until I put the property on all views down to the full-bleed image... Oh okay, thanks for help anyway. I'll get back to this later myself and try to figure out. – Actine Jul 02 '15 at 20:28
  • I designed the class so you only put fitsSystemWindows on the views you want to see fit inside the fragment layout, when the fragment itself is displayed full screen. If you want the full fragment to fit you don't need it. – BladeCoder Jul 02 '15 at 21:30