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