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