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