Search code examples
androidandroid-animationandroid-5.0-lollipopactivity-transitionshared-element-transition

Shared Element Transition Glitch in Android Lollipop


I'm trying to do an Android 5.0 shared element transition in a Master Detail situation from a ListView. I'm sending an Image.

I've run into a glitch on the enter transition where the Image doesn't move at all while the Master activity transitions out. Once the Detail activity is loaded, the Image starts in the top-left of the screen and animates into it's final location. The return transition works perfectly - the image animates from it's Detail position to the proper position in Master.

Here's what it looks like: https://www.youtube.com/watch?v=AzyA8i27qWc

I've set up the permissions correctly I believe, and specified the transition:

<item name="android:windowContentTransitions">true</item>        
<item name="android:windowAllowEnterTransitionOverlap">true</item>
<item name="android:windowAllowReturnTransitionOverlap">true</item>

<!-- specify shared element transitions -->
<item name="android:windowSharedElementEnterTransition">
    @transition/change_image_transform</item>
<item name="android:windowSharedElementExitTransition">
    @transition/change_image_transform</item>

That transition is defined as:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeImageTransform/>
    <changeBounds/>
    <changeTransform/>
</transitionSet>

I've given the two views the same unique transitionName attribute, based on their position in the list.

I'm using makeSceneTransitionAnimation to do the animation:

    View photo = (View) adapter.getView(position, null, listView).findViewById(R.id.photo);
    View navigationBar = findViewById(android.R.id.navigationBarBackground);

    ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this,
            Pair.create(photo, photo.getTransitionName()),
            Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME));

    Bundle extras = new Bundle();
    Patient selectedPatient = patientList.get(position);
    extras.putParcelable("patient", selectedPatient);
    extras.putInt("position", position);

    Intent intent = new Intent(PatientListActivity.this, PatientActivity_.class);
    intent.putExtras(extras);

    startActivity(intent, options.toBundle());

(I'm also specifying the navigationBar as shared to avoid it fading out, which works fine)

I don't understand why the return transition works fine but the enter transition doesn't. I've seen other questions where the solution was to delay the enter transition, which I have tried doing but the problem remains. Here's what I added to onCreate in my Detail activity:

super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_patient);

    patient = getIntent().getExtras().getParcelable("patient");
    patient = app.getPatientByName(patient.getName());

    patientPhotoView.setImageDrawable(patient.getPhoto());

    /* Set up transition functionality */
    position = getIntent().getExtras().getInt("position");
    patientPhotoView.setTransitionName("patientPhoto" + position);
    patientNameView.setTransitionName("patientName" + position);

    postponeEnterTransition();
    final ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver();
    observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            final ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver();
            observer.removeOnPreDrawListener(this);
            startPostponedEnterTransition();
            return true;
        }
    });

In my Detail Activity, there is no transitionName element, but I am setting it in code. The relevant part of the incoming activity (the Detail) is:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PatientActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="200dip"
            android:orientation="vertical"
            android:id="@+id/patient_top_layout">
            <ImageView
                android:id="@+id/patientPhoto"
                android:layout_width="92dip"
                android:layout_height="92dip"
                android:layout_below="@+id/connectionBar"
                android:layout_marginRight="12dip"
                android:layout_marginTop="12dip"
                android:contentDescription="Patient photo"
                android:src="@drawable/ic_profile_default" />
         </RelativeLayout>
    </LinearLayout>
</RelativeLayout

Happy to post other relevant code. Any ideas?

SOLVED

The user Chris P in the Android Development Google+ page figured this out for me. In my Master Activity in the onClick method, I had:

View photo = (View) adapter.getView(position, null, listView).findViewById(R.id.photo);

The solution was to replace that line with:

View photo = (View) listView.getChildAt(position).findViewById(R.id.photo);

I guess calling the adapter's getView method was re-creating the view itself - I'm guessing that method should ONLY be called when the list is first created.


Solution

  • Tried to leave this in comments, but it was too long. I tried out your layout and didn't have any problems. I didn't even need the postponeEnterTransition call. That makes things a little trickier to debug.

    The way Transitions work is by capturing the state of things before a change and then after a change and then animating the difference. In Activity Transitions, the "before" change is pulled from screen location and size of the shared element in the calling Activity. It then applies those bounds the current view hierarchy. It then requests a layout and captures the "after" state.

    There's a sensitive point between the time the "before" state is captured and the "after" state is captured. If layout changes for any reason in there such that the values are captured in the after state aren't just a relayout of the "before" state, the Transition is going to be thrown off. Also possible is making a change to layout immediately after the Transition has started.

    Are you using a delay loading mechanism or are you really fully loading the Patient in the onCreate call? Are you setting all the surrounding values at that time as well? I see that you have:

            <ImageView ...
                android:layout_below="@+id/connectionBar"
                ... />
    

    So you may be getting a relayout after the connectionBar changes.