22

I have SingleFramgnetActivity whose purpose is only to hold and replace fragments inside it.

layout looks like this:

<?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>

I'm replacing the Fragments inside the FrameLayout. When I set the fitsSystemWindows to true on the Fragment layout, it is not responding. Actually it is working only when Activity is created, but once I replace the Fragment inside the FrameLayout, the fitsSystemWindows parameter is ignored and the layout is below the status bar and navigation bar.

I found some solution with custom FrameLayout which is using deprecated methods, but for some reason it is not working for me (same result as with normal FrameLayout) and I also do not like the idea to use deprecated methods.

Community
  • 1
  • 1
Stepan Sanda
  • 2,322
  • 7
  • 31
  • 55
  • are you aware of what fitsSystemWindow does? – JAAD Sep 03 '16 at 13:54
  • I hope that I get it right, basically when it is set on true, the view and all its children should not be displayed under the system windows as status bar or navigation bar. When it is set to false, it should be displayed under the system windows. So basically I can set that I want to render whole Fragment layout between status bar and navigation bar (fitsSystemWindow=true) and background image under the status bar and navigation bar (fitsSystemWindow = false). I can make it exactly as I want until I'm not replacing the fragments. – Stepan Sanda Sep 03 '16 at 14:08
  • 2
    I had the same problem. I ended up making a FrameLayout that saves the window insets and re-dispatches them to new children. I put the code in a [Github gist](https://gist.github.com/PaeP3nguin/4e41f7e76be452fe2f78d3c534fb8dd1). Let me know if that works for you! – paep3nguin Apr 03 '17 at 02:09

4 Answers4

15

Your FrameLayout is not aware of window inset sizes, because it's parent - LinearLayout hasn't dispatched it any. As a workaround, you can subclass LinearLayout and pass insets to children on your own:

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

You can have a look to my this answer, which will explain detailed how this works, and also how to use ViewCompat.setOnApplyWindowInsetsListener API.

Saket
  • 2,945
  • 1
  • 29
  • 31
azizbekian
  • 60,783
  • 13
  • 169
  • 249
  • 1
    Always use `ViewCompat.setOnApplyWindowInsetsListener` when supporting API below 20 because http://stackoverflow.com/questions/35028395/android-view-windowinsets-classnotfoundexception/42380189#42380189. – Eugen Pechanec Mar 01 '17 at 23:33
  • @azizbekian your explanation is useful, but it still doesn't help me to solve issue with changed fragments. fitSystemWindows works in fragment layout when fragment is first opened, but is ignored if the fragment replaces another fragment. I am talking about fitSystemWindows on non root view -second level. On first level it works. All views in hierarchy in both activity and fragments are Coordinator layouts or implement the method from your answer. – lobzik Mar 11 '17 at 18:05
  • @lobzik, Inspect your view hierarchy and find out the parent view, that doesn't dispatch insets to its children. You may use `ViewCompat.setOnApplyWindowInsetsListener` and check which `ViewGroup` exactly returns 0 as inset size. – azizbekian Mar 11 '17 at 18:07
  • @azizbekian how it can be one view not dispatching insets if it works fine until fragment is replaced? As I see from debug my root layout in fragment doesn't receive onApplyWindowInsets when the fragment is replacing another one. But it does get this call when it is opened first time. I don't know how to solve it – lobzik Mar 11 '17 at 20:10
  • @lobzik, does the root layout of your replaced fragment receive insets from its parent? – azizbekian Mar 11 '17 at 20:13
  • @azizbekian Fragment A does recieve it first time opened. than A is replaced to B - B doesn't receive insets. Than returning A from back stack - A doesn't receive insets and also has wrong layout position. – lobzik Mar 11 '17 at 21:37
  • @lobzik, that's because the insets are consumed the first time they are applied. Look to [my other answer's](http://stackoverflow.com/questions/33585225/what-are-windowinsets/38929893#38929893) `OnApplyWindowInsetsListener` implementation, particularly this part: `insets.consumeSystemWindowInsets()`. Make your view **NOT** consume the insets. – azizbekian Mar 11 '17 at 21:42
  • @azizbekian, Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/137872/discussion-between-lobzik-and-azizbekian). – lobzik Mar 12 '17 at 08:50
  • @lobzik, see my [this](http://stackoverflow.com/a/43055856/1083957) answer. – azizbekian Mar 27 '17 at 20:28
  • 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:27
3

You could also build a custom WindowInsetsFrameLayout and use a OnHierarchyChangedListener to request applying the insets again:

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

        }
    });
}

Check out this detailed answer: https://stackoverflow.com/a/47349880/3979479

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

I think the problem revolves around onApplyWindowInsets getting called before the fragment view hierarchy gets attached. An effective solution is to get the following override on a view somewhere in the view hierarchy of the fragment.

  @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();
        }
    }

A complete solution (if you don't have to use CoordinatorLayout) follows. Make sure fitSystemWindows=true does not appear ANYWHERE in views higher in the heirarchy. Maybe not anywhere else. I suspect (but am not sure) that consumeSystemWindowInsets eats the insets for views that are further on in the layout order of the view tree.

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

Suspicion, not fact: that only one view in the entire hierarchy gets a chance to eat the window insets, UNLESS you have CoordinatorLayout in the hierarchy, which allows more than one direct child to have FitSystemWindow=true. If you do have a CoordinatorLayout, your mileage may vary.

This entire feature in Android seems to be an unholy mess.

Robin Davies
  • 7,547
  • 1
  • 35
  • 50
  • I should add that `CoordinatorLayout` caches the last `WindowInsets` it receives, and if a subsequent `WindowInsets` is received, for example via `View#requestApplyInsets()`, the 'new' inset will be `Object#equal()` to the cached one, so it won't dispatch it to its child `Behavior` instances, so anything below it in the hierarchy won't be notified. You'll need to dispatch in manually in this case. – technicalflaw May 16 '19 at 18:26
0

a) you can use CoordinatorLayout as your root view inside fragment

or

b) you can create custom linear layout what will call requestApplyInsets and use it as your root view inside fragment

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

and then inside fragment you can catch applying insets

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()
    }
}
John
  • 1,447
  • 15
  • 16