1/* 2 * Copyright (C) 2014 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 17package android.support.v4.app; 18 19import android.graphics.Rect; 20import android.transition.Transition; 21import android.transition.TransitionManager; 22import android.transition.TransitionSet; 23import android.view.View; 24import android.view.ViewGroup; 25import android.view.ViewTreeObserver; 26 27import java.util.ArrayList; 28import java.util.List; 29import java.util.Map; 30 31class FragmentTransitionCompat21 { 32 public static String getTransitionName(View view) { 33 return view.getTransitionName(); 34 } 35 36 public static Object cloneTransition(Object transition) { 37 if (transition != null) { 38 transition = ((Transition)transition).clone(); 39 } 40 return transition; 41 } 42 43 public static Object captureExitingViews(Object exitTransition, View root, 44 ArrayList<View> viewList, Map<String, View> namedViews, View nonExistentView) { 45 if (exitTransition != null) { 46 captureTransitioningViews(viewList, root); 47 if (namedViews != null) { 48 viewList.removeAll(namedViews.values()); 49 } 50 if (viewList.isEmpty()) { 51 exitTransition = null; 52 } else { 53 viewList.add(nonExistentView); 54 addTargets((Transition) exitTransition, viewList); 55 } 56 } 57 return exitTransition; 58 } 59 60 public static void excludeTarget(Object transitionObject, View view, boolean exclude) { 61 Transition transition = (Transition) transitionObject; 62 transition.excludeTarget(view, exclude); 63 } 64 65 public static void beginDelayedTransition(ViewGroup sceneRoot, Object transitionObject) { 66 Transition transition = (Transition) transitionObject; 67 TransitionManager.beginDelayedTransition(sceneRoot, transition); 68 } 69 70 public static void setEpicenter(Object transitionObject, View view) { 71 Transition transition = (Transition) transitionObject; 72 final Rect epicenter = getBoundsOnScreen(view); 73 74 transition.setEpicenterCallback(new Transition.EpicenterCallback() { 75 @Override 76 public Rect onGetEpicenter(Transition transition) { 77 return epicenter; 78 } 79 }); 80 } 81 82 public static Object wrapSharedElementTransition(Object transitionObj) { 83 if (transitionObj == null) { 84 return null; 85 } 86 Transition transition = (Transition) transitionObj; 87 if (transition == null) { 88 return null; 89 } 90 TransitionSet transitionSet = new TransitionSet(); 91 transitionSet.addTransition(transition); 92 return transitionSet; 93 } 94 95 private static void excludeViews(Transition transition, Transition fromTransition, 96 ArrayList<View> views, boolean exclude) { 97 if (transition != null) { 98 final int viewCount = fromTransition == null ? 0 : views.size(); 99 for (int i = 0; i < viewCount; i++) { 100 transition.excludeTarget(views.get(i), exclude); 101 } 102 } 103 } 104 105 /** 106 * Exclude (or remove the exclude) of shared element views from the enter and exit transitions. 107 * 108 * @param enterTransitionObj The enter transition 109 * @param exitTransitionObj The exit transition 110 * @param sharedElementTransitionObj The shared element transition 111 * @param views The shared element target views. 112 * @param exclude <code>true</code> to exclude or <code>false</code> to remove the excluded 113 * views. 114 */ 115 public static void excludeSharedElementViews(Object enterTransitionObj, 116 Object exitTransitionObj, Object sharedElementTransitionObj, ArrayList<View> views, 117 boolean exclude) { 118 Transition enterTransition = (Transition) enterTransitionObj; 119 Transition exitTransition = (Transition) exitTransitionObj; 120 Transition sharedElementTransition = (Transition) sharedElementTransitionObj; 121 excludeViews(enterTransition, sharedElementTransition, views, exclude); 122 excludeViews(exitTransition, sharedElementTransition, views, exclude); 123 } 124 125 /** 126 * Prepares the enter transition by adding a non-existent view to the transition's target list 127 * and setting it epicenter callback. By adding a non-existent view to the target list, 128 * we can prevent any view from being targeted at the beginning of the transition. 129 * We will add to the views before the end state of the transition is captured so that the 130 * views will appear. At the start of the transition, we clear the list of targets so that 131 * we can restore the state of the transition and use it again. 132 * 133 * <p>The shared element transition maps its shared elements immediately prior to 134 * capturing the final state of the Transition.</p> 135 */ 136 public static void addTransitionTargets(Object enterTransitionObject, 137 Object sharedElementTransitionObject, Object exitTransitionObject, final View container, 138 final ViewRetriever inFragment, final View nonExistentView, 139 EpicenterView epicenterView, final Map<String, String> nameOverrides, 140 final ArrayList<View> enteringViews, final ArrayList<View> exitingViews, 141 final Map<String, View> namedViews, final Map<String, View> renamedViews, 142 final ArrayList<View> sharedElementTargets) { 143 final Transition enterTransition = (Transition) enterTransitionObject; 144 final Transition exitTransition = (Transition) exitTransitionObject; 145 final Transition sharedElementTransition = (Transition) sharedElementTransitionObject; 146 excludeViews(enterTransition, exitTransition, exitingViews, true); 147 if (enterTransitionObject != null || sharedElementTransitionObject != null) { 148 if (enterTransition != null) { 149 enterTransition.addTarget(nonExistentView); 150 } 151 if (sharedElementTransitionObject != null) { 152 setSharedElementTargets(sharedElementTransition, nonExistentView, 153 namedViews, sharedElementTargets); 154 excludeViews(enterTransition, sharedElementTransition, sharedElementTargets, true); 155 excludeViews(exitTransition, sharedElementTransition, sharedElementTargets, true); 156 } 157 158 container.getViewTreeObserver().addOnPreDrawListener( 159 new ViewTreeObserver.OnPreDrawListener() { 160 @Override 161 public boolean onPreDraw() { 162 container.getViewTreeObserver().removeOnPreDrawListener(this); 163 if (enterTransition != null) { 164 enterTransition.removeTarget(nonExistentView); 165 } 166 if (inFragment != null) { 167 View fragmentView = inFragment.getView(); 168 if (fragmentView != null) { 169 if (!nameOverrides.isEmpty()) { 170 findNamedViews(renamedViews, fragmentView); 171 renamedViews.keySet().retainAll(nameOverrides.values()); 172 for (Map.Entry<String, String> entry : nameOverrides 173 .entrySet()) { 174 String to = entry.getValue(); 175 View view = renamedViews.get(to); 176 if (view != null) { 177 String from = entry.getKey(); 178 view.setTransitionName(from); 179 } 180 } 181 } 182 if (enterTransition != null) { 183 captureTransitioningViews(enteringViews, fragmentView); 184 enteringViews.removeAll(renamedViews.values()); 185 enteringViews.add(nonExistentView); 186 addTargets(enterTransition, enteringViews); 187 } 188 } 189 } 190 excludeViews(exitTransition, enterTransition, enteringViews, true); 191 192 return true; 193 } 194 }); 195 setSharedElementEpicenter(enterTransition, epicenterView); 196 } 197 } 198 199 public static Object mergeTransitions(Object enterTransitionObject, 200 Object exitTransitionObject, Object sharedElementTransitionObject, 201 boolean allowOverlap) { 202 boolean overlap = true; 203 Transition enterTransition = (Transition) enterTransitionObject; 204 Transition exitTransition = (Transition) exitTransitionObject; 205 Transition sharedElementTransition = (Transition) sharedElementTransitionObject; 206 207 if (enterTransition != null && exitTransition != null) { 208 overlap = allowOverlap; 209 } 210 211 // Wrap the transitions. Explicit targets like in enter and exit will cause the 212 // views to be targeted regardless of excluded views. If that happens, then the 213 // excluded fragments views (hidden fragments) will still be in the transition. 214 215 Transition transition; 216 if (overlap) { 217 // Regular transition -- do it all together 218 TransitionSet transitionSet = new TransitionSet(); 219 if (enterTransition != null) { 220 transitionSet.addTransition(enterTransition); 221 } 222 if (exitTransition != null) { 223 transitionSet.addTransition(exitTransition); 224 } 225 if (sharedElementTransition != null) { 226 transitionSet.addTransition(sharedElementTransition); 227 } 228 transition = transitionSet; 229 } else { 230 // First do exit, then enter, but allow shared element transition to happen 231 // during both. 232 Transition staggered = null; 233 if (exitTransition != null && enterTransition != null) { 234 staggered = new TransitionSet() 235 .addTransition(exitTransition) 236 .addTransition(enterTransition) 237 .setOrdering(TransitionSet.ORDERING_SEQUENTIAL); 238 } else if (exitTransition != null) { 239 staggered = exitTransition; 240 } else if (enterTransition != null) { 241 staggered = enterTransition; 242 } 243 if (sharedElementTransition != null) { 244 TransitionSet together = new TransitionSet(); 245 if (staggered != null) { 246 together.addTransition(staggered); 247 } 248 together.addTransition(sharedElementTransition); 249 transition = together; 250 } else { 251 transition = staggered; 252 } 253 } 254 return transition; 255 } 256 257 /** 258 * Finds all children of the shared elements and sets the wrapping TransitionSet 259 * targets to point to those. It also limits transitions that have no targets to the 260 * specific shared elements. This allows developers to target child views of the 261 * shared elements specifically, but this doesn't happen by default. 262 */ 263 public static void setSharedElementTargets(Object transitionObj, 264 View nonExistentView, Map<String, View> namedViews, 265 ArrayList<View> sharedElementTargets) { 266 TransitionSet transition = (TransitionSet) transitionObj; 267 sharedElementTargets.clear(); 268 sharedElementTargets.addAll(namedViews.values()); 269 270 final List<View> views = transition.getTargets(); 271 views.clear(); 272 final int count = sharedElementTargets.size(); 273 for (int i = 0; i < count; i++) { 274 final View view = sharedElementTargets.get(i); 275 bfsAddViewChildren(views, view); 276 } 277 sharedElementTargets.add(nonExistentView); 278 addTargets(transition, sharedElementTargets); 279 } 280 281 /** 282 * Uses a breadth-first scheme to add startView and all of its children to views. 283 * It won't add a child if it is already in views. 284 */ 285 private static void bfsAddViewChildren(final List<View> views, final View startView) { 286 final int startIndex = views.size(); 287 if (containedBeforeIndex(views, startView, startIndex)) { 288 return; // This child is already in the list, so all its children are also. 289 } 290 views.add(startView); 291 for (int index = startIndex; index < views.size(); index++) { 292 final View view = views.get(index); 293 if (view instanceof ViewGroup) { 294 ViewGroup viewGroup = (ViewGroup) view; 295 final int childCount = viewGroup.getChildCount(); 296 for (int childIndex = 0; childIndex < childCount; childIndex++) { 297 final View child = viewGroup.getChildAt(childIndex); 298 if (!containedBeforeIndex(views, child, startIndex)) { 299 views.add(child); 300 } 301 } 302 } 303 } 304 } 305 306 /** 307 * Does a linear search through views for view, limited to maxIndex. 308 */ 309 private static boolean containedBeforeIndex(final List<View> views, final View view, 310 final int maxIndex) { 311 for (int i = 0; i < maxIndex; i++) { 312 if (views.get(i) == view) { 313 return true; 314 } 315 } 316 return false; 317 } 318 319 private static void setSharedElementEpicenter(Transition transition, 320 final EpicenterView epicenterView) { 321 if (transition != null) { 322 transition.setEpicenterCallback(new Transition.EpicenterCallback() { 323 private Rect mEpicenter; 324 325 @Override 326 public Rect onGetEpicenter(Transition transition) { 327 if (mEpicenter == null && epicenterView.epicenter != null) { 328 mEpicenter = getBoundsOnScreen(epicenterView.epicenter); 329 } 330 return mEpicenter; 331 } 332 }); 333 } 334 } 335 336 private static Rect getBoundsOnScreen(View view) { 337 Rect epicenter = new Rect(); 338 int[] loc = new int[2]; 339 view.getLocationOnScreen(loc); 340 // not as good as View.getBoundsOnScreen, but that's not public 341 epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight()); 342 return epicenter; 343 } 344 345 private static void captureTransitioningViews(ArrayList<View> transitioningViews, View view) { 346 if (view.getVisibility() == View.VISIBLE) { 347 if (view instanceof ViewGroup) { 348 ViewGroup viewGroup = (ViewGroup) view; 349 if (viewGroup.isTransitionGroup()) { 350 transitioningViews.add(viewGroup); 351 } else { 352 int count = viewGroup.getChildCount(); 353 for (int i = 0; i < count; i++) { 354 View child = viewGroup.getChildAt(i); 355 captureTransitioningViews(transitioningViews, child); 356 } 357 } 358 } else { 359 transitioningViews.add(view); 360 } 361 } 362 } 363 364 public static void findNamedViews(Map<String, View> namedViews, View view) { 365 if (view.getVisibility() == View.VISIBLE) { 366 String transitionName = view.getTransitionName(); 367 if (transitionName != null) { 368 namedViews.put(transitionName, view); 369 } 370 if (view instanceof ViewGroup) { 371 ViewGroup viewGroup = (ViewGroup) view; 372 int count = viewGroup.getChildCount(); 373 for (int i = 0; i < count; i++) { 374 View child = viewGroup.getChildAt(i); 375 findNamedViews(namedViews, child); 376 } 377 } 378 } 379 } 380 381 public static void cleanupTransitions(final View sceneRoot, final View nonExistentView, 382 Object enterTransitionObject, final ArrayList<View> enteringViews, 383 Object exitTransitionObject, final ArrayList<View> exitingViews, 384 Object sharedElementTransitionObject, final ArrayList<View> sharedElementTargets, 385 Object overallTransitionObject, final ArrayList<View> hiddenViews, 386 final Map<String, View> renamedViews) { 387 final Transition enterTransition = (Transition) enterTransitionObject; 388 final Transition exitTransition = (Transition) exitTransitionObject; 389 final Transition sharedElementTransition = (Transition) sharedElementTransitionObject; 390 final Transition overallTransition = (Transition) overallTransitionObject; 391 if (overallTransition != null) { 392 sceneRoot.getViewTreeObserver().addOnPreDrawListener( 393 new ViewTreeObserver.OnPreDrawListener() { 394 @Override 395 public boolean onPreDraw() { 396 sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); 397 if (enterTransition != null) { 398 removeTargets(enterTransition, enteringViews); 399 excludeViews(enterTransition, exitTransition, exitingViews, false); 400 excludeViews(enterTransition, sharedElementTransition, sharedElementTargets, 401 false); 402 } 403 if (exitTransition != null) { 404 removeTargets(exitTransition, exitingViews); 405 excludeViews(exitTransition, enterTransition, enteringViews, false); 406 excludeViews(exitTransition, sharedElementTransition, sharedElementTargets, 407 false); 408 } 409 if (sharedElementTransition != null) { 410 removeTargets(sharedElementTransition, sharedElementTargets); 411 } 412 for (Map.Entry<String, View> entry : renamedViews.entrySet()) { 413 View view = entry.getValue(); 414 String name = entry.getKey(); 415 view.setTransitionName(name); 416 } 417 int numViews = hiddenViews.size(); 418 for (int i = 0; i < numViews; i++) { 419 overallTransition.excludeTarget(hiddenViews.get(i), false); 420 } 421 overallTransition.excludeTarget(nonExistentView, false); 422 return true; 423 } 424 }); 425 } 426 } 427 428 /** 429 * This method removes the views from transitions that target ONLY those views. 430 * The views list should match those added in addTargets and should contain 431 * one view that is not in the view hierarchy (state.nonExistentView). 432 */ 433 public static void removeTargets(Object transitionObject, ArrayList<View> views) { 434 Transition transition = (Transition) transitionObject; 435 if (transition instanceof TransitionSet) { 436 TransitionSet set = (TransitionSet) transition; 437 int numTransitions = set.getTransitionCount(); 438 for (int i = 0; i < numTransitions; i++) { 439 Transition child = set.getTransitionAt(i); 440 removeTargets(child, views); 441 } 442 } else if (!hasSimpleTarget(transition)) { 443 List<View> targets = transition.getTargets(); 444 if (targets != null && targets.size() == views.size() && 445 targets.containsAll(views)) { 446 // We have an exact match. We must have added these earlier in addTargets 447 for (int i = views.size() - 1; i >= 0; i--) { 448 transition.removeTarget(views.get(i)); 449 } 450 } 451 } 452 } 453 454 /** 455 * This method adds views as targets to the transition, but only if the transition 456 * doesn't already have a target. It is best for views to contain one View object 457 * that does not exist in the view hierarchy (state.nonExistentView) so that 458 * when they are removed later, a list match will suffice to remove the targets. 459 * Otherwise, if you happened to have targeted the exact views for the transition, 460 * the removeTargets call will remove them unexpectedly. 461 */ 462 public static void addTargets(Object transitionObject, ArrayList<View> views) { 463 Transition transition = (Transition) transitionObject; 464 if (transition instanceof TransitionSet) { 465 TransitionSet set = (TransitionSet) transition; 466 int numTransitions = set.getTransitionCount(); 467 for (int i = 0; i < numTransitions; i++) { 468 Transition child = set.getTransitionAt(i); 469 addTargets(child, views); 470 } 471 } else if (!hasSimpleTarget(transition)) { 472 List<View> targets = transition.getTargets(); 473 if (isNullOrEmpty(targets)) { 474 // We can just add the target views 475 int numViews = views.size(); 476 for (int i = 0; i < numViews; i++) { 477 transition.addTarget(views.get(i)); 478 } 479 } 480 } 481 } 482 483 private static boolean hasSimpleTarget(Transition transition) { 484 return !isNullOrEmpty(transition.getTargetIds()) || 485 !isNullOrEmpty(transition.getTargetNames()) || 486 !isNullOrEmpty(transition.getTargetTypes()); 487 } 488 489 private static boolean isNullOrEmpty(List list) { 490 return list == null || list.isEmpty(); 491 } 492 493 public interface ViewRetriever { 494 View getView(); 495 } 496 497 public static class EpicenterView { 498 public View epicenter; 499 } 500} 501