Windows Insets in Fragments

This post is the second in a small series I’m writing about fragment transitions. The first is available here:

https://medium.com/google-developers/fragment-transitions-ea2726c3f36f


Before I go any further, I’m going to assume you know what window insets are and how they’re dispatched. If you don’t, I suggest you watch this talk (and yes, it’s from me 🙋).

TK add video/link from talk


I have a confession to make. When I was working on the code for the first blog post in this series, I cheated a bit with the videos. I actually hit an issue with window insets which meant that what I actually ended up with the following:

Woops, not exactly what I showed in the first post. I did not want to overcomplicate the first post, so decided to write this up separately. Anyway, you can see that we lose all status bar handling, and the views get pushed up behind the status bar.

The problem

Both of these fragments make heavy use of the window insets to draw behind the system bars. Fragment A uses a CoordinatorLayout and AppBarLayout. Fragment B uses custom window inset handling via a OnApplyWindowInsetsListener. As you can see, the transition messes with both.

So why is this happening? When you’re using fragment transitions, what actually happens to the exiting (Fragment A) and entering (Fragment B) content views is the following:

  1. Transition is commit()ed.
  2. Since we’re using an exit transition (fade out), View A stays in place and the transition is started on it.
  3. View B is added to the container view and immediately set to be invisible.
  4. Fragment B’s enter and ‘shared element enter’ transitions are started on the appropriate views.
  5. View B is set to be visible.
  6. When Fragment A’s exit transition has finished, View A is removed.

That all sounds fine, so why does it affect window insets? It’s all due the fact that during the transition, both fragments’ views are present in the container. That sounds perfectly OK right?

Well in my scenario, both of my fragment views want to handle and consume the window insets, but only one will actually receive them: the first (in child order). This is due to how ViewGroups dispatch window insets, which is by iterating through it’s children until one of them consumes the insets. If the first child (fragment A here) consumes the insets, any subsequent children (fragment B here) won’t get them, and we end up in this situation.

Lets step through again, but this time add in the time when window insets are dispatched:

  1. Transition is commit()ed.
  2. View A stays in place and the exit transition is run on it.
  3. View B is added to the container view and immediately set to be invisible.
  4. Window insets are dispatched. We want View B (child 1) to get them but View A (child 0) gets them again.
  5. Fragment B’s enter and ‘shared element enter’ transitions are started.
  6. View B is set to be visible.
  7. When Fragment A’s exit transition has finished, View A is removed.

The fix

The fix is actually relatively simple: we just need to make sure that both views receive the window insets.

The way I’ve done that is by adding an OnApplyWindowInsetsListener to the fragment container view (in my activity in this case) which manually dispatches any insets to all of it’s children, not just until one consumes the insets.

fragment_container.setOnApplyWindowInsetsListener { view, insets ->
  var consumed = false

  (view as ViewGroup).forEach { child ->
    // Dispatch the insets to the child
    val childResult = child.dispatchApplyWindowInsets(insets)
    // If the child consumed the insets, record it
    if (childResult.isConsumed) {
      consumed = true
    }
  }

  // If any of the children consumed the insets, return an appropriate value
  if (consumed) insets.consumeSystemWindowInsets() else insets
}

After we apply that, both fragments receive the window insets and we get the result I actually shown in the first post: