62

I am trying to implement transitions between fragments which have "shared elements" as described in the new material design specs. The only method I can find is the ActivityOptionsCompat.makeSceneTransitionAnimation, which I believe works on Activity only. I've been searching for this same functionality but with/for fragments.

Alex Lockwood
  • 83,063
  • 39
  • 206
  • 250
unchosen
  • 874
  • 1
  • 12
  • 14
  • 1
    Have you checked FragmentTransaction.addSharedElement -method ; http://developer.android.com/reference/android/support/v4/app/FragmentTransaction.html#addSharedElement(android.view.View,java.lang.String) ? – harism Oct 25 '14 at 11:55
  • I tried using that actually, but it didn't seem to work, at least from an imageview in a listview item. There are probably a bunch of undocumented limitations. Disabling transitions and animations on the transaction didn't seem to help though. – sbaar Oct 26 '14 at 09:13
  • 1
    I wasn't able to get it to work with ImageViews inside list items either. I was able to put together a very simple Activity with 2 fullscreen fragments. Each fragment had 2 Views with black background in different sizes and positions and when I tap the screen it switches the fragment. The shared elements did animate as expected in this case. So it does work, just maybe not when your view is in a list item. I wonder if it's because the list items aren't known until run time? – brockoli Oct 30 '14 at 20:06
  • 3
    I can now confirm that transitioning a view that's inside a list item layout to a view in a new fragment does not work. If I put a view in my first fragments layout, outside the listview, it does work. – brockoli Oct 30 '14 at 23:53
  • 2
    @broccoli I found solution for listview\recyclerview. You need unique transition name for each item. Read more: http://www.androidauthority.com/using-shared-element-transitions-activities-fragments-631996/ – IliaEremin Oct 15 '15 at 10:57
  • Here're [a series of posts](http://www.androiddesignpatterns.com/2014/12/activity-fragment-transitions-in-android-lollipop-part1.html) which can help to understand Fragments' transition. – Lym Zoy Jan 05 '17 at 07:57

8 Answers8

49

I had the same problem but had it working by adding a new fragment from another fragment. The following link is very helpful in getting started on this: https://developer.android.com/training/material/animations.html#Transitions

Following is my code that works. I'm animating an ImageView from one fragment to the other. Make sure the View you want to animate has the same android:transitionName in both fragments. The other content doesn't really matter.

As a test, you could copy this to both your layout xml files. Make sure the image exists.

<ImageView
android:transitionName="MyTransition"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/test_image" />

Then I have 1 file in my res/transition folder, named change_image_transform.xml.

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeImageTransform />
</transitionSet>

Now you can get started. Lets say you have Fragment A containing the image and want to add Fragment B.

Run this in Fragment A:

@Override
public void onClick(View v) {
    switch(v.getId()) {
        case R.id.product_detail_image_click_area:
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                setSharedElementReturnTransition(TransitionInflater.from(getActivity()).inflateTransition(R.transition.change_image_transform));
                setExitTransition(TransitionInflater.from(getActivity()).inflateTransition(android.R.transition.explode));

                // Create new fragment to add (Fragment B)
                Fragment fragment = new ImageFragment();
                fragment.setSharedElementEnterTransition(TransitionInflater.from(getActivity()).inflateTransition(R.transition.change_image_transform));
                fragment.setEnterTransition(TransitionInflater.from(getActivity()).inflateTransition(android.R.transition.explode));

                // Our shared element (in Fragment A)
                mProductImage   = (ImageView) mLayout.findViewById(R.id.product_detail_image);

                // Add Fragment B
                FragmentTransaction ft = getFragmentManager().beginTransaction()
                        .replace(R.id.container, fragment)
                        .addToBackStack("transaction")
                        .addSharedElement(mProductImage, "MyTransition");
                ft.commit();
            }
            else {
                // Code to run on older devices
            }
            break;
    }
}
WindsurferOak
  • 4,861
  • 1
  • 31
  • 36
