How to use shared element transitions in Navigation Controller
Asked Answered
W

9

49

I would like to add a shared elements transition using the navigation architecture components, when navigating to an other fragment. But I have no idea how. Also in the documentations there is nothing about it. Can someone help me?

Wayward answered 30/5, 2018 at 8:1 Comment(2)
issuetracker.google.com/issues/79665225Volant
This looks interesting: github.com/lion4ik/aac-navigation-shared-elements-transitionUtham
C
23

I took reference from this github sample https://github.com/serbelga/android_navigation_shared_elements

cardView.setOnClickListener{
  val extras = FragmentNavigatorExtras(
    imageView to "imageView"
  )
  findNavController().navigate(R.id.detailAction, null, null, extras)
}

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)

It is working properly.

Caravel answered 7/6, 2019 at 6:47 Comment(4)
This worked like a charm, I'm using the actual latest version of Navigation: 2.1.0-beta02Laritalariviere
how should I use this in java?Aeneous
does exit transiton also wowrking for this?Condorcet
add more details about transition names alsoBloodworth
R
28

FirstFragment

val extras = FragmentNavigatorExtras(
    imageView to "secondTransitionName")
view.findNavController().navigate(R.id.confirmationAction,
    null, // Bundle of args
    null, // NavOptions
    extras)

first_fragment.xml

<ImageView
    android:id="@+id/imageView"
    android:transitionName="firstTransitionName"
    ...
 />

SecondFragment

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View {
    sharedElementEnterTransition = ChangeBounds().apply {
        duration = 750
    }
    sharedElementReturnTransition= ChangeBounds().apply {
        duration = 750
    }
    return inflater.inflate(R.layout.second_fragment, container, false)
}

second_fragment.xml

<ImageView
    android:transitionName="secondTransitionName"
    ...
 />

I tested it. It is worked.

Removal answered 16/10, 2018 at 7:13 Comment(7)
It works for elements in Fragments A. I want to make transition between item in recyclerView (in Fragment A) and Fragment B and this approach doesn't work. How to adapt it to my case?Debi
@AlexandrSushkov I have the same problem. Did you find solution?Finitude
@Removal I set transitionName in the code but still not working could you give an example how to do this?Finitude
@Removal try setting a unique transitionname to each imageview in the recyclerview.Necrolatry
i can get this working for the enter transition, (fragment A has a recycler view of items, Fragmwnt B holds a details view) but the exit transition isnt workingPuke
@AlexandrSushkov I posted an answer below which works with the RecyclerViewSalian
isn't the transition name supposed to be the same in the first and second images?Heavily
C
23

I took reference from this github sample https://github.com/serbelga/android_navigation_shared_elements

cardView.setOnClickListener{
  val extras = FragmentNavigatorExtras(
    imageView to "imageView"
  )
  findNavController().navigate(R.id.detailAction, null, null, extras)
}

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)

It is working properly.

Caravel answered 7/6, 2019 at 6:47 Comment(4)
This worked like a charm, I'm using the actual latest version of Navigation: 2.1.0-beta02Laritalariviere
how should I use this in java?Aeneous
does exit transiton also wowrking for this?Condorcet
add more details about transition names alsoBloodworth
C
15

Since 1.0.0-alpha06 the navigation component supports adding shared element transitions between destinations. Just add FragmentNavigatorExtras to navigate() call. More details: https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element

val extras = FragmentNavigatorExtras(
    imageView to "header_image",
    titleView to "header_title")
view.findNavController().navigate(R.id.confirmationAction,
    null, // Bundle of args
    null, // NavOptions
    extras)
Consummate answered 24/9, 2018 at 20:56 Comment(5)
Indeed, there are FragmentNavigatorExtras, but they don't seem to have any effect on shared animations, although they do get processed, as I got an exception when there was not transition name in source view.Mouldon
Does this work for anyone? Mine doesn't seem to do anything.Offence
Not working for me either. Please let us know if it is working for anyone.Embank
@Mouldon - for exception. You just need to add transition name into your XML. Or you can use ViewCompat#setTransitionNameEmbank
i have this working but the exit transition doesnt seem to workPuke
S
10

To make this work from a recyclerView's ImageView the setup everything like the following:

val adapter = PostAdapter() { transitionView, post ->
    findNavController().navigate(
        R.id.action_postsFragment_to_postsDetailFragment,
        null,
        null,
        FragmentNavigatorExtras(transitionView to getString(R.string.transition_image)))
}

within the adapter this does the trick:

