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 public boolean onPreDraw() { 161 container.getViewTreeObserver().removeOnPreDrawListener(this); 162 if (enterTransition != null) { 163 enterTransition.removeTarget(nonExistentView); 164 } 165 if (inFragment != null) { 166 View fragmentView = inFragment.getView(); 167 if (fragmentView != null) { 168 if (!nameOverrides.isEmpty()) { 169 findNamedViews(renamedViews, fragmentView); 170 renamedViews.keySet().retainAll(nameOverrides.values()); 171 for (Map.Entry<String, String> entry : nameOverrides 172 .entrySet()) { 173 String to = entry.getValue(); 174 View view = renamedViews.get(to); 175 if (view != null) { 176 String from = entry.getKey(); 177 view.setTransitionName(from); 178 } 179 } 180 } 181 if (enterTransition != null) { 182 captureTransitioningViews(enteringViews, fragmentView); 183 enteringViews.removeAll(renamedViews.values()); 184 enteringViews.add(nonExistentView); 185 addTargets(enterTransition, enteringViews); 186 } 187 } 188 } 189 excludeViews(exitTransition, enterTransition, enteringViews, true); 190 191 return true; 192 } 193 }); 194 setSharedElementEpicenter(enterTransition, epicenterView); 195 } 196 } 197 198 public static Object mergeTransitions(Object enterTransitionObject, 199 Object exitTransitionObject, Object sharedElementTransitionObject, 200 boolean allowOverlap) { 201 boolean overlap = true; 202 Transition enterTransition = (Transition) enterTransitionObject; 203 Transition exitTransition = (Transition) exitTransitionObject; 204 Transition sharedElementTransition = (Transition) sharedElementTransitionObject; 205 206 if (enterTransition != null && exitTransition != null) { 207 overlap = allowOverlap; 208 } 209 210 // Wrap the transitions. Explicit targets like in enter and exit will cause the 211 // views to be targeted regardless of excluded views. If that happens, then the 212 // excluded fragments views (hidden fragments) will still be in the transition. 213 214 Transition transition; 215 if (overlap) { 216 // Regular transition -- do it all together 217 TransitionSet transitionSet = new TransitionSet(); 218 if (enterTransition != null) { 219 transitionSet.addTransition(enterTransition); 220 } 221 if (exitTransition != null) { 222 transitionSet.addTransition(exitTransition); 223 } 224 if (sharedElementTransition != null) { 225 transitionSet.addTransition(sharedElementTransition); 226 } 227 transition = transitionSet; 228 } else { 229 // First do exit, then enter, but allow shared element transition to happen 230 // during both. 231 Transition staggered = null; 232 if (exitTransition != null && enterTransition != null) { 233 staggered = new TransitionSet() 234 .addTransition(exitTransition) 235 .addTransition(enterTransition) 236 .setOrdering(TransitionSet.ORDERING_SEQUENTIAL); 237 } else if (exitTransition != null) { 238 staggered = exitTransition; 239 } else if (enterTransition != null) { 240 staggered = enterTransition; 241 } 242 if (sharedElementTransition != null) { 243 TransitionSet together = new TransitionSet(); 244 if (staggered != null) { 245 together.addTransition(staggered); 246 } 247 together.addTransition(sharedElementTransition); 248 transition = together; 249 } else { 250 transition = staggered; 251 } 252 } 253 return transition; 254 } 255 256 /** 257 * Finds all children of the shared elements and sets the wrapping TransitionSet 258 * targets to point to those. It also limits transitions that have no targets to the 259 * specific shared elements. This allows developers to target child views of the 260 * shared elements specifically, but this doesn't happen by default. 261 */ 262 public static void setSharedElementTargets(Object transitionObj, 263 View nonExistentView, Map<String, View> namedViews, 264 ArrayList<View> sharedElementTargets) { 265 TransitionSet transition = (TransitionSet) transitionObj; 266 sharedElementTargets.clear(); 267 sharedElementTargets.addAll(namedViews.values()); 268 269 final List<View> views = transition.getTargets(); 270 views.clear(); 271 final int count = sharedElementTargets.size(); 272 for (int i = 0; i < count; i++) { 273 final View view = sharedElementTargets.get(i); 274 bfsAddViewChildren(views, view); 275 } 276 sharedElementTargets.add(nonExistentView); 277 addTargets(transition, sharedElementTargets); 278 } 279 280 /** 281 * Uses a breadth-first scheme to add startView and all of its children to views. 282 * It won't add a child if it is already in views. 283 */ 284 private static void bfsAddViewChildren(final List<View> views, final View startView) { 285 final int startIndex = views.size(); 286 if (containedBeforeIndex(views, startView, startIndex)) { 287 return; // This child is already in the list, so all its children are also. 288 } 289 views.add(startView); 290 for (int index = startIndex; index < views.size(); index++) { 291 final View view = views.get(index); 292 if (view instanceof ViewGroup) { 293 ViewGroup viewGroup = (ViewGroup) view; 294 final int childCount = viewGroup.getChildCount(); 295 for (int childIndex = 0; childIndex < childCount; childIndex++) { 296 final View child = viewGroup.getChildAt(childIndex); 297 if (!containedBeforeIndex(views, child, startIndex)) { 298 views.add(child); 299 } 300 } 301 } 302 } 303 } 304 305 /** 306 * Does a linear search through views for view, limited to maxIndex. 307 */ 308 private static boolean containedBeforeIndex(final List<View> views, final View view, 309 final int maxIndex) { 310 for (int i = 0; i < maxIndex; i++) { 311 if (views.get(i) == view) { 312 return true; 313 } 314 } 315 return false; 316 } 317 318 private static void setSharedElementEpicenter(Transition transition, 319 final EpicenterView epicenterView) { 320 if (transition != null) { 321 transition.setEpicenterCallback(new Transition.EpicenterCallback() { 322 private Rect mEpicenter; 323 324 @Override 325 public Rect onGetEpicenter(Transition transition) { 326 if (mEpicenter == null && epicenterView.epicenter != null) { 327 mEpicenter = getBoundsOnScreen(epicenterView.epicenter); 328 } 329 return mEpicenter; 330 } 331 }); 332 } 333 } 334 335 private static Rect getBoundsOnScreen(View view) { 336 Rect epicenter = new Rect(); 337 int[] loc = new int[2]; 338 view.getLocationOnScreen(loc); 339 // not as good as View.getBoundsOnScreen, but that's not public 340 epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight()); 341 return epicenter; 342 } 343 344 private static void captureTransitioningViews(ArrayList<View> transitioningViews, View view) { 345 if (view.getVisibility() == View.VISIBLE) { 346 if (view instanceof ViewGroup) { 347 ViewGroup viewGroup = (ViewGroup) view; 348 if (viewGroup.isTransitionGroup()) { 349 transitioningViews.add(viewGroup); 350 } else { 351 int count = viewGroup.getChildCount(); 352 for (int i = 0; i < count; i++) { 353 View child = viewGroup.getChildAt(i); 354 captureTransitioningViews(transitioningViews, child); 355 } 356 } 357 } else { 358 transitioningViews.add(view); 359 } 360 } 361 } 362 363 public static void findNamedViews(Map<String, View> namedViews, View view) { 364 if (view.getVisibility() == View.VISIBLE) { 365 String transitionName = view.getTransitionName(); 366 if (transitionName != null) { 367 namedViews.put(transitionName, view); 368 } 369 if (view instanceof ViewGroup) { 370 ViewGroup viewGroup = (ViewGroup) view; 371 int count = viewGroup.getChildCount(); 372 for (int i = 0; i < count; i++) { 373 View child = viewGroup.getChildAt(i); 374 findNamedViews(namedViews, child); 375 } 376 } 377 } 378 } 379 380 public static void cleanupTransitions(final View sceneRoot, final View nonExistentView, 381 Object enterTransitionObject, final ArrayList<View> enteringViews, 382 Object exitTransitionObject, final ArrayList<View> exitingViews, 383 Object sharedElementTransitionObject, final ArrayList<View> sharedElementTargets, 384 Object overallTransitionObject, final ArrayList<View> hiddenViews, 385 final Map<String, View> renamedViews) { 386 final Transition enterTransition = (Transition) enterTransitionObject; 387 final Transition exitTransition = (Transition) exitTransitionObject; 388 final Transition sharedElementTransition = (Transition) sharedElementTransitionObject; 389 final Transition overallTransition = (Transition) overallTransitionObject; 390 if (overallTransition != null) { 391 sceneRoot.getViewTreeObserver().addOnPreDrawListener( 392 new ViewTreeObserver.OnPreDrawListener() { 393 public boolean onPreDraw() { 394 sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); 395 if (enterTransition != null) { 396 removeTargets(enterTransition, enteringViews); 397 excludeViews(enterTransition, exitTransition, exitingViews, false); 398 excludeViews(enterTransition, sharedElementTransition, sharedElementTargets, 399 false); 400 } 401 if (exitTransition != null) { 402 removeTargets(exitTransition, exitingViews); 403 excludeViews(exitTransition, enterTransition, enteringViews, false); 404 excludeViews(exitTransition, sharedElementTransition, sharedElementTargets, 405 false); 406 } 407 if (sharedElementTransition != null) { 408 removeTargets(sharedElementTransition, sharedElementTargets); 409 } 410 for (Map.Entry<String, View> entry : renamedViews.entrySet()) { 411 View view = entry.getValue(); 412 String name = entry.getKey(); 413 view.setTransitionName(name); 414 } 415 int numViews = hiddenViews.size(); 416 for (int i = 0; i < numViews; i++) { 417 overallTransition.excludeTarget(hiddenViews.get(i), false); 418 } 419 overallTransition.excludeTarget(nonExistentView, false); 420 return true; 421 } 422 }); 423 } 424 } 425 426 /** 427 * This method removes the views from transitions that target ONLY those views. 428 * The views list should match those added in addTargets and should contain 429 * one view that is not in the view hierarchy (state.nonExistentView). 430 */ 431 public static void removeTargets(Object transitionObject, ArrayList<View> views) { 432 Transition transition = (Transition) transitionObject; 433 if (transition instanceof TransitionSet) { 434 TransitionSet set = (TransitionSet) transition; 435 int numTransitions = set.getTransitionCount(); 436 for (int i = 0; i < numTransitions; i++) { 437 Transition child = set.getTransitionAt(i); 438 removeTargets(child, views); 439 } 440 } else if (!hasSimpleTarget(transition)) { 441 List<View> targets = transition.getTargets(); 442 if (targets != null && targets.size() == views.size() && 443 targets.containsAll(views)) { 444 // We have an exact match. We must have added these earlier in addTargets 445 for (int i = views.size() - 1; i >= 0; i--) { 446 transition.removeTarget(views.get(i)); 447 } 448 } 449 } 450 } 451 452 /** 453 * This method adds views as targets to the transition, but only if the transition 454 * doesn't already have a target. It is best for views to contain one View object 455 * that does not exist in the view hierarchy (state.nonExistentView) so that 456 * when they are removed later, a list match will suffice to remove the targets. 457 * Otherwise, if you happened to have targeted the exact views for the transition, 458 * the removeTargets call will remove them unexpectedly. 459 */ 460 public static void addTargets(Object transitionObject, ArrayList<View> views) { 461 Transition transition = (Transition) transitionObject; 462 if (transition instanceof TransitionSet) { 463 TransitionSet set = (TransitionSet) transition; 464 int numTransitions = set.getTransitionCount(); 465 for (int i = 0; i < numTransitions; i++) { 466 Transition child = set.getTransitionAt(i); 467 addTargets(child, views); 468 } 469 } else if (!hasSimpleTarget(transition)) { 470 List<View> targets = transition.getTargets(); 471 if (isNullOrEmpty(targets)) { 472 // We can just add the target views 473 int numViews = views.size(); 474 for (int i = 0; i < numViews; i++) { 475 transition.addTarget(views.get(i)); 476 } 477 } 478 } 479 } 480 481 private static boolean hasSimpleTarget(Transition transition) { 482 return !isNullOrEmpty(transition.getTargetIds()) || 483 !isNullOrEmpty(transition.getTargetNames()) || 484 !isNullOrEmpty(transition.getTargetTypes()); 485 } 486 487 private static boolean isNullOrEmpty(List list) { 488 return list == null || list.isEmpty(); 489 } 490 491 public interface ViewRetriever { 492 View getView(); 493 } 494 495 public static class EpicenterView { 496 public View epicenter; 497 } 498} 499