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 package androidx.fragment.app; 17 18 import static org.junit.Assert.assertEquals; 19 import static org.junit.Assert.assertFalse; 20 import static org.junit.Assert.assertNotNull; 21 import static org.junit.Assert.assertNull; 22 import static org.junit.Assert.assertTrue; 23 24 import android.app.Instrumentation; 25 import android.os.Bundle; 26 import android.os.Parcelable; 27 import android.support.test.InstrumentationRegistry; 28 import android.support.test.filters.MediumTest; 29 import android.support.test.rule.ActivityTestRule; 30 import android.support.test.runner.AndroidJUnit4; 31 import android.util.Pair; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.animation.Animation; 36 import android.view.animation.AnimationUtils; 37 import android.view.animation.TranslateAnimation; 38 39 import androidx.annotation.AnimRes; 40 import androidx.core.view.ViewCompat; 41 import androidx.fragment.app.test.FragmentTestActivity; 42 import androidx.fragment.test.R; 43 44 import org.junit.Before; 45 import org.junit.Rule; 46 import org.junit.Test; 47 import org.junit.runner.RunWith; 48 49 import java.util.concurrent.CountDownLatch; 50 import java.util.concurrent.TimeUnit; 51 52 @MediumTest 53 @RunWith(AndroidJUnit4.class) 54 public 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