Fragment shared element transitions don't work with ViewPager

AndroidAndroid ViewpagerAndroid Transitions

Android Problem Overview


My app contains a view which consists of a ViewPager consisting of a handful of fragments. When you click on an item in one of these fragments, the expected behavior is for the shared element (in this case an image) to transition to the fragment which displays more information about the clicked content.

Here is a very simple video of what it should look like:

https://dl.dropboxusercontent.com/u/97787025/device-2015-06-03-114842.mp4

This is just using a Fragment->Fragment transition.

The problem arises when you place the starting fragment inside a ViewPager. I suspect this is because the ViewPager uses its parent fragment's child fragment manager, which is different than the fragment manager of the activity, which is handling the fragment transaction. Here is a video of what happens:

https://dl.dropboxusercontent.com/u/97787025/device-2015-06-03-120029.mp4

I'm pretty certain the issue here as I explained above is the child fragment manager vs the activity's fragment manager. Here is how I am making the transition:

SimpleFragment fragment = new SimpleFragment();

FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.am_list_pane, fragment, fragment.getClass().getSimpleName());

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
	TransitionSet enterTransition = new TransitionSet();
	enterTransition.addTransition(new ChangeBounds());
	enterTransition.addTransition(new ChangeClipBounds());
	enterTransition.addTransition(new ChangeImageTransform());
	enterTransition.addTransition(new ChangeTransform());

	TransitionSet returnTransition = new TransitionSet();
	returnTransition.addTransition(new ChangeBounds());
	returnTransition.addTransition(new ChangeClipBounds());
	returnTransition.addTransition(new ChangeImageTransform());
	returnTransition.addTransition(new ChangeTransform());

	fragment.setSharedElementEnterTransition(enterTransition);
	fragment.setSharedElementReturnTransition(returnTransition);

	transaction.addSharedElement(iv, iv.getTransitionName());
}

transaction.addToBackStack(fragment.getClass().getName());

transaction.commit();

This works fine when both fragments are managed by the activity's fragment manager, but when I load up a ViewPager like this:

ViewPager pager = (ViewPager) view.findViewById(R.id.pager);
pager.setAdapter(new Adapter(getChildFragmentManager()));

The children of the ViewPager are not being managed by the activity, and it doesn't work anymore.

Is this an oversight by the Android team? Is there any way to pull this off? Thanks.

Android Solutions


Solution 1 - Android

Probably you've already found an answer to this but in case you haven't, here's what I did to fix it after a few hours of scratching my head.

The problem I think is a combination of two factors:

  • The Fragments in ViewPager load with a delay, meaning that the activity returns a lot faster than its fragments which are contained inside the ViewPager

  • If you are like me, your ViewPager's child fragments are most likely the same type. This means, they all share the same transition name (if you've set them in your xml layout), unless you set them in code and only set it once, on the visible fragment.

To fix both of these problems, this is what I did:

1. Fixing the delayed loading problem:

Inside your activity (the one that contains the ViewPager), add this line after super.onCreate() and before setContentView():

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ActivityCompat.postponeEnterTransition(this); // This is the line you need to add

    setContentView(R.layout.feeds_content_list_activity);
    ...
}

2. Fixing the problem with multiple fragments with the same transition name:

Now there are quite a few ways of going about doing this but this is what I ended up with inside my "detail" activity, i.e. the activity that contains the ViewPager (in the onCreate() but you can do it anywhere really):

_viewPager.setAdapter(_sectionsPagerAdapter);
_viewPager.setCurrentItem(position);
...
...
_pagerAdapter.getItem(position).setTransitionName(getResources().getString(R.string.transition_contenet_topic));

You need to be careful since the Activity may not yet be attached to your ViewPager fragment so it's easier to just pass in the transition name from the activity to the fragment if you're loading it from a resource

The actual implementation is as simple as you expect:

public void setTransitionName(String transitionName) {
    _transitionName = transitionName;
}

Then inside your fragment's onViewCreated(), add this line:

public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    ...
    if (_transitionName != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
        setTransitionNameLollipop();
    }
    ...
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void setTransitionNamesLollipop() {
    _imgTopic.setTransitionName(_transitionName);
}

The last piece of the puzzle is to find out when your fragment is fully loaded and then call ActivityCompat.startPostponedEnterTransition(getActivity());.

In my case, my fragments were not fully loaded until later since I'm loading most things off the UI thread which means I had to figure out a way to call this when everything was loaded but if that's not your case, you can call this right after you call setTransitionNameLollipop().

The problem with this approach is that the exit transition may not work unless you're very careful and reset the transition name on the "visible" fragment right before you exit the activity to navigate back. That can easily be done like so:

  1. Listen to page change on your ViewPager
  2. Remove the transition name(s) as soon as your fragment is out of view
  3. Set the transition name(s) on the visible fragment.
  4. Instead of calling finish(), call ActivityCompat.finishAfterTransition(activity);

This can get very complex very soon if you back transition needs to transition to a RecyclerView which was my case. For that, there's a much better answer provided by @Alex Lockwood here: https://stackoverflow.com/questions/27304834/viewpager-fragments-shared-element-transitions which has a very well written example code (albeit a lot more complicated than what I just wrote) here: https://github.com/alexjlockwood/activity-transitions/tree/master/app/src/main/java/com/alexjlockwood/activity/transitions

In my case, I didn't have to go so far as to implement his solution and the above solution that I posted worked for my case.

In case you have multiple shared elements, I'm sure you can figure out how to extend the methods in order to cater to them.

Solution 2 - Android

On the activity supportPostponeEnterTransition();

And when your fragments are loaded (try to sync them, maybe with EventBus or whatever)

startPostponedEnterTransition();

Refer to this sample

http://www.androiddesignpatterns.com/2015/03/activity-postponed-shared-element-transitions-part3b.html

Solution 3 - Android

I have been banging my head against the wall with this one recently. All I wanted was a Fragment in a ViewPager to launch another Fragment with a nice expanding card type shared element transition. None of the suggestions above worked for me so I decided to try launching an Activity styled like a Dialog:

<style name="AppTheme.CustomDialog" parent="MyTheme">
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:backgroundDimEnabled">true</item>
</style>

Then to launch the Activity from the Fragment:

Intent intent = new Intent(getContext(), MyDialogActivity.class);
ActivityOptionsCompat options = ActivityOptionsCompat
                        .makeSceneTransitionAnimation(getActivity(),
                                Pair.create(sharedView, "target_transition"));
ActivityCompat.startActivity(getActivity(), intent, options.toBundle());

In the Fragment layout put an android:transition_name on the sharedView and in the Activity layout have android:transition_name="target_transition" (same as Pair.create() second argument).

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJMRboostiesView Question on Stackoverflow
Solution 1 - AndroidkhaView Answer on Stackoverflow
Solution 2 - Androidflorent champignyView Answer on Stackoverflow
Solution 3 - AndroidMarkView Answer on Stackoverflow