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