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 */ 16package android.support.v4.app; 17 18import android.graphics.Rect; 19import android.os.Build; 20import android.support.annotation.RequiresApi; 21import android.support.v4.util.ArrayMap; 22import android.support.v4.view.ViewCompat; 23import android.util.SparseArray; 24import android.view.View; 25import android.view.ViewGroup; 26 27import java.util.ArrayList; 28import java.util.Collection; 29import java.util.List; 30import java.util.Map; 31 32/** 33 * Contains the Fragment Transition functionality for both ordered and reordered 34 * Fragment Transactions. With reordered fragment transactions, all Views have been 35 * added to the View hierarchy prior to calling startTransitions. With ordered 36 * fragment transactions, Views will be removed and added after calling startTransitions. 37 */ 38class FragmentTransition { 39 /** 40 * The inverse of all BackStackRecord operation commands. This assumes that 41 * REPLACE operations have already been replaced by add/remove operations. 42 */ 43 private static final int[] INVERSE_OPS = { 44 BackStackRecord.OP_NULL, // inverse of OP_NULL (error) 45 BackStackRecord.OP_REMOVE, // inverse of OP_ADD 46 BackStackRecord.OP_NULL, // inverse of OP_REPLACE (error) 47 BackStackRecord.OP_ADD, // inverse of OP_REMOVE 48 BackStackRecord.OP_SHOW, // inverse of OP_HIDE 49 BackStackRecord.OP_HIDE, // inverse of OP_SHOW 50 BackStackRecord.OP_ATTACH, // inverse of OP_DETACH 51 BackStackRecord.OP_DETACH, // inverse of OP_ATTACH 52 BackStackRecord.OP_UNSET_PRIMARY_NAV, // inverse of OP_SET_PRIMARY_NAV 53 BackStackRecord.OP_SET_PRIMARY_NAV, // inverse of OP_UNSET_PRIMARY_NAV 54 }; 55 56 /** 57 * The main entry point for Fragment Transitions, this starts the transitions 58 * set on the leaving Fragment's {@link Fragment#getExitTransition()}, the 59 * entering Fragment's {@link Fragment#getEnterTransition()} and 60 * {@link Fragment#getSharedElementEnterTransition()}. When popping, 61 * the leaving Fragment's {@link Fragment#getReturnTransition()} and 62 * {@link Fragment#getSharedElementReturnTransition()} and the entering 63 * {@link Fragment#getReenterTransition()} will be run. 64 * <p> 65 * With reordered Fragment Transitions, all Views have been added to the 66 * View hierarchy prior to calling this method. The incoming Fragment's Views 67 * will be INVISIBLE. With ordered Fragment Transitions, this method 68 * is called before any change has been made to the hierarchy. That means 69 * that the added Fragments have not created their Views yet and the hierarchy 70 * is unknown. 71 * 72 * @param fragmentManager The executing FragmentManagerImpl 73 * @param records The list of transactions being executed. 74 * @param isRecordPop For each transaction, whether it is a pop transaction or not. 75 * @param startIndex The first index into records and isRecordPop to execute as 76 * part of this transition. 77 * @param endIndex One past the last index into records and isRecordPop to execute 78 * as part of this transition. 79 * @param isReordered true if this is a reordered transaction, meaning that the 80 * Views of incoming fragments have been added. false if the 81 * transaction has yet to be run and Views haven't been created. 82 */ 83 static void startTransitions(FragmentManagerImpl fragmentManager, 84 ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, 85 int startIndex, int endIndex, boolean isReordered) { 86 if (fragmentManager.mCurState < Fragment.CREATED) { 87 return; 88 } 89 90 if (Build.VERSION.SDK_INT >= 21) { 91 SparseArray<FragmentContainerTransition> transitioningFragments = 92 new SparseArray<>(); 93 for (int i = startIndex; i < endIndex; i++) { 94 final BackStackRecord record = records.get(i); 95 final boolean isPop = isRecordPop.get(i); 96 if (isPop) { 97 calculatePopFragments(record, transitioningFragments, isReordered); 98 } else { 99 calculateFragments(record, transitioningFragments, isReordered); 100 } 101 } 102 103 if (transitioningFragments.size() != 0) { 104 final View nonExistentView = new View(fragmentManager.mHost.getContext()); 105 final int numContainers = transitioningFragments.size(); 106 for (int i = 0; i < numContainers; i++) { 107 int containerId = transitioningFragments.keyAt(i); 108 ArrayMap<String, String> nameOverrides = calculateNameOverrides(containerId, 109 records, isRecordPop, startIndex, endIndex); 110 111 FragmentContainerTransition containerTransition = 112 transitioningFragments.valueAt(i); 113 114 if (isReordered) { 115 configureTransitionsReordered(fragmentManager, containerId, 116 containerTransition, nonExistentView, nameOverrides); 117 } else { 118 configureTransitionsOrdered(fragmentManager, containerId, 119 containerTransition, nonExistentView, nameOverrides); 120 } 121 } 122 } 123 } 124 } 125 126 /** 127 * Iterates through the transactions that affect a given fragment container 128 * and tracks the shared element names across transactions. This is most useful 129 * in pop transactions where the names of shared elements are known. 130 * 131 * @param containerId The container ID that is executing the transition. 132 * @param records The list of transactions being executed. 133 * @param isRecordPop For each transaction, whether it is a pop transaction or not. 134 * @param startIndex The first index into records and isRecordPop to execute as 135 * part of this transition. 136 * @param endIndex One past the last index into records and isRecordPop to execute 137 * as part of this transition. 138 * @return A map from the initial shared element name to the final shared element name 139 * before any onMapSharedElements is run. 140 */ 141 private static ArrayMap<String, String> calculateNameOverrides(int containerId, 142 ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, 143 int startIndex, int endIndex) { 144 ArrayMap<String, String> nameOverrides = new ArrayMap<>(); 145 for (int recordNum = endIndex - 1; recordNum >= startIndex; recordNum--) { 146 final BackStackRecord record = records.get(recordNum); 147 if (!record.interactsWith(containerId)) { 148 continue; 149 } 150 final boolean isPop = isRecordPop.get(recordNum); 151 if (record.mSharedElementSourceNames != null) { 152 final int numSharedElements = record.mSharedElementSourceNames.size(); 153 final ArrayList<String> sources; 154 final ArrayList<String> targets; 155 if (isPop) { 156 targets = record.mSharedElementSourceNames; 157 sources = record.mSharedElementTargetNames; 158 } else { 159 sources = record.mSharedElementSourceNames; 160 targets = record.mSharedElementTargetNames; 161 } 162 for (int i = 0; i < numSharedElements; i++) { 163 String sourceName = sources.get(i); 164 String targetName = targets.get(i); 165 String previousTarget = nameOverrides.remove(targetName); 166 if (previousTarget != null) { 167 nameOverrides.put(sourceName, previousTarget); 168 } else { 169 nameOverrides.put(sourceName, targetName); 170 } 171 } 172 } 173 } 174 return nameOverrides; 175 } 176 177 /** 178 * Configures a transition for a single fragment container for which the transaction was 179 * reordered. That means that all Fragment Views have been added and incoming fragment 180 * Views are marked invisible. 181 * 182 * @param fragmentManager The executing FragmentManagerImpl 183 * @param containerId The container ID that is executing the transition. 184 * @param fragments A structure holding the transitioning fragments in this container. 185 * @param nonExistentView A View that does not exist in the hierarchy. This is used to 186 * prevent transitions from acting on other Views when there is no 187 * other target. 188 * @param nameOverrides A map of the shared element names from the starting fragment to 189 * the final fragment's Views as given in 190 * {@link FragmentTransaction#addSharedElement(View, String)}. 191 */ 192 @RequiresApi(21) 193 private static void configureTransitionsReordered(FragmentManagerImpl fragmentManager, 194 int containerId, FragmentContainerTransition fragments, 195 View nonExistentView, ArrayMap<String, String> nameOverrides) { 196 ViewGroup sceneRoot = null; 197 if (fragmentManager.mContainer.onHasView()) { 198 sceneRoot = (ViewGroup) fragmentManager.mContainer.onFindViewById(containerId); 199 } 200 if (sceneRoot == null) { 201 return; 202 } 203 final Fragment inFragment = fragments.lastIn; 204 final Fragment outFragment = fragments.firstOut; 205 final boolean inIsPop = fragments.lastInIsPop; 206 final boolean outIsPop = fragments.firstOutIsPop; 207 208 ArrayList<View> sharedElementsIn = new ArrayList<>(); 209 ArrayList<View> sharedElementsOut = new ArrayList<>(); 210 Object enterTransition = getEnterTransition(inFragment, inIsPop); 211 Object exitTransition = getExitTransition(outFragment, outIsPop); 212 213 Object sharedElementTransition = configureSharedElementsReordered(sceneRoot, 214 nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn, 215 enterTransition, exitTransition); 216 217 if (enterTransition == null && sharedElementTransition == null 218 && exitTransition == null) { 219 return; // no transitions! 220 } 221 222 ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition, 223 outFragment, sharedElementsOut, nonExistentView); 224 225 ArrayList<View> enteringViews = configureEnteringExitingViews(enterTransition, 226 inFragment, sharedElementsIn, nonExistentView); 227 228 setViewVisibility(enteringViews, View.INVISIBLE); 229 230 Object transition = mergeTransitions(enterTransition, exitTransition, 231 sharedElementTransition, inFragment, inIsPop); 232 233 if (transition != null) { 234 replaceHide(exitTransition, outFragment, exitingViews); 235 ArrayList<String> inNames = 236 FragmentTransitionCompat21.prepareSetNameOverridesReordered(sharedElementsIn); 237 FragmentTransitionCompat21.scheduleRemoveTargets(transition, 238 enterTransition, enteringViews, exitTransition, exitingViews, 239 sharedElementTransition, sharedElementsIn); 240 FragmentTransitionCompat21.beginDelayedTransition(sceneRoot, transition); 241 FragmentTransitionCompat21.setNameOverridesReordered(sceneRoot, sharedElementsOut, 242 sharedElementsIn, inNames, nameOverrides); 243 setViewVisibility(enteringViews, View.VISIBLE); 244 FragmentTransitionCompat21.swapSharedElementTargets(sharedElementTransition, 245 sharedElementsOut, sharedElementsIn); 246 } 247 } 248 249 /** 250 * Replace hide operations with visibility changes on the exiting views. Instead of making 251 * the entire fragment's view GONE, make each exiting view INVISIBLE. At the end of the 252 * transition, make the fragment's view GONE. 253 */ 254 @RequiresApi(21) 255 private static void replaceHide(Object exitTransition, Fragment exitingFragment, 256 final ArrayList<View> exitingViews) { 257 if (exitingFragment != null && exitTransition != null && exitingFragment.mAdded 258 && exitingFragment.mHidden && exitingFragment.mHiddenChanged) { 259 exitingFragment.setHideReplaced(true); 260 FragmentTransitionCompat21.scheduleHideFragmentView(exitTransition, 261 exitingFragment.getView(), exitingViews); 262 final ViewGroup container = exitingFragment.mContainer; 263 OneShotPreDrawListener.add(container, new Runnable() { 264 @Override 265 public void run() { 266 setViewVisibility(exitingViews, View.INVISIBLE); 267 } 268 }); 269 } 270 } 271 272 /** 273 * Configures a transition for a single fragment container for which the transaction was 274 * ordrered. That means that the transaction has not been executed yet, so incoming 275 * Views are not yet known. 276 * 277 * @param fragmentManager The executing FragmentManagerImpl 278 * @param containerId The container ID that is executing the transition. 279 * @param fragments A structure holding the transitioning fragments in this container. 280 * @param nonExistentView A View that does not exist in the hierarchy. This is used to 281 * prevent transitions from acting on other Views when there is no 282 * other target. 283 * @param nameOverrides A map of the shared element names from the starting fragment to 284 * the final fragment's Views as given in 285 * {@link FragmentTransaction#addSharedElement(View, String)}. 286 */ 287 @RequiresApi(21) 288 private static void configureTransitionsOrdered(FragmentManagerImpl fragmentManager, 289 int containerId, FragmentContainerTransition fragments, 290 View nonExistentView, ArrayMap<String, String> nameOverrides) { 291 ViewGroup sceneRoot = null; 292 if (fragmentManager.mContainer.onHasView()) { 293 sceneRoot = (ViewGroup) fragmentManager.mContainer.onFindViewById(containerId); 294 } 295 if (sceneRoot == null) { 296 return; 297 } 298 final Fragment inFragment = fragments.lastIn; 299 final Fragment outFragment = fragments.firstOut; 300 final boolean inIsPop = fragments.lastInIsPop; 301 final boolean outIsPop = fragments.firstOutIsPop; 302 303 Object enterTransition = getEnterTransition(inFragment, inIsPop); 304 Object exitTransition = getExitTransition(outFragment, outIsPop); 305 306 ArrayList<View> sharedElementsOut = new ArrayList<>(); 307 ArrayList<View> sharedElementsIn = new ArrayList<>(); 308 309 Object sharedElementTransition = configureSharedElementsOrdered(sceneRoot, 310 nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn, 311 enterTransition, exitTransition); 312 313 if (enterTransition == null && sharedElementTransition == null 314 && exitTransition == null) { 315 return; // no transitions! 316 } 317 318 ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition, 319 outFragment, sharedElementsOut, nonExistentView); 320 321 if (exitingViews == null || exitingViews.isEmpty()) { 322 exitTransition = null; 323 } 324 325 // Ensure the entering transition doesn't target anything until the views are made 326 // visible 327 FragmentTransitionCompat21.addTarget(enterTransition, nonExistentView); 328 329 Object transition = mergeTransitions(enterTransition, exitTransition, 330 sharedElementTransition, inFragment, fragments.lastInIsPop); 331 332 if (transition != null) { 333 final ArrayList<View> enteringViews = new ArrayList<>(); 334 FragmentTransitionCompat21.scheduleRemoveTargets(transition, 335 enterTransition, enteringViews, exitTransition, exitingViews, 336 sharedElementTransition, sharedElementsIn); 337 scheduleTargetChange(sceneRoot, inFragment, nonExistentView, sharedElementsIn, 338 enterTransition, enteringViews, exitTransition, exitingViews); 339 FragmentTransitionCompat21.setNameOverridesOrdered(sceneRoot, sharedElementsIn, 340 nameOverrides); 341 342 FragmentTransitionCompat21.beginDelayedTransition(sceneRoot, transition); 343 FragmentTransitionCompat21.scheduleNameReset(sceneRoot, sharedElementsIn, 344 nameOverrides); 345 } 346 } 347 348 /** 349 * This method is used for fragment transitions for ordrered transactions to change the 350 * enter and exit transition targets after the call to 351 * {@link FragmentTransitionCompat21#beginDelayedTransition(ViewGroup, Object)}. The exit 352 * transition must ensure that it does not target any Views and the enter transition must start 353 * targeting the Views of the incoming Fragment. 354 * 355 * @param sceneRoot The fragment container View 356 * @param inFragment The last fragment that is entering 357 * @param nonExistentView A view that does not exist in the hierarchy that is used as a 358 * transition target to ensure no View is targeted. 359 * @param sharedElementsIn The shared element Views of the incoming fragment 360 * @param enterTransition The enter transition of the incoming fragment 361 * @param enteringViews The entering Views of the incoming fragment 362 * @param exitTransition The exit transition of the outgoing fragment 363 * @param exitingViews The exiting views of the outgoing fragment 364 */ 365 @RequiresApi(21) 366 private static void scheduleTargetChange(final ViewGroup sceneRoot, 367 final Fragment inFragment, final View nonExistentView, 368 final ArrayList<View> sharedElementsIn, 369 final Object enterTransition, final ArrayList<View> enteringViews, 370 final Object exitTransition, final ArrayList<View> exitingViews) { 371 OneShotPreDrawListener.add(sceneRoot, new Runnable() { 372 @Override 373 public void run() { 374 if (enterTransition != null) { 375 FragmentTransitionCompat21.removeTarget(enterTransition, 376 nonExistentView); 377 ArrayList<View> views = configureEnteringExitingViews( 378 enterTransition, inFragment, sharedElementsIn, nonExistentView); 379 enteringViews.addAll(views); 380 } 381 382 if (exitingViews != null) { 383 if (exitTransition != null) { 384 ArrayList<View> tempExiting = new ArrayList<>(); 385 tempExiting.add(nonExistentView); 386 FragmentTransitionCompat21.replaceTargets(exitTransition, exitingViews, 387 tempExiting); 388 } 389 exitingViews.clear(); 390 exitingViews.add(nonExistentView); 391 } 392 } 393 }); 394 } 395 396 /** 397 * Returns a TransitionSet containing the shared element transition. The wrapping TransitionSet 398 * targets all shared elements to ensure that no other Views are targeted. The shared element 399 * transition can then target any or all shared elements without worrying about accidentally 400 * targeting entering or exiting Views. 401 * 402 * @param inFragment The incoming fragment 403 * @param outFragment the outgoing fragment 404 * @param isPop True if this is a pop transaction or false if it is a normal (add) transaction. 405 * @return A TransitionSet wrapping the shared element transition or null if no such transition 406 * exists. 407 */ 408 @RequiresApi(21) 409 private static Object getSharedElementTransition(Fragment inFragment, 410 Fragment outFragment, boolean isPop) { 411 if (inFragment == null || outFragment == null) { 412 return null; 413 } 414 Object transition = FragmentTransitionCompat21.cloneTransition(isPop 415 ? outFragment.getSharedElementReturnTransition() 416 : inFragment.getSharedElementEnterTransition()); 417 return FragmentTransitionCompat21.wrapTransitionInSet(transition); 418 } 419 420 /** 421 * Returns a clone of the enter transition or null if no such transition exists. 422 */ 423 @RequiresApi(21) 424 private static Object getEnterTransition(Fragment inFragment, boolean isPop) { 425 if (inFragment == null) { 426 return null; 427 } 428 return FragmentTransitionCompat21.cloneTransition(isPop 429 ? inFragment.getReenterTransition() 430 : inFragment.getEnterTransition()); 431 } 432 433 /** 434 * Returns a clone of the exit transition or null if no such transition exists. 435 */ 436 @RequiresApi(21) 437 private static Object getExitTransition(Fragment outFragment, boolean isPop) { 438 if (outFragment == null) { 439 return null; 440 } 441 return FragmentTransitionCompat21.cloneTransition(isPop 442 ? outFragment.getReturnTransition() 443 : outFragment.getExitTransition()); 444 } 445 446 /** 447 * Configures the shared elements of a reordered fragment transaction's transition. 448 * This retrieves the shared elements of the outgoing and incoming fragments, maps the 449 * views, and sets up the epicenter on the transitions. 450 * <p> 451 * The epicenter of exit and shared element transitions is the first shared element 452 * in the outgoing fragment. The epicenter of the entering transition is the first shared 453 * element in the incoming fragment. 454 * 455 * @param sceneRoot The fragment container View 456 * @param nonExistentView A View that does not exist in the hierarchy. This is used to 457 * prevent transitions from acting on other Views when there is no 458 * other target. 459 * @param nameOverrides A map of the shared element names from the starting fragment to 460 * the final fragment's Views as given in 461 * {@link FragmentTransaction#addSharedElement(View, String)}. 462 * @param fragments A structure holding the transitioning fragments in this container. 463 * @param sharedElementsOut A list modified to contain the shared elements in the outgoing 464 * fragment 465 * @param sharedElementsIn A list modified to contain the shared elements in the incoming 466 * fragment 467 * @param enterTransition The transition used for entering Views, modified by applying the 468 * epicenter 469 * @param exitTransition The transition used for exiting Views, modified by applying the 470 * epicenter 471 * @return The shared element transition or null if no shared elements exist 472 */ 473 @RequiresApi(21) 474 private static Object configureSharedElementsReordered(final ViewGroup sceneRoot, 475 final View nonExistentView, final ArrayMap<String, String> nameOverrides, 476 final FragmentContainerTransition fragments, 477 final ArrayList<View> sharedElementsOut, 478 final ArrayList<View> sharedElementsIn, 479 final Object enterTransition, final Object exitTransition) { 480 final Fragment inFragment = fragments.lastIn; 481 final Fragment outFragment = fragments.firstOut; 482 if (inFragment != null) { 483 inFragment.getView().setVisibility(View.VISIBLE); 484 } 485 if (inFragment == null || outFragment == null) { 486 return null; // no shared element without a fragment 487 } 488 489 final boolean inIsPop = fragments.lastInIsPop; 490 Object sharedElementTransition = nameOverrides.isEmpty() ? null 491 : getSharedElementTransition(inFragment, outFragment, inIsPop); 492 493 final ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides, 494 sharedElementTransition, fragments); 495 496 final ArrayMap<String, View> inSharedElements = captureInSharedElements(nameOverrides, 497 sharedElementTransition, fragments); 498 499 if (nameOverrides.isEmpty()) { 500 sharedElementTransition = null; 501 if (outSharedElements != null) { 502 outSharedElements.clear(); 503 } 504 if (inSharedElements != null) { 505 inSharedElements.clear(); 506 } 507 } else { 508 addSharedElementsWithMatchingNames(sharedElementsOut, outSharedElements, 509 nameOverrides.keySet()); 510 addSharedElementsWithMatchingNames(sharedElementsIn, inSharedElements, 511 nameOverrides.values()); 512 } 513 514 if (enterTransition == null && exitTransition == null && sharedElementTransition == null) { 515 // don't call onSharedElementStart/End since there is no transition 516 return null; 517 } 518 519 callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true); 520 521 final Rect epicenter; 522 final View epicenterView; 523 if (sharedElementTransition != null) { 524 sharedElementsIn.add(nonExistentView); 525 FragmentTransitionCompat21.setSharedElementTargets(sharedElementTransition, 526 nonExistentView, sharedElementsOut); 527 final boolean outIsPop = fragments.firstOutIsPop; 528 final BackStackRecord outTransaction = fragments.firstOutTransaction; 529 setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop, 530 outTransaction); 531 epicenter = new Rect(); 532 epicenterView = getInEpicenterView(inSharedElements, fragments, 533 enterTransition, inIsPop); 534 if (epicenterView != null) { 535 FragmentTransitionCompat21.setEpicenter(enterTransition, epicenter); 536 } 537 } else { 538 epicenter = null; 539 epicenterView = null; 540 } 541 542 OneShotPreDrawListener.add(sceneRoot, new Runnable() { 543 @Override 544 public void run() { 545 callSharedElementStartEnd(inFragment, outFragment, inIsPop, 546 inSharedElements, false); 547 if (epicenterView != null) { 548 FragmentTransitionCompat21.getBoundsOnScreen(epicenterView, epicenter); 549 } 550 } 551 }); 552 return sharedElementTransition; 553 } 554 555 /** 556 * Add Views from sharedElements into views that have the transitionName in the 557 * nameOverridesSet. 558 * 559 * @param views Views list to add shared elements to 560 * @param sharedElements List of shared elements 561 * @param nameOverridesSet The transition names for all views to be copied from 562 * sharedElements to views. 563 */ 564 private static void addSharedElementsWithMatchingNames(ArrayList<View> views, 565 ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet) { 566 for (int i = sharedElements.size() - 1; i >= 0; i--) { 567 View view = sharedElements.valueAt(i); 568 if (nameOverridesSet.contains(ViewCompat.getTransitionName(view))) { 569 views.add(view); 570 } 571 } 572 } 573 574 /** 575 * Configures the shared elements of an ordered fragment transaction's transition. 576 * This retrieves the shared elements of the incoming fragments, and schedules capturing 577 * the incoming fragment's shared elements. It also maps the views, and sets up the epicenter 578 * on the transitions. 579 * <p> 580 * The epicenter of exit and shared element transitions is the first shared element 581 * in the outgoing fragment. The epicenter of the entering transition is the first shared 582 * element in the incoming fragment. 583 * 584 * @param sceneRoot The fragment container View 585 * @param nonExistentView A View that does not exist in the hierarchy. This is used to 586 * prevent transitions from acting on other Views when there is no 587 * other target. 588 * @param nameOverrides A map of the shared element names from the starting fragment to 589 * the final fragment's Views as given in 590 * {@link FragmentTransaction#addSharedElement(View, String)}. 591 * @param fragments A structure holding the transitioning fragments in this container. 592 * @param sharedElementsOut A list modified to contain the shared elements in the outgoing 593 * fragment 594 * @param sharedElementsIn A list modified to contain the shared elements in the incoming 595 * fragment 596 * @param enterTransition The transition used for entering Views, modified by applying the 597 * epicenter 598 * @param exitTransition The transition used for exiting Views, modified by applying the 599 * epicenter 600 * @return The shared element transition or null if no shared elements exist 601 */ 602 @RequiresApi(21) 603 private static Object configureSharedElementsOrdered(final ViewGroup sceneRoot, 604 final View nonExistentView, final ArrayMap<String, String> nameOverrides, 605 final FragmentContainerTransition fragments, 606 final ArrayList<View> sharedElementsOut, 607 final ArrayList<View> sharedElementsIn, 608 final Object enterTransition, final Object exitTransition) { 609 final Fragment inFragment = fragments.lastIn; 610 final Fragment outFragment = fragments.firstOut; 611 612 if (inFragment == null || outFragment == null) { 613 return null; // no transition 614 } 615 616 final boolean inIsPop = fragments.lastInIsPop; 617 Object sharedElementTransition = nameOverrides.isEmpty() ? null 618 : getSharedElementTransition(inFragment, outFragment, inIsPop); 619 620 ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides, 621 sharedElementTransition, fragments); 622 623 if (nameOverrides.isEmpty()) { 624 sharedElementTransition = null; 625 } else { 626 sharedElementsOut.addAll(outSharedElements.values()); 627 } 628 629 if (enterTransition == null && exitTransition == null && sharedElementTransition == null) { 630 // don't call onSharedElementStart/End since there is no transition 631 return null; 632 } 633 634 callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true); 635 636 final Rect inEpicenter; 637 if (sharedElementTransition != null) { 638 inEpicenter = new Rect(); 639 FragmentTransitionCompat21.setSharedElementTargets(sharedElementTransition, 640 nonExistentView, sharedElementsOut); 641 final boolean outIsPop = fragments.firstOutIsPop; 642 final BackStackRecord outTransaction = fragments.firstOutTransaction; 643 setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop, 644 outTransaction); 645 if (enterTransition != null) { 646 FragmentTransitionCompat21.setEpicenter(enterTransition, inEpicenter); 647 } 648 } else { 649 inEpicenter = null; 650 } 651 652 653 final Object finalSharedElementTransition = sharedElementTransition; 654 OneShotPreDrawListener.add(sceneRoot, new Runnable() { 655 @Override 656 public void run() { 657 ArrayMap<String, View> inSharedElements = captureInSharedElements( 658 nameOverrides, finalSharedElementTransition, fragments); 659 660 if (inSharedElements != null) { 661 sharedElementsIn.addAll(inSharedElements.values()); 662 sharedElementsIn.add(nonExistentView); 663 } 664 665 callSharedElementStartEnd(inFragment, outFragment, inIsPop, 666 inSharedElements, false); 667 if (finalSharedElementTransition != null) { 668 FragmentTransitionCompat21.swapSharedElementTargets( 669 finalSharedElementTransition, sharedElementsOut, 670 sharedElementsIn); 671 672 final View inEpicenterView = getInEpicenterView(inSharedElements, 673 fragments, enterTransition, inIsPop); 674 if (inEpicenterView != null) { 675 FragmentTransitionCompat21.getBoundsOnScreen(inEpicenterView, 676 inEpicenter); 677 } 678 } 679 } 680 }); 681 682 return sharedElementTransition; 683 } 684 685 /** 686 * Finds the shared elements in the outgoing fragment. It also calls 687 * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control 688 * of the shared element mapping. {@code nameOverrides} is updated to match the 689 * actual transition name of the mapped shared elements. 690 * 691 * @param nameOverrides A map of the shared element names from the starting fragment to 692 * the final fragment's Views as given in 693 * {@link FragmentTransaction#addSharedElement(View, String)}. 694 * @param sharedElementTransition The shared element transition 695 * @param fragments A structure holding the transitioning fragments in this container. 696 * @return The mapping of shared element names to the Views in the hierarchy or null 697 * if there is no shared element transition. 698 */ 699 @RequiresApi(21) 700 private static ArrayMap<String, View> captureOutSharedElements( 701 ArrayMap<String, String> nameOverrides, Object sharedElementTransition, 702 FragmentContainerTransition fragments) { 703 if (nameOverrides.isEmpty() || sharedElementTransition == null) { 704 nameOverrides.clear(); 705 return null; 706 } 707 final Fragment outFragment = fragments.firstOut; 708 final ArrayMap<String, View> outSharedElements = new ArrayMap<>(); 709 FragmentTransitionCompat21.findNamedViews(outSharedElements, outFragment.getView()); 710 711 final SharedElementCallback sharedElementCallback; 712 final ArrayList<String> names; 713 final BackStackRecord outTransaction = fragments.firstOutTransaction; 714 if (fragments.firstOutIsPop) { 715 sharedElementCallback = outFragment.getEnterTransitionCallback(); 716 names = outTransaction.mSharedElementTargetNames; 717 } else { 718 sharedElementCallback = outFragment.getExitTransitionCallback(); 719 names = outTransaction.mSharedElementSourceNames; 720 } 721 722 outSharedElements.retainAll(names); 723 if (sharedElementCallback != null) { 724 sharedElementCallback.onMapSharedElements(names, outSharedElements); 725 for (int i = names.size() - 1; i >= 0; i--) { 726 String name = names.get(i); 727 View view = outSharedElements.get(name); 728 if (view == null) { 729 nameOverrides.remove(name); 730 } else if (!name.equals(ViewCompat.getTransitionName(view))) { 731 String targetValue = nameOverrides.remove(name); 732 nameOverrides.put(ViewCompat.getTransitionName(view), targetValue); 733 } 734 } 735 } else { 736 nameOverrides.retainAll(outSharedElements.keySet()); 737 } 738 return outSharedElements; 739 } 740 741 /** 742 * Finds the shared elements in the incoming fragment. It also calls 743 * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control 744 * of the shared element mapping. {@code nameOverrides} is updated to match the 745 * actual transition name of the mapped shared elements. 746 * 747 * @param nameOverrides A map of the shared element names from the starting fragment to 748 * the final fragment's Views as given in 749 * {@link FragmentTransaction#addSharedElement(View, String)}. 750 * @param sharedElementTransition The shared element transition 751 * @param fragments A structure holding the transitioning fragments in this container. 752 * @return The mapping of shared element names to the Views in the hierarchy or null 753 * if there is no shared element transition. 754 */ 755 @RequiresApi(21) 756 private static ArrayMap<String, View> captureInSharedElements( 757 ArrayMap<String, String> nameOverrides, Object sharedElementTransition, 758 FragmentContainerTransition fragments) { 759 Fragment inFragment = fragments.lastIn; 760 final View fragmentView = inFragment.getView(); 761 if (nameOverrides.isEmpty() || sharedElementTransition == null || fragmentView == null) { 762 nameOverrides.clear(); 763 return null; 764 } 765 final ArrayMap<String, View> inSharedElements = new ArrayMap<>(); 766 FragmentTransitionCompat21.findNamedViews(inSharedElements, fragmentView); 767 768 final SharedElementCallback sharedElementCallback; 769 final ArrayList<String> names; 770 final BackStackRecord inTransaction = fragments.lastInTransaction; 771 if (fragments.lastInIsPop) { 772 sharedElementCallback = inFragment.getExitTransitionCallback(); 773 names = inTransaction.mSharedElementSourceNames; 774 } else { 775 sharedElementCallback = inFragment.getEnterTransitionCallback(); 776 names = inTransaction.mSharedElementTargetNames; 777 } 778 779 if (names != null) { 780 inSharedElements.retainAll(names); 781 } 782 if (sharedElementCallback != null) { 783 sharedElementCallback.onMapSharedElements(names, inSharedElements); 784 for (int i = names.size() - 1; i >= 0; i--) { 785 String name = names.get(i); 786 View view = inSharedElements.get(name); 787 if (view == null) { 788 String key = findKeyForValue(nameOverrides, name); 789 if (key != null) { 790 nameOverrides.remove(key); 791 } 792 } else if (!name.equals(ViewCompat.getTransitionName(view))) { 793 String key = findKeyForValue(nameOverrides, name); 794 if (key != null) { 795 nameOverrides.put(key, ViewCompat.getTransitionName(view)); 796 } 797 } 798 } 799 } else { 800 retainValues(nameOverrides, inSharedElements); 801 } 802 return inSharedElements; 803 } 804 805 /** 806 * Utility to find the String key in {@code map} that maps to {@code value}. 807 */ 808 private static String findKeyForValue(ArrayMap<String, String> map, String value) { 809 final int numElements = map.size(); 810 for (int i = 0; i < numElements; i++) { 811 if (value.equals(map.valueAt(i))) { 812 return map.keyAt(i); 813 } 814 } 815 return null; 816 } 817 818 /** 819 * Returns the View in the incoming Fragment that should be used as the epicenter. 820 * 821 * @param inSharedElements The mapping of shared element names to Views in the 822 * incoming fragment. 823 * @param fragments A structure holding the transitioning fragments in this container. 824 * @param enterTransition The transition used for the incoming Fragment's views 825 * @param inIsPop Is the incoming fragment being added as a pop transaction? 826 */ 827 private static View getInEpicenterView(ArrayMap<String, View> inSharedElements, 828 FragmentContainerTransition fragments, 829 Object enterTransition, boolean inIsPop) { 830 BackStackRecord inTransaction = fragments.lastInTransaction; 831 if (enterTransition != null && inSharedElements != null 832 && inTransaction.mSharedElementSourceNames != null 833 && !inTransaction.mSharedElementSourceNames.isEmpty()) { 834 final String targetName = inIsPop 835 ? inTransaction.mSharedElementSourceNames.get(0) 836 : inTransaction.mSharedElementTargetNames.get(0); 837 return inSharedElements.get(targetName); 838 } 839 return null; 840 } 841 842 /** 843 * Sets the epicenter for the exit transition. 844 * 845 * @param sharedElementTransition The shared element transition 846 * @param exitTransition The transition for the outgoing fragment's views 847 * @param outSharedElements Shared elements in the outgoing fragment 848 * @param outIsPop Is the outgoing fragment being removed as a pop transaction? 849 * @param outTransaction The transaction that caused the fragment to be removed. 850 */ 851 @RequiresApi(21) 852 private static void setOutEpicenter(Object sharedElementTransition, 853 Object exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop, 854 BackStackRecord outTransaction) { 855 if (outTransaction.mSharedElementSourceNames != null 856 && !outTransaction.mSharedElementSourceNames.isEmpty()) { 857 final String sourceName = outIsPop 858 ? outTransaction.mSharedElementTargetNames.get(0) 859 : outTransaction.mSharedElementSourceNames.get(0); 860 final View outEpicenterView = outSharedElements.get(sourceName); 861 FragmentTransitionCompat21.setEpicenter(sharedElementTransition, outEpicenterView); 862 863 if (exitTransition != null) { 864 FragmentTransitionCompat21.setEpicenter(exitTransition, outEpicenterView); 865 } 866 } 867 } 868 869 /** 870 * A utility to retain only the mappings in {@code nameOverrides} that have a value 871 * that has a key in {@code namedViews}. This is a useful equivalent to 872 * {@link ArrayMap#retainAll(Collection)} for values. 873 */ 874 private static void retainValues(ArrayMap<String, String> nameOverrides, 875 ArrayMap<String, View> namedViews) { 876 for (int i = nameOverrides.size() - 1; i >= 0; i--) { 877 final String targetName = nameOverrides.valueAt(i); 878 if (!namedViews.containsKey(targetName)) { 879 nameOverrides.removeAt(i); 880 } 881 } 882 } 883 884 /** 885 * Calls the {@link SharedElementCallback#onSharedElementStart(List, List, List)} or 886 * {@link SharedElementCallback#onSharedElementEnd(List, List, List)} on the appropriate 887 * incoming or outgoing fragment. 888 * 889 * @param inFragment The incoming fragment 890 * @param outFragment The outgoing fragment 891 * @param isPop Is the incoming fragment part of a pop transaction? 892 * @param sharedElements The shared element Views 893 * @param isStart Call the start or end call on the SharedElementCallback 894 */ 895 private static void callSharedElementStartEnd(Fragment inFragment, Fragment outFragment, 896 boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart) { 897 SharedElementCallback sharedElementCallback = isPop 898 ? outFragment.getEnterTransitionCallback() 899 : inFragment.getEnterTransitionCallback(); 900 if (sharedElementCallback != null) { 901 ArrayList<View> views = new ArrayList<>(); 902 ArrayList<String> names = new ArrayList<>(); 903 final int count = sharedElements == null ? 0 : sharedElements.size(); 904 for (int i = 0; i < count; i++) { 905 names.add(sharedElements.keyAt(i)); 906 views.add(sharedElements.valueAt(i)); 907 } 908 if (isStart) { 909 sharedElementCallback.onSharedElementStart(names, views, null); 910 } else { 911 sharedElementCallback.onSharedElementEnd(names, views, null); 912 } 913 } 914 } 915 916 @RequiresApi(21) 917 private static ArrayList<View> configureEnteringExitingViews(Object transition, 918 Fragment fragment, ArrayList<View> sharedElements, View nonExistentView) { 919 ArrayList<View> viewList = null; 920 if (transition != null) { 921 viewList = new ArrayList<>(); 922 View root = fragment.getView(); 923 if (root != null) { 924 FragmentTransitionCompat21.captureTransitioningViews(viewList, root); 925 } 926 if (sharedElements != null) { 927 viewList.removeAll(sharedElements); 928 } 929 if (!viewList.isEmpty()) { 930 viewList.add(nonExistentView); 931 FragmentTransitionCompat21.addTargets(transition, viewList); 932 } 933 } 934 return viewList; 935 } 936 937 /** 938 * Sets the visibility of all Views in {@code views} to {@code visibility}. 939 */ 940 private static void setViewVisibility(ArrayList<View> views, int visibility) { 941 if (views == null) { 942 return; 943 } 944 for (int i = views.size() - 1; i >= 0; i--) { 945 final View view = views.get(i); 946 view.setVisibility(visibility); 947 } 948 } 949 950 /** 951 * Merges exit, shared element, and enter transitions so that they act together or 952 * sequentially as defined in the fragments. 953 */ 954 @RequiresApi(21) 955 private static Object mergeTransitions(Object enterTransition, 956 Object exitTransition, Object sharedElementTransition, Fragment inFragment, 957 boolean isPop) { 958 boolean overlap = true; 959 if (enterTransition != null && exitTransition != null && inFragment != null) { 960 overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() : 961 inFragment.getAllowEnterTransitionOverlap(); 962 } 963 964 // Wrap the transitions. Explicit targets like in enter and exit will cause the 965 // views to be targeted regardless of excluded views. If that happens, then the 966 // excluded fragments views (hidden fragments) will still be in the transition. 967 968 Object transition; 969 if (overlap) { 970 // Regular transition -- do it all together 971 transition = FragmentTransitionCompat21.mergeTransitionsTogether(exitTransition, 972 enterTransition, sharedElementTransition); 973 } else { 974 // First do exit, then enter, but allow shared element transition to happen 975 // during both. 976 transition = FragmentTransitionCompat21.mergeTransitionsInSequence(exitTransition, 977 enterTransition, sharedElementTransition); 978 } 979 return transition; 980 } 981 982 /** 983 * Finds the first removed fragment and last added fragments when going forward. 984 * If none of the fragments have transitions, then both lists will be empty. 985 * 986 * @param transitioningFragments Keyed on the container ID, the first fragments to be removed, 987 * and last fragments to be added. This will be modified by 988 * this method. 989 */ 990 public static void calculateFragments(BackStackRecord transaction, 991 SparseArray<FragmentContainerTransition> transitioningFragments, 992 boolean isReordered) { 993 final int numOps = transaction.mOps.size(); 994 for (int opNum = 0; opNum < numOps; opNum++) { 995 final BackStackRecord.Op op = transaction.mOps.get(opNum); 996 addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered); 997 } 998 } 999 1000 /** 1001 * Finds the first removed fragment and last added fragments when popping the back stack. 1002 * If none of the fragments have transitions, then both lists will be empty. 1003 * 1004 * @param transitioningFragments Keyed on the container ID, the first fragments to be removed, 1005 * and last fragments to be added. This will be modified by 1006 * this method. 1007 */ 1008 public static void calculatePopFragments(BackStackRecord transaction, 1009 SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered) { 1010 if (!transaction.mManager.mContainer.onHasView()) { 1011 return; // nothing to see, so no transitions 1012 } 1013 final int numOps = transaction.mOps.size(); 1014 for (int opNum = numOps - 1; opNum >= 0; opNum--) { 1015 final BackStackRecord.Op op = transaction.mOps.get(opNum); 1016 addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered); 1017 } 1018 } 1019 1020 /** 1021 * Examines the {@code command} and may set the first out or last in fragment for the fragment's 1022 * container. 1023 * 1024 * @param transaction The executing transaction 1025 * @param op The operation being run. 1026 * @param transitioningFragments A structure holding the first in and last out fragments 1027 * for each fragment container. 1028 * @param isPop Is the operation a pop? 1029 * @param isReorderedTransaction True if the operations have been partially executed and the 1030 * added fragments have Views in the hierarchy or false if the 1031 * operations haven't been executed yet. 1032 */ 1033 @SuppressWarnings("ReferenceEquality") 1034 private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op, 1035 SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop, 1036 boolean isReorderedTransaction) { 1037 final Fragment fragment = op.fragment; 1038 if (fragment == null) { 1039 return; // no fragment, no transition 1040 } 1041 final int containerId = fragment.mContainerId; 1042 if (containerId == 0) { 1043 return; // no container, no transition 1044 } 1045 final int command = isPop ? INVERSE_OPS[op.cmd] : op.cmd; 1046 boolean setLastIn = false; 1047 boolean wasRemoved = false; 1048 boolean setFirstOut = false; 1049 boolean wasAdded = false; 1050 switch (command) { 1051 case BackStackRecord.OP_SHOW: 1052 if (isReorderedTransaction) { 1053 setLastIn = fragment.mHiddenChanged && !fragment.mHidden && fragment.mAdded; 1054 } else { 1055 setLastIn = fragment.mHidden; 1056 } 1057 wasAdded = true; 1058 break; 1059 case BackStackRecord.OP_ADD: 1060 case BackStackRecord.OP_ATTACH: 1061 if (isReorderedTransaction) { 1062 setLastIn = fragment.mIsNewlyAdded; 1063 } else { 1064 setLastIn = !fragment.mAdded && !fragment.mHidden; 1065 } 1066 wasAdded = true; 1067 break; 1068 case BackStackRecord.OP_HIDE: 1069 if (isReorderedTransaction) { 1070 setFirstOut = fragment.mHiddenChanged && fragment.mAdded && fragment.mHidden; 1071 } else { 1072 setFirstOut = fragment.mAdded && !fragment.mHidden; 1073 } 1074 wasRemoved = true; 1075 break; 1076 case BackStackRecord.OP_REMOVE: 1077 case BackStackRecord.OP_DETACH: 1078 if (isReorderedTransaction) { 1079 setFirstOut = !fragment.mAdded && fragment.mView != null 1080 && fragment.mView.getVisibility() == View.VISIBLE 1081 && fragment.mPostponedAlpha >= 0; 1082 } else { 1083 setFirstOut = fragment.mAdded && !fragment.mHidden; 1084 } 1085 wasRemoved = true; 1086 break; 1087 } 1088 FragmentContainerTransition containerTransition = transitioningFragments.get(containerId); 1089 if (setLastIn) { 1090 containerTransition = 1091 ensureContainer(containerTransition, transitioningFragments, containerId); 1092 containerTransition.lastIn = fragment; 1093 containerTransition.lastInIsPop = isPop; 1094 containerTransition.lastInTransaction = transaction; 1095 } 1096 if (!isReorderedTransaction && wasAdded) { 1097 if (containerTransition != null && containerTransition.firstOut == fragment) { 1098 containerTransition.firstOut = null; 1099 } 1100 1101 /* 1102 * Ensure that fragments that are entering are at least at the CREATED state 1103 * so that they may load Transitions using TransitionInflater. 1104 */ 1105 FragmentManagerImpl manager = transaction.mManager; 1106 if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED 1107 && !transaction.mReorderingAllowed) { 1108 manager.makeActive(fragment); 1109 manager.moveToState(fragment, Fragment.CREATED, 0, 0, false); 1110 } 1111 } 1112 if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) { 1113 containerTransition = 1114 ensureContainer(containerTransition, transitioningFragments, containerId); 1115 containerTransition.firstOut = fragment; 1116 containerTransition.firstOutIsPop = isPop; 1117 containerTransition.firstOutTransaction = transaction; 1118 } 1119 1120 if (!isReorderedTransaction && wasRemoved 1121 && (containerTransition != null && containerTransition.lastIn == fragment)) { 1122 containerTransition.lastIn = null; 1123 } 1124 } 1125 1126 /** 1127 * Ensures that a FragmentContainerTransition has been added to the SparseArray. If so, 1128 * it returns the existing one. If not, one is created and added to the SparseArray and 1129 * returned. 1130 */ 1131 private static FragmentContainerTransition ensureContainer( 1132 FragmentContainerTransition containerTransition, 1133 SparseArray<FragmentContainerTransition> transitioningFragments, int containerId) { 1134 if (containerTransition == null) { 1135 containerTransition = new FragmentContainerTransition(); 1136 transitioningFragments.put(containerId, containerTransition); 1137 } 1138 return containerTransition; 1139 } 1140 1141 /** 1142 * Tracks the last fragment added and first fragment removed for fragment transitions. 1143 * This also tracks which fragments are changed by push or pop transactions. 1144 */ 1145 static class FragmentContainerTransition { 1146 /** 1147 * The last fragment added/attached/shown in its container 1148 */ 1149 public Fragment lastIn; 1150 1151 /** 1152 * true when lastIn was added during a pop transaction or false if added with a push 1153 */ 1154 public boolean lastInIsPop; 1155 1156 /** 1157 * The transaction that included the last in fragment 1158 */ 1159 public BackStackRecord lastInTransaction; 1160 1161 /** 1162 * The first fragment with a View that was removed/detached/hidden in its container. 1163 */ 1164 public Fragment firstOut; 1165 1166 /** 1167 * true when firstOut was removed during a pop transaction or false otherwise 1168 */ 1169 public boolean firstOutIsPop; 1170 1171 /** 1172 * The transaction that included the first out fragment 1173 */ 1174 public BackStackRecord firstOutTransaction; 1175 } 1176} 1177