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.assertFalse;
20import static org.junit.Assert.assertNotNull;
21import static org.junit.Assert.assertNull;
22import static org.junit.Assert.assertTrue;
23
24import android.app.Instrumentation;
25import android.os.Bundle;
26import android.os.Parcelable;
27import android.support.test.InstrumentationRegistry;
28import android.support.test.filters.MediumTest;
29import android.support.test.rule.ActivityTestRule;
30import android.support.test.runner.AndroidJUnit4;
31import android.util.Pair;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.ViewGroup;
35import android.view.animation.Animation;
36import android.view.animation.AnimationUtils;
37import android.view.animation.TranslateAnimation;
38
39import androidx.annotation.AnimRes;
40import androidx.core.view.ViewCompat;
41import androidx.fragment.app.test.FragmentTestActivity;
42import androidx.fragment.test.R;
43
44import org.junit.Before;
45import org.junit.Rule;
46import org.junit.Test;
47import org.junit.runner.RunWith;
48
49import java.util.concurrent.CountDownLatch;
50import java.util.concurrent.TimeUnit;
51
52@MediumTest
53@RunWith(AndroidJUnit4.class)
54public class FragmentAnimationTest {
55    // These are pretend resource IDs for animators. We don't need real ones since we
56    // load them by overriding onCreateAnimator
57    @AnimRes
58    private static final int ENTER = 1;
59    @AnimRes
60    private static final int EXIT = 2;
61    @AnimRes
62    private static final int POP_ENTER = 3;
63    @AnimRes
64    private static final int POP_EXIT = 4;
65
66    @Rule
67    public ActivityTestRule<FragmentTestActivity> mActivityRule =
68            new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class);
69
70    private Instrumentation mInstrumentation;
71
72    @Before
73    public void setupContainer() {
74        mInstrumentation = InstrumentationRegistry.getInstrumentation();
75        FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
76    }
77
78    // Ensure that adding and popping a Fragment uses the enter and popExit animators
79    @Test
80    public void addAnimators() throws Throwable {
81        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
82
83        // One fragment with a view
84        final AnimatorFragment fragment = new AnimatorFragment();
85        fm.beginTransaction()
86                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
87                .add(R.id.fragmentContainer, fragment)
88                .addToBackStack(null)
89                .commit();
90        FragmentTestUtil.waitForExecution(mActivityRule);
91
92        assertEnterPopExit(fragment);
93    }
94
95    // Ensure that removing and popping a Fragment uses the exit and popEnter animators
96    @Test
97    public void removeAnimators() throws Throwable {
98        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
99
100        // One fragment with a view
101        final AnimatorFragment fragment = new AnimatorFragment();
102        fm.beginTransaction().add(R.id.fragmentContainer, fragment, "1").commit();
103        FragmentTestUtil.waitForExecution(mActivityRule);
104
105        fm.beginTransaction()
106                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
107                .remove(fragment)
108                .addToBackStack(null)
109                .commit();
110        FragmentTestUtil.waitForExecution(mActivityRule);
111
112        assertExitPopEnter(fragment);
113    }
114
115    // Ensure that showing and popping a Fragment uses the enter and popExit animators
116    @Test
117    public void showAnimators() throws Throwable {
118        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
119
120        // One fragment with a view
121        final AnimatorFragment fragment = new AnimatorFragment();
122        fm.beginTransaction().add(R.id.fragmentContainer, fragment).hide(fragment).commit();
123        FragmentTestUtil.waitForExecution(mActivityRule);
124
125        fm.beginTransaction()
126                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
127                .show(fragment)
128                .addToBackStack(null)
129                .commit();
130        FragmentTestUtil.waitForExecution(mActivityRule);
131
132        assertEnterPopExit(fragment);
133    }
134
135    // Ensure that hiding and popping a Fragment uses the exit and popEnter animators
136    @Test
137    public void hideAnimators() throws Throwable {
138        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
139
140        // One fragment with a view
141        final AnimatorFragment fragment = new AnimatorFragment();
142        fm.beginTransaction().add(R.id.fragmentContainer, fragment, "1").commit();
143        FragmentTestUtil.waitForExecution(mActivityRule);
144
145        fm.beginTransaction()
146                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
147                .hide(fragment)
148                .addToBackStack(null)
149                .commit();
150        FragmentTestUtil.waitForExecution(mActivityRule);
151
152        assertExitPopEnter(fragment);
153    }
154
155    // Ensure that attaching and popping a Fragment uses the enter and popExit animators
156    @Test
157    public void attachAnimators() throws Throwable {
158        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
159
160        // One fragment with a view
161        final AnimatorFragment fragment = new AnimatorFragment();
162        fm.beginTransaction().add(R.id.fragmentContainer, fragment).detach(fragment).commit();
163        FragmentTestUtil.waitForExecution(mActivityRule);
164
165        fm.beginTransaction()
166                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
167                .attach(fragment)
168                .addToBackStack(null)
169                .commit();
170        FragmentTestUtil.waitForExecution(mActivityRule);
171
172        assertEnterPopExit(fragment);
173    }
174
175    // Ensure that detaching and popping a Fragment uses the exit and popEnter animators
176    @Test
177    public void detachAnimators() throws Throwable {
178        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
179
180        // One fragment with a view
181        final AnimatorFragment fragment = new AnimatorFragment();
182        fm.beginTransaction().add(R.id.fragmentContainer, fragment, "1").commit();
183        FragmentTestUtil.waitForExecution(mActivityRule);
184
185        fm.beginTransaction()
186                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
187                .detach(fragment)
188                .addToBackStack(null)
189                .commit();
190        FragmentTestUtil.waitForExecution(mActivityRule);
191
192        assertExitPopEnter(fragment);
193    }
194
195    // Replace should exit the existing fragments and enter the added fragment, then
196    // popping should popExit the removed fragment and popEnter the added fragments
197    @Test
198    public void replaceAnimators() throws Throwable {
199        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
200
201        // One fragment with a view
202        final AnimatorFragment fragment1 = new AnimatorFragment();
203        final AnimatorFragment fragment2 = new AnimatorFragment();
204        fm.beginTransaction()
205                .add(R.id.fragmentContainer, fragment1, "1")
206                .add(R.id.fragmentContainer, fragment2, "2")
207                .commit();
208        FragmentTestUtil.waitForExecution(mActivityRule);
209
210        final AnimatorFragment fragment3 = new AnimatorFragment();
211        fm.beginTransaction()
212                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
213                .replace(R.id.fragmentContainer, fragment3)
214                .addToBackStack(null)
215                .commit();
216        FragmentTestUtil.waitForExecution(mActivityRule);
217
218        assertFragmentAnimation(fragment1, 1, false, EXIT);
219        assertFragmentAnimation(fragment2, 1, false, EXIT);
220        assertFragmentAnimation(fragment3, 1, true, ENTER);
221
222        fm.popBackStack();
223        FragmentTestUtil.waitForExecution(mActivityRule);
224
225        assertFragmentAnimation(fragment3, 2, false, POP_EXIT);
226        final AnimatorFragment replacement1 = (AnimatorFragment) fm.findFragmentByTag("1");
227        final AnimatorFragment replacement2 = (AnimatorFragment) fm.findFragmentByTag("1");
228        int expectedAnimations = replacement1 == fragment1 ? 2 : 1;
229        assertFragmentAnimation(replacement1, expectedAnimations, true, POP_ENTER);
230        assertFragmentAnimation(replacement2, expectedAnimations, true, POP_ENTER);
231    }
232
233    // Ensure that adding and popping a Fragment uses the enter and popExit animators,
234    // but the animators are delayed when an entering Fragment is postponed.
235    @Test
236    public void postponedAddAnimators() throws Throwable {
237        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
238
239        final AnimatorFragment fragment = new AnimatorFragment();
240        fragment.postponeEnterTransition();
241        fm.beginTransaction()
242                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
243                .add(R.id.fragmentContainer, fragment)
244                .addToBackStack(null)
245                .setReorderingAllowed(true)
246                .commit();
247        FragmentTestUtil.waitForExecution(mActivityRule);
248
249        assertPostponed(fragment, 0);
250        fragment.startPostponedEnterTransition();
251
252        FragmentTestUtil.waitForExecution(mActivityRule);
253        assertEnterPopExit(fragment);
254    }
255
256    // Ensure that removing and popping a Fragment uses the exit and popEnter animators,
257    // but the animators are delayed when an entering Fragment is postponed.
258    @Test
259    public void postponedRemoveAnimators() throws Throwable {
260        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
261
262        final AnimatorFragment fragment = new AnimatorFragment();
263        fm.beginTransaction().add(R.id.fragmentContainer, fragment, "1").commit();
264        FragmentTestUtil.waitForExecution(mActivityRule);
265
266        fm.beginTransaction()
267                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
268                .remove(fragment)
269                .addToBackStack(null)
270                .setReorderingAllowed(true)
271                .commit();
272        FragmentTestUtil.waitForExecution(mActivityRule);
273
274        assertExitPostponedPopEnter(fragment);
275    }
276
277    // Ensure that adding and popping a Fragment is postponed in both directions
278    // when the fragments have been marked for postponing.
279    @Test
280    public void postponedAddRemove() throws Throwable {
281        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
282
283        final AnimatorFragment fragment1 = new AnimatorFragment();
284        fm.beginTransaction()
285                .add(R.id.fragmentContainer, fragment1)
286                .addToBackStack(null)
287                .setReorderingAllowed(true)
288                .commit();
289        FragmentTestUtil.waitForExecution(mActivityRule);
290
291        final AnimatorFragment fragment2 = new AnimatorFragment();
292        fragment2.postponeEnterTransition();
293
294        fm.beginTransaction()
295                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
296                .replace(R.id.fragmentContainer, fragment2)
297                .addToBackStack(null)
298                .setReorderingAllowed(true)
299                .commit();
300
301        FragmentTestUtil.waitForExecution(mActivityRule);
302
303        assertPostponed(fragment2, 0);
304        assertNotNull(fragment1.getView());
305        assertEquals(View.VISIBLE, fragment1.getView().getVisibility());
306        assertEquals(1f, fragment1.getView().getAlpha(), 0f);
307        assertTrue(ViewCompat.isAttachedToWindow(fragment1.getView()));
308
309        fragment2.startPostponedEnterTransition();
310        FragmentTestUtil.waitForExecution(mActivityRule);
311
312        assertExitPostponedPopEnter(fragment1);
313    }
314
315    // Popping a postponed transaction should result in no animators
316    @Test
317    public void popPostponed() throws Throwable {
318        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
319
320        final AnimatorFragment fragment1 = new AnimatorFragment();
321        fm.beginTransaction()
322                .add(R.id.fragmentContainer, fragment1)
323                .setReorderingAllowed(true)
324                .commit();
325        FragmentTestUtil.waitForExecution(mActivityRule);
326        assertEquals(0, fragment1.numAnimators);
327
328        final AnimatorFragment fragment2 = new AnimatorFragment();
329        fragment2.postponeEnterTransition();
330
331        fm.beginTransaction()
332                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
333                .replace(R.id.fragmentContainer, fragment2)
334                .addToBackStack(null)
335                .setReorderingAllowed(true)
336                .commit();
337
338        FragmentTestUtil.waitForExecution(mActivityRule);
339
340        assertPostponed(fragment2, 0);
341
342        // Now pop the postponed transaction
343        FragmentTestUtil.popBackStackImmediate(mActivityRule);
344
345        assertNotNull(fragment1.getView());
346        assertEquals(View.VISIBLE, fragment1.getView().getVisibility());
347        assertEquals(1f, fragment1.getView().getAlpha(), 0f);
348        assertTrue(ViewCompat.isAttachedToWindow(fragment1.getView()));
349        assertTrue(fragment1.isAdded());
350
351        assertNull(fragment2.getView());
352        assertFalse(fragment2.isAdded());
353
354        assertEquals(0, fragment1.numAnimators);
355        assertEquals(0, fragment2.numAnimators);
356        assertNull(fragment1.animation);
357        assertNull(fragment2.animation);
358    }
359
360    // Make sure that if the state was saved while a Fragment was animating that its
361    // state is proper after restoring.
362    @Test
363    public void saveWhileAnimatingAway() throws Throwable {
364        final FragmentController fc1 = FragmentTestUtil.createController(mActivityRule);
365        FragmentTestUtil.resume(mActivityRule, fc1, null);
366
367        final FragmentManager fm1 = fc1.getSupportFragmentManager();
368
369        StrictViewFragment fragment1 = new StrictViewFragment();
370        fragment1.setLayoutId(R.layout.scene1);
371        fm1.beginTransaction()
372                .add(R.id.fragmentContainer, fragment1, "1")
373                .commit();
374        FragmentTestUtil.waitForExecution(mActivityRule);
375
376        StrictViewFragment fragment2 = new StrictViewFragment();
377
378        fm1.beginTransaction()
379                .setCustomAnimations(0, 0, 0, R.anim.long_fade_out)
380                .replace(R.id.fragmentContainer, fragment2, "2")
381                .addToBackStack(null)
382                .commit();
383        mInstrumentation.runOnMainSync(new Runnable() {
384            @Override
385            public void run() {
386                fm1.executePendingTransactions();
387            }
388        });
389        FragmentTestUtil.waitForExecution(mActivityRule);
390
391        fm1.popBackStack();
392
393        mInstrumentation.runOnMainSync(new Runnable() {
394            @Override
395            public void run() {
396                fm1.executePendingTransactions();
397            }
398        });
399        FragmentTestUtil.waitForExecution(mActivityRule);
400        // Now fragment2 should be animating away
401        assertFalse(fragment2.isAdded());
402        assertEquals(fragment2, fm1.findFragmentByTag("2")); // still exists because it is animating
403
404        Pair<Parcelable, FragmentManagerNonConfig> state =
405                FragmentTestUtil.destroy(mActivityRule, fc1);
406
407        final FragmentController fc2 = FragmentTestUtil.createController(mActivityRule);
408        FragmentTestUtil.resume(mActivityRule, fc2, state);
409
410        final FragmentManager fm2 = fc2.getSupportFragmentManager();
411        Fragment fragment2restored = fm2.findFragmentByTag("2");
412        assertNull(fragment2restored);
413
414        Fragment fragment1restored = fm2.findFragmentByTag("1");
415        assertNotNull(fragment1restored);
416        assertNotNull(fragment1restored.getView());
417    }
418
419    // When an animation is running on a Fragment's View, the view shouldn't be
420    // prevented from being removed. There's no way to directly test this, so we have to
421    // test to see if the animation is still running.
422    @Test
423    public void clearAnimations() throws Throwable {
424        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
425
426        final StrictViewFragment fragment1 = new StrictViewFragment();
427        fm.beginTransaction()
428                .add(R.id.fragmentContainer, fragment1)
429                .setReorderingAllowed(true)
430                .commit();
431        FragmentTestUtil.waitForExecution(mActivityRule);
432
433        final View fragmentView = fragment1.getView();
434
435        final TranslateAnimation xAnimation = new TranslateAnimation(0, 1000, 0, 0);
436        mActivityRule.runOnUiThread(new Runnable() {
437            @Override
438            public void run() {
439                fragmentView.startAnimation(xAnimation);
440            }
441        });
442
443        FragmentTestUtil.waitForExecution(mActivityRule);
444        FragmentTestUtil.popBackStackImmediate(mActivityRule);
445        mActivityRule.runOnUiThread(new Runnable() {
446            @Override
447            public void run() {
448                assertNull(fragmentView.getAnimation());
449            }
450        });
451    }
452
453    // When a view is animated out, is parent should be null after the animation completes
454    @Test
455    public void parentNullAfterAnimation() throws Throwable {
456        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
457
458        final EndAnimationListenerFragment fragment1 = new EndAnimationListenerFragment();
459        fm.beginTransaction()
460                .add(R.id.fragmentContainer, fragment1)
461                .commit();
462        FragmentTestUtil.waitForExecution(mActivityRule);
463
464        final EndAnimationListenerFragment fragment2 = new EndAnimationListenerFragment();
465
466        fm.beginTransaction()
467                .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out,
468                        android.R.anim.fade_in, android.R.anim.fade_out)
469                .replace(R.id.fragmentContainer, fragment2)
470                .addToBackStack(null)
471                .commit();
472
473        FragmentTestUtil.waitForExecution(mActivityRule);
474
475        assertTrue(fragment1.exitLatch.await(1, TimeUnit.SECONDS));
476        assertTrue(fragment2.enterLatch.await(1, TimeUnit.SECONDS));
477
478        mActivityRule.runOnUiThread(new Runnable() {
479            @Override
480            public void run() {
481                assertNotNull(fragment1.view);
482                assertNotNull(fragment2.view);
483                assertNull(fragment1.view.getParent());
484            }
485        });
486
487        // Now pop the transaction
488        FragmentTestUtil.popBackStackImmediate(mActivityRule);
489
490        assertTrue(fragment2.exitLatch.await(1, TimeUnit.SECONDS));
491        assertTrue(fragment1.enterLatch.await(1, TimeUnit.SECONDS));
492
493        mActivityRule.runOnUiThread(new Runnable() {
494            @Override
495            public void run() {
496                assertNull(fragment2.view.getParent());
497            }
498        });
499    }
500
501    private void assertEnterPopExit(AnimatorFragment fragment) throws Throwable {
502        assertFragmentAnimation(fragment, 1, true, ENTER);
503
504        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
505        fm.popBackStack();
506        FragmentTestUtil.waitForExecution(mActivityRule);
507
508        assertFragmentAnimation(fragment, 2, false, POP_EXIT);
509    }
510
511    private void assertExitPopEnter(AnimatorFragment fragment) throws Throwable {
512        assertFragmentAnimation(fragment, 1, false, EXIT);
513
514        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
515        fm.popBackStack();
516        FragmentTestUtil.waitForExecution(mActivityRule);
517
518        AnimatorFragment replacement = (AnimatorFragment) fm.findFragmentByTag("1");
519
520        boolean isSameFragment = replacement == fragment;
521        int expectedAnimators = isSameFragment ? 2 : 1;
522        assertFragmentAnimation(replacement, expectedAnimators, true, POP_ENTER);
523    }
524
525    private void assertExitPostponedPopEnter(AnimatorFragment fragment) throws Throwable {
526        assertFragmentAnimation(fragment, 1, false, EXIT);
527
528        fragment.postponeEnterTransition();
529        FragmentTestUtil.popBackStackImmediate(mActivityRule);
530
531        assertPostponed(fragment, 1);
532
533        fragment.startPostponedEnterTransition();
534        FragmentTestUtil.waitForExecution(mActivityRule);
535        assertFragmentAnimation(fragment, 2, true, POP_ENTER);
536    }
537
538    private void assertFragmentAnimation(AnimatorFragment fragment, int numAnimators,
539            boolean isEnter, int animatorResourceId) throws InterruptedException {
540        assertEquals(numAnimators, fragment.numAnimators);
541        assertEquals(isEnter, fragment.enter);
542        assertEquals(animatorResourceId, fragment.resourceId);
543        assertNotNull(fragment.animation);
544        assertTrue(FragmentTestUtil.waitForAnimationEnd(1000, fragment.animation));
545        assertTrue(fragment.animation.hasStarted());
546    }
547
548    private void assertPostponed(AnimatorFragment fragment, int expectedAnimators)
549            throws InterruptedException {
550        assertTrue(fragment.mOnCreateViewCalled);
551        assertEquals(View.VISIBLE, fragment.getView().getVisibility());
552        assertEquals(0f, fragment.getView().getAlpha(), 0f);
553        assertEquals(expectedAnimators, fragment.numAnimators);
554    }
555
556    public static class AnimatorFragment extends StrictViewFragment {
557        public int numAnimators;
558        public Animation animation;
559        public boolean enter;
560        public int resourceId;
561
562        @Override
563        public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
564            if (nextAnim == 0) {
565                return null;
566            }
567            this.numAnimators++;
568            this.animation = new TranslateAnimation(-10, 0, 0, 0);
569            this.animation.setDuration(1);
570            this.resourceId = nextAnim;
571            this.enter = enter;
572            return this.animation;
573        }
574    }
575
576    public static class EndAnimationListenerFragment extends StrictViewFragment {
577        public View view;
578        public final CountDownLatch enterLatch = new CountDownLatch(1);
579        public final CountDownLatch exitLatch = new CountDownLatch(1);
580
581        @Override
582        public View onCreateView(LayoutInflater inflater, ViewGroup container,
583                Bundle savedInstanceState) {
584            if (view != null) {
585                return view;
586            }
587            view = super.onCreateView(inflater, container, savedInstanceState);
588            return view;
589        }
590
591        @Override
592        public Animation onCreateAnimation(int transit, final boolean enter, int nextAnim) {
593            if (nextAnim == 0) {
594                return null;
595            }
596            Animation anim = AnimationUtils.loadAnimation(getActivity(), nextAnim);
597            if (anim != null) {
598                anim.setAnimationListener(new Animation.AnimationListener() {
599                    @Override
600                    public void onAnimationStart(Animation animation) {
601                    }
602
603                    @Override
604                    public void onAnimationEnd(Animation animation) {
605                        if (enter) {
606                            enterLatch.countDown();
607                        } else {
608                            exitLatch.countDown();
609                        }
610                    }
611
612                    @Override
613                    public void onAnimationRepeat(Animation animation) {
614
615                    }
616                });
617            }
618            return anim;
619        }
620    }
621}
622