1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.support.v4.app;
17 
18 import static junit.framework.Assert.assertNull;
19 
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertTrue;
22 import static org.junit.Assert.fail;
23 import static org.mockito.Mockito.mock;
24 import static org.mockito.Mockito.reset;
25 import static org.mockito.Mockito.verify;
26 
27 import android.app.Instrumentation;
28 import android.graphics.Rect;
29 import android.os.Build;
30 import android.os.Bundle;
31 import android.support.fragment.test.R;
32 import android.support.test.InstrumentationRegistry;
33 import android.support.test.filters.MediumTest;
34 import android.support.test.filters.SdkSuppress;
35 import android.support.test.rule.ActivityTestRule;
36 import android.support.v4.app.test.FragmentTestActivity;
37 import android.transition.TransitionSet;
38 import android.view.View;
39 
40 import org.junit.After;
41 import org.junit.Before;
42 import org.junit.Rule;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 import org.junit.runners.Parameterized;
46 import org.mockito.ArgumentCaptor;
47 
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.Map;
51 
52 @MediumTest
53 @RunWith(Parameterized.class)
54 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
55 public class FragmentTransitionTest {
56     private final boolean mReorderingAllowed;
57 
58     @Parameterized.Parameters
data()59     public static Object[] data() {
60         return new Boolean[] {
61                 false, true
62         };
63     }
64 
65     @Rule
66     public ActivityTestRule<FragmentTestActivity> mActivityRule =
67             new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class);
68 
69     private Instrumentation mInstrumentation;
70     private FragmentManager mFragmentManager;
71     private int mOnBackStackChangedTimes;
72     private FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener;
73 
FragmentTransitionTest(final boolean reordering)74     public FragmentTransitionTest(final boolean reordering) {
75         mReorderingAllowed = reordering;
76     }
77 
78     @Before
setup()79     public void setup() throws Throwable {
80         mInstrumentation = InstrumentationRegistry.getInstrumentation();
81         mFragmentManager = mActivityRule.getActivity().getSupportFragmentManager();
82         FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
83         mOnBackStackChangedTimes = 0;
84         mOnBackStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
85             @Override
86             public void onBackStackChanged() {
87                 mOnBackStackChangedTimes++;
88             }
89         };
90         mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
91     }
92 
93     @After
teardown()94     public void teardown() throws Throwable {
95         mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener);
96         mOnBackStackChangedListener = null;
97     }
98 
99     // Test that normal view transitions (enter, exit, reenter, return) run with
100     // a single fragment.
101     @Test
enterExitTransitions()102     public void enterExitTransitions() throws Throwable {
103         // enter transition
104         TransitionFragment fragment = setupInitialFragment();
105         final View blue = findBlue();
106         final View green = findBlue();
107 
108         // exit transition
109         mFragmentManager.beginTransaction()
110                 .setReorderingAllowed(mReorderingAllowed)
111                 .remove(fragment)
112                 .addToBackStack(null)
113                 .commit();
114 
115         fragment.waitForTransition();
116         verifyAndClearTransition(fragment.exitTransition, null, green, blue);
117         verifyNoOtherTransitions(fragment);
118         assertEquals(2, mOnBackStackChangedTimes);
119 
120         // reenter transition
121         FragmentTestUtil.popBackStackImmediate(mActivityRule);
122         fragment.waitForTransition();
123         final View green2 = findGreen();
124         final View blue2 = findBlue();
125         verifyAndClearTransition(fragment.reenterTransition, null, green2, blue2);
126         verifyNoOtherTransitions(fragment);
127         assertEquals(3, mOnBackStackChangedTimes);
128 
129         // return transition
130         FragmentTestUtil.popBackStackImmediate(mActivityRule);
131         fragment.waitForTransition();
132         verifyAndClearTransition(fragment.returnTransition, null, green2, blue2);
133         verifyNoOtherTransitions(fragment);
134         assertEquals(4, mOnBackStackChangedTimes);
135     }
136 
137     // Test that shared elements transition from one fragment to the next
138     // and back during pop.
139     @Test
sharedElement()140     public void sharedElement() throws Throwable {
141         TransitionFragment fragment1 = setupInitialFragment();
142 
143         // Now do a transition to scene2
144         TransitionFragment fragment2 = new TransitionFragment();
145         fragment2.setLayoutId(R.layout.scene2);
146 
147         verifyTransition(fragment1, fragment2, "blueSquare");
148 
149         // Now pop the back stack
150         verifyPopTransition(1, fragment2, fragment1);
151     }
152 
153     // Test that shared element transitions through multiple fragments work together
154     @Test
intermediateFragment()155     public void intermediateFragment() throws Throwable {
156         TransitionFragment fragment1 = setupInitialFragment();
157 
158         final TransitionFragment fragment2 = new TransitionFragment();
159         fragment2.setLayoutId(R.layout.scene3);
160 
161         verifyTransition(fragment1, fragment2, "shared");
162 
163         final TransitionFragment fragment3 = new TransitionFragment();
164         fragment3.setLayoutId(R.layout.scene2);
165 
166         verifyTransition(fragment2, fragment3, "blueSquare");
167 
168         // Should transfer backwards when popping multiple:
169         verifyPopTransition(2, fragment3, fragment1, fragment2);
170     }
171 
172     // Adding/removing the same fragment multiple times shouldn't mess anything up
173     @Test
removeAdded()174     public void removeAdded() throws Throwable {
175         final TransitionFragment fragment1 = setupInitialFragment();
176 
177         final View startBlue = findBlue();
178         final View startGreen = findGreen();
179 
180         final TransitionFragment fragment2 = new TransitionFragment();
181         fragment2.setLayoutId(R.layout.scene2);
182 
183         mInstrumentation.runOnMainSync(new Runnable() {
184             @Override
185             public void run() {
186                 mFragmentManager.beginTransaction()
187                         .setReorderingAllowed(mReorderingAllowed)
188                         .replace(R.id.fragmentContainer, fragment2)
189                         .replace(R.id.fragmentContainer, fragment1)
190                         .replace(R.id.fragmentContainer, fragment2)
191                         .addToBackStack(null)
192                         .commit();
193             }
194         });
195         FragmentTestUtil.waitForExecution(mActivityRule);
196         assertEquals(2, mOnBackStackChangedTimes);
197 
198         // should be a normal transition from fragment1 to fragment2
199         fragment2.waitForTransition();
200         final View endBlue = findBlue();
201         final View endGreen = findGreen();
202         verifyAndClearTransition(fragment1.exitTransition, null, startBlue, startGreen);
203         verifyAndClearTransition(fragment2.enterTransition, null, endBlue, endGreen);
204         verifyNoOtherTransitions(fragment1);
205         verifyNoOtherTransitions(fragment2);
206 
207         // Pop should also do the same thing
208         FragmentTestUtil.popBackStackImmediate(mActivityRule);
209         assertEquals(3, mOnBackStackChangedTimes);
210 
211         fragment1.waitForTransition();
212         final View popBlue = findBlue();
213         final View popGreen = findGreen();
214         verifyAndClearTransition(fragment1.reenterTransition, null, popBlue, popGreen);
215         verifyAndClearTransition(fragment2.returnTransition, null, endBlue, endGreen);
216         verifyNoOtherTransitions(fragment1);
217         verifyNoOtherTransitions(fragment2);
218     }
219 
220     // Make sure that shared elements on two different fragment containers don't interact
221     @Test
crossContainer()222     public void crossContainer() throws Throwable {
223         FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
224         TransitionFragment fragment1 = new TransitionFragment();
225         fragment1.setLayoutId(R.layout.scene1);
226         TransitionFragment fragment2 = new TransitionFragment();
227         fragment2.setLayoutId(R.layout.scene1);
228         mFragmentManager.beginTransaction()
229                 .setReorderingAllowed(mReorderingAllowed)
230                 .add(R.id.fragmentContainer1, fragment1)
231                 .add(R.id.fragmentContainer2, fragment2)
232                 .addToBackStack(null)
233                 .commit();
234         FragmentTestUtil.waitForExecution(mActivityRule);
235         assertEquals(1, mOnBackStackChangedTimes);
236 
237         fragment1.waitForTransition();
238         final View greenSquare1 = findViewById(fragment1, R.id.greenSquare);
239         final View blueSquare1 = findViewById(fragment1, R.id.blueSquare);
240         verifyAndClearTransition(fragment1.enterTransition, null, greenSquare1, blueSquare1);
241         verifyNoOtherTransitions(fragment1);
242         fragment2.waitForTransition();
243         final View greenSquare2 = findViewById(fragment2, R.id.greenSquare);
244         final View blueSquare2 = findViewById(fragment2, R.id.blueSquare);
245         verifyAndClearTransition(fragment2.enterTransition, null, greenSquare2, blueSquare2);
246         verifyNoOtherTransitions(fragment2);
247 
248         // Make sure the correct transitions are run when the target names
249         // are different in both shared elements. We may fool the system.
250         verifyCrossTransition(false, fragment1, fragment2);
251 
252         // Make sure the correct transitions are run when the source names
253         // are different in both shared elements. We may fool the system.
254         verifyCrossTransition(true, fragment1, fragment2);
255     }
256 
257     // Make sure that onSharedElementStart and onSharedElementEnd are called
258     @Test
callStartEndWithSharedElements()259     public void callStartEndWithSharedElements() throws Throwable {
260         TransitionFragment fragment1 = setupInitialFragment();
261 
262         // Now do a transition to scene2
263         TransitionFragment fragment2 = new TransitionFragment();
264         fragment2.setLayoutId(R.layout.scene2);
265 
266         SharedElementCallback enterCallback = mock(SharedElementCallback.class);
267         fragment2.setEnterSharedElementCallback(enterCallback);
268 
269         final View startBlue = findBlue();
270 
271         verifyTransition(fragment1, fragment2, "blueSquare");
272 
273         ArgumentCaptor<List> names = ArgumentCaptor.forClass(List.class);
274         ArgumentCaptor<List> views = ArgumentCaptor.forClass(List.class);
275         ArgumentCaptor<List> snapshots = ArgumentCaptor.forClass(List.class);
276         verify(enterCallback).onSharedElementStart(names.capture(), views.capture(),
277                 snapshots.capture());
278         assertEquals(1, names.getValue().size());
279         assertEquals(1, views.getValue().size());
280         assertNull(snapshots.getValue());
281         assertEquals("blueSquare", names.getValue().get(0));
282         assertEquals(startBlue, views.getValue().get(0));
283 
284         final View endBlue = findBlue();
285 
286         verify(enterCallback).onSharedElementEnd(names.capture(), views.capture(),
287                 snapshots.capture());
288         assertEquals(1, names.getValue().size());
289         assertEquals(1, views.getValue().size());
290         assertNull(snapshots.getValue());
291         assertEquals("blueSquare", names.getValue().get(0));
292         assertEquals(endBlue, views.getValue().get(0));
293 
294         // Now pop the back stack
295         reset(enterCallback);
296         verifyPopTransition(1, fragment2, fragment1);
297 
298         verify(enterCallback).onSharedElementStart(names.capture(), views.capture(),
299                 snapshots.capture());
300         assertEquals(1, names.getValue().size());
301         assertEquals(1, views.getValue().size());
302         assertNull(snapshots.getValue());
303         assertEquals("blueSquare", names.getValue().get(0));
304         assertEquals(endBlue, views.getValue().get(0));
305 
306         final View reenterBlue = findBlue();
307 
308         verify(enterCallback).onSharedElementEnd(names.capture(), views.capture(),
309                 snapshots.capture());
310         assertEquals(1, names.getValue().size());
311         assertEquals(1, views.getValue().size());
312         assertNull(snapshots.getValue());
313         assertEquals("blueSquare", names.getValue().get(0));
314         assertEquals(reenterBlue, views.getValue().get(0));
315     }
316 
317     // Make sure that onMapSharedElement works to change the shared element going out
318     @Test
onMapSharedElementOut()319     public void onMapSharedElementOut() throws Throwable {
320         final TransitionFragment fragment1 = setupInitialFragment();
321 
322         // Now do a transition to scene2
323         TransitionFragment fragment2 = new TransitionFragment();
324         fragment2.setLayoutId(R.layout.scene2);
325 
326         final View startBlue = findBlue();
327         final View startGreen = findGreen();
328 
329         final Rect startGreenBounds = getBoundsOnScreen(startGreen);
330 
331         SharedElementCallback mapOut = new SharedElementCallback() {
332             @Override
333             public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
334                 assertEquals(1, names.size());
335                 assertEquals("blueSquare", names.get(0));
336                 assertEquals(1, sharedElements.size());
337                 assertEquals(startBlue, sharedElements.get("blueSquare"));
338                 sharedElements.put("blueSquare", startGreen);
339             }
340         };
341         fragment1.setExitSharedElementCallback(mapOut);
342 
343         mFragmentManager.beginTransaction()
344                 .addSharedElement(startBlue, "blueSquare")
345                 .replace(R.id.fragmentContainer, fragment2)
346                 .setReorderingAllowed(mReorderingAllowed)
347                 .addToBackStack(null)
348                 .commit();
349         FragmentTestUtil.waitForExecution(mActivityRule);
350 
351         fragment1.waitForTransition();
352         fragment2.waitForTransition();
353 
354         final View endBlue = findBlue();
355         final Rect endBlueBounds = getBoundsOnScreen(endBlue);
356 
357         verifyAndClearTransition(fragment2.sharedElementEnter, startGreenBounds, startGreen,
358                 endBlue);
359 
360         SharedElementCallback mapBack = new SharedElementCallback() {
361             @Override
362             public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
363                 assertEquals(1, names.size());
364                 assertEquals("blueSquare", names.get(0));
365                 assertEquals(1, sharedElements.size());
366                 final View expectedBlue = findViewById(fragment1, R.id.blueSquare);
367                 assertEquals(expectedBlue, sharedElements.get("blueSquare"));
368                 final View greenSquare = findViewById(fragment1, R.id.greenSquare);
369                 sharedElements.put("blueSquare", greenSquare);
370             }
371         };
372         fragment1.setExitSharedElementCallback(mapBack);
373 
374         FragmentTestUtil.popBackStackImmediate(mActivityRule);
375 
376         fragment1.waitForTransition();
377         fragment2.waitForTransition();
378 
379         final View reenterGreen = findGreen();
380         verifyAndClearTransition(fragment2.sharedElementReturn, endBlueBounds, endBlue,
381                 reenterGreen);
382     }
383 
384     // Make sure that onMapSharedElement works to change the shared element target
385     @Test
onMapSharedElementIn()386     public void onMapSharedElementIn() throws Throwable {
387         TransitionFragment fragment1 = setupInitialFragment();
388 
389         // Now do a transition to scene2
390         final TransitionFragment fragment2 = new TransitionFragment();
391         fragment2.setLayoutId(R.layout.scene2);
392 
393         final View startBlue = findBlue();
394         final Rect startBlueBounds = getBoundsOnScreen(startBlue);
395 
396         SharedElementCallback mapIn = new SharedElementCallback() {
397             @Override
398             public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
399                 assertEquals(1, names.size());
400                 assertEquals("blueSquare", names.get(0));
401                 assertEquals(1, sharedElements.size());
402                 final View blueSquare = findViewById(fragment2, R.id.blueSquare);
403                 assertEquals(blueSquare, sharedElements.get("blueSquare"));
404                 final View greenSquare = findViewById(fragment2, R.id.greenSquare);
405                 sharedElements.put("blueSquare", greenSquare);
406             }
407         };
408         fragment2.setEnterSharedElementCallback(mapIn);
409 
410         mFragmentManager.beginTransaction()
411                 .addSharedElement(startBlue, "blueSquare")
412                 .replace(R.id.fragmentContainer, fragment2)
413                 .setReorderingAllowed(mReorderingAllowed)
414                 .addToBackStack(null)
415                 .commit();
416         FragmentTestUtil.waitForExecution(mActivityRule);
417 
418         fragment1.waitForTransition();
419         fragment2.waitForTransition();
420 
421         final View endGreen = findGreen();
422         final View endBlue = findBlue();
423         final Rect endGreenBounds = getBoundsOnScreen(endGreen);
424 
425         verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue,
426                 endGreen);
427 
428         SharedElementCallback mapBack = new SharedElementCallback() {
429             @Override
430             public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
431                 assertEquals(1, names.size());
432                 assertEquals("blueSquare", names.get(0));
433                 assertEquals(1, sharedElements.size());
434                 assertEquals(endBlue, sharedElements.get("blueSquare"));
435                 sharedElements.put("blueSquare", endGreen);
436             }
437         };
438         fragment2.setEnterSharedElementCallback(mapBack);
439 
440         FragmentTestUtil.popBackStackImmediate(mActivityRule);
441 
442         fragment1.waitForTransition();
443         fragment2.waitForTransition();
444 
445         final View reenterBlue = findBlue();
446         verifyAndClearTransition(fragment2.sharedElementReturn, endGreenBounds, endGreen,
447                 reenterBlue);
448     }
449 
450     // Ensure that shared element transitions that have targets properly target the views
451     @Test
complexSharedElementTransition()452     public void complexSharedElementTransition() throws Throwable {
453         TransitionFragment fragment1 = setupInitialFragment();
454 
455         // Now do a transition to scene2
456         ComplexTransitionFragment fragment2 = new ComplexTransitionFragment();
457         fragment2.setLayoutId(R.layout.scene2);
458 
459         final View startBlue = findBlue();
460         final View startGreen = findGreen();
461         final Rect startBlueBounds = getBoundsOnScreen(startBlue);
462 
463         mFragmentManager.beginTransaction()
464                 .addSharedElement(startBlue, "blueSquare")
465                 .addSharedElement(startGreen, "greenSquare")
466                 .replace(R.id.fragmentContainer, fragment2)
467                 .addToBackStack(null)
468                 .setReorderingAllowed(true)
469                 .commit();
470         FragmentTestUtil.waitForExecution(mActivityRule);
471         assertEquals(2, mOnBackStackChangedTimes);
472 
473         fragment1.waitForTransition();
474         fragment2.waitForTransition();
475 
476         final View endBlue = findBlue();
477         final View endGreen = findGreen();
478         final Rect endBlueBounds = getBoundsOnScreen(endBlue);
479 
480         verifyAndClearTransition(fragment2.sharedElementEnterTransition1, startBlueBounds,
481                 startBlue, endBlue);
482         verifyAndClearTransition(fragment2.sharedElementEnterTransition2, startBlueBounds,
483                 startGreen, endGreen);
484 
485         // Now see if it works when popped
486         FragmentTestUtil.popBackStackImmediate(mActivityRule);
487         assertEquals(3, mOnBackStackChangedTimes);
488 
489         fragment1.waitForTransition();
490         fragment2.waitForTransition();
491 
492         final View reenterBlue = findBlue();
493         final View reenterGreen = findGreen();
494 
495         verifyAndClearTransition(fragment2.sharedElementReturnTransition1, endBlueBounds,
496                 endBlue, reenterBlue);
497         verifyAndClearTransition(fragment2.sharedElementReturnTransition2, endBlueBounds,
498                 endGreen, reenterGreen);
499     }
500 
501     // Ensure that after transitions have executed that they don't have any targets or other
502     // unfortunate modifications.
503     @Test
transitionsEndUnchanged()504     public void transitionsEndUnchanged() throws Throwable {
505         TransitionFragment fragment1 = setupInitialFragment();
506 
507         // Now do a transition to scene2
508         TransitionFragment fragment2 = new TransitionFragment();
509         fragment2.setLayoutId(R.layout.scene2);
510 
511         verifyTransition(fragment1, fragment2, "blueSquare");
512         assertEquals(0, fragment1.exitTransition.getTargets().size());
513         assertEquals(0, fragment2.sharedElementEnter.getTargets().size());
514         assertEquals(0, fragment2.enterTransition.getTargets().size());
515         assertNull(fragment1.exitTransition.getEpicenterCallback());
516         assertNull(fragment2.enterTransition.getEpicenterCallback());
517         assertNull(fragment2.sharedElementEnter.getEpicenterCallback());
518 
519         // Now pop the back stack
520         verifyPopTransition(1, fragment2, fragment1);
521 
522         assertEquals(0, fragment2.returnTransition.getTargets().size());
523         assertEquals(0, fragment2.sharedElementReturn.getTargets().size());
524         assertEquals(0, fragment1.reenterTransition.getTargets().size());
525         assertNull(fragment2.returnTransition.getEpicenterCallback());
526         assertNull(fragment2.sharedElementReturn.getEpicenterCallback());
527         assertNull(fragment2.reenterTransition.getEpicenterCallback());
528     }
529 
530     // Ensure that transitions are done when a fragment is shown and hidden
531     @Test
showHideTransition()532     public void showHideTransition() throws Throwable {
533         TransitionFragment fragment1 = setupInitialFragment();
534         TransitionFragment fragment2 = new TransitionFragment();
535         fragment2.setLayoutId(R.layout.scene2);
536 
537         final View startBlue = findBlue();
538         final View startGreen = findGreen();
539 
540         mFragmentManager.beginTransaction()
541                 .setReorderingAllowed(mReorderingAllowed)
542                 .add(R.id.fragmentContainer, fragment2)
543                 .hide(fragment1)
544                 .addToBackStack(null)
545                 .commit();
546 
547         FragmentTestUtil.waitForExecution(mActivityRule);
548         fragment1.waitForTransition();
549         fragment2.waitForTransition();
550 
551         final View endGreen = findViewById(fragment2, R.id.greenSquare);
552         final View endBlue = findViewById(fragment2, R.id.blueSquare);
553 
554         assertEquals(View.GONE, fragment1.getView().getVisibility());
555         assertEquals(View.VISIBLE, startGreen.getVisibility());
556         assertEquals(View.VISIBLE, startBlue.getVisibility());
557 
558         verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue);
559         verifyNoOtherTransitions(fragment1);
560 
561         verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue);
562         verifyNoOtherTransitions(fragment2);
563 
564         FragmentTestUtil.popBackStackImmediate(mActivityRule);
565 
566         FragmentTestUtil.waitForExecution(mActivityRule);
567         fragment1.waitForTransition();
568         fragment2.waitForTransition();
569 
570         verifyAndClearTransition(fragment1.reenterTransition, null, startGreen, startBlue);
571         verifyNoOtherTransitions(fragment1);
572 
573         assertEquals(View.VISIBLE, fragment1.getView().getVisibility());
574         assertEquals(View.VISIBLE, startGreen.getVisibility());
575         assertEquals(View.VISIBLE, startBlue.getVisibility());
576 
577         verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue);
578         verifyNoOtherTransitions(fragment2);
579     }
580 
581     // Ensure that transitions are done when a fragment is attached and detached
582     @Test
attachDetachTransition()583     public void attachDetachTransition() throws Throwable {
584         TransitionFragment fragment1 = setupInitialFragment();
585         TransitionFragment fragment2 = new TransitionFragment();
586         fragment2.setLayoutId(R.layout.scene2);
587 
588         final View startBlue = findBlue();
589         final View startGreen = findGreen();
590 
591         mFragmentManager.beginTransaction()
592                 .setReorderingAllowed(mReorderingAllowed)
593                 .add(R.id.fragmentContainer, fragment2)
594                 .detach(fragment1)
595                 .addToBackStack(null)
596                 .commit();
597 
598         FragmentTestUtil.waitForExecution(mActivityRule);
599 
600         final View endGreen = findViewById(fragment2, R.id.greenSquare);
601         final View endBlue = findViewById(fragment2, R.id.blueSquare);
602 
603         verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue);
604         verifyNoOtherTransitions(fragment1);
605 
606         verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue);
607         verifyNoOtherTransitions(fragment2);
608 
609         FragmentTestUtil.popBackStackImmediate(mActivityRule);
610 
611         FragmentTestUtil.waitForExecution(mActivityRule);
612 
613         final View reenterBlue = findBlue();
614         final View reenterGreen = findGreen();
615 
616         verifyAndClearTransition(fragment1.reenterTransition, null, reenterGreen, reenterBlue);
617         verifyNoOtherTransitions(fragment1);
618 
619         verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue);
620         verifyNoOtherTransitions(fragment2);
621     }
622 
623     // Ensure that shared element without matching transition name doesn't error out
624     @Test
sharedElementMismatch()625     public void sharedElementMismatch() throws Throwable {
626         final TransitionFragment fragment1 = setupInitialFragment();
627 
628         // Now do a transition to scene2
629         TransitionFragment fragment2 = new TransitionFragment();
630         fragment2.setLayoutId(R.layout.scene2);
631 
632         final View startBlue = findBlue();
633         final View startGreen = findGreen();
634         final Rect startBlueBounds = getBoundsOnScreen(startBlue);
635 
636         mFragmentManager.beginTransaction()
637                 .addSharedElement(startBlue, "fooSquare")
638                 .replace(R.id.fragmentContainer, fragment2)
639                 .setReorderingAllowed(mReorderingAllowed)
640                 .addToBackStack(null)
641                 .commit();
642         FragmentTestUtil.waitForExecution(mActivityRule);
643 
644         fragment1.waitForTransition();
645         fragment2.waitForTransition();
646 
647         final View endBlue = findBlue();
648         final View endGreen = findGreen();
649 
650         if (mReorderingAllowed) {
651             verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue);
652         } else {
653             verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen);
654             verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue);
655         }
656         verifyNoOtherTransitions(fragment1);
657 
658         verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue);
659         verifyNoOtherTransitions(fragment2);
660     }
661 
662     // Ensure that using the same source or target shared element results in an exception.
663     @Test
sharedDuplicateTargetNames()664     public void sharedDuplicateTargetNames() throws Throwable {
665         setupInitialFragment();
666 
667         final View startBlue = findBlue();
668         final View startGreen = findGreen();
669 
670         FragmentTransaction ft = mFragmentManager.beginTransaction();
671         ft.addSharedElement(startBlue, "blueSquare");
672         try {
673             ft.addSharedElement(startGreen, "blueSquare");
674             fail("Expected IllegalArgumentException");
675         } catch (IllegalArgumentException e) {
676             // expected
677         }
678 
679         try {
680             ft.addSharedElement(startBlue, "greenSquare");
681             fail("Expected IllegalArgumentException");
682         } catch (IllegalArgumentException e) {
683             // expected
684         }
685     }
686 
687     // Test that invisible fragment views don't participate in transitions
688     @Test
invisibleNoTransitions()689     public void invisibleNoTransitions() throws Throwable {
690         if (!mReorderingAllowed) {
691             return; // only reordered transitions can avoid interaction
692         }
693         // enter transition
694         TransitionFragment fragment = new InvisibleFragment();
695         fragment.setLayoutId(R.layout.scene1);
696         mFragmentManager.beginTransaction()
697                 .setReorderingAllowed(mReorderingAllowed)
698                 .add(R.id.fragmentContainer, fragment)
699                 .addToBackStack(null)
700                 .commit();
701         FragmentTestUtil.waitForExecution(mActivityRule);
702         fragment.waitForNoTransition();
703         verifyNoOtherTransitions(fragment);
704 
705         // exit transition
706         mFragmentManager.beginTransaction()
707                 .setReorderingAllowed(mReorderingAllowed)
708                 .remove(fragment)
709                 .addToBackStack(null)
710                 .commit();
711 
712         fragment.waitForNoTransition();
713         verifyNoOtherTransitions(fragment);
714 
715         // reenter transition
716         FragmentTestUtil.popBackStackImmediate(mActivityRule);
717         fragment.waitForNoTransition();
718         verifyNoOtherTransitions(fragment);
719 
720         // return transition
721         FragmentTestUtil.popBackStackImmediate(mActivityRule);
722         fragment.waitForNoTransition();
723         verifyNoOtherTransitions(fragment);
724     }
725 
726     // No crash when transitioning a shared element and there is no shared element transition.
727     @Test
noSharedElementTransition()728     public void noSharedElementTransition() throws Throwable {
729         TransitionFragment fragment1 = setupInitialFragment();
730 
731         final View startBlue = findBlue();
732         final View startGreen = findGreen();
733         final Rect startBlueBounds = getBoundsOnScreen(startBlue);
734 
735         TransitionFragment fragment2 = new TransitionFragment();
736         fragment2.setLayoutId(R.layout.scene2);
737 
738         mFragmentManager.beginTransaction()
739                 .setReorderingAllowed(mReorderingAllowed)
740                 .addSharedElement(startBlue, "blueSquare")
741                 .replace(R.id.fragmentContainer, fragment2)
742                 .addToBackStack(null)
743                 .commit();
744 
745         fragment1.waitForTransition();
746         fragment2.waitForTransition();
747         final View midGreen = findGreen();
748         final View midBlue = findBlue();
749         final Rect midBlueBounds = getBoundsOnScreen(midBlue);
750         verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen);
751         verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue, midBlue);
752         verifyAndClearTransition(fragment2.enterTransition, midBlueBounds, midGreen);
753         verifyNoOtherTransitions(fragment1);
754         verifyNoOtherTransitions(fragment2);
755 
756         final TransitionFragment fragment3 = new TransitionFragment();
757         fragment3.setLayoutId(R.layout.scene3);
758 
759         mActivityRule.runOnUiThread(new Runnable() {
760             @Override
761             public void run() {
762                 FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
763                 fm.popBackStack();
764                 fm.beginTransaction()
765                         .setReorderingAllowed(mReorderingAllowed)
766                         .replace(R.id.fragmentContainer, fragment3)
767                         .addToBackStack(null)
768                         .commit();
769             }
770         });
771 
772         // This shouldn't give an error.
773         FragmentTestUtil.executePendingTransactions(mActivityRule);
774 
775         fragment2.waitForTransition();
776         // It does not transition properly for ordered transactions, though.
777         if (mReorderingAllowed) {
778             verifyAndClearTransition(fragment2.returnTransition, null, midGreen, midBlue);
779             final View endGreen = findGreen();
780             final View endBlue = findBlue();
781             final View endRed = findRed();
782             verifyAndClearTransition(fragment3.enterTransition, null, endGreen, endBlue, endRed);
783             verifyNoOtherTransitions(fragment2);
784             verifyNoOtherTransitions(fragment3);
785         } else {
786             // fragment3 doesn't get a transition since it conflicts with the pop transition
787             verifyNoOtherTransitions(fragment3);
788             // Everything else is just doing its best. Ordered transactions can't handle
789             // multiple transitions acting together except for popping multiple together.
790         }
791     }
792 
setupInitialFragment()793     private TransitionFragment setupInitialFragment() throws Throwable {
794         TransitionFragment fragment1 = new TransitionFragment();
795         fragment1.setLayoutId(R.layout.scene1);
796         mFragmentManager.beginTransaction()
797                 .setReorderingAllowed(mReorderingAllowed)
798                 .add(R.id.fragmentContainer, fragment1)
799                 .addToBackStack(null)
800                 .commit();
801         FragmentTestUtil.waitForExecution(mActivityRule);
802         assertEquals(1, mOnBackStackChangedTimes);
803         fragment1.waitForTransition();
804         final View blueSquare1 = findBlue();
805         final View greenSquare1 = findGreen();
806         verifyAndClearTransition(fragment1.enterTransition, null, blueSquare1, greenSquare1);
807         verifyNoOtherTransitions(fragment1);
808         return fragment1;
809     }
810 
findViewById(Fragment fragment, int id)811     private View findViewById(Fragment fragment, int id) {
812         return fragment.getView().findViewById(id);
813     }
814 
findGreen()815     private View findGreen() {
816         return mActivityRule.getActivity().findViewById(R.id.greenSquare);
817     }
818 
findBlue()819     private View findBlue() {
820         return mActivityRule.getActivity().findViewById(R.id.blueSquare);
821     }
822 
findRed()823     private View findRed() {
824         return mActivityRule.getActivity().findViewById(R.id.redSquare);
825     }
826 
verifyAndClearTransition(TargetTracking transition, Rect epicenter, View... expected)827     private void verifyAndClearTransition(TargetTracking transition, Rect epicenter,
828             View... expected) {
829         if (epicenter == null) {
830             assertNull(transition.getCapturedEpicenter());
831         } else {
832             assertEquals(epicenter, transition.getCapturedEpicenter());
833         }
834         ArrayList<View> targets = transition.getTrackedTargets();
835         StringBuilder sb = new StringBuilder();
836         sb
837                 .append("Expected: [")
838                 .append(expected.length)
839                 .append("] {");
840         boolean isFirst = true;
841         for (View view : expected) {
842             if (isFirst) {
843                 isFirst = false;
844             } else {
845                 sb.append(", ");
846             }
847             sb.append(view);
848         }
849         sb
850                 .append("}, but got: [")
851                 .append(targets.size())
852                 .append("] {");
853         isFirst = true;
854         for (View view : targets) {
855             if (isFirst) {
856                 isFirst = false;
857             } else {
858                 sb.append(", ");
859             }
860             sb.append(view);
861         }
862         sb.append("}");
863         String errorMessage = sb.toString();
864 
865         assertEquals(errorMessage, expected.length, targets.size());
866         for (View view : expected) {
867             assertTrue(errorMessage, targets.contains(view));
868         }
869         transition.clearTargets();
870     }
871 
verifyNoOtherTransitions(TransitionFragment fragment)872     private void verifyNoOtherTransitions(TransitionFragment fragment) {
873         assertEquals(0, fragment.enterTransition.targets.size());
874         assertEquals(0, fragment.exitTransition.targets.size());
875         assertEquals(0, fragment.reenterTransition.targets.size());
876         assertEquals(0, fragment.returnTransition.targets.size());
877         assertEquals(0, fragment.sharedElementEnter.targets.size());
878         assertEquals(0, fragment.sharedElementReturn.targets.size());
879     }
880 
verifyTransition(TransitionFragment from, TransitionFragment to, String sharedElementName)881     private void verifyTransition(TransitionFragment from, TransitionFragment to,
882             String sharedElementName) throws Throwable {
883         final int startOnBackStackChanged = mOnBackStackChangedTimes;
884         final View startBlue = findBlue();
885         final View startGreen = findGreen();
886         final View startRed = findRed();
887 
888         final Rect startBlueRect = getBoundsOnScreen(startBlue);
889 
890         mFragmentManager.beginTransaction()
891                 .setReorderingAllowed(mReorderingAllowed)
892                 .addSharedElement(startBlue, sharedElementName)
893                 .replace(R.id.fragmentContainer, to)
894                 .addToBackStack(null)
895                 .commit();
896 
897         FragmentTestUtil.waitForExecution(mActivityRule);
898         assertEquals(startOnBackStackChanged + 1, mOnBackStackChangedTimes);
899 
900         to.waitForTransition();
901         final View endGreen = findGreen();
902         final View endBlue = findBlue();
903         final View endRed = findRed();
904         final Rect endBlueRect = getBoundsOnScreen(endBlue);
905 
906         if (startRed != null) {
907             verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen, startRed);
908         } else {
909             verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen);
910         }
911         verifyNoOtherTransitions(from);
912 
913         if (endRed != null) {
914             verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen, endRed);
915         } else {
916             verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen);
917         }
918         verifyAndClearTransition(to.sharedElementEnter, startBlueRect, startBlue, endBlue);
919         verifyNoOtherTransitions(to);
920     }
921 
verifyCrossTransition(boolean swapSource, TransitionFragment from1, TransitionFragment from2)922     private void verifyCrossTransition(boolean swapSource,
923             TransitionFragment from1, TransitionFragment from2) throws Throwable {
924         final int startNumOnBackStackChanged = mOnBackStackChangedTimes;
925         final int changesPerOperation = mReorderingAllowed ? 1 : 2;
926 
927         final TransitionFragment to1 = new TransitionFragment();
928         to1.setLayoutId(R.layout.scene2);
929         final TransitionFragment to2 = new TransitionFragment();
930         to2.setLayoutId(R.layout.scene2);
931 
932         final View fromExit1 = findViewById(from1, R.id.greenSquare);
933         final View fromShared1 = findViewById(from1, R.id.blueSquare);
934         final Rect fromSharedRect1 = getBoundsOnScreen(fromShared1);
935 
936         final int fromExitId2 = swapSource ? R.id.blueSquare : R.id.greenSquare;
937         final int fromSharedId2 = swapSource ? R.id.greenSquare : R.id.blueSquare;
938         final View fromExit2 = findViewById(from2, fromExitId2);
939         final View fromShared2 = findViewById(from2, fromSharedId2);
940         final Rect fromSharedRect2 = getBoundsOnScreen(fromShared2);
941 
942         final String sharedElementName = swapSource ? "blueSquare" : "greenSquare";
943 
944         mActivityRule.runOnUiThread(new Runnable() {
945             @Override
946             public void run() {
947                 mFragmentManager.beginTransaction()
948                         .setReorderingAllowed(mReorderingAllowed)
949                         .addSharedElement(fromShared1, "blueSquare")
950                         .replace(R.id.fragmentContainer1, to1)
951                         .addToBackStack(null)
952                         .commit();
953                 mFragmentManager.beginTransaction()
954                         .setReorderingAllowed(mReorderingAllowed)
955                         .addSharedElement(fromShared2, sharedElementName)
956                         .replace(R.id.fragmentContainer2, to2)
957                         .addToBackStack(null)
958                         .commit();
959             }
960         });
961         FragmentTestUtil.waitForExecution(mActivityRule);
962         assertEquals(startNumOnBackStackChanged + changesPerOperation, mOnBackStackChangedTimes);
963 
964         from1.waitForTransition();
965         from2.waitForTransition();
966         to1.waitForTransition();
967         to2.waitForTransition();
968 
969         final View toEnter1 = findViewById(to1, R.id.greenSquare);
970         final View toShared1 = findViewById(to1, R.id.blueSquare);
971         final Rect toSharedRect1 = getBoundsOnScreen(toShared1);
972 
973         final View toEnter2 = findViewById(to2, fromSharedId2);
974         final View toShared2 = findViewById(to2, fromExitId2);
975         final Rect toSharedRect2 = getBoundsOnScreen(toShared2);
976 
977         verifyAndClearTransition(from1.exitTransition, fromSharedRect1, fromExit1);
978         verifyAndClearTransition(from2.exitTransition, fromSharedRect2, fromExit2);
979         verifyNoOtherTransitions(from1);
980         verifyNoOtherTransitions(from2);
981 
982         verifyAndClearTransition(to1.enterTransition, toSharedRect1, toEnter1);
983         verifyAndClearTransition(to2.enterTransition, toSharedRect2, toEnter2);
984         verifyAndClearTransition(to1.sharedElementEnter, fromSharedRect1, fromShared1, toShared1);
985         verifyAndClearTransition(to2.sharedElementEnter, fromSharedRect2, fromShared2, toShared2);
986         verifyNoOtherTransitions(to1);
987         verifyNoOtherTransitions(to2);
988 
989         // Now pop it back
990         mActivityRule.runOnUiThread(new Runnable() {
991             @Override
992             public void run() {
993                 mFragmentManager.popBackStack();
994                 mFragmentManager.popBackStack();
995             }
996         });
997         FragmentTestUtil.waitForExecution(mActivityRule);
998         assertEquals(startNumOnBackStackChanged + changesPerOperation + 1,
999                 mOnBackStackChangedTimes);
1000 
1001         from1.waitForTransition();
1002         from2.waitForTransition();
1003         to1.waitForTransition();
1004         to2.waitForTransition();
1005 
1006         final View returnEnter1 = findViewById(from1, R.id.greenSquare);
1007         final View returnShared1 = findViewById(from1, R.id.blueSquare);
1008 
1009         final View returnEnter2 = findViewById(from2, fromExitId2);
1010         final View returnShared2 = findViewById(from2, fromSharedId2);
1011 
1012         verifyAndClearTransition(to1.returnTransition, toSharedRect1, toEnter1);
1013         verifyAndClearTransition(to2.returnTransition, toSharedRect2, toEnter2);
1014         verifyAndClearTransition(to1.sharedElementReturn, toSharedRect1, toShared1, returnShared1);
1015         verifyAndClearTransition(to2.sharedElementReturn, toSharedRect2, toShared2, returnShared2);
1016         verifyNoOtherTransitions(to1);
1017         verifyNoOtherTransitions(to2);
1018 
1019         verifyAndClearTransition(from1.reenterTransition, fromSharedRect1, returnEnter1);
1020         verifyAndClearTransition(from2.reenterTransition, fromSharedRect2, returnEnter2);
1021         verifyNoOtherTransitions(from1);
1022         verifyNoOtherTransitions(from2);
1023     }
1024 
verifyPopTransition(final int numPops, TransitionFragment from, TransitionFragment to, TransitionFragment... others)1025     private void verifyPopTransition(final int numPops, TransitionFragment from,
1026             TransitionFragment to, TransitionFragment... others) throws Throwable {
1027         final int startOnBackStackChanged = mOnBackStackChangedTimes;
1028         final View startBlue = findBlue();
1029         final View startGreen = findGreen();
1030         final View startRed = findRed();
1031         final Rect startSharedRect = getBoundsOnScreen(startBlue);
1032 
1033         mInstrumentation.runOnMainSync(new Runnable() {
1034             @Override
1035             public void run() {
1036                 for (int i = 0; i < numPops; i++) {
1037                     mFragmentManager.popBackStack();
1038                 }
1039             }
1040         });
1041         FragmentTestUtil.waitForExecution(mActivityRule);
1042         assertEquals(startOnBackStackChanged + 1, mOnBackStackChangedTimes);
1043 
1044         to.waitForTransition();
1045         final View endGreen = findGreen();
1046         final View endBlue = findBlue();
1047         final View endRed = findRed();
1048         final Rect endSharedRect = getBoundsOnScreen(endBlue);
1049 
1050         if (startRed != null) {
1051             verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen, startRed);
1052         } else {
1053             verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen);
1054         }
1055         verifyAndClearTransition(from.sharedElementReturn, startSharedRect, startBlue, endBlue);
1056         verifyNoOtherTransitions(from);
1057 
1058         if (endRed != null) {
1059             verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen, endRed);
1060         } else {
1061             verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen);
1062         }
1063         verifyNoOtherTransitions(to);
1064 
1065         if (others != null) {
1066             for (TransitionFragment fragment : others) {
1067                 verifyNoOtherTransitions(fragment);
1068             }
1069         }
1070     }
1071 
getBoundsOnScreen(View view)1072     private static Rect getBoundsOnScreen(View view) {
1073         final int[] loc = new int[2];
1074         view.getLocationOnScreen(loc);
1075         return new Rect(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
1076     }
1077 
1078     public static class ComplexTransitionFragment extends TransitionFragment {
1079         public final TrackingTransition sharedElementEnterTransition1 = new TrackingTransition();
1080         public final TrackingTransition sharedElementEnterTransition2 = new TrackingTransition();
1081         public final TrackingTransition sharedElementReturnTransition1 = new TrackingTransition();
1082         public final TrackingTransition sharedElementReturnTransition2 = new TrackingTransition();
1083 
1084         public final TransitionSet sharedElementEnterTransition = new TransitionSet()
1085                 .addTransition(sharedElementEnterTransition1)
1086                 .addTransition(sharedElementEnterTransition2);
1087         public final TransitionSet sharedElementReturnTransition = new TransitionSet()
1088                 .addTransition(sharedElementReturnTransition1)
1089                 .addTransition(sharedElementReturnTransition2);
1090 
ComplexTransitionFragment()1091         public ComplexTransitionFragment() {
1092             sharedElementEnterTransition1.addTarget(R.id.blueSquare);
1093             sharedElementEnterTransition2.addTarget(R.id.greenSquare);
1094             sharedElementReturnTransition1.addTarget(R.id.blueSquare);
1095             sharedElementReturnTransition2.addTarget(R.id.greenSquare);
1096             setSharedElementEnterTransition(sharedElementEnterTransition);
1097             setSharedElementReturnTransition(sharedElementReturnTransition);
1098         }
1099     }
1100 
1101     public static class InvisibleFragment extends TransitionFragment {
1102         @Override
onViewCreated(View view, Bundle savedInstanceState)1103         public void onViewCreated(View view, Bundle savedInstanceState) {
1104             view.setVisibility(View.INVISIBLE);
1105             super.onViewCreated(view, savedInstanceState);
1106         }
1107     }
1108 }
1109