itemView.setOnClickListener {
    ViewCompat.setTransitionName(imageView, itemView.context.getString(R.string.transition_image))
    onClickedAction?.invoke(imageView, post)
}

You don't have to specify the transition name within the adapter's item's xml but simply set it from code as soon as the item gets clicked.

The onClickedAction looks like:

private val onClickedAction: ((transitionView: View, post: Post) -> Unit)?

and you pass it to your ViewHolder.

In the second Fragment you set the transition name to the ImageView in xml:

android:transitionName="@string/transition_image"

and assign the transition like

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val transition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
    sharedElementEnterTransition = transition
    sharedElementReturnTransition = transition
}
Salian answered 30/5, 2019 at 20:33 Comment(2)
The return transition still doesn't work. i.e. when clicking the up button in the detail page and returning to the recyclerView. Any clue how I can make it work?Ellmyer
the reason this works is because you are ensuring the transitionName for the recyclerview item is unique in that fragment (this is important!) by only setting it once it has been clicked. I found this helpful too: medium.com/@rajnishsuryavanshi223/…Heavily
P
6

For Java

To make shared element create a method like :

void sharedNavigation(int id, View... views) {
        FragmentNavigator.Extras.Builder extras = new FragmentNavigator.Extras.Builder();
        for (View view : views)
            extras.addSharedElement(view, view.getTransitionName());
        FragmentNavigator.Extras build = extras.build();
        Navigation.findNavController(getView()).navigate(id,
                null,
                null,
                build);
    }

At the destination class or base class you have to add below code in your onCreate().

@Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setSharedElementEnterTransition(TransitionInflater.from(getContext())
                .inflateTransition(android.R.transition.move));
    }

And to make transition animation give the id and views to the sharedNavigation() like below :

sharedNavigation(R.id.action_splashFragment_to_loginFragment,
                        getView().findViewById(R.id.logo));
Purulence answered 2/11, 2019 at 7:10 Comment(0)
V
4

So let's say that you have two Fragments, FragmentSecond and FragmentThird. Both have ImageView with the same transitionName, let's say : "imageView"

android:transitionName="imageView"

Just define a normal action between these fragments.

In FragmentSecond, let's add our extras

val extras = FragmentNavigatorExtras( binding.image to "imageView")

findNavController().navigate(R.id.action_secondFragment_to_thirdFragment , null, null , extras)

So we're saying that we want to share that ImageView, with that transitionName, with ThirdFragment

And then in ThirdFragment :

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
        setHasOptionsMenu(true)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Glide.with(this).load(IMAGE_URI).into(binding.headerImage)
    }

The only thing that we have to do is load the image in the two fragments from the same URL. The URL can be passed between fragments using a Bundle Object and pass it in the navigate call or as a destination argument in the navigation graph.

If you need it, i am preparing a sample about Navigation and there's SharedElementTransition too :

https://github.com/matteopasotti/navigation-sample

Vermination answered 12/5, 2019 at 13:5 Comment(0)
D
3

It seems it is not (yet?) supported. The transaction is actually built in androidx.navigation.fragment.FragmentNavigator:

@Override
public void navigate(@NonNull Destination destination, @Nullable Bundle args,
                        @Nullable NavOptions navOptions) {
    final Fragment frag = destination.createFragment(args);
    final FragmentTransaction ft = mFragmentManager.beginTransaction();

    int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
    int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
    int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
    int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
    if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
        enterAnim = enterAnim != -1 ? enterAnim : 0;
        exitAnim = exitAnim != -1 ? exitAnim : 0;
        popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
        popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
        ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
    }

    ft.replace(mContainerId, frag);

    final StateFragment oldState = getState();
    if (oldState != null) {
        ft.remove(oldState);
    }

    final @IdRes int destId = destination.getId();
    final StateFragment newState = new StateFragment();
    newState.mCurrentDestId = destId;
    ft.add(newState, StateFragment.FRAGMENT_TAG);

    final boolean initialNavigation = mFragmentManager.getFragments().isEmpty();
    final boolean isClearTask = navOptions != null && navOptions.shouldClearTask();
    // TODO Build first class singleTop behavior for fragments
    final boolean isSingleTopReplacement = navOptions != null && oldState != null
            && navOptions.shouldLaunchSingleTop()
            && oldState.mCurrentDestId == destId;
    if (!initialNavigation && !isClearTask && !isSingleTopReplacement) {
        ft.addToBackStack(getBackStackName(destId));
    } else {
        ft.runOnCommit(new Runnable() {
            @Override
            public void run() {
                dispatchOnNavigatorNavigated(destId, isSingleTopReplacement
                        ? BACK_STACK_UNCHANGED
                        : BACK_STACK_DESTINATION_ADDED);
            }
        });
    }
    ft.commit();
    mFragmentManager.executePendingTransactions();
}

