Postponing Fragment Transitions

Last week at Google I/O, I finally got to talk about what I have been working on for the last 6 months. You should try out data binding:

http://developer.android.com/tools/data-binding/guide.html

It is a super-fast code-generating data binding system that optimized to work well with Android. We’re looking for feedback, so try it out and let us know what you think.

That said, I’ve had a couple of posts in the back of my mind that I haven’t been able to get to. One bit of feedback that I had heard about Fragment Transitions is that we don’t get the postponeEnterTransition() that we get with Activity Transitions. That is especially important when one or both fragments have a RecyclerView.

When I wrote Fragment Transitions, I ran across the problem that FragmentTransaction is a Transaction; one fragment should be removed and the other should be added in an atomic step. This left the developer to handle the postponeEnterTransition.

In the situation where Fragment A is replaced by B, my recommendation was this:

  1. Transaction 1
    • add B,
    • hide B
  2. Transition 2: when B is ready
    • show B
    • remove A
    • add shared elements
    • add to back stack

That works great when B needs to be postponed. If, for example, A has a recycler view, the popBackStack() will immediate remove B and add A. Since A has a recycler view, the contents aren’t ready and the shared elements won’t be ready; you’ll lose the shared element transition.

This can be alleviated by only hiding fragment A.

  1. Transaction 1:
    • add B
    • hide B
    • add to back stack
  2. Transaction 2: when B is ready
    • show B
    • hide A
    • add shared elements
    • add to back stack

In this scheme, there is an intermediate state that you add at which B is added and hidden, When you pop the back stack, you will end up in the intermediate state, so you’ll have to automatically pop the back stack to remove B when you get there. If you don’t want that intermediate state, you might be able to avoid it. I didn’t try it, but you would have to detect when you’ve popped back from Transaction 2 and just remove B instead of popping the back stack.

Now the hidden fragment, A, will not lose its view hierarchy when it is hidden. Thus, the view is ready when going back. However, if you do an orientation change, the view hierarchy will be rebuilt. This is fine if your application is ready, even when the view is gone, but if your view needs layout (like recycler view), this isn’t enough. You may be able to trigger the system into laying out the hidden fragment A, but I don’t know the solution to that right off. The other issue is, of course, memory usage. While A is hidden and won’t take part in layout, it does consume resources. If your back stack is particularly large, this could become a noticeable pressure on the heap.

So, to support getting fragment A ready when going back, we need another stage in the transactions:

  1. Transaction 1:
    • add B
    • hide B
    • add to back stack
  2. Transaction 2: when B is ready
    • show B
    • hide A
    • add shared elements
    • add to back stack
  3. Transaction 3:
    • show A
    • remove A
    • add to back stack

In this scheme, there are two intermediate states. The first one has Fragment B hidden and Fragment A showing. Fragment B can hang out until is ready to show. On the way back, that intermediate state is not really necessary and should be popped automatically. The second intermediate state shows B and hides A. When pushing up the stack, it can immediately proceed to the final state, but when popping, it needs to pause there until Fragment A is ready to show.

I made a utility to handle the 3 transitions as a unit and watch the back stack to detect when we’ve arrived in an intermediate state and proceed when the fragment is ready to transition (both ways). In my utility, I’ve made an interface for the Fragment to implement:

public interface DelayedTransitionFragment {
    boolean isTransitionDelayed();
}

If the fragment does not implement the interface, it won’t support delayed transitions. If isTransitionDelayed() returns true, then the fragment will be responsible for calling transitionReady() on the utility.

In my example, I had each fragment contain a RecyclerView that needed to be laid out prior to the transition continuing. Instead of being hidden (Visibility.GONE), the fragment View needed to be invisible. Once it was laid out, I continued the transition:

final RecyclerView recyclerView = (RecyclerView) getView();
recyclerView.setVisibility(View.INVISIBLE); // force a layout
recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        recyclerView.getViewTreeObserver()
            .removeOnPreDrawListener(this);
        FragmentTransitionUtil.getInstance(getFragmentManager())
            .transitionReady();
        return false;
    }
});

In trying this out, I found a few bugs in Fragment Transitions that I needed to work around. Obviously, I’ll fix those, but you’ll need to work around them until the bugs are fixed. In the fragment, I need to reset the enter and exit transitions onHiddenChanged and explicitly exclude the shared elements. My utility only provides for a single shared element, but you can expand it to support multiple. Again, this is only because of a bug; future versions shouldn’t require this hack.

@Override
public void onHiddenChanged(boolean hidden) {
    super.onHiddenChanged(hidden);
    resetTransitions();
}

private void resetTransitions() {
    // Work around bug in fragment transitions in 22.2.0
    // We need to reset the transition after the view is created
    String sharedElementName =
            FragmentTransitionUtil.getInstance(getFragmentManager())
                .getTransitionName();
    Slide enter = new Slide(Gravity.LEFT);
    Slide exit = new Slide(Gravity.RIGHT);
    if (sharedElementName != null) {
        enter.excludeTarget(sharedElementName, true);
        exit.excludeTarget(sharedElementName, true);
    }
    setEnterTransition(enter);
    setExitTransition(exit);
}

My sample project uses minimal data binding, so if you aren’t trying out the beta Android Studio, you’ll have to do just a little jiggering to get the sample to work.

Take a look at this project and let me know what you think:

https://drive.google.com/file/d/0BzzMYfCAYzn4bHpqWGhKbV81cU0/view?usp=sharing