/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v4.app; import static junit.framework.Assert.assertNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import android.app.Instrumentation; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.support.fragment.test.R; import android.support.test.InstrumentationRegistry; import android.support.test.filters.MediumTest; import android.support.test.filters.SdkSuppress; import android.support.test.rule.ActivityTestRule; import android.support.v4.app.test.FragmentTestActivity; import android.transition.TransitionSet; import android.view.View; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.mockito.ArgumentCaptor; import java.util.ArrayList; import java.util.List; import java.util.Map; @MediumTest @RunWith(Parameterized.class) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) public class FragmentTransitionTest { private final boolean mOptimize; @Parameterized.Parameters public static Object[] data() { return new Boolean[] { false, true }; } @Rule public ActivityTestRule<FragmentTestActivity> mActivityRule = new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class); private Instrumentation mInstrumentation; private FragmentManager mFragmentManager; public FragmentTransitionTest(final boolean optimize) { mOptimize = optimize; } @Before public void setup() throws Throwable { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mFragmentManager = mActivityRule.getActivity().getSupportFragmentManager(); FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container); } // Test that normal view transitions (enter, exit, reenter, return) run with // a single fragment. @Test public void enterExitTransitions() throws Throwable { // enter transition TransitionFragment fragment = setupInitialFragment(); final View blue = findBlue(); final View green = findBlue(); // exit transition mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .remove(fragment) .addToBackStack(null) .commit(); fragment.waitForTransition(); verifyAndClearTransition(fragment.exitTransition, null, green, blue); verifyNoOtherTransitions(fragment); // reenter transition FragmentTestUtil.popBackStackImmediate(mActivityRule); fragment.waitForTransition(); final View green2 = findGreen(); final View blue2 = findBlue(); verifyAndClearTransition(fragment.reenterTransition, null, green2, blue2); verifyNoOtherTransitions(fragment); // return transition FragmentTestUtil.popBackStackImmediate(mActivityRule); fragment.waitForTransition(); verifyAndClearTransition(fragment.returnTransition, null, green2, blue2); verifyNoOtherTransitions(fragment); } // Test that shared elements transition from one fragment to the next // and back during pop. @Test public void sharedElement() throws Throwable { TransitionFragment fragment1 = setupInitialFragment(); // Now do a transition to scene2 TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene2); verifyTransition(fragment1, fragment2, "blueSquare"); // Now pop the back stack verifyPopTransition(1, fragment2, fragment1); } // Test that shared element transitions through multiple fragments work together @Test public void intermediateFragment() throws Throwable { TransitionFragment fragment1 = setupInitialFragment(); final TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene3); verifyTransition(fragment1, fragment2, "shared"); final TransitionFragment fragment3 = new TransitionFragment(); fragment3.setLayoutId(R.layout.scene2); verifyTransition(fragment2, fragment3, "blueSquare"); // Should transfer backwards when popping multiple: verifyPopTransition(2, fragment3, fragment1, fragment2); } // Adding/removing the same fragment multiple times shouldn't mess anything up @Test public void removeAdded() throws Throwable { final TransitionFragment fragment1 = setupInitialFragment(); final View startBlue = findBlue(); final View startGreen = findGreen(); final TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene2); mInstrumentation.runOnMainSync(new Runnable() { @Override public void run() { mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .replace(R.id.fragmentContainer, fragment2) .replace(R.id.fragmentContainer, fragment1) .replace(R.id.fragmentContainer, fragment2) .addToBackStack(null) .commit(); } }); FragmentTestUtil.waitForExecution(mActivityRule); // should be a normal transition from fragment1 to fragment2 fragment2.waitForTransition(); final View endBlue = findBlue(); final View endGreen = findGreen(); verifyAndClearTransition(fragment1.exitTransition, null, startBlue, startGreen); verifyAndClearTransition(fragment2.enterTransition, null, endBlue, endGreen); verifyNoOtherTransitions(fragment1); verifyNoOtherTransitions(fragment2); // Pop should also do the same thing FragmentTestUtil.popBackStackImmediate(mActivityRule); fragment1.waitForTransition(); final View popBlue = findBlue(); final View popGreen = findGreen(); verifyAndClearTransition(fragment1.reenterTransition, null, popBlue, popGreen); verifyAndClearTransition(fragment2.returnTransition, null, endBlue, endGreen); verifyNoOtherTransitions(fragment1); verifyNoOtherTransitions(fragment2); } // Make sure that shared elements on two different fragment containers don't interact @Test public void crossContainer() throws Throwable { FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container); TransitionFragment fragment1 = new TransitionFragment(); fragment1.setLayoutId(R.layout.scene1); TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene1); mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .add(R.id.fragmentContainer1, fragment1) .add(R.id.fragmentContainer2, fragment2) .addToBackStack(null) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); fragment1.waitForTransition(); final View greenSquare1 = findViewById(fragment1, R.id.greenSquare); final View blueSquare1 = findViewById(fragment1, R.id.blueSquare); verifyAndClearTransition(fragment1.enterTransition, null, greenSquare1, blueSquare1); verifyNoOtherTransitions(fragment1); fragment2.waitForTransition(); final View greenSquare2 = findViewById(fragment2, R.id.greenSquare); final View blueSquare2 = findViewById(fragment2, R.id.blueSquare); verifyAndClearTransition(fragment2.enterTransition, null, greenSquare2, blueSquare2); verifyNoOtherTransitions(fragment2); // Make sure the correct transitions are run when the target names // are different in both shared elements. We may fool the system. verifyCrossTransition(false, fragment1, fragment2); // Make sure the correct transitions are run when the source names // are different in both shared elements. We may fool the system. verifyCrossTransition(true, fragment1, fragment2); } // Make sure that onSharedElementStart and onSharedElementEnd are called @Test public void callStartEndWithSharedElements() throws Throwable { TransitionFragment fragment1 = setupInitialFragment(); // Now do a transition to scene2 TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene2); SharedElementCallback enterCallback = mock(SharedElementCallback.class); fragment2.setEnterSharedElementCallback(enterCallback); final View startBlue = findBlue(); verifyTransition(fragment1, fragment2, "blueSquare"); ArgumentCaptor<List> names = ArgumentCaptor.forClass(List.class); ArgumentCaptor<List> views = ArgumentCaptor.forClass(List.class); ArgumentCaptor<List> snapshots = ArgumentCaptor.forClass(List.class); verify(enterCallback).onSharedElementStart(names.capture(), views.capture(), snapshots.capture()); assertEquals(1, names.getValue().size()); assertEquals(1, views.getValue().size()); assertNull(snapshots.getValue()); assertEquals("blueSquare", names.getValue().get(0)); assertEquals(startBlue, views.getValue().get(0)); final View endBlue = findBlue(); verify(enterCallback).onSharedElementEnd(names.capture(), views.capture(), snapshots.capture()); assertEquals(1, names.getValue().size()); assertEquals(1, views.getValue().size()); assertNull(snapshots.getValue()); assertEquals("blueSquare", names.getValue().get(0)); assertEquals(endBlue, views.getValue().get(0)); // Now pop the back stack reset(enterCallback); verifyPopTransition(1, fragment2, fragment1); verify(enterCallback).onSharedElementStart(names.capture(), views.capture(), snapshots.capture()); assertEquals(1, names.getValue().size()); assertEquals(1, views.getValue().size()); assertNull(snapshots.getValue()); assertEquals("blueSquare", names.getValue().get(0)); assertEquals(endBlue, views.getValue().get(0)); final View reenterBlue = findBlue(); verify(enterCallback).onSharedElementEnd(names.capture(), views.capture(), snapshots.capture()); assertEquals(1, names.getValue().size()); assertEquals(1, views.getValue().size()); assertNull(snapshots.getValue()); assertEquals("blueSquare", names.getValue().get(0)); assertEquals(reenterBlue, views.getValue().get(0)); } // Make sure that onMapSharedElement works to change the shared element going out @Test public void onMapSharedElementOut() throws Throwable { final TransitionFragment fragment1 = setupInitialFragment(); // Now do a transition to scene2 TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene2); final View startBlue = findBlue(); final View startGreen = findGreen(); final Rect startGreenBounds = getBoundsOnScreen(startGreen); SharedElementCallback mapOut = new SharedElementCallback() { @Override public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { assertEquals(1, names.size()); assertEquals("blueSquare", names.get(0)); assertEquals(1, sharedElements.size()); assertEquals(startBlue, sharedElements.get("blueSquare")); sharedElements.put("blueSquare", startGreen); } }; fragment1.setExitSharedElementCallback(mapOut); mFragmentManager.beginTransaction() .addSharedElement(startBlue, "blueSquare") .replace(R.id.fragmentContainer, fragment2) .setAllowOptimization(mOptimize) .addToBackStack(null) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); fragment1.waitForTransition(); fragment2.waitForTransition(); final View endBlue = findBlue(); final Rect endBlueBounds = getBoundsOnScreen(endBlue); verifyAndClearTransition(fragment2.sharedElementEnter, startGreenBounds, startGreen, endBlue); SharedElementCallback mapBack = new SharedElementCallback() { @Override public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { assertEquals(1, names.size()); assertEquals("blueSquare", names.get(0)); assertEquals(1, sharedElements.size()); final View expectedBlue = findViewById(fragment1, R.id.blueSquare); assertEquals(expectedBlue, sharedElements.get("blueSquare")); final View greenSquare = findViewById(fragment1, R.id.greenSquare); sharedElements.put("blueSquare", greenSquare); } }; fragment1.setExitSharedElementCallback(mapBack); FragmentTestUtil.popBackStackImmediate(mActivityRule); fragment1.waitForTransition(); fragment2.waitForTransition(); final View reenterGreen = findGreen(); verifyAndClearTransition(fragment2.sharedElementReturn, endBlueBounds, endBlue, reenterGreen); } // Make sure that onMapSharedElement works to change the shared element target @Test public void onMapSharedElementIn() throws Throwable { TransitionFragment fragment1 = setupInitialFragment(); // Now do a transition to scene2 final TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene2); final View startBlue = findBlue(); final Rect startBlueBounds = getBoundsOnScreen(startBlue); SharedElementCallback mapIn = new SharedElementCallback() { @Override public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { assertEquals(1, names.size()); assertEquals("blueSquare", names.get(0)); assertEquals(1, sharedElements.size()); final View blueSquare = findViewById(fragment2, R.id.blueSquare); assertEquals(blueSquare, sharedElements.get("blueSquare")); final View greenSquare = findViewById(fragment2, R.id.greenSquare); sharedElements.put("blueSquare", greenSquare); } }; fragment2.setEnterSharedElementCallback(mapIn); mFragmentManager.beginTransaction() .addSharedElement(startBlue, "blueSquare") .replace(R.id.fragmentContainer, fragment2) .setAllowOptimization(mOptimize) .addToBackStack(null) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); fragment1.waitForTransition(); fragment2.waitForTransition(); final View endGreen = findGreen(); final View endBlue = findBlue(); final Rect endGreenBounds = getBoundsOnScreen(endGreen); verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue, endGreen); SharedElementCallback mapBack = new SharedElementCallback() { @Override public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { assertEquals(1, names.size()); assertEquals("blueSquare", names.get(0)); assertEquals(1, sharedElements.size()); assertEquals(endBlue, sharedElements.get("blueSquare")); sharedElements.put("blueSquare", endGreen); } }; fragment2.setEnterSharedElementCallback(mapBack); FragmentTestUtil.popBackStackImmediate(mActivityRule); fragment1.waitForTransition(); fragment2.waitForTransition(); final View reenterBlue = findBlue(); verifyAndClearTransition(fragment2.sharedElementReturn, endGreenBounds, endGreen, reenterBlue); } // Ensure that shared element transitions that have targets properly target the views @Test public void complexSharedElementTransition() throws Throwable { TransitionFragment fragment1 = setupInitialFragment(); // Now do a transition to scene2 ComplexTransitionFragment fragment2 = new ComplexTransitionFragment(); fragment2.setLayoutId(R.layout.scene2); final View startBlue = findBlue(); final View startGreen = findGreen(); final Rect startBlueBounds = getBoundsOnScreen(startBlue); mFragmentManager.beginTransaction() .addSharedElement(startBlue, "blueSquare") .addSharedElement(startGreen, "greenSquare") .replace(R.id.fragmentContainer, fragment2) .addToBackStack(null) .setAllowOptimization(true) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); fragment1.waitForTransition(); fragment2.waitForTransition(); final View endBlue = findBlue(); final View endGreen = findGreen(); final Rect endBlueBounds = getBoundsOnScreen(endBlue); verifyAndClearTransition(fragment2.sharedElementEnterTransition1, startBlueBounds, startBlue, endBlue); verifyAndClearTransition(fragment2.sharedElementEnterTransition2, startBlueBounds, startGreen, endGreen); // Now see if it works when popped FragmentTestUtil.popBackStackImmediate(mActivityRule); fragment1.waitForTransition(); fragment2.waitForTransition(); final View reenterBlue = findBlue(); final View reenterGreen = findGreen(); verifyAndClearTransition(fragment2.sharedElementReturnTransition1, endBlueBounds, endBlue, reenterBlue); verifyAndClearTransition(fragment2.sharedElementReturnTransition2, endBlueBounds, endGreen, reenterGreen); } // Ensure that after transitions have executed that they don't have any targets or other // unfortunate modifications. @Test public void transitionsEndUnchanged() throws Throwable { TransitionFragment fragment1 = setupInitialFragment(); // Now do a transition to scene2 TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene2); verifyTransition(fragment1, fragment2, "blueSquare"); assertEquals(0, fragment1.exitTransition.getTargets().size()); assertEquals(0, fragment2.sharedElementEnter.getTargets().size()); assertEquals(0, fragment2.enterTransition.getTargets().size()); assertNull(fragment1.exitTransition.getEpicenterCallback()); assertNull(fragment2.enterTransition.getEpicenterCallback()); assertNull(fragment2.sharedElementEnter.getEpicenterCallback()); // Now pop the back stack verifyPopTransition(1, fragment2, fragment1); assertEquals(0, fragment2.returnTransition.getTargets().size()); assertEquals(0, fragment2.sharedElementReturn.getTargets().size()); assertEquals(0, fragment1.reenterTransition.getTargets().size()); assertNull(fragment2.returnTransition.getEpicenterCallback()); assertNull(fragment2.sharedElementReturn.getEpicenterCallback()); assertNull(fragment2.reenterTransition.getEpicenterCallback()); } // Ensure that transitions are done when a fragment is shown and hidden @Test public void showHideTransition() throws Throwable { TransitionFragment fragment1 = setupInitialFragment(); TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene2); final View startBlue = findBlue(); final View startGreen = findGreen(); mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .add(R.id.fragmentContainer, fragment2) .hide(fragment1) .addToBackStack(null) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); fragment1.waitForTransition(); fragment2.waitForTransition(); final View endGreen = findViewById(fragment2, R.id.greenSquare); final View endBlue = findViewById(fragment2, R.id.blueSquare); assertEquals(View.GONE, fragment1.getView().getVisibility()); assertEquals(View.VISIBLE, startGreen.getVisibility()); assertEquals(View.VISIBLE, startBlue.getVisibility()); verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue); verifyNoOtherTransitions(fragment1); verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue); verifyNoOtherTransitions(fragment2); FragmentTestUtil.popBackStackImmediate(mActivityRule); FragmentTestUtil.waitForExecution(mActivityRule); fragment1.waitForTransition(); fragment2.waitForTransition(); verifyAndClearTransition(fragment1.reenterTransition, null, startGreen, startBlue); verifyNoOtherTransitions(fragment1); assertEquals(View.VISIBLE, fragment1.getView().getVisibility()); assertEquals(View.VISIBLE, startGreen.getVisibility()); assertEquals(View.VISIBLE, startBlue.getVisibility()); verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue); verifyNoOtherTransitions(fragment2); } // Ensure that transitions are done when a fragment is attached and detached @Test public void attachDetachTransition() throws Throwable { TransitionFragment fragment1 = setupInitialFragment(); TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene2); final View startBlue = findBlue(); final View startGreen = findGreen(); mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .add(R.id.fragmentContainer, fragment2) .detach(fragment1) .addToBackStack(null) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); final View endGreen = findViewById(fragment2, R.id.greenSquare); final View endBlue = findViewById(fragment2, R.id.blueSquare); verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue); verifyNoOtherTransitions(fragment1); verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue); verifyNoOtherTransitions(fragment2); FragmentTestUtil.popBackStackImmediate(mActivityRule); FragmentTestUtil.waitForExecution(mActivityRule); final View reenterBlue = findBlue(); final View reenterGreen = findGreen(); verifyAndClearTransition(fragment1.reenterTransition, null, reenterGreen, reenterBlue); verifyNoOtherTransitions(fragment1); verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue); verifyNoOtherTransitions(fragment2); } // Ensure that shared element without matching transition name doesn't error out @Test public void sharedElementMismatch() throws Throwable { final TransitionFragment fragment1 = setupInitialFragment(); // Now do a transition to scene2 TransitionFragment fragment2 = new TransitionFragment(); fragment2.setLayoutId(R.layout.scene2); final View startBlue = findBlue(); final View startGreen = findGreen(); final Rect startBlueBounds = getBoundsOnScreen(startBlue); mFragmentManager.beginTransaction() .addSharedElement(startBlue, "fooSquare") .replace(R.id.fragmentContainer, fragment2) .setAllowOptimization(mOptimize) .addToBackStack(null) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); fragment1.waitForTransition(); fragment2.waitForTransition(); final View endBlue = findBlue(); final View endGreen = findGreen(); if (mOptimize) { verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue); } else { verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen); verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue); } verifyNoOtherTransitions(fragment1); verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue); verifyNoOtherTransitions(fragment2); } // Ensure that using the same source or target shared element results in an exception. @Test public void sharedDuplicateTargetNames() throws Throwable { setupInitialFragment(); final View startBlue = findBlue(); final View startGreen = findGreen(); FragmentTransaction ft = mFragmentManager.beginTransaction(); ft.addSharedElement(startBlue, "blueSquare"); try { ft.addSharedElement(startGreen, "blueSquare"); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { // expected } try { ft.addSharedElement(startBlue, "greenSquare"); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { // expected } } // Test that invisible fragment views don't participate in transitions @Test public void invisibleNoTransitions() throws Throwable { if (!mOptimize) { return; // only optimized transitions can avoid interaction } // enter transition TransitionFragment fragment = new InvisibleFragment(); fragment.setLayoutId(R.layout.scene1); mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .add(R.id.fragmentContainer, fragment) .addToBackStack(null) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); fragment.waitForNoTransition(); verifyNoOtherTransitions(fragment); // exit transition mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .remove(fragment) .addToBackStack(null) .commit(); fragment.waitForNoTransition(); verifyNoOtherTransitions(fragment); // reenter transition FragmentTestUtil.popBackStackImmediate(mActivityRule); fragment.waitForNoTransition(); verifyNoOtherTransitions(fragment); // return transition FragmentTestUtil.popBackStackImmediate(mActivityRule); fragment.waitForNoTransition(); verifyNoOtherTransitions(fragment); } private TransitionFragment setupInitialFragment() throws Throwable { TransitionFragment fragment1 = new TransitionFragment(); fragment1.setLayoutId(R.layout.scene1); mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .add(R.id.fragmentContainer, fragment1) .addToBackStack(null) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); fragment1.waitForTransition(); final View blueSquare1 = findBlue(); final View greenSquare1 = findGreen(); verifyAndClearTransition(fragment1.enterTransition, null, blueSquare1, greenSquare1); verifyNoOtherTransitions(fragment1); return fragment1; } private View findViewById(Fragment fragment, int id) { return fragment.getView().findViewById(id); } private View findGreen() { return mActivityRule.getActivity().findViewById(R.id.greenSquare); } private View findBlue() { return mActivityRule.getActivity().findViewById(R.id.blueSquare); } private View findRed() { return mActivityRule.getActivity().findViewById(R.id.redSquare); } private void verifyAndClearTransition(TargetTracking transition, Rect epicenter, View... expected) { if (epicenter == null) { assertNull(transition.getCapturedEpicenter()); } else { assertEquals(epicenter, transition.getCapturedEpicenter()); } ArrayList<View> targets = transition.getTrackedTargets(); StringBuilder sb = new StringBuilder(); sb .append("Expected: [") .append(expected.length) .append("] {"); boolean isFirst = true; for (View view : expected) { if (isFirst) { isFirst = false; } else { sb.append(", "); } sb.append(view); } sb .append("}, but got: [") .append(targets.size()) .append("] {"); isFirst = true; for (View view : targets) { if (isFirst) { isFirst = false; } else { sb.append(", "); } sb.append(view); } sb.append("}"); String errorMessage = sb.toString(); assertEquals(errorMessage, expected.length, targets.size()); for (View view : expected) { assertTrue(errorMessage, targets.contains(view)); } transition.clearTargets(); } private void verifyNoOtherTransitions(TransitionFragment fragment) { assertEquals(0, fragment.enterTransition.targets.size()); assertEquals(0, fragment.exitTransition.targets.size()); assertEquals(0, fragment.reenterTransition.targets.size()); assertEquals(0, fragment.returnTransition.targets.size()); assertEquals(0, fragment.sharedElementEnter.targets.size()); assertEquals(0, fragment.sharedElementReturn.targets.size()); } private void verifyTransition(TransitionFragment from, TransitionFragment to, String sharedElementName) throws Throwable { final View startBlue = findBlue(); final View startGreen = findGreen(); final View startRed = findRed(); final Rect startBlueRect = getBoundsOnScreen(startBlue); mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .addSharedElement(startBlue, sharedElementName) .replace(R.id.fragmentContainer, to) .addToBackStack(null) .commit(); FragmentTestUtil.waitForExecution(mActivityRule); to.waitForTransition(); final View endGreen = findGreen(); final View endBlue = findBlue(); final View endRed = findRed(); final Rect endBlueRect = getBoundsOnScreen(endBlue); if (startRed != null) { verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen, startRed); } else { verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen); } verifyNoOtherTransitions(from); if (endRed != null) { verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen, endRed); } else { verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen); } verifyAndClearTransition(to.sharedElementEnter, startBlueRect, startBlue, endBlue); verifyNoOtherTransitions(to); } private void verifyCrossTransition(boolean swapSource, TransitionFragment from1, TransitionFragment from2) throws Throwable { final TransitionFragment to1 = new TransitionFragment(); to1.setLayoutId(R.layout.scene2); final TransitionFragment to2 = new TransitionFragment(); to2.setLayoutId(R.layout.scene2); final View fromExit1 = findViewById(from1, R.id.greenSquare); final View fromShared1 = findViewById(from1, R.id.blueSquare); final Rect fromSharedRect1 = getBoundsOnScreen(fromShared1); final int fromExitId2 = swapSource ? R.id.blueSquare : R.id.greenSquare; final int fromSharedId2 = swapSource ? R.id.greenSquare : R.id.blueSquare; final View fromExit2 = findViewById(from2, fromExitId2); final View fromShared2 = findViewById(from2, fromSharedId2); final Rect fromSharedRect2 = getBoundsOnScreen(fromShared2); final String sharedElementName = swapSource ? "blueSquare" : "greenSquare"; mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .addSharedElement(fromShared1, "blueSquare") .replace(R.id.fragmentContainer1, to1) .addToBackStack(null) .commit(); mFragmentManager.beginTransaction() .setAllowOptimization(mOptimize) .addSharedElement(fromShared2, sharedElementName) .replace(R.id.fragmentContainer2, to2) .addToBackStack(null) .commit(); } }); FragmentTestUtil.waitForExecution(mActivityRule); from1.waitForTransition(); from2.waitForTransition(); to1.waitForTransition(); to2.waitForTransition(); final View toEnter1 = findViewById(to1, R.id.greenSquare); final View toShared1 = findViewById(to1, R.id.blueSquare); final Rect toSharedRect1 = getBoundsOnScreen(toShared1); final View toEnter2 = findViewById(to2, fromSharedId2); final View toShared2 = findViewById(to2, fromExitId2); final Rect toSharedRect2 = getBoundsOnScreen(toShared2); verifyAndClearTransition(from1.exitTransition, fromSharedRect1, fromExit1); verifyAndClearTransition(from2.exitTransition, fromSharedRect2, fromExit2); verifyNoOtherTransitions(from1); verifyNoOtherTransitions(from2); verifyAndClearTransition(to1.enterTransition, toSharedRect1, toEnter1); verifyAndClearTransition(to2.enterTransition, toSharedRect2, toEnter2); verifyAndClearTransition(to1.sharedElementEnter, fromSharedRect1, fromShared1, toShared1); verifyAndClearTransition(to2.sharedElementEnter, fromSharedRect2, fromShared2, toShared2); verifyNoOtherTransitions(to1); verifyNoOtherTransitions(to2); // Now pop it back mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mFragmentManager.popBackStack(); mFragmentManager.popBackStack(); } }); FragmentTestUtil.waitForExecution(mActivityRule); from1.waitForTransition(); from2.waitForTransition(); to1.waitForTransition(); to2.waitForTransition(); final View returnEnter1 = findViewById(from1, R.id.greenSquare); final View returnShared1 = findViewById(from1, R.id.blueSquare); final View returnEnter2 = findViewById(from2, fromExitId2); final View returnShared2 = findViewById(from2, fromSharedId2); verifyAndClearTransition(to1.returnTransition, toSharedRect1, toEnter1); verifyAndClearTransition(to2.returnTransition, toSharedRect2, toEnter2); verifyAndClearTransition(to1.sharedElementReturn, toSharedRect1, toShared1, returnShared1); verifyAndClearTransition(to2.sharedElementReturn, toSharedRect2, toShared2, returnShared2); verifyNoOtherTransitions(to1); verifyNoOtherTransitions(to2); verifyAndClearTransition(from1.reenterTransition, fromSharedRect1, returnEnter1); verifyAndClearTransition(from2.reenterTransition, fromSharedRect2, returnEnter2); verifyNoOtherTransitions(from1); verifyNoOtherTransitions(from2); } private void verifyPopTransition(final int numPops, TransitionFragment from, TransitionFragment to, TransitionFragment... others) throws Throwable { final View startBlue = findBlue(); final View startGreen = findGreen(); final View startRed = findRed(); final Rect startSharedRect = getBoundsOnScreen(startBlue); mInstrumentation.runOnMainSync(new Runnable() { @Override public void run() { for (int i = 0; i < numPops; i++) { mFragmentManager.popBackStack(); } } }); FragmentTestUtil.waitForExecution(mActivityRule); to.waitForTransition(); final View endGreen = findGreen(); final View endBlue = findBlue(); final View endRed = findRed(); final Rect endSharedRect = getBoundsOnScreen(endBlue); if (startRed != null) { verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen, startRed); } else { verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen); } verifyAndClearTransition(from.sharedElementReturn, startSharedRect, startBlue, endBlue); verifyNoOtherTransitions(from); if (endRed != null) { verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen, endRed); } else { verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen); } verifyNoOtherTransitions(to); if (others != null) { for (TransitionFragment fragment : others) { verifyNoOtherTransitions(fragment); } } } private static Rect getBoundsOnScreen(View view) { final int[] loc = new int[2]; view.getLocationOnScreen(loc); return new Rect(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight()); } public static class ComplexTransitionFragment extends TransitionFragment { public final TrackingTransition sharedElementEnterTransition1 = new TrackingTransition(); public final TrackingTransition sharedElementEnterTransition2 = new TrackingTransition(); public final TrackingTransition sharedElementReturnTransition1 = new TrackingTransition(); public final TrackingTransition sharedElementReturnTransition2 = new TrackingTransition(); public final TransitionSet sharedElementEnterTransition = new TransitionSet() .addTransition(sharedElementEnterTransition1) .addTransition(sharedElementEnterTransition2); public final TransitionSet sharedElementReturnTransition = new TransitionSet() .addTransition(sharedElementReturnTransition1) .addTransition(sharedElementReturnTransition2); public ComplexTransitionFragment() { sharedElementEnterTransition1.addTarget(R.id.blueSquare); sharedElementEnterTransition2.addTarget(R.id.greenSquare); sharedElementReturnTransition1.addTarget(R.id.blueSquare); sharedElementReturnTransition2.addTarget(R.id.greenSquare); setSharedElementEnterTransition(sharedElementEnterTransition); setSharedElementReturnTransition(sharedElementReturnTransition); } } public static class InvisibleFragment extends TransitionFragment { @Override public void onViewCreated(View view, Bundle savedInstanceState) { view.setVisibility(View.INVISIBLE); super.onViewCreated(view, savedInstanceState); } } }