ar34z
  • 2,609
  • 2
  • 24
  • 37
  • 2
    This works for me except it always starts the animation at the top of the screen in the second fragment. So if I have a view in each element of my listview (or in my case RecyclerView) and you tap on one near the bottom of the list and the new placement for that view in the second fragment is at the bottom of the screen, it actually animates from the top to the bottom instead of animating from the place on the screen in the first fragment where the view started. Anyone know why this is? – brockoli Nov 30 '14 at 14:12
  • I think this is because the list view repeatedly contains the same elements. If one would perform a findViewById, the first item would return. I think you should uniquely identify the animated view by assigning the transitionName dynamically when clicking the item. – ar34z Nov 30 '14 at 18:03
  • 2
    I am assigning unique id's to each instance in my Adapter. I'm appending the items position. That being said, I solved my problem. As it turns out you need to include the ChangeTransform to your TransitionSet. This tells the system to save the start position of the view you just selected and use that as the start position for the animation in the new fragment. – brockoli Nov 30 '14 at 22:49
  • @brockoli can you elaborate on " As it turns out you need to include the ChangeTransform to your TransitionSet." ? How do you do that? – stoefln Feb 11 '15 at 16:05
  • @ar34z it's just res/transition, gradle wouldn't build it otherwise. https://developer.android.com/reference/android/transition/Transition.html – Daniel Jonker Feb 17 '15 at 01:48
  • This doesn't work when i `add` the fragment instead of `replace'. Any ideas on how to achieve the transition animation with `add`? -- http://stackoverflow.com/questions/29145031/shared-element-transition-works-with-fragmenttransaction-replace-but-doesnt-w – Vinay W Mar 24 '15 at 11:52
  • 3
    @stoefln check out this link http://www.androidauthority.com/using-shared-element-transitions-activities-fragments-631996/ – Lilo Sep 29 '15 at 15:56
  • @Lilo, nice tutorial. Though note that `addSharedElement` of the `FragmentTransaction` is called outside of the Lollipop if-statement, so it wont work on older devices as it is an API lev. 21 method. It should be fixed. See http://developer.android.com/reference/android/app/FragmentTransaction.html#addSharedElement(android.view.View, java.lang.String) – ar34z Sep 29 '15 at 19:08
  • cay anyone please help me with this SO question https://stackoverflow.com/q/59431465/4291272 – Faisal Shaikh Dec 21 '19 at 14:06
21

The shared element fragment transitions do work with ListViews, as long as the source and target views have the same (and unique) transitionName.

If you make your list view adapter to set unique transitionNames to the views you want (e.g. some constant + specific item id) and also change your detail fragment to set the same transitionNames to the target views at runtime (onCreateView), the transitions actually work!

Dharman
  • 30,962
  • 25
  • 85
  • 135
  • 3
    This fixed it for me. I'm able to animate my shared ImageViews from one fragment to another within the same Activity now. Now my issue is that my ImageViews aren't translating into their new positions, they just appear in their new location but run a scale up animation. I'm using setSharedElementEnterTransition(new ChangeBounds()); when I create my fragment instance. Similar behaviour with ChangeImageTransform() – brockoli Nov 13 '14 at 02:26
  • 3
    Is it possible to get a piece of working code to test this? I have been trying for a while now, and nothing works. – FMontano Nov 25 '14 at 15:58
  • for me, even if each of the image views share the same transitionName, it kinda works. it does work fine if the image is still in memory and only after the first time the animation occurs. the catch is, as i'm using downloaded images, you can see the image view swapping for the fallback one first before loading the proper one. still, i have to forcefully load the same image in fragment B, passing the image url as an argument from fragment A. – takecare May 13 '15 at 11:13
  • @Dimitris cay you please help me with this SO question https://stackoverflow.com/q/59431465/4291272 – Faisal Shaikh Dec 21 '19 at 14:09
11

Shared elements do work with Fragments but there are some things to keep in mind:

  1. Don't try to set the sharedElementsTransition in the onCreateView of your Fragment. You have to define them when creating an instance of your Fragment or in onCreate.

  2. Take note of the official documentation on the possible animations for enter/exit transitions & sharedElementTransition. They are not the same.

  3. Trial and error :)

nickmartens1980
  • 1,593
  • 13
  • 23
Jordy
  • 1,764
  • 1
  • 22
  • 32
  • cay you please help me with this SO question https://stackoverflow.com/q/59431465/4291272 – Faisal Shaikh Dec 21 '19 at 14:08
  • Well this official code sample actually sets `sharedElementTransition` in the `onCreateView` : https://github.com/android/animation-samples/blob/master/GridToPager/app/src/main/java/com/google/samples/gridtopager/fragment/ImagePagerFragment.java – Damercy Mar 01 '22 at 07:19
3

This should be a comment to the accepted answer, as I am unable to comment on it.

The accepted answer (by WindsurferOak and ar34z) works, except for a "minor" problem which caused a null pointer exception when navigating up with the backStack. It seems that setSharedElementReturnTransition() should be called on the target fragment instead of the original fragment.

So instead of:

setSharedElementReturnTransition(TransitionInflater.from(getActivity()).inflateTransition(R.transition.change_image_transform));

it should be

fragment.setSharedElementReturnTransition(TransitionInflater.from(getActivity()).inflateTransition(R.transition.change_image_transform));

https://github.com/tevjef/Rutgers-Course-Tracker/issues/8

Paul L
  • 119
  • 1
  • 10
0

The key is to use a custom transaction with

