Stepan Sanda
Stepan Sanda

Reputation: 2340

Android fitsSystemWindows not working when replacing fragments

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.

Upvotes: 22

Views: 17163

Answers (4)

azizbekian
azizbekian

Reputation: 62189

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.

Upvotes: 15

John
John

Reputation: 1543

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

Upvotes: 0

Robin Davies
Robin Davies

Reputation: 7837

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.

Upvotes: 1

mbo
mbo

Reputation: 4701

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

Upvotes: 3

Related Questions