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