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