Executing a "Shared Element Transition" from an Activity to a Fragment

I have made a github project with just the issue. You can see it / clone it / build it from here: https://git.io/vMPqb


I am trying to get shared elements working for a Fragment transition.

There are two FABs in the project - Feather and Plane. Feather and Plane are shared elements. When Feather is clicked, the SheetDialog is opened, and Feather should animate over to Plane dialog. It does not do that at the moment, and I am trying to determine why.

It may be worthwhile noting that I am running this on API 24 so issues with transitions not being supported below version 21 is not the problem.

Can anyone tell me why shared element transitions are not working?

To echo what is there in the repo, there are four important files:

Main Activity

package test.example.fabpop;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.transition.ChangeBounds;
import android.support.transition.Fade;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class MainActivity extends AppCompatActivity {

    FloatingActionButton fab_feather;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fab_feather = (FloatingActionButton) findViewById(R.id.initial_fab_feather);
    }

    public void fabClick(View view) {
        SheetDialog dialogFragment = new SheetDialog();

        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();

        // This seemingly has no effect. I am trying to get it to work.
        transaction.addSharedElement(fab_feather, "transition_name_plane");

        dialogFragment.setSharedElementEnterTransition(new ChangeBounds());
        dialogFragment.setSharedElementReturnTransition(new Fade(Fade.OUT));

        dialogFragment.show(transaction, "frag_tag");
    }
}

activity_main.xml Layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="test.example.fabpop.MainActivity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:orientation="vertical">
        <TextView
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="Transition to a BottomSheetDialogFragment that shares this FAB"
            android:textAlignment="center"/>

        <!--  Feather FAB  -->
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/initial_fab_feather"
            android:layout_width="52dp"
            android:layout_height="52dp"
            android:layout_margin="16dp"
            android:layout_gravity="center"
            android:onClick="fabClick"
            android:transitionName="transition_name_feather"
            android:src="@drawable/ic_feather"
            />
    </LinearLayout>


</RelativeLayout>

SheetDialog

package test.example.fabpop;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetDialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class SheetDialog extends BottomSheetDialogFragment {

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {

        return inflater.inflate(R.layout.dialog_sheet, container, false);
    }
}

dialog_sheet.xml Layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--  Plane FAB  -->
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/final_fab_plane"
        android:layout_width="75dp"
        android:layout_height="75dp"
        android:layout_margin="16dp"
        android:transitionName="transition_name_plane"
        android:src="@drawable/ic_plane"
        />

</LinearLayout>

Is it not possible to have a shared element on a transition between one Activity and one Fragment? Perhaps is is only possible between Activity to Activity or Fragment to Fragment, but not across the two types? Maybe this is why I cannot get it to work?

Update:

I have now tried adding <item name="android:windowContentTransitions">true</item> to the app's theme.

I have also now tried while ensuring that both transitionName values are the same on both views.

neither of these have helped to fix the issue.


Before we dive in...

Let's first quickly review how the android framework does the magical Shared Element Transition.

A Shared Element Transition is actually just one of android framework's lies. The truth is, when you're doing a Shared Element Transition, you're not actually sharing any view between your Activities, e.i, you're dealing with two separate views. That's because each Activity has its own independent view tree.

Huh?!

Suppose that you're trying to transition a view identified by view_a from ActivityA to a view_b in ActivityB. What the framework does is that, it first looks for certain properties of your view_a, such as the size (width, height) and the position (x,y) in the ActivityA. It then passes these information to ActivityB, apply these properties on the view_b so that it occupies the exact same spot as view_a when ActivityA gets closed. After that, the framework starts the transition by reverse animating your view_b to how it is supposed to look in ActivityB. This is how the illusion of the view being shared is created. Magic!

Therefore,

What we can deduce from above is that before any animation can be started, one has to make sure that view_b has already been created on ActivityB, otherwise, this wouldn't be possible.

In the case of using a Fragment,

Calling FragmentTransaction.commit()will just schedule your fragment transaction (the fragment will NOT be created immediately after its containing Activity has been created).

So, in your case, when your ActivityB is created, your view_b is missing (as explained, that's because its containing fragment hasn't been created yet).

Solution

Make sure your view_b is created before the animation starts.

For that, you'll have to find a way to tell the framework to not do the usual thing, but to wait instead for your signal before creating the animation.

One way to implement this is to alter your code to be something similar to this one:

ActivityB

class ActivityB extends Activity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
        // Tell the framework to wait.
        postponeEnterTransition();
    }
}

FragmentB

class FragmentB extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View viewB = getView().findViewById(R.id.view_b);
    sharedview.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
                // Tell the framework to start.
                sharedview.getViewTreeObserver().removeOnPreDrawListener(this);
                getActivity().startPostponedEnterTransition(); 
                return true;
         }
       });
       ...
    }
}

Further reading

Getting Started with Activity & Fragment Transitions


What you're looking for is this example: https://github.com/hujiaweibujidao/FabDialogMorph. Note, that what you're trying to achieve is not standard Android transition, you need to create your own morphing transition, based on ChangeBounds transition. Originally, it has been shown in Plaid, thanks to Nick Butcher for that, so you can check it for more tips and background.


Just quoting the Android Docs:

Start an activity with a shared element

  1. Assign a common name to the shared elements in both layouts with the android:transitionName attribute.

From what I see, both of your XMLs don't share the same android:transitionName.

Try changing both android:transitionName in the XML layout (activity_main.xml & dialog_sheet.xml) to a same string, for example: transition_plane_feather

I havent't tested the codes myself, but I think that might be a good starting point.

Bonus Tip

If you're planning to fully implement Material Design without compatibility with lower API levels (pre-Lollipop), I highly recommend checking out a sample made by a Google UI/UX Designer: https://github.com/nickbutcher/plaid

Also, check out the excellent supplementary YouTube video to that sample: https://www.youtube.com/watch?v=EjTJIDKT72M

It shows you some of the best tricks and practices that you can use to fully implement Material Design as well as shared element transitions.