fitsSystemWindows effect gone for fragments added via FragmentTransaction

前端 未结 6 1021

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

相关标签:
6条回答
  • 2020-11-28 02:58

    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.

    0 讨论(0)
  • 2020-11-28 03:06

    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.

    0 讨论(0)
  • 2020-11-28 03:07

    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.

    0 讨论(0)
  • 2020-11-28 03:12

    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 =\

    0 讨论(0)
  • 2020-11-28 03:14

    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) {
    
                }
            });
        }
    
    }
    
    0 讨论(0)
  • 2020-11-28 03:21

    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.

    0 讨论(0)
提交回复
热议问题