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.
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.
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 what it's supposed to be in ActivityB
. And this is how the illusion of the view being shared is created. Magic!
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.
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).
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:
class ActivityB extends Activity{
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
// Tell the framework to wait.
postponeEnterTransition();
}
}
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;
}
});
...
}
}