transaction.addSharedElement(sharedElement, "sharedImage");

Shared Element Transition Between Two Fragments

In this example, one of two different ImageViews should be translated from the ChooserFragment to the DetailFragment.

In the ChooserFragment layout we need the unique transitionName attributes:

<ImageView
    android:id="@+id/image_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_first"
    android:transitionName="fistImage" />

<ImageView
    android:id="@+id/image_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_second"
    android:transitionName="secondImage" />

In the ChooserFragments class, we need to pass the View which was clicked and an ID to the parent Activity wich is handling the replacement of the fragments (we need the ID to know which image resource to show in the DetailFragment). How to pass information to a parent activity in detail is surely covered in another documentation.

view.findViewById(R.id.image_first).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (mCallback != null) {
            mCallback.showDetailFragment(view, 1);
        }
    }
});

view.findViewById(R.id.image_second).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (mCallback != null) {
            mCallback.showDetailFragment(view, 2);
        }
     }
});

In the DetailFragment, the ImageView of the shared element also needs the unique transitionName attribute.

<ImageView
    android:id="@+id/image_shared"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:transitionName="sharedImage" />

In the onCreateView() method of the DetailFragment, we have to decide which image resource should be shown (if we don't do that, the shared element will disappear after the transition).

public static DetailFragment newInstance(Bundle args) {
    DetailFragment fragment = new DetailFragment();
    fragment.setArguments(args);
    return fragment;
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    super.onCreateView(inflater, container, savedInstanceState);
    View view = inflater.inflate(R.layout.fragment_detail, container, false);

    ImageView sharedImage = (ImageView) view.findViewById(R.id.image_shared);

    // Check which resource should be shown.
    int type = getArguments().getInt("type");

    // Show image based on the type.
    switch (type) {
        case 1:
            sharedImage.setBackgroundResource(R.drawable.ic_first);
            break;

        case 2:
            sharedImage.setBackgroundResource(R.drawable.ic_second);
            break;
    }

    return view;
}

The parent Activity is receiving the callbacks and handles the replacement of the fragments.

@Override
public void showDetailFragment(View sharedElement, int type) {
    // Get the chooser fragment, which is shown in the moment.
    Fragment chooserFragment = getFragmentManager().findFragmentById(R.id.fragment_container);

    // Set up the DetailFragment and put the type as argument.
    Bundle args = new Bundle();
    args.putInt("type", type);
    Fragment fragment = DetailFragment.newInstance(args);

    // Set up the transaction.
    FragmentTransaction transaction = getFragmentManager().beginTransaction();

    // Define the shared element transition.
    fragment.setSharedElementEnterTransition(new DetailsTransition());
    fragment.setSharedElementReturnTransition(new DetailsTransition());

    // The rest of the views are just fading in/out.
    fragment.setEnterTransition(new Fade());
    chooserFragment.setExitTransition(new Fade());

    // Now use the image's view and the target transitionName to define the shared element.
    transaction.addSharedElement(sharedElement, "sharedImage");

    // Replace the fragment.
    transaction.replace(R.id.fragment_container, fragment, fragment.getClass().getSimpleName());

    // Enable back navigation with shared element transitions.
    transaction.addToBackStack(fragment.getClass().getSimpleName());

    // Finally press play.
    transaction.commit();
}

Not to forget - the Transition itself. This example moves and scales the shared element.

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class DetailsTransition extends TransitionSet {

    public DetailsTransition() {
        setOrdering(ORDERING_TOGETHER);
        addTransition(new ChangeBounds()).
            addTransition(new ChangeTransform()).
            addTransition(new ChangeImageTransform());
    }

}
Graham
  • 7,431
  • 18
  • 59
  • 84
mbo
  • 4,611
  • 2
  • 34
  • 54
0

I searched for SharedElement in fragments and I find very useful source code on GitHub.

1.first you should define transitionName for your Objects(Like ImageView) in both Fragments layout(We add a button in fragment A for handling click event):

fragment A:

  <ImageView
    android:id="@+id/fragment_a_imageView"
    android:layout_width="128dp"
    android:layout_height="96dp"
    android:layout_alignParentBottom="true"
    android:layout_centerHorizontal="true"
    android:layout_marginBottom="80dp"
    android:scaleType="centerCrop"
    android:src="@drawable/gorilla"
    android:transitionName="@string/simple_fragment_transition />

<Button
    android:id="@+id/fragment_a_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_centerHorizontal="true"
    android:layout_marginBottom="24dp"
    android:text="@string/gorilla" />

fragment B:

    <ImageView
    android:id="@+id/fragment_b_image"
    android:layout_width="match_parent"
    android:layout_height="250dp"
    android:scaleType="centerCrop"
    android:src="@drawable/gorilla"
    android:transitionName="@string/simple_fragment_transition" />
  1. Then you should write this code in your transition file in transition Directory(if you haven't this Directory so create One: res > new > Android Resource Directory > Resource Type = transition > name = change_image_transform ):

change_image_transform.xml:

 <?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
  <changeBounds/>
  <changeTransform/>
  <changeClipBounds/>
  <changeImageTransform/>
</transitionSet>
  1. In the last step you should complete codes in java:

fragment A:

public class FragmentA extends Fragment {

    public static final String TAG = FragmentA.class.getSimpleName();


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_a, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        final ImageView imageView = (ImageView) view.findViewById(R.id.fragment_a_imageView);
        Button button = (Button) view.findViewById(R.id.fragment_a_btn);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                getFragmentManager()
                        .beginTransaction()
                        .addSharedElement(imageView, ViewCompat.getTransitionName(imageView))
                        .addToBackStack(TAG)
                        .replace(R.id.content, new FragmentB())
                        .commit();
            }
        });
    }
}

