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.app; 17 18 import android.graphics.Rect; 19 import android.os.Build; 20 import android.transition.Transition; 21 import android.transition.TransitionListenerAdapter; 22 import android.transition.TransitionManager; 23 import android.transition.TransitionSet; 24 import android.util.ArrayMap; 25 import android.util.SparseArray; 26 import android.view.View; 27 import android.view.ViewGroup; 28 29 import com.android.internal.view.OneShotPreDrawListener; 30 31 import java.util.ArrayList; 32 import java.util.Collection; 33 import java.util.List; 34 import java.util.Map; 35 36 /** 37 * Contains the Fragment Transition functionality for both ordered and reordered 38 * Fragment Transactions. With reordered fragment transactions, all Views have been 39 * added to the View hierarchy prior to calling startTransitions. With ordered 40 * fragment transactions, Views will be removed and added after calling startTransitions. 41 */ 42 class FragmentTransition { 43 /** 44 * The inverse of all BackStackRecord operation commands. This assumes that 45 * REPLACE operations have already been replaced by add/remove operations. 46 */ 47 private static final int[] INVERSE_OPS = { 48 BackStackRecord.OP_NULL, // inverse of OP_NULL (error) 49 BackStackRecord.OP_REMOVE, // inverse of OP_ADD 50 BackStackRecord.OP_NULL, // inverse of OP_REPLACE (error) 51 BackStackRecord.OP_ADD, // inverse of OP_REMOVE 52 BackStackRecord.OP_SHOW, // inverse of OP_HIDE 53 BackStackRecord.OP_HIDE, // inverse of OP_SHOW 54 BackStackRecord.OP_ATTACH, // inverse of OP_DETACH 55 BackStackRecord.OP_DETACH, // inverse of OP_ATTACH 56 BackStackRecord.OP_UNSET_PRIMARY_NAV, // inverse of OP_SET_PRIMARY_NAV 57 BackStackRecord.OP_SET_PRIMARY_NAV, // inverse of OP_UNSET_PRIMARY_NAV 58 }; 59 60 /** 61 * The main entry point for Fragment Transitions, this starts the transitions 62 * set on the leaving Fragment's {@link Fragment#getExitTransition()}, the 63 * entering Fragment's {@link Fragment#getEnterTransition()} and 64 * {@link Fragment#getSharedElementEnterTransition()}. When popping, 65 * the leaving Fragment's {@link Fragment#getReturnTransition()} and 66 * {@link Fragment#getSharedElementReturnTransition()} and the entering 67 * {@link Fragment#getReenterTransition()} will be run. 68 * <p> 69 * With reordered Fragment Transitions, all Views have been added to the 70 * View hierarchy prior to calling this method. The incoming Fragment's Views 71 * will be INVISIBLE. With ordered Fragment Transitions, this method 72 * is called before any change has been made to the hierarchy. That means 73 * that the added Fragments have not created their Views yet and the hierarchy 74 * is unknown. 75 * 76 * @param fragmentManager The executing FragmentManagerImpl 77 * @param records The list of transactions being executed. 78 * @param isRecordPop For each transaction, whether it is a pop transaction or not. 79 * @param startIndex The first index into records and isRecordPop to execute as 80 * part of this transition. 81 * @param endIndex One past the last index into records and isRecordPop to execute 82 * as part of this transition. 83 * @param isReordered true if this is a reordered transaction, meaning that the 84 * Views of incoming fragments have been added. false if the 85 * transaction has yet to be run and Views haven't been created. 86 */ startTransitions(FragmentManagerImpl fragmentManager, ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, int startIndex, int endIndex, boolean isReordered)87 static void startTransitions(FragmentManagerImpl fragmentManager, 88 ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, 89 int startIndex, int endIndex, boolean isReordered) { 90 if (fragmentManager.mCurState < Fragment.CREATED) { 91 return; 92 } 93 SparseArray<FragmentContainerTransition> transitioningFragments = 94 new SparseArray<>(); 95 for (int i = startIndex; i < endIndex; i++) { 96 final BackStackRecord record = records.get(i); 97 final boolean isPop = isRecordPop.get(i); 98 if (isPop) { 99 calculatePopFragments(record, transitioningFragments, isReordered); 100 } else { 101 calculateFragments(record, transitioningFragments, isReordered); 102 } 103 } 104 105 if (transitioningFragments.size() != 0) { 106 final View nonExistentView = new View(fragmentManager.mHost.getContext()); 107 final int numContainers = transitioningFragments.size(); 108 for (int i = 0; i < numContainers; i++) { 109 int containerId = transitioningFragments.keyAt(i); 110 ArrayMap<String, String> nameOverrides = calculateNameOverrides(containerId, 111 records, isRecordPop, startIndex, endIndex); 112 113 FragmentContainerTransition containerTransition = transitioningFragments.valueAt(i); 114 115 if (isReordered) { 116 configureTransitionsReordered(fragmentManager, containerId, 117 containerTransition, nonExistentView, nameOverrides); 118 } else { 119 configureTransitionsOrdered(fragmentManager, containerId, 120 containerTransition, nonExistentView, nameOverrides); 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 */ calculateNameOverrides(int containerId, ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, int startIndex, int endIndex)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 */ configureTransitionsReordered(FragmentManagerImpl fragmentManager, int containerId, FragmentContainerTransition fragments, View nonExistentView, ArrayMap<String, String> nameOverrides)192 private static void configureTransitionsReordered(FragmentManagerImpl fragmentManager, 193 int containerId, FragmentContainerTransition fragments, 194 View nonExistentView, ArrayMap<String, String> nameOverrides) { 195 ViewGroup sceneRoot = null; 196 if (fragmentManager.mContainer.onHasView()) { 197 sceneRoot = fragmentManager.mContainer.onFindViewById(containerId); 198 } 199 if (sceneRoot == null) { 200 return; 201 } 202 final Fragment inFragment = fragments.lastIn; 203 final Fragment outFragment = fragments.firstOut; 204 final boolean inIsPop = fragments.lastInIsPop; 205 final boolean outIsPop = fragments.firstOutIsPop; 206 207 ArrayList<View> sharedElementsIn = new ArrayList<>(); 208 ArrayList<View> sharedElementsOut = new ArrayList<>(); 209 Transition enterTransition = getEnterTransition(inFragment, inIsPop); 210 Transition exitTransition = getExitTransition(outFragment, outIsPop); 211 212 TransitionSet sharedElementTransition = configureSharedElementsReordered(sceneRoot, 213 nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn, 214 enterTransition, exitTransition); 215 216 if (enterTransition == null && sharedElementTransition == null && 217 exitTransition == null) { 218 return; // no transitions! 219 } 220 221 ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition, 222 outFragment, sharedElementsOut, nonExistentView); 223 224 ArrayList<View> enteringViews = configureEnteringExitingViews(enterTransition, 225 inFragment, sharedElementsIn, nonExistentView); 226 227 setViewVisibility(enteringViews, View.INVISIBLE); 228 229 Transition transition = mergeTransitions(enterTransition, exitTransition, 230 sharedElementTransition, inFragment, inIsPop); 231 232 if (transition != null) { 233 replaceHide(exitTransition, outFragment, exitingViews); 234 transition.setNameOverrides(nameOverrides); 235 scheduleRemoveTargets(transition, 236 enterTransition, enteringViews, exitTransition, exitingViews, 237 sharedElementTransition, sharedElementsIn); 238 TransitionManager.beginDelayedTransition(sceneRoot, transition); 239 setViewVisibility(enteringViews, View.VISIBLE); 240 // Swap the shared element targets 241 if (sharedElementTransition != null) { 242 sharedElementTransition.getTargets().clear(); 243 sharedElementTransition.getTargets().addAll(sharedElementsIn); 244 replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn); 245 } 246 } 247 } 248 249 /** 250 * Configures a transition for a single fragment container for which the transaction was 251 * ordered. That means that the transaction has not been executed yet, so incoming 252 * Views are not yet known. 253 * 254 * @param fragmentManager The executing FragmentManagerImpl 255 * @param containerId The container ID that is executing the transition. 256 * @param fragments A structure holding the transitioning fragments in this container. 257 * @param nonExistentView A View that does not exist in the hierarchy. This is used to 258 * prevent transitions from acting on other Views when there is no 259 * other target. 260 * @param nameOverrides A map of the shared element names from the starting fragment to 261 * the final fragment's Views as given in 262 * {@link FragmentTransaction#addSharedElement(View, String)}. 263 */ configureTransitionsOrdered(FragmentManagerImpl fragmentManager, int containerId, FragmentContainerTransition fragments, View nonExistentView, ArrayMap<String, String> nameOverrides)264 private static void configureTransitionsOrdered(FragmentManagerImpl fragmentManager, 265 int containerId, FragmentContainerTransition fragments, 266 View nonExistentView, ArrayMap<String, String> nameOverrides) { 267 ViewGroup sceneRoot = null; 268 if (fragmentManager.mContainer.onHasView()) { 269 sceneRoot = fragmentManager.mContainer.onFindViewById(containerId); 270 } 271 if (sceneRoot == null) { 272 return; 273 } 274 final Fragment inFragment = fragments.lastIn; 275 final Fragment outFragment = fragments.firstOut; 276 final boolean inIsPop = fragments.lastInIsPop; 277 final boolean outIsPop = fragments.firstOutIsPop; 278 279 Transition enterTransition = getEnterTransition(inFragment, inIsPop); 280 Transition exitTransition = getExitTransition(outFragment, outIsPop); 281 282 ArrayList<View> sharedElementsOut = new ArrayList<>(); 283 ArrayList<View> sharedElementsIn = new ArrayList<>(); 284 285 TransitionSet sharedElementTransition = configureSharedElementsOrdered(sceneRoot, 286 nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn, 287 enterTransition, exitTransition); 288 289 if (enterTransition == null && sharedElementTransition == null && 290 exitTransition == null) { 291 return; // no transitions! 292 } 293 294 ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition, 295 outFragment, sharedElementsOut, nonExistentView); 296 297 if (exitingViews == null || exitingViews.isEmpty()) { 298 exitTransition = null; 299 } 300 301 if (enterTransition != null) { 302 // Ensure the entering transition doesn't target anything until the views are made 303 // visible 304 enterTransition.addTarget(nonExistentView); 305 } 306 307 Transition transition = mergeTransitions(enterTransition, exitTransition, 308 sharedElementTransition, inFragment, fragments.lastInIsPop); 309 310 if (transition != null) { 311 transition.setNameOverrides(nameOverrides); 312 final ArrayList<View> enteringViews = new ArrayList<>(); 313 scheduleRemoveTargets(transition, 314 enterTransition, enteringViews, exitTransition, exitingViews, 315 sharedElementTransition, sharedElementsIn); 316 scheduleTargetChange(sceneRoot, inFragment, nonExistentView, sharedElementsIn, 317 enterTransition, enteringViews, exitTransition, exitingViews); 318 319 TransitionManager.beginDelayedTransition(sceneRoot, transition); 320 } 321 } 322 323 /** 324 * Replace hide operations with visibility changes on the exiting views. Instead of making 325 * the entire fragment's view GONE, make each exiting view INVISIBLE. At the end of the 326 * transition, make the fragment's view GONE. 327 */ replaceHide(Transition exitTransition, Fragment exitingFragment, final ArrayList<View> exitingViews)328 private static void replaceHide(Transition exitTransition, Fragment exitingFragment, 329 final ArrayList<View> exitingViews) { 330 if (exitingFragment != null && exitTransition != null && exitingFragment.mAdded 331 && exitingFragment.mHidden && exitingFragment.mHiddenChanged) { 332 exitingFragment.setHideReplaced(true); 333 final View fragmentView = exitingFragment.getView(); 334 OneShotPreDrawListener.add(exitingFragment.mContainer, () -> { 335 setViewVisibility(exitingViews, View.INVISIBLE); 336 }); 337 exitTransition.addListener(new TransitionListenerAdapter() { 338 @Override 339 public void onTransitionEnd(Transition transition) { 340 transition.removeListener(this); 341 fragmentView.setVisibility(View.GONE); 342 setViewVisibility(exitingViews, View.VISIBLE); 343 } 344 }); 345 } 346 } 347 348 /** 349 * This method is used for fragment transitions for ordered transactions to change the 350 * enter and exit transition targets after the call to 351 * {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}. The exit transition 352 * must ensure that it does not target any Views and the enter transition must start targeting 353 * 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 */ scheduleTargetChange(final ViewGroup sceneRoot, final Fragment inFragment, final View nonExistentView, final ArrayList<View> sharedElementsIn, final Transition enterTransition, final ArrayList<View> enteringViews, final Transition exitTransition, final ArrayList<View> exitingViews)365 private static void scheduleTargetChange(final ViewGroup sceneRoot, 366 final Fragment inFragment, final View nonExistentView, 367 final ArrayList<View> sharedElementsIn, 368 final Transition enterTransition, final ArrayList<View> enteringViews, 369 final Transition exitTransition, final ArrayList<View> exitingViews) { 370 371 OneShotPreDrawListener.add(sceneRoot, () -> { 372 if (enterTransition != null) { 373 enterTransition.removeTarget(nonExistentView); 374 ArrayList<View> views = configureEnteringExitingViews( 375 enterTransition, inFragment, sharedElementsIn, nonExistentView); 376 enteringViews.addAll(views); 377 } 378 379 if (exitingViews != null) { 380 if (exitTransition != null) { 381 ArrayList<View> tempExiting = new ArrayList<>(); 382 tempExiting.add(nonExistentView); 383 replaceTargets(exitTransition, exitingViews, tempExiting); 384 } 385 exitingViews.clear(); 386 exitingViews.add(nonExistentView); 387 } 388 }); 389 } 390 391 /** 392 * Returns a TransitionSet containing the shared element transition. The wrapping TransitionSet 393 * targets all shared elements to ensure that no other Views are targeted. The shared element 394 * transition can then target any or all shared elements without worrying about accidentally 395 * targeting entering or exiting Views. 396 * 397 * @param inFragment The incoming fragment 398 * @param outFragment the outgoing fragment 399 * @param isPop True if this is a pop transaction or false if it is a normal (add) transaction. 400 * @return A TransitionSet wrapping the shared element transition or null if no such transition 401 * exists. 402 */ getSharedElementTransition(Fragment inFragment, Fragment outFragment, boolean isPop)403 private static TransitionSet getSharedElementTransition(Fragment inFragment, 404 Fragment outFragment, boolean isPop) { 405 if (inFragment == null || outFragment == null) { 406 return null; 407 } 408 Transition transition = cloneTransition(isPop 409 ? outFragment.getSharedElementReturnTransition() 410 : inFragment.getSharedElementEnterTransition()); 411 if (transition == null) { 412 return null; 413 } 414 TransitionSet transitionSet = new TransitionSet(); 415 transitionSet.addTransition(transition); 416 return transitionSet; 417 } 418 419 /** 420 * Returns a clone of the enter transition or null if no such transition exists. 421 */ getEnterTransition(Fragment inFragment, boolean isPop)422 private static Transition getEnterTransition(Fragment inFragment, boolean isPop) { 423 if (inFragment == null) { 424 return null; 425 } 426 return cloneTransition(isPop ? inFragment.getReenterTransition() : 427 inFragment.getEnterTransition()); 428 } 429 430 /** 431 * Returns a clone of the exit transition or null if no such transition exists. 432 */ getExitTransition(Fragment outFragment, boolean isPop)433 private static Transition getExitTransition(Fragment outFragment, boolean isPop) { 434 if (outFragment == null) { 435 return null; 436 } 437 return cloneTransition(isPop ? outFragment.getReturnTransition() : 438 outFragment.getExitTransition()); 439 } 440 441 /** 442 * Returns a clone of a transition or null if it is null 443 */ cloneTransition(Transition transition)444 private static Transition cloneTransition(Transition transition) { 445 if (transition != null) { 446 transition = transition.clone(); 447 } 448 return transition; 449 } 450 451 /** 452 * Configures the shared elements of an reordered fragment transaction's transition. 453 * This retrieves the shared elements of the outgoing and incoming fragments, maps the 454 * views, and sets up the epicenter on the transitions. 455 * <p> 456 * The epicenter of exit and shared element transitions is the first shared element 457 * in the outgoing fragment. The epicenter of the entering transition is the first shared 458 * element in the incoming fragment. 459 * 460 * @param sceneRoot The fragment container View 461 * @param nonExistentView A View that does not exist in the hierarchy. This is used to 462 * prevent transitions from acting on other Views when there is no 463 * other target. 464 * @param nameOverrides A map of the shared element names from the starting fragment to 465 * the final fragment's Views as given in 466 * {@link FragmentTransaction#addSharedElement(View, String)}. 467 * @param fragments A structure holding the transitioning fragments in this container. 468 * @param sharedElementsOut A list modified to contain the shared elements in the outgoing 469 * fragment 470 * @param sharedElementsIn A list modified to contain the shared elements in the incoming 471 * fragment 472 * @param enterTransition The transition used for entering Views, modified by applying the 473 * epicenter 474 * @param exitTransition The transition used for exiting Views, modified by applying the 475 * epicenter 476 * @return The shared element transition or null if no shared elements exist 477 */ configureSharedElementsReordered(final ViewGroup sceneRoot, final View nonExistentView, ArrayMap<String, String> nameOverrides, final FragmentContainerTransition fragments, final ArrayList<View> sharedElementsOut, final ArrayList<View> sharedElementsIn, final Transition enterTransition, final Transition exitTransition)478 private static TransitionSet configureSharedElementsReordered(final ViewGroup sceneRoot, 479 final View nonExistentView, ArrayMap<String, String> nameOverrides, 480 final FragmentContainerTransition fragments, 481 final ArrayList<View> sharedElementsOut, 482 final ArrayList<View> sharedElementsIn, 483 final Transition enterTransition, final Transition exitTransition) { 484 final Fragment inFragment = fragments.lastIn; 485 final Fragment outFragment = fragments.firstOut; 486 if (inFragment != null) { 487 inFragment.getView().setVisibility(View.VISIBLE); 488 } 489 if (inFragment == null || outFragment == null) { 490 return null; // no shared element without a fragment 491 } 492 493 final boolean inIsPop = fragments.lastInIsPop; 494 TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null 495 : getSharedElementTransition(inFragment, outFragment, inIsPop); 496 497 ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides, 498 sharedElementTransition, fragments); 499 500 ArrayMap<String, View> inSharedElements = captureInSharedElements(nameOverrides, 501 sharedElementTransition, fragments); 502 503 if (nameOverrides.isEmpty()) { 504 sharedElementTransition = null; 505 if (outSharedElements != null) { 506 outSharedElements.clear(); 507 } 508 if (inSharedElements != null) { 509 inSharedElements.clear(); 510 } 511 } else { 512 addSharedElementsWithMatchingNames(sharedElementsOut, outSharedElements, 513 nameOverrides.keySet()); 514 addSharedElementsWithMatchingNames(sharedElementsIn, inSharedElements, 515 nameOverrides.values()); 516 } 517 518 if (enterTransition == null && exitTransition == null && sharedElementTransition == null) { 519 // don't call onSharedElementStart/End since there is no transition 520 return null; 521 } 522 523 callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true); 524 525 final Rect epicenter; 526 final View epicenterView; 527 if (sharedElementTransition != null) { 528 sharedElementsIn.add(nonExistentView); 529 setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut); 530 final boolean outIsPop = fragments.firstOutIsPop; 531 final BackStackRecord outTransaction = fragments.firstOutTransaction; 532 setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop, 533 outTransaction); 534 epicenter = new Rect(); 535 epicenterView = getInEpicenterView(inSharedElements, fragments, 536 enterTransition, inIsPop); 537 if (epicenterView != null) { 538 enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() { 539 @Override 540 public Rect onGetEpicenter(Transition transition) { 541 return epicenter; 542 } 543 }); 544 } 545 } else { 546 epicenter = null; 547 epicenterView = null; 548 } 549 550 OneShotPreDrawListener.add(sceneRoot, () -> { 551 callSharedElementStartEnd(inFragment, outFragment, inIsPop, 552 inSharedElements, false); 553 if (epicenterView != null) { 554 epicenterView.getBoundsOnScreen(epicenter); 555 } 556 }); 557 return sharedElementTransition; 558 } 559 560 /** 561 * Add Views from sharedElements into views that have the transitionName in the 562 * nameOverridesSet. 563 * 564 * @param views Views list to add shared elements to 565 * @param sharedElements List of shared elements 566 * @param nameOverridesSet The transition names for all views to be copied from 567 * sharedElements to views. 568 */ addSharedElementsWithMatchingNames(ArrayList<View> views, ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet)569 private static void addSharedElementsWithMatchingNames(ArrayList<View> views, 570 ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet) { 571 for (int i = sharedElements.size() - 1; i >= 0; i--) { 572 View view = sharedElements.valueAt(i); 573 if (view != null && nameOverridesSet.contains(view.getTransitionName())) { 574 views.add(view); 575 } 576 } 577 } 578 579 /** 580 * Configures the shared elements of an ordered fragment transaction's transition. 581 * This retrieves the shared elements of the incoming fragments, and schedules capturing 582 * the incoming fragment's shared elements. It also maps the views, and sets up the epicenter 583 * on the transitions. 584 * <p> 585 * The epicenter of exit and shared element transitions is the first shared element 586 * in the outgoing fragment. The epicenter of the entering transition is the first shared 587 * element in the incoming fragment. 588 * 589 * @param sceneRoot The fragment container View 590 * @param nonExistentView A View that does not exist in the hierarchy. This is used to 591 * prevent transitions from acting on other Views when there is no 592 * other target. 593 * @param nameOverrides A map of the shared element names from the starting fragment to 594 * the final fragment's Views as given in 595 * {@link FragmentTransaction#addSharedElement(View, String)}. 596 * @param fragments A structure holding the transitioning fragments in this container. 597 * @param sharedElementsOut A list modified to contain the shared elements in the outgoing 598 * fragment 599 * @param sharedElementsIn A list modified to contain the shared elements in the incoming 600 * fragment 601 * @param enterTransition The transition used for entering Views, modified by applying the 602 * epicenter 603 * @param exitTransition The transition used for exiting Views, modified by applying the 604 * epicenter 605 * @return The shared element transition or null if no shared elements exist 606 */ configureSharedElementsOrdered(final ViewGroup sceneRoot, final View nonExistentView, ArrayMap<String, String> nameOverrides, final FragmentContainerTransition fragments, final ArrayList<View> sharedElementsOut, final ArrayList<View> sharedElementsIn, final Transition enterTransition, final Transition exitTransition)607 private static TransitionSet configureSharedElementsOrdered(final ViewGroup sceneRoot, 608 final View nonExistentView, ArrayMap<String, String> nameOverrides, 609 final FragmentContainerTransition fragments, 610 final ArrayList<View> sharedElementsOut, 611 final ArrayList<View> sharedElementsIn, 612 final Transition enterTransition, final Transition exitTransition) { 613 final Fragment inFragment = fragments.lastIn; 614 final Fragment outFragment = fragments.firstOut; 615 616 if (inFragment == null || outFragment == null) { 617 return null; // no transition 618 } 619 620 final boolean inIsPop = fragments.lastInIsPop; 621 TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null 622 : getSharedElementTransition(inFragment, outFragment, inIsPop); 623 624 ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides, 625 sharedElementTransition, fragments); 626 627 if (nameOverrides.isEmpty()) { 628 sharedElementTransition = null; 629 } else { 630 sharedElementsOut.addAll(outSharedElements.values()); 631 } 632 633 if (enterTransition == null && exitTransition == null && sharedElementTransition == null) { 634 // don't call onSharedElementStart/End since there is no transition 635 return null; 636 } 637 638 callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true); 639 640 final Rect inEpicenter; 641 if (sharedElementTransition != null) { 642 inEpicenter = new Rect(); 643 setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut); 644 final boolean outIsPop = fragments.firstOutIsPop; 645 final BackStackRecord outTransaction = fragments.firstOutTransaction; 646 setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop, 647 outTransaction); 648 if (enterTransition != null) { 649 enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() { 650 @Override 651 public Rect onGetEpicenter(Transition transition) { 652 if (inEpicenter.isEmpty()) { 653 return null; 654 } 655 return inEpicenter; 656 } 657 }); 658 } 659 } else { 660 inEpicenter = null; 661 } 662 663 TransitionSet finalSharedElementTransition = sharedElementTransition; 664 665 OneShotPreDrawListener.add(sceneRoot, () -> { 666 ArrayMap<String, View> inSharedElements = captureInSharedElements( 667 nameOverrides, finalSharedElementTransition, fragments); 668 669 if (inSharedElements != null) { 670 sharedElementsIn.addAll(inSharedElements.values()); 671 sharedElementsIn.add(nonExistentView); 672 } 673 674 callSharedElementStartEnd(inFragment, outFragment, inIsPop, 675 inSharedElements, false); 676 if (finalSharedElementTransition != null) { 677 finalSharedElementTransition.getTargets().clear(); 678 finalSharedElementTransition.getTargets().addAll(sharedElementsIn); 679 replaceTargets(finalSharedElementTransition, sharedElementsOut, 680 sharedElementsIn); 681 682 final View inEpicenterView = getInEpicenterView(inSharedElements, 683 fragments, enterTransition, inIsPop); 684 if (inEpicenterView != null) { 685 inEpicenterView.getBoundsOnScreen(inEpicenter); 686 } 687 } 688 }); 689 return sharedElementTransition; 690 } 691 692 /** 693 * Finds the shared elements in the outgoing fragment. It also calls 694 * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control 695 * of the shared element mapping. {@code nameOverrides} is updated to match the 696 * actual transition name of the mapped shared elements. 697 * 698 * @param nameOverrides A map of the shared element names from the starting fragment to 699 * the final fragment's Views as given in 700 * {@link FragmentTransaction#addSharedElement(View, String)}. 701 * @param sharedElementTransition The shared element transition 702 * @param fragments A structure holding the transitioning fragments in this container. 703 * @return The mapping of shared element names to the Views in the hierarchy or null 704 * if there is no shared element transition. 705 */ captureOutSharedElements( ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition, FragmentContainerTransition fragments)706 private static ArrayMap<String, View> captureOutSharedElements( 707 ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition, 708 FragmentContainerTransition fragments) { 709 if (nameOverrides.isEmpty() || sharedElementTransition == null) { 710 nameOverrides.clear(); 711 return null; 712 } 713 final Fragment outFragment = fragments.firstOut; 714 final ArrayMap<String, View> outSharedElements = new ArrayMap<>(); 715 outFragment.getView().findNamedViews(outSharedElements); 716 717 final SharedElementCallback sharedElementCallback; 718 final ArrayList<String> names; 719 final BackStackRecord outTransaction = fragments.firstOutTransaction; 720 if (fragments.firstOutIsPop) { 721 sharedElementCallback = outFragment.getEnterTransitionCallback(); 722 names = outTransaction.mSharedElementTargetNames; 723 } else { 724 sharedElementCallback = outFragment.getExitTransitionCallback(); 725 names = outTransaction.mSharedElementSourceNames; 726 } 727 728 outSharedElements.retainAll(names); 729 if (sharedElementCallback != null) { 730 sharedElementCallback.onMapSharedElements(names, outSharedElements); 731 for (int i = names.size() - 1; i >= 0; i--) { 732 String name = names.get(i); 733 View view = outSharedElements.get(name); 734 if (view == null) { 735 nameOverrides.remove(name); 736 } else if (!name.equals(view.getTransitionName())) { 737 String targetValue = nameOverrides.remove(name); 738 nameOverrides.put(view.getTransitionName(), targetValue); 739 } 740 } 741 } else { 742 nameOverrides.retainAll(outSharedElements.keySet()); 743 } 744 return outSharedElements; 745 } 746 747 /** 748 * Finds the shared elements in the incoming fragment. It also calls 749 * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control 750 * of the shared element mapping. {@code nameOverrides} is updated to match the 751 * actual transition name of the mapped shared elements. 752 * 753 * @param nameOverrides A map of the shared element names from the starting fragment to 754 * the final fragment's Views as given in 755 * {@link FragmentTransaction#addSharedElement(View, String)}. 756 * @param sharedElementTransition The shared element transition 757 * @param fragments A structure holding the transitioning fragments in this container. 758 * @return The mapping of shared element names to the Views in the hierarchy or null 759 * if there is no shared element transition. 760 */ captureInSharedElements( ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition, FragmentContainerTransition fragments)761 private static ArrayMap<String, View> captureInSharedElements( 762 ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition, 763 FragmentContainerTransition fragments) { 764 Fragment inFragment = fragments.lastIn; 765 final View fragmentView = inFragment.getView(); 766 if (nameOverrides.isEmpty() || sharedElementTransition == null || fragmentView == null) { 767 nameOverrides.clear(); 768 return null; 769 } 770 final ArrayMap<String, View> inSharedElements = new ArrayMap<>(); 771 fragmentView.findNamedViews(inSharedElements); 772 773 final SharedElementCallback sharedElementCallback; 774 final ArrayList<String> names; 775 final BackStackRecord inTransaction = fragments.lastInTransaction; 776 if (fragments.lastInIsPop) { 777 sharedElementCallback = inFragment.getExitTransitionCallback(); 778 names = inTransaction.mSharedElementSourceNames; 779 } else { 780 sharedElementCallback = inFragment.getEnterTransitionCallback(); 781 names = inTransaction.mSharedElementTargetNames; 782 } 783 784 if (names != null) { 785 inSharedElements.retainAll(names); 786 } 787 if (names != null && sharedElementCallback != null) { 788 sharedElementCallback.onMapSharedElements(names, inSharedElements); 789 for (int i = names.size() - 1; i >= 0; i--) { 790 String name = names.get(i); 791 View view = inSharedElements.get(name); 792 if (view == null) { 793 String key = findKeyForValue(nameOverrides, name); 794 if (key != null) { 795 nameOverrides.remove(key); 796 } 797 } else if (!name.equals(view.getTransitionName())) { 798 String key = findKeyForValue(nameOverrides, name); 799 if (key != null) { 800 nameOverrides.put(key, view.getTransitionName()); 801 } 802 } 803 } 804 } else { 805 retainValues(nameOverrides, inSharedElements); 806 } 807 return inSharedElements; 808 } 809 810 /** 811 * Utility to find the String key in {@code map} that maps to {@code value}. 812 */ findKeyForValue(ArrayMap<String, String> map, String value)813 private static String findKeyForValue(ArrayMap<String, String> map, String value) { 814 final int numElements = map.size(); 815 for (int i = 0; i < numElements; i++) { 816 if (value.equals(map.valueAt(i))) { 817 return map.keyAt(i); 818 } 819 } 820 return null; 821 } 822 823 /** 824 * Returns the View in the incoming Fragment that should be used as the epicenter. 825 * 826 * @param inSharedElements The mapping of shared element names to Views in the 827 * incoming fragment. 828 * @param fragments A structure holding the transitioning fragments in this container. 829 * @param enterTransition The transition used for the incoming Fragment's views 830 * @param inIsPop Is the incoming fragment being added as a pop transaction? 831 */ getInEpicenterView(ArrayMap<String, View> inSharedElements, FragmentContainerTransition fragments, Transition enterTransition, boolean inIsPop)832 private static View getInEpicenterView(ArrayMap<String, View> inSharedElements, 833 FragmentContainerTransition fragments, 834 Transition enterTransition, boolean inIsPop) { 835 BackStackRecord inTransaction = fragments.lastInTransaction; 836 if (enterTransition != null && inSharedElements != null 837 && inTransaction.mSharedElementSourceNames != null 838 && !inTransaction.mSharedElementSourceNames.isEmpty()) { 839 final String targetName = inIsPop 840 ? inTransaction.mSharedElementSourceNames.get(0) 841 : inTransaction.mSharedElementTargetNames.get(0); 842 return inSharedElements.get(targetName); 843 } 844 return null; 845 } 846 847 /** 848 * Sets the epicenter for the exit transition. 849 * 850 * @param sharedElementTransition The shared element transition 851 * @param exitTransition The transition for the outgoing fragment's views 852 * @param outSharedElements Shared elements in the outgoing fragment 853 * @param outIsPop Is the outgoing fragment being removed as a pop transaction? 854 * @param outTransaction The transaction that caused the fragment to be removed. 855 */ setOutEpicenter(TransitionSet sharedElementTransition, Transition exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop, BackStackRecord outTransaction)856 private static void setOutEpicenter(TransitionSet sharedElementTransition, 857 Transition exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop, 858 BackStackRecord outTransaction) { 859 if (outTransaction.mSharedElementSourceNames != null && 860 !outTransaction.mSharedElementSourceNames.isEmpty()) { 861 final String sourceName = outIsPop 862 ? outTransaction.mSharedElementTargetNames.get(0) 863 : outTransaction.mSharedElementSourceNames.get(0); 864 final View outEpicenterView = outSharedElements.get(sourceName); 865 setEpicenter(sharedElementTransition, outEpicenterView); 866 867 if (exitTransition != null) { 868 setEpicenter(exitTransition, outEpicenterView); 869 } 870 } 871 } 872 873 /** 874 * Sets a transition epicenter to the rectangle of a given View. 875 */ setEpicenter(Transition transition, View view)876 private static void setEpicenter(Transition transition, View view) { 877 if (view != null) { 878 final Rect epicenter = new Rect(); 879 view.getBoundsOnScreen(epicenter); 880 881 transition.setEpicenterCallback(new Transition.EpicenterCallback() { 882 @Override 883 public Rect onGetEpicenter(Transition transition) { 884 return epicenter; 885 } 886 }); 887 } 888 } 889 890 /** 891 * A utility to retain only the mappings in {@code nameOverrides} that have a value 892 * that has a key in {@code namedViews}. This is a useful equivalent to 893 * {@link ArrayMap#retainAll(Collection)} for values. 894 */ retainValues(ArrayMap<String, String> nameOverrides, ArrayMap<String, View> namedViews)895 private static void retainValues(ArrayMap<String, String> nameOverrides, 896 ArrayMap<String, View> namedViews) { 897 for (int i = nameOverrides.size() - 1; i >= 0; i--) { 898 final String targetName = nameOverrides.valueAt(i); 899 if (!namedViews.containsKey(targetName)) { 900 nameOverrides.removeAt(i); 901 } 902 } 903 } 904 905 /** 906 * Calls the {@link SharedElementCallback#onSharedElementStart(List, List, List)} or 907 * {@link SharedElementCallback#onSharedElementEnd(List, List, List)} on the appropriate 908 * incoming or outgoing fragment. 909 * 910 * @param inFragment The incoming fragment 911 * @param outFragment The outgoing fragment 912 * @param isPop Is the incoming fragment part of a pop transaction? 913 * @param sharedElements The shared element Views 914 * @param isStart Call the start or end call on the SharedElementCallback 915 */ callSharedElementStartEnd(Fragment inFragment, Fragment outFragment, boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart)916 private static void callSharedElementStartEnd(Fragment inFragment, Fragment outFragment, 917 boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart) { 918 SharedElementCallback sharedElementCallback = isPop 919 ? outFragment.getEnterTransitionCallback() 920 : inFragment.getEnterTransitionCallback(); 921 if (sharedElementCallback != null) { 922 ArrayList<View> views = new ArrayList<>(); 923 ArrayList<String> names = new ArrayList<>(); 924 final int count = sharedElements == null ? 0 : sharedElements.size(); 925 for (int i = 0; i < count; i++) { 926 names.add(sharedElements.keyAt(i)); 927 views.add(sharedElements.valueAt(i)); 928 } 929 if (isStart) { 930 sharedElementCallback.onSharedElementStart(names, views, null); 931 } else { 932 sharedElementCallback.onSharedElementEnd(names, views, null); 933 } 934 } 935 } 936 937 /** 938 * Finds all children of the shared elements and sets the wrapping TransitionSet 939 * targets to point to those. It also limits transitions that have no targets to the 940 * specific shared elements. This allows developers to target child views of the 941 * shared elements specifically, but this doesn't happen by default. 942 */ setSharedElementTargets(TransitionSet transition, View nonExistentView, ArrayList<View> sharedViews)943 private static void setSharedElementTargets(TransitionSet transition, 944 View nonExistentView, ArrayList<View> sharedViews) { 945 final List<View> views = transition.getTargets(); 946 views.clear(); 947 final int count = sharedViews.size(); 948 for (int i = 0; i < count; i++) { 949 final View view = sharedViews.get(i); 950 bfsAddViewChildren(views, view); 951 } 952 views.add(nonExistentView); 953 sharedViews.add(nonExistentView); 954 addTargets(transition, sharedViews); 955 } 956 957 /** 958 * Uses a breadth-first scheme to add startView and all of its children to views. 959 * It won't add a child if it is already in views. 960 */ bfsAddViewChildren(final List<View> views, final View startView)961 private static void bfsAddViewChildren(final List<View> views, final View startView) { 962 final int startIndex = views.size(); 963 if (containedBeforeIndex(views, startView, startIndex)) { 964 return; // This child is already in the list, so all its children are also. 965 } 966 views.add(startView); 967 for (int index = startIndex; index < views.size(); index++) { 968 final View view = views.get(index); 969 if (view instanceof ViewGroup) { 970 ViewGroup viewGroup = (ViewGroup) view; 971 final int childCount = viewGroup.getChildCount(); 972 for (int childIndex = 0; childIndex < childCount; childIndex++) { 973 final View child = viewGroup.getChildAt(childIndex); 974 if (!containedBeforeIndex(views, child, startIndex)) { 975 views.add(child); 976 } 977 } 978 } 979 } 980 } 981 982 /** 983 * Does a linear search through views for view, limited to maxIndex. 984 */ containedBeforeIndex(final List<View> views, final View view, final int maxIndex)985 private static boolean containedBeforeIndex(final List<View> views, final View view, 986 final int maxIndex) { 987 for (int i = 0; i < maxIndex; i++) { 988 if (views.get(i) == view) { 989 return true; 990 } 991 } 992 return false; 993 } 994 995 /** 996 * After the transition has started, remove all targets that we added to the transitions 997 * so that the transitions are left in a clean state. 998 */ scheduleRemoveTargets(final Transition overalTransition, final Transition enterTransition, final ArrayList<View> enteringViews, final Transition exitTransition, final ArrayList<View> exitingViews, final TransitionSet sharedElementTransition, final ArrayList<View> sharedElementsIn)999 private static void scheduleRemoveTargets(final Transition overalTransition, 1000 final Transition enterTransition, final ArrayList<View> enteringViews, 1001 final Transition exitTransition, final ArrayList<View> exitingViews, 1002 final TransitionSet sharedElementTransition, final ArrayList<View> sharedElementsIn) { 1003 overalTransition.addListener(new TransitionListenerAdapter() { 1004 @Override 1005 public void onTransitionStart(Transition transition) { 1006 if (enterTransition != null) { 1007 replaceTargets(enterTransition, enteringViews, null); 1008 } 1009 if (exitTransition != null) { 1010 replaceTargets(exitTransition, exitingViews, null); 1011 } 1012 if (sharedElementTransition != null) { 1013 replaceTargets(sharedElementTransition, sharedElementsIn, null); 1014 } 1015 } 1016 1017 @Override 1018 public void onTransitionEnd(Transition transition) { 1019 transition.removeListener(this); 1020 } 1021 }); 1022 } 1023 1024 /** 1025 * This method removes the views from transitions that target ONLY those views and 1026 * replaces them with the new targets list. 1027 * The views list should match those added in addTargets and should contain 1028 * one view that is not in the view hierarchy (state.nonExistentView). 1029 */ replaceTargets(Transition transition, ArrayList<View> oldTargets, ArrayList<View> newTargets)1030 public static void replaceTargets(Transition transition, ArrayList<View> oldTargets, 1031 ArrayList<View> newTargets) { 1032 if (transition instanceof TransitionSet) { 1033 TransitionSet set = (TransitionSet) transition; 1034 int numTransitions = set.getTransitionCount(); 1035 for (int i = 0; i < numTransitions; i++) { 1036 Transition child = set.getTransitionAt(i); 1037 replaceTargets(child, oldTargets, newTargets); 1038 } 1039 } else if (!hasSimpleTarget(transition)) { 1040 List<View> targets = transition.getTargets(); 1041 if (targets != null && targets.size() == oldTargets.size() && 1042 targets.containsAll(oldTargets)) { 1043 // We have an exact match. We must have added these earlier in addTargets 1044 final int targetCount = newTargets == null ? 0 : newTargets.size(); 1045 for (int i = 0; i < targetCount; i++) { 1046 transition.addTarget(newTargets.get(i)); 1047 } 1048 for (int i = oldTargets.size() - 1; i >= 0; i--) { 1049 transition.removeTarget(oldTargets.get(i)); 1050 } 1051 } 1052 } 1053 } 1054 1055 /** 1056 * This method adds views as targets to the transition, but only if the transition 1057 * doesn't already have a target. It is best for views to contain one View object 1058 * that does not exist in the view hierarchy (state.nonExistentView) so that 1059 * when they are removed later, a list match will suffice to remove the targets. 1060 * Otherwise, if you happened to have targeted the exact views for the transition, 1061 * the replaceTargets call will remove them unexpectedly. 1062 */ addTargets(Transition transition, ArrayList<View> views)1063 public static void addTargets(Transition transition, ArrayList<View> views) { 1064 if (transition == null) { 1065 return; 1066 } 1067 if (transition instanceof TransitionSet) { 1068 TransitionSet set = (TransitionSet) transition; 1069 int numTransitions = set.getTransitionCount(); 1070 for (int i = 0; i < numTransitions; i++) { 1071 Transition child = set.getTransitionAt(i); 1072 addTargets(child, views); 1073 } 1074 } else if (!hasSimpleTarget(transition)) { 1075 List<View> targets = transition.getTargets(); 1076 if (isNullOrEmpty(targets)) { 1077 // We can just add the target views 1078 int numViews = views.size(); 1079 for (int i = 0; i < numViews; i++) { 1080 transition.addTarget(views.get(i)); 1081 } 1082 } 1083 } 1084 } 1085 1086 /** 1087 * Returns true if there are any targets based on ID, transition or type. 1088 */ hasSimpleTarget(Transition transition)1089 private static boolean hasSimpleTarget(Transition transition) { 1090 return !isNullOrEmpty(transition.getTargetIds()) || 1091 !isNullOrEmpty(transition.getTargetNames()) || 1092 !isNullOrEmpty(transition.getTargetTypes()); 1093 } 1094 1095 /** 1096 * Simple utility to detect if a list is null or has no elements. 1097 */ isNullOrEmpty(List list)1098 private static boolean isNullOrEmpty(List list) { 1099 return list == null || list.isEmpty(); 1100 } 1101 configureEnteringExitingViews(Transition transition, Fragment fragment, ArrayList<View> sharedElements, View nonExistentView)1102 private static ArrayList<View> configureEnteringExitingViews(Transition transition, 1103 Fragment fragment, ArrayList<View> sharedElements, View nonExistentView) { 1104 ArrayList<View> viewList = null; 1105 if (transition != null) { 1106 viewList = new ArrayList<>(); 1107 View root = fragment.getView(); 1108 if (root != null) { 1109 root.captureTransitioningViews(viewList); 1110 } 1111 if (sharedElements != null) { 1112 viewList.removeAll(sharedElements); 1113 } 1114 if (!viewList.isEmpty()) { 1115 viewList.add(nonExistentView); 1116 addTargets(transition, viewList); 1117 } 1118 } 1119 return viewList; 1120 } 1121 1122 /** 1123 * Sets the visibility of all Views in {@code views} to {@code visibility}. 1124 */ setViewVisibility(ArrayList<View> views, @View.Visibility int visibility)1125 private static void setViewVisibility(ArrayList<View> views, @View.Visibility int visibility) { 1126 if (views == null) { 1127 return; 1128 } 1129 for (int i = views.size() - 1; i >= 0; i--) { 1130 final View view = views.get(i); 1131 view.setVisibility(visibility); 1132 } 1133 } 1134 1135 /** 1136 * Merges exit, shared element, and enter transitions so that they act together or 1137 * sequentially as defined in the fragments. 1138 */ mergeTransitions(Transition enterTransition, Transition exitTransition, Transition sharedElementTransition, Fragment inFragment, boolean isPop)1139 private static Transition mergeTransitions(Transition enterTransition, 1140 Transition exitTransition, Transition sharedElementTransition, Fragment inFragment, 1141 boolean isPop) { 1142 boolean overlap = true; 1143 if (enterTransition != null && exitTransition != null && inFragment != null) { 1144 overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() : 1145 inFragment.getAllowEnterTransitionOverlap(); 1146 } 1147 1148 // Wrap the transitions. Explicit targets like in enter and exit will cause the 1149 // views to be targeted regardless of excluded views. If that happens, then the 1150 // excluded fragments views (hidden fragments) will still be in the transition. 1151 1152 Transition transition; 1153 if (overlap) { 1154 // Regular transition -- do it all together 1155 TransitionSet transitionSet = new TransitionSet(); 1156 if (enterTransition != null) { 1157 transitionSet.addTransition(enterTransition); 1158 } 1159 if (exitTransition != null) { 1160 transitionSet.addTransition(exitTransition); 1161 } 1162 if (sharedElementTransition != null) { 1163 transitionSet.addTransition(sharedElementTransition); 1164 } 1165 transition = transitionSet; 1166 } else { 1167 // First do exit, then enter, but allow shared element transition to happen 1168 // during both. 1169 Transition staggered = null; 1170 if (exitTransition != null && enterTransition != null) { 1171 staggered = new TransitionSet() 1172 .addTransition(exitTransition) 1173 .addTransition(enterTransition) 1174 .setOrdering(TransitionSet.ORDERING_SEQUENTIAL); 1175 } else if (exitTransition != null) { 1176 staggered = exitTransition; 1177 } else if (enterTransition != null) { 1178 staggered = enterTransition; 1179 } 1180 if (sharedElementTransition != null) { 1181 TransitionSet together = new TransitionSet(); 1182 if (staggered != null) { 1183 together.addTransition(staggered); 1184 } 1185 together.addTransition(sharedElementTransition); 1186 transition = together; 1187 } else { 1188 transition = staggered; 1189 } 1190 } 1191 return transition; 1192 } 1193 1194 /** 1195 * Finds the first removed fragment and last added fragments when going forward. 1196 * If none of the fragments have transitions, then both lists will be empty. 1197 * 1198 * @param transitioningFragments Keyed on the container ID, the first fragments to be removed, 1199 * and last fragments to be added. This will be modified by 1200 * this method. 1201 */ calculateFragments(BackStackRecord transaction, SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered)1202 public static void calculateFragments(BackStackRecord transaction, 1203 SparseArray<FragmentContainerTransition> transitioningFragments, 1204 boolean isReordered) { 1205 final int numOps = transaction.mOps.size(); 1206 for (int opNum = 0; opNum < numOps; opNum++) { 1207 final BackStackRecord.Op op = transaction.mOps.get(opNum); 1208 addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered); 1209 } 1210 } 1211 1212 /** 1213 * Finds the first removed fragment and last added fragments when popping the back stack. 1214 * If none of the fragments have transitions, then both lists will be empty. 1215 * 1216 * @param transitioningFragments Keyed on the container ID, the first fragments to be removed, 1217 * and last fragments to be added. This will be modified by 1218 * this method. 1219 */ calculatePopFragments(BackStackRecord transaction, SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered)1220 public static void calculatePopFragments(BackStackRecord transaction, 1221 SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered) { 1222 if (!transaction.mManager.mContainer.onHasView()) { 1223 return; // nothing to see, so no transitions 1224 } 1225 final int numOps = transaction.mOps.size(); 1226 for (int opNum = numOps - 1; opNum >= 0; opNum--) { 1227 final BackStackRecord.Op op = transaction.mOps.get(opNum); 1228 addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered); 1229 } 1230 } 1231 1232 /** 1233 * Examines the {@code command} and may set the first out or last in fragment for the fragment's 1234 * container. 1235 * 1236 * @param transaction The executing transaction 1237 * @param op The operation being run. 1238 * @param transitioningFragments A structure holding the first in and last out fragments 1239 * for each fragment container. 1240 * @param isPop Is the operation a pop? 1241 * @param isReorderedTransaction True if the operations have been partially executed and the 1242 * added fragments have Views in the hierarchy or false if the 1243 * operations haven't been executed yet. 1244 */ 1245 @SuppressWarnings("ReferenceEquality") addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op, SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop, boolean isReorderedTransaction)1246 private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op, 1247 SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop, 1248 boolean isReorderedTransaction) { 1249 final Fragment fragment = op.fragment; 1250 if (fragment == null) { 1251 return; // no fragment, no transition 1252 } 1253 final int containerId = fragment.mContainerId; 1254 if (containerId == 0) { 1255 return; // no container, no transition 1256 } 1257 final int command = isPop ? INVERSE_OPS[op.cmd] : op.cmd; 1258 boolean setLastIn = false; 1259 boolean wasRemoved = false; 1260 boolean setFirstOut = false; 1261 boolean wasAdded = false; 1262 switch (command) { 1263 case BackStackRecord.OP_SHOW: 1264 if (isReorderedTransaction) { 1265 setLastIn = fragment.mHiddenChanged && !fragment.mHidden && 1266 fragment.mAdded; 1267 } else { 1268 setLastIn = fragment.mHidden; 1269 } 1270 wasAdded = true; 1271 break; 1272 case BackStackRecord.OP_ADD: 1273 case BackStackRecord.OP_ATTACH: 1274 if (isReorderedTransaction) { 1275 setLastIn = fragment.mIsNewlyAdded; 1276 } else { 1277 setLastIn = !fragment.mAdded && !fragment.mHidden; 1278 } 1279 wasAdded = true; 1280 break; 1281 case BackStackRecord.OP_HIDE: 1282 if (isReorderedTransaction) { 1283 setFirstOut = fragment.mHiddenChanged && fragment.mAdded && 1284 fragment.mHidden; 1285 } else { 1286 setFirstOut = fragment.mAdded && !fragment.mHidden; 1287 } 1288 wasRemoved = true; 1289 break; 1290 case BackStackRecord.OP_REMOVE: 1291 case BackStackRecord.OP_DETACH: 1292 if (isReorderedTransaction) { 1293 setFirstOut = !fragment.mAdded && fragment.mView != null 1294 && fragment.mView.getVisibility() == View.VISIBLE 1295 && fragment.mView.getTransitionAlpha() > 0; 1296 } else { 1297 setFirstOut = fragment.mAdded && !fragment.mHidden; 1298 } 1299 wasRemoved = true; 1300 break; 1301 } 1302 FragmentContainerTransition containerTransition = transitioningFragments.get(containerId); 1303 if (setLastIn) { 1304 containerTransition = 1305 ensureContainer(containerTransition, transitioningFragments, containerId); 1306 containerTransition.lastIn = fragment; 1307 containerTransition.lastInIsPop = isPop; 1308 containerTransition.lastInTransaction = transaction; 1309 } 1310 if (!isReorderedTransaction && wasAdded) { 1311 if (containerTransition != null && containerTransition.firstOut == fragment) { 1312 containerTransition.firstOut = null; 1313 } 1314 1315 /* 1316 * Ensure that fragments that are entering are at least at the CREATED state 1317 * so that they may load Transitions using TransitionInflater. 1318 */ 1319 FragmentManagerImpl manager = transaction.mManager; 1320 if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED && 1321 manager.mHost.getContext().getApplicationInfo().targetSdkVersion >= 1322 Build.VERSION_CODES.N && !transaction.mReorderingAllowed) { 1323 manager.makeActive(fragment); 1324 manager.moveToState(fragment, Fragment.CREATED, 0, 0, false); 1325 } 1326 } 1327 if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) { 1328 containerTransition = 1329 ensureContainer(containerTransition, transitioningFragments, containerId); 1330 containerTransition.firstOut = fragment; 1331 containerTransition.firstOutIsPop = isPop; 1332 containerTransition.firstOutTransaction = transaction; 1333 } 1334 1335 if (!isReorderedTransaction && wasRemoved && 1336 (containerTransition != null && containerTransition.lastIn == fragment)) { 1337 containerTransition.lastIn = null; 1338 } 1339 } 1340 1341 /** 1342 * Ensures that a FragmentContainerTransition has been added to the SparseArray. If so, 1343 * it returns the existing one. If not, one is created and added to the SparseArray and 1344 * returned. 1345 */ ensureContainer( FragmentContainerTransition containerTransition, SparseArray<FragmentContainerTransition> transitioningFragments, int containerId)1346 private static FragmentContainerTransition ensureContainer( 1347 FragmentContainerTransition containerTransition, 1348 SparseArray<FragmentContainerTransition> transitioningFragments, int containerId) { 1349 if (containerTransition == null) { 1350 containerTransition = new FragmentContainerTransition(); 1351 transitioningFragments.put(containerId, containerTransition); 1352 } 1353 return containerTransition; 1354 } 1355 1356 /** 1357 * Tracks the last fragment added and first fragment removed for fragment transitions. 1358 * This also tracks which fragments are changed by push or pop transactions. 1359 */ 1360 public static class FragmentContainerTransition { 1361 /** 1362 * The last fragment added/attached/shown in its container 1363 */ 1364 public Fragment lastIn; 1365 1366 /** 1367 * true when lastIn was added during a pop transaction or false if added with a push 1368 */ 1369 public boolean lastInIsPop; 1370 1371 /** 1372 * The transaction that included the last in fragment 1373 */ 1374 public BackStackRecord lastInTransaction; 1375 1376 /** 1377 * The first fragment with a View that was removed/detached/hidden in its container. 1378 */ 1379 public Fragment firstOut; 1380 1381 /** 1382 * true when firstOut was removed during a pop transaction or false otherwise 1383 */ 1384 public boolean firstOutIsPop; 1385 1386 /** 1387 * The transaction that included the first out fragment 1388 */ 1389 public BackStackRecord firstOutTransaction; 1390 } 1391 } 1392