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

Advertisements

8 thoughts on “Postponing Fragment Transitions

  1. Thanks for the great explanation and sample project.

    FYI
    When the activity is rebuilt after an orientation change, we end up with a new FragmentManager, meaning we’re now working with a new instance in the WeakHashMap with an empty `tags` SparseArray, so moving back down the stack no longer works as desired.

    Changing `tags` to a static field fixes the issue.

    • Hi, I have added static to ‘tags ‘ but issue still persists when rotating. I am guessing you mean change this

      private final SparseArray tags = new SparseArray();

      to this

      private static final SparseArray tags = new SparseArray();

      Or are there more changes needed?

      Also, thanks for the blog post, it is very useful.

  2. Would this technique work with nested fragments?
    I have a fragment containing a tablayout and a viewpager, each tab has a recyclerview, where each list item displays a detail view.

    I tried your Util method, but wasn’t able to get the re-enter transitions to play correctly. Just wondering if you had a suggestion for fixing this? Would I need the parent fragment to call isDelayedTransaction on the child fragment that calls transition()?

  3. Could you explain the workaround bug a bit more? What happens because of the bug and is there a bug report ticket you could refer me to, so I can check it’s status?

    Also, I’m having trouble with the resetTransitions method. I’m currently using v4 support fragments, and I need to pass getSupportFragmentManager() into the getInstance() method on your Utils class. However, when onHiddenChanged gets called, my activity doesn’t seem attached and I cannot get a reference to an instance of the supportmanager. Is there a way to delay the resetTransitions method to occur after my fragment has been attached?

    • It has been some time since I posted this, so you should try it out without calling resetTransitions to see. I believe the bug was that the transitions were being corrupted after the execution (targets were not being properly cleared). If I recall, the transition would work once and then not again. That bug should be long fixed by now, so just use the latest release and it should work for you. I believe the bug with in some version 22, and was fixed in 23.

      You shouldn’t need the reset transitions hack anymore, so you can just eliminate it from your code.

    • Yes, but this solution is not ideal. The fragment transaction API and the transition API aren’t currently happy with each other, so I need to make different changes to make them work better.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s