The animations are here (added from XML navigation), but nowhere can we change the behavior of this, and call addSharedElement() on the transaction.


However, I believe that we may do this from activity shared element transitions.

This is not recommended as it is only between activities, and this goes against the latest Google recommendations to go with single-activity applications.

I think it's possible, as the arguments are passed before the call to startActivity() in androidx.navigation.fragment.ActivityNavigator:

@Override
public void navigate(@NonNull Destination destination, @Nullable Bundle args,
        @Nullable NavOptions navOptions) {
    if (destination.getIntent() == null) {
        throw new IllegalStateException("Destination " + destination.getId()
                + " does not have an Intent set.");
    }
    Intent intent = new Intent(destination.getIntent());
    if (args != null) {
        intent.putExtras(args);
        String dataPattern = destination.getDataPattern();
        if (!TextUtils.isEmpty(dataPattern)) {
            // Fill in the data pattern with the args to build a valid URI
            StringBuffer data = new StringBuffer();
            Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
            Matcher matcher = fillInPattern.matcher(dataPattern);
            while (matcher.find()) {
                String argName = matcher.group(1);
                if (args.containsKey(argName)) {
                    matcher.appendReplacement(data, "");
                    data.append(Uri.encode(args.getString(argName)));
                } else {
                    throw new IllegalArgumentException("Could not find " + argName + " in "
                            + args + " to fill data pattern " + dataPattern);
                }
            }
            matcher.appendTail(data);
            intent.setData(Uri.parse(data.toString()));
        }
    }
    if (navOptions != null && navOptions.shouldClearTask()) {
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
    }
    if (navOptions != null && navOptions.shouldLaunchDocument()
            && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
    } else if (!(mContext instanceof Activity)) {
        // If we're not launching from an Activity context we have to launch in a new task.
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    }
    if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
    }
    if (mHostActivity != null) {
        final Intent hostIntent = mHostActivity.getIntent();
        if (hostIntent != null) {
            final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
            if (hostCurrentId != 0) {
                intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
            }
        }
    }
    final int destId = destination.getId();
    intent.putExtra(EXTRA_NAV_CURRENT, destId);
    NavOptions.addPopAnimationsToIntent(intent, navOptions);
    mContext.startActivity(intent);
    if (navOptions != null && mHostActivity != null) {
        int enterAnim = navOptions.getEnterAnim();
        int exitAnim = navOptions.getExitAnim();
        if (enterAnim != -1 || exitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            mHostActivity.overridePendingTransition(enterAnim, exitAnim);
        }
    }

    // You can't pop the back stack from the caller of a new Activity,
    // so we don't add this navigator to the controller's back stack
    dispatchOnNavigatorNavigated(destId, BACK_STACK_UNCHANGED);
}

We would need to populate the arguments like so:

val args = Bundle()

// If there's a shared view and the device supports it, animate the transition
if (sharedView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    val transitionName = "my_transition_name"
    args.putAll(ActivityOptions.makeSceneTransitionAnimation(this, sharedView, transitionName).toBundle())
}

navController.navigate(R.id.myDestination, args)

I have not tested this.

Dotard answered 30/5, 2018 at 20:45 Comment(0)
C
2

I was finally able to get this to work: On Fragment B:

val transition = TransitionInflater.from(this.activity).inflateTransition(android.R.transition.move)

sharedElementEnterTransition = ChangeBounds().apply {
            enterTransition = transition
        }

Just make sure you have your transition names right in your views and you have NO entertTransition on Fragment B

Castello answered 18/10, 2018 at 8:58 Comment(0)
T
2

With the latest library version you can just write the following:

view.findNavController().navigate(
    R.id.action_firstFragment_to_secondFragment, 
    null,  
    null,
    FragmentNavigator.Extras.Builder().addSharedElements(
        mapOf(
           firstSharedElementView to "firstSharedElementName",
           secondSharedElementView to "secondSharedElementName"
        )
    ).build()
)

For the transition to work you also have to specify the sharedElementEnterTransition and/or the sharedElementReturnTransition in the destination Fragments onCreateView method just as Xzin explained in his answer.

Tweeny answered 20/5, 2019 at 11:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.