fragment B:

public class FragmentB extends Fragment {

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

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_b, container, false);
    }
}

don't forget to show your "A" fragment in your activity:

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.content, new SimpleFragmentA())
                .commit();
    }

source : https://github.com/mikescamell/shared-element-transitions

Yasin Hajilou
  • 247
  • 3
  • 11
-1

How to start shared element transition using Fragments?

I assume you want to transition of your Image using Fragment (instead of Activity)

  • it wont work perfectly if you have already set AppTheme

  • keep the transition name of source and destination same

You have to do three things for transition:

1.Set transitionName to the source View(xml or programatically) -> before calling makeFragmentTransition

private void setImageZoom(boolean isImageZoom) {
    ImageView imageView = this.findViewById(R.id.image);

    if (isImageZoom) {
        imageView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                ViewCompat.setTransitionName(imageView, "imageTransition");
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    makeFragmentTransition(imageView);
            }
            }
        });
    }
}

2.Fragment Transition

  • Set TransitionSet for the specicific Transition animation

  • apply them on Fragment

  • call addSharedElement(View, transitionName) while fragmentTransition

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public void makeFragmentTransition(ImageView sourceTransitionView) { //transtionName for sourceView
    //MUST set transitionName before calling this method(programattically or give ->transitionName to the view in xml) String sourceTransitionName = ViewCompat.getTransitionName(sourceTransitionView); TransitionSet transitionSet = new TransitionSet(); transitionSet.setDuration(500); transitionSet.addTransition(new ChangeBounds()); //to expand boundaries transitionSet.addTransition(new ChangeTransform()); //for transtion vertically transitionSet.addTransition(new ChangeImageTransform()); // image transform work transitionSet.setOrdering(TransitionSet.ORDERING_TOGETHER);

      ImageTransitionFragment fragment = new ImageTransitionFragment();
      fragment.setSharedElementEnterTransition(transitionSet);
      fragment.setSharedElementReturnTransition(transitionSet);
      fragment.setAllowReturnTransitionOverlap(false);
    
      try {
    
          getHostActivity().getSupportFragmentManager()
                  .beginTransaction()
                  //sharedElement is set here for fragment 
                  //it will throw exception if transitionName is not same for source and destionationView
                  .addSharedElement(sourceTransitionView, sourceTransitionName)
                  //R.id.fragmentView is the View in activity on which fragment will load...
                  .replace(R.id.fragmentView, fragment)
                  .addToBackStack(null)
                  .commit();
      } catch (Exception e) {
          //
          String string = e.toString();
      }
    

    }

3.set desitionNation transitionName in ImageView

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/destionationTransitionPage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:transitionName="@string/pageTransition"
android:background="@color/black_color">


<com.android.foundation.ui.component.FNImageView
    android:id="@+id/destinationImageView"
    android:layout_width="@dimen/_400dp"
    android:layout_gravity="center"

    android:transitionName="imageTransition"
    android:layout_height="@dimen/_400dp" />
</FrameLayout>

Please respond if anything is not clear or it need more improvement

  • I am facing issue of Transition it is not working on my project, same code working in core project. – Beatle Refractor Feb 11 '20 at 15:53
  • Thank you for this code snippet, which might provide some limited short-term help. A proper explanation [would greatly improve](//meta.stackexchange.com/q/114762) its long-term value by showing *why* this is a good solution to the problem, and would make it more useful to future readers with other, similar questions. Please [edit] your answer to add some explanation, including the assumptions you've made. – Toby Speight Feb 11 '20 at 16:21
  • Thank you Toby, your feedback is really greatfull, I will improve my answer as soon as possible, as I have solved the question. – Beatle Refractor Feb 19 '20 at 08:48