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 */ 16package androidx.fragment.app; 17 18import static org.junit.Assert.assertEquals; 19import static org.junit.Assert.assertNull; 20import static org.junit.Assert.assertTrue; 21import static org.junit.Assert.fail; 22import static org.mockito.Mockito.mock; 23import static org.mockito.Mockito.reset; 24import static org.mockito.Mockito.verify; 25 26import android.app.Instrumentation; 27import android.graphics.Rect; 28import android.os.Build; 29import android.os.Bundle; 30import android.support.test.InstrumentationRegistry; 31import android.support.test.filters.MediumTest; 32import android.support.test.filters.SdkSuppress; 33import android.support.test.rule.ActivityTestRule; 34import android.transition.TransitionSet; 35import android.view.View; 36 37import androidx.core.app.SharedElementCallback; 38import androidx.fragment.app.test.FragmentTestActivity; 39import androidx.fragment.test.R; 40 41import org.junit.After; 42import org.junit.Before; 43import org.junit.Rule; 44import org.junit.Test; 45import org.junit.runner.RunWith; 46import org.junit.runners.Parameterized; 47import org.mockito.ArgumentCaptor; 48 49import java.util.ArrayList; 50import java.util.List; 51import java.util.Map; 52 53@MediumTest 54@RunWith(Parameterized.class) 55@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) 56public class FragmentTransitionTest { 57 private final boolean mReorderingAllowed; 58 59 @Parameterized.Parameters 60 public static Object[] data() { 61 return new Boolean[] { 62 false, true 63 }; 64 } 65 66 @Rule 67 public ActivityTestRule<FragmentTestActivity> mActivityRule = 68 new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class); 69 70 private Instrumentation mInstrumentation; 71 private FragmentManager mFragmentManager; 72 private int mOnBackStackChangedTimes; 73 private FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener; 74 75 public FragmentTransitionTest(final boolean reordering) { 76 mReorderingAllowed = reordering; 77 } 78 79 @Before 80 public void setup() throws Throwable { 81 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 82 mFragmentManager = mActivityRule.getActivity().getSupportFragmentManager(); 83 FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container); 84 mOnBackStackChangedTimes = 0; 85 mOnBackStackChangedListener = new FragmentManager.OnBackStackChangedListener() { 86 @Override 87 public void onBackStackChanged() { 88 mOnBackStackChangedTimes++; 89 } 90 }; 91 mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener); 92 } 93 94 @After 95 public void teardown() throws Throwable { 96 mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener); 97 mOnBackStackChangedListener = null; 98 } 99 100 // Test that normal view transitions (enter, exit, reenter, return) run with 101 // a single fragment. 102 @Test 103 public void enterExitTransitions() throws Throwable { 104 // enter transition 105 TransitionFragment fragment = setupInitialFragment(); 106 final View blue = findBlue(); 107 final View green = findBlue(); 108 109 // exit transition 110 mFragmentManager.beginTransaction() 111 .setReorderingAllowed(mReorderingAllowed) 112 .remove(fragment) 113 .addToBackStack(null) 114 .commit(); 115 116 fragment.waitForTransition(); 117 verifyAndClearTransition(fragment.exitTransition, null, green, blue); 118 verifyNoOtherTransitions(fragment); 119 assertEquals(2, mOnBackStackChangedTimes); 120 121 // reenter transition 122 FragmentTestUtil.popBackStackImmediate(mActivityRule); 123 fragment.waitForTransition(); 124 final View green2 = findGreen(); 125 final View blue2 = findBlue(); 126 verifyAndClearTransition(fragment.reenterTransition, null, green2, blue2); 127 verifyNoOtherTransitions(fragment); 128 assertEquals(3, mOnBackStackChangedTimes); 129 130 // return transition 131 FragmentTestUtil.popBackStackImmediate(mActivityRule); 132 fragment.waitForTransition(); 133 verifyAndClearTransition(fragment.returnTransition, null, green2, blue2); 134 verifyNoOtherTransitions(fragment); 135 assertEquals(4, mOnBackStackChangedTimes); 136 } 137 138 // Test that shared elements transition from one fragment to the next 139 // and back during pop. 140 @Test 141 public void sharedElement() throws Throwable { 142 TransitionFragment fragment1 = setupInitialFragment(); 143 144 // Now do a transition to scene2 145 TransitionFragment fragment2 = new TransitionFragment(); 146 fragment2.setLayoutId(R.layout.scene2); 147 148 verifyTransition(fragment1, fragment2, "blueSquare"); 149 150 // Now pop the back stack 151 verifyPopTransition(1, fragment2, fragment1); 152 } 153 154 // Test that shared element transitions through multiple fragments work together 155 @Test 156 public void intermediateFragment() throws Throwable { 157 TransitionFragment fragment1 = setupInitialFragment(); 158 159 final TransitionFragment fragment2 = new TransitionFragment(); 160 fragment2.setLayoutId(R.layout.scene3); 161 162 verifyTransition(fragment1, fragment2, "shared"); 163 164 final TransitionFragment fragment3 = new TransitionFragment(); 165 fragment3.setLayoutId(R.layout.scene2); 166 167 verifyTransition(fragment2, fragment3, "blueSquare"); 168 169 // Should transfer backwards when popping multiple: 170 verifyPopTransition(2, fragment3, fragment1, fragment2); 171 } 172 173 // Adding/removing the same fragment multiple times shouldn't mess anything up 174 @Test 175 public void removeAdded() throws Throwable { 176 final TransitionFragment fragment1 = setupInitialFragment(); 177 178 final View startBlue = findBlue(); 179 final View startGreen = findGreen(); 180 181 final TransitionFragment fragment2 = new TransitionFragment(); 182 fragment2.setLayoutId(R.layout.scene2); 183 184 mInstrumentation.runOnMainSync(new Runnable() { 185 @Override 186 public void run() { 187 mFragmentManager.beginTransaction() 188 .setReorderingAllowed(mReorderingAllowed) 189 .replace(R.id.fragmentContainer, fragment2) 190 .replace(R.id.fragmentContainer, fragment1) 191 .replace(R.id.fragmentContainer, fragment2) 192 .addToBackStack(null) 193 .commit(); 194 } 195 }); 196 FragmentTestUtil.waitForExecution(mActivityRule); 197 assertEquals(2, mOnBackStackChangedTimes); 198 199 // should be a normal transition from fragment1 to fragment2 200 fragment2.waitForTransition(); 201 final View endBlue = findBlue(); 202 final View endGreen = findGreen(); 203 verifyAndClearTransition(fragment1.exitTransition, null, startBlue, startGreen); 204 verifyAndClearTransition(fragment2.enterTransition, null, endBlue, endGreen); 205 verifyNoOtherTransitions(fragment1); 206 verifyNoOtherTransitions(fragment2); 207 208 // Pop should also do the same thing 209 FragmentTestUtil.popBackStackImmediate(mActivityRule); 210 assertEquals(3, mOnBackStackChangedTimes); 211 212 fragment1.waitForTransition(); 213 final View popBlue = findBlue(); 214 final View popGreen = findGreen(); 215 verifyAndClearTransition(fragment1.reenterTransition, null, popBlue, popGreen); 216 verifyAndClearTransition(fragment2.returnTransition, null, endBlue, endGreen); 217 verifyNoOtherTransitions(fragment1); 218 verifyNoOtherTransitions(fragment2); 219 } 220 221 // Make sure that shared elements on two different fragment containers don't interact 222 @Test 223 public void crossContainer() throws Throwable { 224 FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container); 225 TransitionFragment fragment1 = new TransitionFragment(); 226 fragment1.setLayoutId(R.layout.scene1); 227 TransitionFragment fragment2 = new TransitionFragment(); 228 fragment2.setLayoutId(R.layout.scene1); 229 mFragmentManager.beginTransaction() 230 .setReorderingAllowed(mReorderingAllowed) 231 .add(R.id.fragmentContainer1, fragment1) 232 .add(R.id.fragmentContainer2, fragment2) 233 .addToBackStack(null) 234 .commit(); 235 FragmentTestUtil.waitForExecution(mActivityRule); 236 assertEquals(1, mOnBackStackChangedTimes); 237 238 fragment1.waitForTransition(); 239 final View greenSquare1 = findViewById(fragment1, R.id.greenSquare); 240 final View blueSquare1 = findViewById(fragment1, R.id.blueSquare); 241 verifyAndClearTransition(fragment1.enterTransition, null, greenSquare1, blueSquare1); 242 verifyNoOtherTransitions(fragment1); 243 fragment2.waitForTransition(); 244 final View greenSquare2 = findViewById(fragment2, R.id.greenSquare); 245 final View blueSquare2 = findViewById(fragment2, R.id.blueSquare); 246 verifyAndClearTransition(fragment2.enterTransition, null, greenSquare2, blueSquare2); 247 verifyNoOtherTransitions(fragment2); 248 249 // Make sure the correct transitions are run when the target names 250 // are different in both shared elements. We may fool the system. 251 verifyCrossTransition(false, fragment1, fragment2); 252 253 // Make sure the correct transitions are run when the source names 254 // are different in both shared elements. We may fool the system. 255 verifyCrossTransition(true, fragment1, fragment2); 256 } 257 258 // Make sure that onSharedElementStart and onSharedElementEnd are called 259 @Test 260 public void callStartEndWithSharedElements() throws Throwable { 261 TransitionFragment fragment1 = setupInitialFragment(); 262 263 // Now do a transition to scene2 264 TransitionFragment fragment2 = new TransitionFragment(); 265 fragment2.setLayoutId(R.layout.scene2); 266 267 SharedElementCallback enterCallback = mock(SharedElementCallback.class); 268 fragment2.setEnterSharedElementCallback(enterCallback); 269 270 final View startBlue = findBlue(); 271 272 verifyTransition(fragment1, fragment2, "blueSquare"); 273 274 ArgumentCaptor<List> names = ArgumentCaptor.forClass(List.class); 275 ArgumentCaptor<List> views = ArgumentCaptor.forClass(List.class); 276 ArgumentCaptor<List> snapshots = ArgumentCaptor.forClass(List.class); 277 verify(enterCallback).onSharedElementStart(names.capture(), views.capture(), 278 snapshots.capture()); 279 assertEquals(1, names.getValue().size()); 280 assertEquals(1, views.getValue().size()); 281 assertNull(snapshots.getValue()); 282 assertEquals("blueSquare", names.getValue().get(0)); 283 assertEquals(startBlue, views.getValue().get(0)); 284 285 final View endBlue = findBlue(); 286 287 verify(enterCallback).onSharedElementEnd(names.capture(), views.capture(), 288 snapshots.capture()); 289 assertEquals(1, names.getValue().size()); 290 assertEquals(1, views.getValue().size()); 291 assertNull(snapshots.getValue()); 292 assertEquals("blueSquare", names.getValue().get(0)); 293 assertEquals(endBlue, views.getValue().get(0)); 294 295 // Now pop the back stack 296 reset(enterCallback); 297 verifyPopTransition(1, fragment2, fragment1); 298 299 verify(enterCallback).onSharedElementStart(names.capture(), views.capture(), 300 snapshots.capture()); 301 assertEquals(1, names.getValue().size()); 302 assertEquals(1, views.getValue().size()); 303 assertNull(snapshots.getValue()); 304 assertEquals("blueSquare", names.getValue().get(0)); 305 assertEquals(endBlue, views.getValue().get(0)); 306 307 final View reenterBlue = findBlue(); 308 309 verify(enterCallback).onSharedElementEnd(names.capture(), views.capture(), 310 snapshots.capture()); 311 assertEquals(1, names.getValue().size()); 312 assertEquals(1, views.getValue().size()); 313 assertNull(snapshots.getValue()); 314 assertEquals("blueSquare", names.getValue().get(0)); 315 assertEquals(reenterBlue, views.getValue().get(0)); 316 } 317 318 // Make sure that onMapSharedElement works to change the shared element going out 319 @Test 320 public void onMapSharedElementOut() throws Throwable { 321 final TransitionFragment fragment1 = setupInitialFragment(); 322 323 // Now do a transition to scene2 324 TransitionFragment fragment2 = new TransitionFragment(); 325 fragment2.setLayoutId(R.layout.scene2); 326 327 final View startBlue = findBlue(); 328 final View startGreen = findGreen(); 329 330 final Rect startGreenBounds = getBoundsOnScreen(startGreen); 331 332 SharedElementCallback mapOut = new SharedElementCallback() { 333 @Override 334 public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { 335 assertEquals(1, names.size()); 336 assertEquals("blueSquare", names.get(0)); 337 assertEquals(1, sharedElements.size()); 338 assertEquals(startBlue, sharedElements.get("blueSquare")); 339 sharedElements.put("blueSquare", startGreen); 340 } 341 }; 342 fragment1.setExitSharedElementCallback(mapOut); 343 344 mFragmentManager.beginTransaction() 345 .addSharedElement(startBlue, "blueSquare") 346 .replace(R.id.fragmentContainer, fragment2) 347 .setReorderingAllowed(mReorderingAllowed) 348 .addToBackStack(null) 349 .commit(); 350 FragmentTestUtil.waitForExecution(mActivityRule); 351 352 fragment1.waitForTransition(); 353 fragment2.waitForTransition(); 354 355 final View endBlue = findBlue(); 356 final Rect endBlueBounds = getBoundsOnScreen(endBlue); 357 358 verifyAndClearTransition(fragment2.sharedElementEnter, startGreenBounds, startGreen, 359 endBlue); 360 361 SharedElementCallback mapBack = new SharedElementCallback() { 362 @Override 363 public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { 364 assertEquals(1, names.size()); 365 assertEquals("blueSquare", names.get(0)); 366 assertEquals(1, sharedElements.size()); 367 final View expectedBlue = findViewById(fragment1, R.id.blueSquare); 368 assertEquals(expectedBlue, sharedElements.get("blueSquare")); 369 final View greenSquare = findViewById(fragment1, R.id.greenSquare); 370 sharedElements.put("blueSquare", greenSquare); 371 } 372 }; 373 fragment1.setExitSharedElementCallback(mapBack); 374 375 FragmentTestUtil.popBackStackImmediate(mActivityRule); 376 377 fragment1.waitForTransition(); 378 fragment2.waitForTransition(); 379 380 final View reenterGreen = findGreen(); 381 verifyAndClearTransition(fragment2.sharedElementReturn, endBlueBounds, endBlue, 382 reenterGreen); 383 } 384 385 // Make sure that onMapSharedElement works to change the shared element target 386 @Test 387 public void onMapSharedElementIn() throws Throwable { 388 TransitionFragment fragment1 = setupInitialFragment(); 389 390 // Now do a transition to scene2 391 final TransitionFragment fragment2 = new TransitionFragment(); 392 fragment2.setLayoutId(R.layout.scene2); 393 394 final View startBlue = findBlue(); 395 final Rect startBlueBounds = getBoundsOnScreen(startBlue); 396 397 SharedElementCallback mapIn = new SharedElementCallback() { 398 @Override 399 public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { 400 assertEquals(1, names.size()); 401 assertEquals("blueSquare", names.get(0)); 402 assertEquals(1, sharedElements.size()); 403 final View blueSquare = findViewById(fragment2, R.id.blueSquare); 404 assertEquals(blueSquare, sharedElements.get("blueSquare")); 405 final View greenSquare = findViewById(fragment2, R.id.greenSquare); 406 sharedElements.put("blueSquare", greenSquare); 407 } 408 }; 409 fragment2.setEnterSharedElementCallback(mapIn); 410 411 mFragmentManager.beginTransaction() 412 .addSharedElement(startBlue, "blueSquare") 413 .replace(R.id.fragmentContainer, fragment2) 414 .setReorderingAllowed(mReorderingAllowed) 415 .addToBackStack(null) 416 .commit(); 417 FragmentTestUtil.waitForExecution(mActivityRule); 418 419 fragment1.waitForTransition(); 420 fragment2.waitForTransition(); 421 422 final View endGreen = findGreen(); 423 final View endBlue = findBlue(); 424 final Rect endGreenBounds = getBoundsOnScreen(endGreen); 425 426 verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue, 427 endGreen); 428 429 SharedElementCallback mapBack = new SharedElementCallback() { 430 @Override 431 public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { 432 assertEquals(1, names.size()); 433 assertEquals("blueSquare", names.get(0)); 434 assertEquals(1, sharedElements.size()); 435 assertEquals(endBlue, sharedElements.get("blueSquare")); 436 sharedElements.put("blueSquare", endGreen); 437 } 438 }; 439 fragment2.setEnterSharedElementCallback(mapBack); 440 441 FragmentTestUtil.popBackStackImmediate(mActivityRule); 442 443 fragment1.waitForTransition(); 444 fragment2.waitForTransition(); 445 446 final View reenterBlue = findBlue(); 447 verifyAndClearTransition(fragment2.sharedElementReturn, endGreenBounds, endGreen, 448 reenterBlue); 449 } 450 451 // Ensure that shared element transitions that have targets properly target the views 452 @Test 453 public void complexSharedElementTransition() throws Throwable { 454 TransitionFragment fragment1 = setupInitialFragment(); 455 456 // Now do a transition to scene2 457 ComplexTransitionFragment fragment2 = new ComplexTransitionFragment(); 458 fragment2.setLayoutId(R.layout.scene2); 459 460 final View startBlue = findBlue(); 461 final View startGreen = findGreen(); 462 final Rect startBlueBounds = getBoundsOnScreen(startBlue); 463 464 mFragmentManager.beginTransaction() 465 .addSharedElement(startBlue, "blueSquare") 466 .addSharedElement(startGreen, "greenSquare") 467 .replace(R.id.fragmentContainer, fragment2) 468 .addToBackStack(null) 469 .setReorderingAllowed(true) 470 .commit(); 471 FragmentTestUtil.waitForExecution(mActivityRule); 472 assertEquals(2, mOnBackStackChangedTimes); 473 474 fragment1.waitForTransition(); 475 fragment2.waitForTransition(); 476 477 final View endBlue = findBlue(); 478 final View endGreen = findGreen(); 479 final Rect endBlueBounds = getBoundsOnScreen(endBlue); 480 481 verifyAndClearTransition(fragment2.sharedElementEnterTransition1, startBlueBounds, 482 startBlue, endBlue); 483 verifyAndClearTransition(fragment2.sharedElementEnterTransition2, startBlueBounds, 484 startGreen, endGreen); 485 486 // Now see if it works when popped 487 FragmentTestUtil.popBackStackImmediate(mActivityRule); 488 assertEquals(3, mOnBackStackChangedTimes); 489 490 fragment1.waitForTransition(); 491 fragment2.waitForTransition(); 492 493 final View reenterBlue = findBlue(); 494 final View reenterGreen = findGreen(); 495 496 verifyAndClearTransition(fragment2.sharedElementReturnTransition1, endBlueBounds, 497 endBlue, reenterBlue); 498 verifyAndClearTransition(fragment2.sharedElementReturnTransition2, endBlueBounds, 499 endGreen, reenterGreen); 500 } 501 502 // Ensure that after transitions have executed that they don't have any targets or other 503 // unfortunate modifications. 504 @Test 505 public void transitionsEndUnchanged() throws Throwable { 506 TransitionFragment fragment1 = setupInitialFragment(); 507 508 // Now do a transition to scene2 509 TransitionFragment fragment2 = new TransitionFragment(); 510 fragment2.setLayoutId(R.layout.scene2); 511 512 verifyTransition(fragment1, fragment2, "blueSquare"); 513 assertEquals(0, fragment1.exitTransition.getTargets().size()); 514 assertEquals(0, fragment2.sharedElementEnter.getTargets().size()); 515 assertEquals(0, fragment2.enterTransition.getTargets().size()); 516 assertNull(fragment1.exitTransition.getEpicenterCallback()); 517 assertNull(fragment2.enterTransition.getEpicenterCallback()); 518 assertNull(fragment2.sharedElementEnter.getEpicenterCallback()); 519 520 // Now pop the back stack 521 verifyPopTransition(1, fragment2, fragment1); 522 523 assertEquals(0, fragment2.returnTransition.getTargets().size()); 524 assertEquals(0, fragment2.sharedElementReturn.getTargets().size()); 525 assertEquals(0, fragment1.reenterTransition.getTargets().size()); 526 assertNull(fragment2.returnTransition.getEpicenterCallback()); 527 assertNull(fragment2.sharedElementReturn.getEpicenterCallback()); 528 assertNull(fragment2.reenterTransition.getEpicenterCallback()); 529 } 530 531 // Ensure that transitions are done when a fragment is shown and hidden 532 @Test 533 public void showHideTransition() throws Throwable { 534 TransitionFragment fragment1 = setupInitialFragment(); 535 TransitionFragment fragment2 = new TransitionFragment(); 536 fragment2.setLayoutId(R.layout.scene2); 537 538 final View startBlue = findBlue(); 539 final View startGreen = findGreen(); 540 541 mFragmentManager.beginTransaction() 542 .setReorderingAllowed(mReorderingAllowed) 543 .add(R.id.fragmentContainer, fragment2) 544 .hide(fragment1) 545 .addToBackStack(null) 546 .commit(); 547 548 FragmentTestUtil.waitForExecution(mActivityRule); 549 fragment1.waitForTransition(); 550 fragment2.waitForTransition(); 551 552 final View endGreen = findViewById(fragment2, R.id.greenSquare); 553 final View endBlue = findViewById(fragment2, R.id.blueSquare); 554 555 assertEquals(View.GONE, fragment1.getView().getVisibility()); 556 assertEquals(View.VISIBLE, startGreen.getVisibility()); 557 assertEquals(View.VISIBLE, startBlue.getVisibility()); 558 559 verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue); 560 verifyNoOtherTransitions(fragment1); 561 562 verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue); 563 verifyNoOtherTransitions(fragment2); 564 565 FragmentTestUtil.popBackStackImmediate(mActivityRule); 566 567 FragmentTestUtil.waitForExecution(mActivityRule); 568 fragment1.waitForTransition(); 569 fragment2.waitForTransition(); 570 571 verifyAndClearTransition(fragment1.reenterTransition, null, startGreen, startBlue); 572 verifyNoOtherTransitions(fragment1); 573 574 assertEquals(View.VISIBLE, fragment1.getView().getVisibility()); 575 assertEquals(View.VISIBLE, startGreen.getVisibility()); 576 assertEquals(View.VISIBLE, startBlue.getVisibility()); 577 578 verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue); 579 verifyNoOtherTransitions(fragment2); 580 } 581 582 // Ensure that transitions are done when a fragment is attached and detached 583 @Test 584 public void attachDetachTransition() throws Throwable { 585 TransitionFragment fragment1 = setupInitialFragment(); 586 TransitionFragment fragment2 = new TransitionFragment(); 587 fragment2.setLayoutId(R.layout.scene2); 588 589 final View startBlue = findBlue(); 590 final View startGreen = findGreen(); 591 592 mFragmentManager.beginTransaction() 593 .setReorderingAllowed(mReorderingAllowed) 594 .add(R.id.fragmentContainer, fragment2) 595 .detach(fragment1) 596 .addToBackStack(null) 597 .commit(); 598 599 FragmentTestUtil.waitForExecution(mActivityRule); 600 601 final View endGreen = findViewById(fragment2, R.id.greenSquare); 602 final View endBlue = findViewById(fragment2, R.id.blueSquare); 603 604 verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue); 605 verifyNoOtherTransitions(fragment1); 606 607 verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue); 608 verifyNoOtherTransitions(fragment2); 609 610 FragmentTestUtil.popBackStackImmediate(mActivityRule); 611 612 FragmentTestUtil.waitForExecution(mActivityRule); 613 614 final View reenterBlue = findBlue(); 615 final View reenterGreen = findGreen(); 616 617 verifyAndClearTransition(fragment1.reenterTransition, null, reenterGreen, reenterBlue); 618 verifyNoOtherTransitions(fragment1); 619 620 verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue); 621 verifyNoOtherTransitions(fragment2); 622 } 623 624 // Ensure that shared element without matching transition name doesn't error out 625 @Test 626 public void sharedElementMismatch() throws Throwable { 627 final TransitionFragment fragment1 = setupInitialFragment(); 628 629 // Now do a transition to scene2 630 TransitionFragment fragment2 = new TransitionFragment(); 631 fragment2.setLayoutId(R.layout.scene2); 632 633 final View startBlue = findBlue(); 634 final View startGreen = findGreen(); 635 final Rect startBlueBounds = getBoundsOnScreen(startBlue); 636 637 mFragmentManager.beginTransaction() 638 .addSharedElement(startBlue, "fooSquare") 639 .replace(R.id.fragmentContainer, fragment2) 640 .setReorderingAllowed(mReorderingAllowed) 641 .addToBackStack(null) 642 .commit(); 643 FragmentTestUtil.waitForExecution(mActivityRule); 644 645 fragment1.waitForTransition(); 646 fragment2.waitForTransition(); 647 648 final View endBlue = findBlue(); 649 final View endGreen = findGreen(); 650 651 if (mReorderingAllowed) { 652 verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue); 653 } else { 654 verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen); 655 verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue); 656 } 657 verifyNoOtherTransitions(fragment1); 658 659 verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue); 660 verifyNoOtherTransitions(fragment2); 661 } 662 663 // Ensure that using the same source or target shared element results in an exception. 664 @Test 665 public void sharedDuplicateTargetNames() throws Throwable { 666 setupInitialFragment(); 667 668 final View startBlue = findBlue(); 669 final View startGreen = findGreen(); 670 671 FragmentTransaction ft = mFragmentManager.beginTransaction(); 672 ft.addSharedElement(startBlue, "blueSquare"); 673 try { 674 ft.addSharedElement(startGreen, "blueSquare"); 675 fail("Expected IllegalArgumentException"); 676 } catch (IllegalArgumentException e) { 677 // expected 678 } 679 680 try { 681 ft.addSharedElement(startBlue, "greenSquare"); 682 fail("Expected IllegalArgumentException"); 683 } catch (IllegalArgumentException e) { 684 // expected 685 } 686 } 687 688 // Test that invisible fragment views don't participate in transitions 689 @Test 690 public void invisibleNoTransitions() throws Throwable { 691 if (!mReorderingAllowed) { 692 return; // only reordered transitions can avoid interaction 693 } 694 // enter transition 695 TransitionFragment fragment = new InvisibleFragment(); 696 fragment.setLayoutId(R.layout.scene1); 697 mFragmentManager.beginTransaction() 698 .setReorderingAllowed(mReorderingAllowed) 699 .add(R.id.fragmentContainer, fragment) 700 .addToBackStack(null) 701 .commit(); 702 FragmentTestUtil.waitForExecution(mActivityRule); 703 fragment.waitForNoTransition(); 704 verifyNoOtherTransitions(fragment); 705 706 // exit transition 707 mFragmentManager.beginTransaction() 708 .setReorderingAllowed(mReorderingAllowed) 709 .remove(fragment) 710 .addToBackStack(null) 711 .commit(); 712 713 fragment.waitForNoTransition(); 714 verifyNoOtherTransitions(fragment); 715 716 // reenter transition 717 FragmentTestUtil.popBackStackImmediate(mActivityRule); 718 fragment.waitForNoTransition(); 719 verifyNoOtherTransitions(fragment); 720 721 // return transition 722 FragmentTestUtil.popBackStackImmediate(mActivityRule); 723 fragment.waitForNoTransition(); 724 verifyNoOtherTransitions(fragment); 725 } 726 727 // No crash when transitioning a shared element and there is no shared element transition. 728 @Test 729 public void noSharedElementTransition() throws Throwable { 730 TransitionFragment fragment1 = setupInitialFragment(); 731 732 final View startBlue = findBlue(); 733 final View startGreen = findGreen(); 734 final Rect startBlueBounds = getBoundsOnScreen(startBlue); 735 736 TransitionFragment fragment2 = new TransitionFragment(); 737 fragment2.setLayoutId(R.layout.scene2); 738 739 mFragmentManager.beginTransaction() 740 .setReorderingAllowed(mReorderingAllowed) 741 .addSharedElement(startBlue, "blueSquare") 742 .replace(R.id.fragmentContainer, fragment2) 743 .addToBackStack(null) 744 .commit(); 745 746 fragment1.waitForTransition(); 747 fragment2.waitForTransition(); 748 final View midGreen = findGreen(); 749 final View midBlue = findBlue(); 750 final Rect midBlueBounds = getBoundsOnScreen(midBlue); 751 verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen); 752 verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue, midBlue); 753 verifyAndClearTransition(fragment2.enterTransition, midBlueBounds, midGreen); 754 verifyNoOtherTransitions(fragment1); 755 verifyNoOtherTransitions(fragment2); 756 757 final TransitionFragment fragment3 = new TransitionFragment(); 758 fragment3.setLayoutId(R.layout.scene3); 759 760 mActivityRule.runOnUiThread(new Runnable() { 761 @Override 762 public void run() { 763 FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager(); 764 fm.popBackStack(); 765 fm.beginTransaction() 766 .setReorderingAllowed(mReorderingAllowed) 767 .replace(R.id.fragmentContainer, fragment3) 768 .addToBackStack(null) 769 .commit(); 770 } 771 }); 772 773 // This shouldn't give an error. 774 FragmentTestUtil.executePendingTransactions(mActivityRule); 775 776 fragment2.waitForTransition(); 777 // It does not transition properly for ordered transactions, though. 778 if (mReorderingAllowed) { 779 verifyAndClearTransition(fragment2.returnTransition, null, midGreen, midBlue); 780 final View endGreen = findGreen(); 781 final View endBlue = findBlue(); 782 final View endRed = findRed(); 783 verifyAndClearTransition(fragment3.enterTransition, null, endGreen, endBlue, endRed); 784 verifyNoOtherTransitions(fragment2); 785 verifyNoOtherTransitions(fragment3); 786 } else { 787 // fragment3 doesn't get a transition since it conflicts with the pop transition 788 verifyNoOtherTransitions(fragment3); 789 // Everything else is just doing its best. Ordered transactions can't handle 790 // multiple transitions acting together except for popping multiple together. 791 } 792 } 793 794 // When there is no matching shared element, the transition name should not be changed 795 @Test 796 public void noMatchingSharedElementRetainName() throws Throwable { 797 TransitionFragment fragment1 = setupInitialFragment(); 798 799 final View startBlue = findBlue(); 800 final View startGreen = findGreen(); 801 final Rect startGreenBounds = getBoundsOnScreen(startGreen); 802 803 TransitionFragment fragment2 = new TransitionFragment(); 804 fragment2.setLayoutId(R.layout.scene3); 805 806 mFragmentManager.beginTransaction() 807 .setReorderingAllowed(mReorderingAllowed) 808 .addSharedElement(startGreen, "greenSquare") 809 .addSharedElement(startBlue, "blueSquare") 810 .replace(R.id.fragmentContainer, fragment2) 811 .addToBackStack(null) 812 .commit(); 813 814 fragment2.waitForTransition(); 815 final View midGreen = findGreen(); 816 final View midBlue = findBlue(); 817 final View midRed = findRed(); 818 final Rect midGreenBounds = getBoundsOnScreen(midGreen); 819 if (mReorderingAllowed) { 820 verifyAndClearTransition(fragment2.sharedElementEnter, startGreenBounds, startGreen, 821 midGreen); 822 } else { 823 verifyAndClearTransition(fragment2.sharedElementEnter, startGreenBounds, startGreen, 824 midGreen, startBlue); 825 } 826 verifyAndClearTransition(fragment2.enterTransition, midGreenBounds, midBlue, midRed); 827 verifyNoOtherTransitions(fragment2); 828 829 FragmentTestUtil.popBackStackImmediate(mActivityRule); 830 fragment2.waitForTransition(); 831 fragment1.waitForTransition(); 832 833 final View endBlue = findBlue(); 834 final View endGreen = findGreen(); 835 836 assertEquals("blueSquare", endBlue.getTransitionName()); 837 assertEquals("greenSquare", endGreen.getTransitionName()); 838 } 839 840 private TransitionFragment setupInitialFragment() throws Throwable { 841 TransitionFragment fragment1 = new TransitionFragment(); 842 fragment1.setLayoutId(R.layout.scene1); 843 mFragmentManager.beginTransaction() 844 .setReorderingAllowed(mReorderingAllowed) 845 .add(R.id.fragmentContainer, fragment1) 846 .addToBackStack(null) 847 .commit(); 848 FragmentTestUtil.waitForExecution(mActivityRule); 849 assertEquals(1, mOnBackStackChangedTimes); 850 fragment1.waitForTransition(); 851 final View blueSquare1 = findBlue(); 852 final View greenSquare1 = findGreen(); 853 verifyAndClearTransition(fragment1.enterTransition, null, blueSquare1, greenSquare1); 854 verifyNoOtherTransitions(fragment1); 855 return fragment1; 856 } 857 858 private View findViewById(Fragment fragment, int id) { 859 return fragment.getView().findViewById(id); 860 } 861 862 private View findGreen() { 863 return mActivityRule.getActivity().findViewById(R.id.greenSquare); 864 } 865 866 private View findBlue() { 867 return mActivityRule.getActivity().findViewById(R.id.blueSquare); 868 } 869 870 private View findRed() { 871 return mActivityRule.getActivity().findViewById(R.id.redSquare); 872 } 873 874 private void verifyAndClearTransition(TargetTracking transition, Rect epicenter, 875 View... expected) { 876 if (epicenter == null) { 877 assertNull(transition.getCapturedEpicenter()); 878 } else { 879 assertEquals(epicenter, transition.getCapturedEpicenter()); 880 } 881 ArrayList<View> targets = transition.getTrackedTargets(); 882 StringBuilder sb = new StringBuilder(); 883 sb 884 .append("Expected: [") 885 .append(expected.length) 886 .append("] {"); 887 boolean isFirst = true; 888 for (View view : expected) { 889 if (isFirst) { 890 isFirst = false; 891 } else { 892 sb.append(", "); 893 } 894 sb.append(view); 895 } 896 sb 897 .append("}, but got: [") 898 .append(targets.size()) 899 .append("] {"); 900 isFirst = true; 901 for (View view : targets) { 902 if (isFirst) { 903 isFirst = false; 904 } else { 905 sb.append(", "); 906 } 907 sb.append(view); 908 } 909 sb.append("}"); 910 String errorMessage = sb.toString(); 911 912 assertEquals(errorMessage, expected.length, targets.size()); 913 for (View view : expected) { 914 assertTrue(errorMessage, targets.contains(view)); 915 } 916 transition.clearTargets(); 917 } 918 919 private void verifyNoOtherTransitions(TransitionFragment fragment) { 920 assertEquals(0, fragment.enterTransition.targets.size()); 921 assertEquals(0, fragment.exitTransition.targets.size()); 922 assertEquals(0, fragment.reenterTransition.targets.size()); 923 assertEquals(0, fragment.returnTransition.targets.size()); 924 assertEquals(0, fragment.sharedElementEnter.targets.size()); 925 assertEquals(0, fragment.sharedElementReturn.targets.size()); 926 } 927 928 private void verifyTransition(TransitionFragment from, TransitionFragment to, 929 String sharedElementName) throws Throwable { 930 final int startOnBackStackChanged = mOnBackStackChangedTimes; 931 final View startBlue = findBlue(); 932 final View startGreen = findGreen(); 933 final View startRed = findRed(); 934 935 final Rect startBlueRect = getBoundsOnScreen(startBlue); 936 937 mFragmentManager.beginTransaction() 938 .setReorderingAllowed(mReorderingAllowed) 939 .addSharedElement(startBlue, sharedElementName) 940 .replace(R.id.fragmentContainer, to) 941 .addToBackStack(null) 942 .commit(); 943 944 FragmentTestUtil.waitForExecution(mActivityRule); 945 assertEquals(startOnBackStackChanged + 1, mOnBackStackChangedTimes); 946 947 to.waitForTransition(); 948 final View endGreen = findGreen(); 949 final View endBlue = findBlue(); 950 final View endRed = findRed(); 951 final Rect endBlueRect = getBoundsOnScreen(endBlue); 952 953 if (startRed != null) { 954 verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen, startRed); 955 } else { 956 verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen); 957 } 958 verifyNoOtherTransitions(from); 959 960 if (endRed != null) { 961 verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen, endRed); 962 } else { 963 verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen); 964 } 965 verifyAndClearTransition(to.sharedElementEnter, startBlueRect, startBlue, endBlue); 966 verifyNoOtherTransitions(to); 967 } 968 969 private void verifyCrossTransition(boolean swapSource, 970 TransitionFragment from1, TransitionFragment from2) throws Throwable { 971 final int startNumOnBackStackChanged = mOnBackStackChangedTimes; 972 final int changesPerOperation = mReorderingAllowed ? 1 : 2; 973 974 final TransitionFragment to1 = new TransitionFragment(); 975 to1.setLayoutId(R.layout.scene2); 976 final TransitionFragment to2 = new TransitionFragment(); 977 to2.setLayoutId(R.layout.scene2); 978 979 final View fromExit1 = findViewById(from1, R.id.greenSquare); 980 final View fromShared1 = findViewById(from1, R.id.blueSquare); 981 final Rect fromSharedRect1 = getBoundsOnScreen(fromShared1); 982 983 final int fromExitId2 = swapSource ? R.id.blueSquare : R.id.greenSquare; 984 final int fromSharedId2 = swapSource ? R.id.greenSquare : R.id.blueSquare; 985 final View fromExit2 = findViewById(from2, fromExitId2); 986 final View fromShared2 = findViewById(from2, fromSharedId2); 987 final Rect fromSharedRect2 = getBoundsOnScreen(fromShared2); 988 989 final String sharedElementName = swapSource ? "blueSquare" : "greenSquare"; 990 991 mActivityRule.runOnUiThread(new Runnable() { 992 @Override 993 public void run() { 994 mFragmentManager.beginTransaction() 995 .setReorderingAllowed(mReorderingAllowed) 996 .addSharedElement(fromShared1, "blueSquare") 997 .replace(R.id.fragmentContainer1, to1) 998 .addToBackStack(null) 999 .commit(); 1000 mFragmentManager.beginTransaction() 1001 .setReorderingAllowed(mReorderingAllowed) 1002 .addSharedElement(fromShared2, sharedElementName) 1003 .replace(R.id.fragmentContainer2, to2) 1004 .addToBackStack(null) 1005 .commit(); 1006 } 1007 }); 1008 FragmentTestUtil.waitForExecution(mActivityRule); 1009 assertEquals(startNumOnBackStackChanged + changesPerOperation, mOnBackStackChangedTimes); 1010 1011 from1.waitForTransition(); 1012 from2.waitForTransition(); 1013 to1.waitForTransition(); 1014 to2.waitForTransition(); 1015 1016 final View toEnter1 = findViewById(to1, R.id.greenSquare); 1017 final View toShared1 = findViewById(to1, R.id.blueSquare); 1018 final Rect toSharedRect1 = getBoundsOnScreen(toShared1); 1019 1020 final View toEnter2 = findViewById(to2, fromSharedId2); 1021 final View toShared2 = findViewById(to2, fromExitId2); 1022 final Rect toSharedRect2 = getBoundsOnScreen(toShared2); 1023 1024 verifyAndClearTransition(from1.exitTransition, fromSharedRect1, fromExit1); 1025 verifyAndClearTransition(from2.exitTransition, fromSharedRect2, fromExit2); 1026 verifyNoOtherTransitions(from1); 1027 verifyNoOtherTransitions(from2); 1028 1029 verifyAndClearTransition(to1.enterTransition, toSharedRect1, toEnter1); 1030 verifyAndClearTransition(to2.enterTransition, toSharedRect2, toEnter2); 1031 verifyAndClearTransition(to1.sharedElementEnter, fromSharedRect1, fromShared1, toShared1); 1032 verifyAndClearTransition(to2.sharedElementEnter, fromSharedRect2, fromShared2, toShared2); 1033 verifyNoOtherTransitions(to1); 1034 verifyNoOtherTransitions(to2); 1035 1036 // Now pop it back 1037 mActivityRule.runOnUiThread(new Runnable() { 1038 @Override 1039 public void run() { 1040 mFragmentManager.popBackStack(); 1041 mFragmentManager.popBackStack(); 1042 } 1043 }); 1044 FragmentTestUtil.waitForExecution(mActivityRule); 1045 assertEquals(startNumOnBackStackChanged + changesPerOperation + 1, 1046 mOnBackStackChangedTimes); 1047 1048 from1.waitForTransition(); 1049 from2.waitForTransition(); 1050 to1.waitForTransition(); 1051 to2.waitForTransition(); 1052 1053 final View returnEnter1 = findViewById(from1, R.id.greenSquare); 1054 final View returnShared1 = findViewById(from1, R.id.blueSquare); 1055 1056 final View returnEnter2 = findViewById(from2, fromExitId2); 1057 final View returnShared2 = findViewById(from2, fromSharedId2); 1058 1059 verifyAndClearTransition(to1.returnTransition, toSharedRect1, toEnter1); 1060 verifyAndClearTransition(to2.returnTransition, toSharedRect2, toEnter2); 1061 verifyAndClearTransition(to1.sharedElementReturn, toSharedRect1, toShared1, returnShared1); 1062 verifyAndClearTransition(to2.sharedElementReturn, toSharedRect2, toShared2, returnShared2); 1063 verifyNoOtherTransitions(to1); 1064 verifyNoOtherTransitions(to2); 1065 1066 verifyAndClearTransition(from1.reenterTransition, fromSharedRect1, returnEnter1); 1067 verifyAndClearTransition(from2.reenterTransition, fromSharedRect2, returnEnter2); 1068 verifyNoOtherTransitions(from1); 1069 verifyNoOtherTransitions(from2); 1070 } 1071 1072 private void verifyPopTransition(final int numPops, TransitionFragment from, 1073 TransitionFragment to, TransitionFragment... others) throws Throwable { 1074 final int startOnBackStackChanged = mOnBackStackChangedTimes; 1075 final View startBlue = findBlue(); 1076 final View startGreen = findGreen(); 1077 final View startRed = findRed(); 1078 final Rect startSharedRect = getBoundsOnScreen(startBlue); 1079 1080 mInstrumentation.runOnMainSync(new Runnable() { 1081 @Override 1082 public void run() { 1083 for (int i = 0; i < numPops; i++) { 1084 mFragmentManager.popBackStack(); 1085 } 1086 } 1087 }); 1088 FragmentTestUtil.waitForExecution(mActivityRule); 1089 assertEquals(startOnBackStackChanged + 1, mOnBackStackChangedTimes); 1090 1091 to.waitForTransition(); 1092 final View endGreen = findGreen(); 1093 final View endBlue = findBlue(); 1094 final View endRed = findRed(); 1095 final Rect endSharedRect = getBoundsOnScreen(endBlue); 1096 1097 if (startRed != null) { 1098 verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen, startRed); 1099 } else { 1100 verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen); 1101 } 1102 verifyAndClearTransition(from.sharedElementReturn, startSharedRect, startBlue, endBlue); 1103 verifyNoOtherTransitions(from); 1104 1105 if (endRed != null) { 1106 verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen, endRed); 1107 } else { 1108 verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen); 1109 } 1110 verifyNoOtherTransitions(to); 1111 1112 if (others != null) { 1113 for (TransitionFragment fragment : others) { 1114 verifyNoOtherTransitions(fragment); 1115 } 1116 } 1117 } 1118 1119 private static Rect getBoundsOnScreen(View view) { 1120 final int[] loc = new int[2]; 1121 view.getLocationOnScreen(loc); 1122 return new Rect(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight()); 1123 } 1124 1125 public static class ComplexTransitionFragment extends TransitionFragment { 1126 public final TrackingTransition sharedElementEnterTransition1 = new TrackingTransition(); 1127 public final TrackingTransition sharedElementEnterTransition2 = new TrackingTransition(); 1128 public final TrackingTransition sharedElementReturnTransition1 = new TrackingTransition(); 1129 public final TrackingTransition sharedElementReturnTransition2 = new TrackingTransition(); 1130 1131 public final TransitionSet sharedElementEnterTransition = new TransitionSet() 1132 .addTransition(sharedElementEnterTransition1) 1133 .addTransition(sharedElementEnterTransition2); 1134 public final TransitionSet sharedElementReturnTransition = new TransitionSet() 1135 .addTransition(sharedElementReturnTransition1) 1136 .addTransition(sharedElementReturnTransition2); 1137 1138 public ComplexTransitionFragment() { 1139 sharedElementEnterTransition1.addTarget(R.id.blueSquare); 1140 sharedElementEnterTransition2.addTarget(R.id.greenSquare); 1141 sharedElementReturnTransition1.addTarget(R.id.blueSquare); 1142 sharedElementReturnTransition2.addTarget(R.id.greenSquare); 1143 setSharedElementEnterTransition(sharedElementEnterTransition); 1144 setSharedElementReturnTransition(sharedElementReturnTransition); 1145 } 1146 } 1147 1148 public static class InvisibleFragment extends TransitionFragment { 1149 @Override 1150 public void onViewCreated(View view, Bundle savedInstanceState) { 1151 view.setVisibility(View.INVISIBLE); 1152 super.onViewCreated(view, savedInstanceState); 1153 } 1154 } 1155} 1156