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