blob: 6f63e981ea45cfd1793eda8e615c666200d865ba [file] [log] [blame]
/*
* 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.fragment.cts;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.SharedElementCallback;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.transition.TransitionSet;
import android.view.View;
import com.android.compatibility.common.util.transition.TargetTracking;
import com.android.compatibility.common.util.transition.TrackingTransition;
import org.junit.After;
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.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@MediumTest
@RunWith(Parameterized.class)
public class FragmentTransitionTest {
private final boolean mReordered;
private int mOnBackStackChangedTimes;
private FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener;
@Parameterized.Parameters
public static Object[] data() {
return new Boolean[] {
false, true
};
}
@Rule
public ActivityTestRule<FragmentTestActivity> mActivityRule =
new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class);
private FragmentManager mFragmentManager;
public FragmentTransitionTest(final boolean reordered) {
mReordered = reordered;
}
@Before
public void setup() throws Throwable {
mFragmentManager = mActivityRule.getActivity().getFragmentManager();
FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
mOnBackStackChangedTimes = 0;
mOnBackStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
mOnBackStackChangedTimes++;
}
};
mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
}
@After
public void teardown() throws Throwable {
mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener);
mOnBackStackChangedListener = null;
}
// 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()
.setReorderingAllowed(mReordered)
.remove(fragment)
.addToBackStack(null)
.commit();
fragment.waitForTransition();
verifyAndClearTransition(fragment.exitTransition, null, green, blue);
verifyNoOtherTransitions(fragment);
assertEquals(2, mOnBackStackChangedTimes);
// reenter transition
FragmentTestUtil.popBackStackImmediate(mActivityRule);
fragment.waitForTransition();
final View green2 = findGreen();
final View blue2 = findBlue();
verifyAndClearTransition(fragment.reenterTransition, null, green2, blue2);
verifyNoOtherTransitions(fragment);
assertEquals(3, mOnBackStackChangedTimes);
// return transition
FragmentTestUtil.popBackStackImmediate(mActivityRule);
fragment.waitForTransition();
verifyAndClearTransition(fragment.returnTransition, null, green2, blue2);
verifyNoOtherTransitions(fragment);
assertEquals(4, mOnBackStackChangedTimes);
}
// 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");
assertEquals(2, mOnBackStackChangedTimes);
// Now pop the back stack
verifyPopTransition(1, fragment2, fragment1);
assertEquals(3, mOnBackStackChangedTimes);
}
// 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);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mFragmentManager.beginTransaction()
.setReorderingAllowed(mReordered)
.replace(R.id.fragmentContainer, fragment2)
.replace(R.id.fragmentContainer, fragment1)
.replace(R.id.fragmentContainer, fragment2)
.addToBackStack(null)
.commit();
}
});
FragmentTestUtil.waitForExecution(mActivityRule);
assertEquals(2, mOnBackStackChangedTimes);
// should be a normal transition from fragment1 to fragment2
fragment1.waitForTransition();
fragment2.waitForTransition();
FragmentTestUtil.waitForExecution(mActivityRule);
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);
assertEquals(3, mOnBackStackChangedTimes);
fragment1.waitForTransition();
fragment2.waitForTransition();
FragmentTestUtil.waitForExecution(mActivityRule);
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()
.setReorderingAllowed(mReordered)
.add(R.id.fragmentContainer1, fragment1)
.add(R.id.fragmentContainer2, fragment2)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertEquals(1, mOnBackStackChangedTimes);
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 {
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)
.setReorderingAllowed(mReordered)
.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
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)
.setReorderingAllowed(mReordered)
.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)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertEquals(2, mOnBackStackChangedTimes);
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);
assertEquals(3, mOnBackStackChangedTimes);
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()
.setReorderingAllowed(mReordered)
.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()
.setReorderingAllowed(mReordered)
.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)
.setReorderingAllowed(mReordered)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
fragment1.waitForTransition();
fragment2.waitForTransition();
final View endBlue = findBlue();
final View endGreen = findGreen();
if (mReordered) {
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 (!mReordered) {
return; // only reordered transitions can avoid interaction
}
// enter transition
TransitionFragment fragment = new InvisibleFragment();
fragment.setLayoutId(R.layout.scene1);
mFragmentManager.beginTransaction()
.setReorderingAllowed(mReordered)
.add(R.id.fragmentContainer, fragment)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
fragment.waitForNoTransition();
verifyNoOtherTransitions(fragment);
// exit transition
mFragmentManager.beginTransaction()
.setReorderingAllowed(mReordered)
.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);
}
// No crash when transitioning a shared element and there is no shared element transition.
@Test
public void noSharedElementTransition() throws Throwable {
TransitionFragment fragment1 = setupInitialFragment();
final View startBlue = findBlue();
final View startGreen = findGreen();
final Rect startBlueBounds = getBoundsOnScreen(startBlue);
TransitionFragment fragment2 = new TransitionFragment();
fragment2.setLayoutId(R.layout.scene2);
mFragmentManager.beginTransaction()
.setReorderingAllowed(mReordered)
.addSharedElement(startBlue, "blueSquare")
.replace(R.id.fragmentContainer, fragment2)
.addToBackStack(null)
.commit();
fragment1.waitForTransition();
fragment2.waitForTransition();
final View midGreen = findGreen();
final View midBlue = findBlue();
final Rect midBlueBounds = getBoundsOnScreen(midBlue);
verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen);
verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue, midBlue);
verifyAndClearTransition(fragment2.enterTransition, midBlueBounds, midGreen);
verifyNoOtherTransitions(fragment1);
verifyNoOtherTransitions(fragment2);
final TransitionFragment fragment3 = new TransitionFragment();
fragment3.setLayoutId(R.layout.scene3);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mFragmentManager.popBackStack();
mFragmentManager.beginTransaction()
.setReorderingAllowed(mReordered)
.replace(R.id.fragmentContainer, fragment3)
.addToBackStack(null)
.commit();
}
});
// This shouldn't give an error.
FragmentTestUtil.executePendingTransactions(mActivityRule);
fragment2.waitForTransition();
// It does not transition properly for ordered transactions, though.
if (mReordered) {
verifyAndClearTransition(fragment2.returnTransition, null, midGreen, midBlue);
final View endGreen = findGreen();
final View endBlue = findBlue();
final View endRed = findRed();
verifyAndClearTransition(fragment3.enterTransition, null, endGreen, endBlue, endRed);
verifyNoOtherTransitions(fragment2);
verifyNoOtherTransitions(fragment3);
} else {
// fragment3 doesn't get a transition since it conflicts with the pop transition
verifyNoOtherTransitions(fragment3);
// Everything else is just doing its best. Reordered transactions can't handle
// multiple transitions acting together except for popping multiple together.
}
}
private TransitionFragment setupInitialFragment() throws Throwable {
TransitionFragment fragment1 = new TransitionFragment();
fragment1.setLayoutId(R.layout.scene1);
mFragmentManager.beginTransaction()
.setReorderingAllowed(mReordered)
.add(R.id.fragmentContainer, fragment1)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertEquals(1, mOnBackStackChangedTimes);
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();
String errorMessage = "Expected: [" + expected.length + "] {" +
Arrays.stream(expected).map(v -> v.toString()).collect(Collectors.joining(", ")) +
"}, but got: [" + targets.size() + "] {" +
targets.stream().map(v -> v.toString()).collect(Collectors.joining(", ")) +
"}";
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 int startOnBackStackChanged = mOnBackStackChangedTimes;
final View startBlue = findBlue();
final View startGreen = findGreen();
final View startRed = findRed();
final Rect startBlueRect = getBoundsOnScreen(startBlue);
mFragmentManager.beginTransaction()
.setReorderingAllowed(mReordered)
.addSharedElement(startBlue, sharedElementName)
.replace(R.id.fragmentContainer, to)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertEquals(startOnBackStackChanged + 1, mOnBackStackChangedTimes);
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 int startNumOnBackStackChanged = mOnBackStackChangedTimes;
final int changesPerOperation = mReordered ? 1 : 2;
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(() -> {
mFragmentManager.beginTransaction()
.setReorderingAllowed(mReordered)
.addSharedElement(fromShared1, "blueSquare")
.replace(R.id.fragmentContainer1, to1)
.addToBackStack(null)
.commit();
mFragmentManager.beginTransaction()
.setReorderingAllowed(mReordered)
.addSharedElement(fromShared2, sharedElementName)
.replace(R.id.fragmentContainer2, to2)
.addToBackStack(null)
.commit();
});
FragmentTestUtil.waitForExecution(mActivityRule);
assertEquals(startNumOnBackStackChanged + changesPerOperation, mOnBackStackChangedTimes);
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(() -> {
mFragmentManager.popBackStack();
mFragmentManager.popBackStack();
});
FragmentTestUtil.waitForExecution(mActivityRule);
assertEquals(startNumOnBackStackChanged + changesPerOperation + 1,
mOnBackStackChangedTimes);
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 int startOnBackStackChanged = mOnBackStackChangedTimes;
final View startBlue = findBlue();
final View startGreen = findGreen();
final View startRed = findRed();
final Rect startSharedRect = getBoundsOnScreen(startBlue);
mActivityRule.runOnUiThread(() -> {
for (int i = 0; i < numPops; i++) {
mFragmentManager.popBackStack();
}
});
FragmentTestUtil.waitForExecution(mActivityRule);
assertEquals(startOnBackStackChanged + 1, mOnBackStackChangedTimes);
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);
}
}
}