Transition.java revision d82c8ac4db7091d2e976af4c89a1734465d20cd2
1/* 2 * Copyright (C) 2013 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.transition; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.TimeInterpolator; 22import android.util.ArrayMap; 23import android.util.Log; 24import android.util.LongSparseArray; 25import android.util.SparseArray; 26import android.view.SurfaceView; 27import android.view.TextureView; 28import android.view.View; 29import android.view.ViewGroup; 30import android.view.ViewOverlay; 31import android.widget.ListView; 32 33import java.util.ArrayList; 34import java.util.List; 35 36/** 37 * A Transition holds information about animations that will be run on its 38 * targets during a scene change. Subclasses of this abstract class may 39 * choreograph several child transitions ({@link TransitionSet} or they may 40 * perform custom animations themselves. Any Transition has two main jobs: 41 * (1) capture property values, and (2) play animations based on changes to 42 * captured property values. A custom transition knows what property values 43 * on View objects are of interest to it, and also knows how to animate 44 * changes to those values. For example, the {@link Fade} transition tracks 45 * changes to visibility-related properties and is able to construct and run 46 * animations that fade items in or out based on changes to those properties. 47 * 48 * <p>Note: Transitions may not work correctly with either {@link SurfaceView} 49 * or {@link TextureView}, due to the way that these views are displayed 50 * on the screen. For SurfaceView, the problem is that the view is updated from 51 * a non-UI thread, so changes to the view due to transitions (such as moving 52 * and resizing the view) may be out of sync with the display inside those bounds. 53 * TextureView is more compatible with transitions in general, but some 54 * specific transitions (such as {@link Fade}) may not be compatible 55 * with TextureView because they rely on {@link ViewOverlay} functionality, 56 * which does not currently work with TextureView.</p> 57 * 58 * <p>Transitions can be declared in XML resource files inside the <code>res/transition</code> 59 * directory. Transition resources consist of a tag name for one of the Transition 60 * subclasses along with attributes to define some of the attributes of that transition. 61 * For example, here is a minimal resource file that declares a {@link ChangeBounds} transition:</p> 62 * 63 * {@sample development/samples/ApiDemos/res/transition/changebounds.xml ChangeBounds} 64 * 65 * <p>Note that attributes for the transition are not required, just as they are 66 * optional when declared in code; Transitions created from XML resources will use 67 * the same defaults as their code-created equivalents. Here is a slightly more 68 * elaborate example which declares a {@link TransitionSet} transition with 69 * {@link ChangeBounds} and {@link Fade} child transitions:</p> 70 * 71 * {@sample 72 * development/samples/ApiDemos/res/transition/changebounds_fadeout_sequential.xml TransitionSet} 73 * 74 * <p>In this example, the transitionOrdering attribute is used on the TransitionSet 75 * object to change from the default {@link TransitionSet#ORDERING_TOGETHER} behavior 76 * to be {@link TransitionSet#ORDERING_SEQUENTIAL} instead. Also, the {@link Fade} 77 * transition uses a fadingMode of {@link Fade#OUT} instead of the default 78 * out-in behavior. Finally, note the use of the <code>targets</code> sub-tag, which 79 * takes a set of {@link android.R.styleable#TransitionTarget target} tags, each 80 * of which lists a specific <code>targetId</code> which this transition acts upon. 81 * Use of targets is optional, but can be used to either limit the time spent checking 82 * attributes on unchanging views, or limiting the types of animations run on specific views. 83 * In this case, we know that only the <code>grayscaleContainer</code> will be 84 * disappearing, so we choose to limit the {@link Fade} transition to only that view.</p> 85 * 86 * Further information on XML resource descriptions for transitions can be found for 87 * {@link android.R.styleable#Transition}, {@link android.R.styleable#TransitionSet}, 88 * {@link android.R.styleable#TransitionTarget}, and {@link android.R.styleable#Fade}. 89 * 90 */ 91public abstract class Transition implements Cloneable { 92 93 private static final String LOG_TAG = "Transition"; 94 static final boolean DBG = false; 95 96 private String mName = getClass().getName(); 97 98 long mStartDelay = -1; 99 long mDuration = -1; 100 TimeInterpolator mInterpolator = null; 101 ArrayList<Integer> mTargetIds = new ArrayList<Integer>(); 102 ArrayList<View> mTargets = new ArrayList<View>(); 103 private TransitionValuesMaps mStartValues = new TransitionValuesMaps(); 104 private TransitionValuesMaps mEndValues = new TransitionValuesMaps(); 105 TransitionSet mParent = null; 106 107 // Per-animator information used for later canceling when future transitions overlap 108 private static ThreadLocal<ArrayMap<Animator, AnimationInfo>> sRunningAnimators = 109 new ThreadLocal<ArrayMap<Animator, AnimationInfo>>(); 110 111 // Scene Root is set at createAnimator() time in the cloned Transition 112 ViewGroup mSceneRoot = null; 113 114 // Track all animators in use in case the transition gets canceled and needs to 115 // cancel running animators 116 private ArrayList<Animator> mCurrentAnimators = new ArrayList<Animator>(); 117 118 // Number of per-target instances of this Transition currently running. This count is 119 // determined by calls to start() and end() 120 int mNumInstances = 0; 121 122 // Whether this transition is currently paused, due to a call to pause() 123 boolean mPaused = false; 124 125 // The set of listeners to be sent transition lifecycle events. 126 ArrayList<TransitionListener> mListeners = null; 127 128 // The set of animators collected from calls to createAnimator(), 129 // to be run in runAnimators() 130 ArrayList<Animator> mAnimators = new ArrayList<Animator>(); 131 132 /** 133 * Constructs a Transition object with no target objects. A transition with 134 * no targets defaults to running on all target objects in the scene hierarchy 135 * (if the transition is not contained in a TransitionSet), or all target 136 * objects passed down from its parent (if it is in a TransitionSet). 137 */ 138 public Transition() {} 139 140 /** 141 * Sets the duration of this transition. By default, there is no duration 142 * (indicated by a negative number), which means that the Animator created by 143 * the transition will have its own specified duration. If the duration of a 144 * Transition is set, that duration will override the Animator duration. 145 * 146 * @param duration The length of the animation, in milliseconds. 147 * @return This transition object. 148 * @attr ref android.R.styleable#Transition_duration 149 */ 150 public Transition setDuration(long duration) { 151 mDuration = duration; 152 return this; 153 } 154 155 /** 156 * Returns the duration set on this transition. If no duration has been set, 157 * the returned value will be negative, indicating that resulting animators will 158 * retain their own durations. 159 * 160 * @return The duration set on this transition, in milliseconds, if one has been 161 * set, otherwise returns a negative number. 162 */ 163 public long getDuration() { 164 return mDuration; 165 } 166 167 /** 168 * Sets the startDelay of this transition. By default, there is no delay 169 * (indicated by a negative number), which means that the Animator created by 170 * the transition will have its own specified startDelay. If the delay of a 171 * Transition is set, that delay will override the Animator delay. 172 * 173 * @param startDelay The length of the delay, in milliseconds. 174 * @return This transition object. 175 * @attr ref android.R.styleable#Transition_startDelay 176 */ 177 public Transition setStartDelay(long startDelay) { 178 mStartDelay = startDelay; 179 return this; 180 } 181 182 /** 183 * Returns the startDelay set on this transition. If no startDelay has been set, 184 * the returned value will be negative, indicating that resulting animators will 185 * retain their own startDelays. 186 * 187 * @return The startDelay set on this transition, in milliseconds, if one has 188 * been set, otherwise returns a negative number. 189 */ 190 public long getStartDelay() { 191 return mStartDelay; 192 } 193 194 /** 195 * Sets the interpolator of this transition. By default, the interpolator 196 * is null, which means that the Animator created by the transition 197 * will have its own specified interpolator. If the interpolator of a 198 * Transition is set, that interpolator will override the Animator interpolator. 199 * 200 * @param interpolator The time interpolator used by the transition 201 * @return This transition object. 202 * @attr ref android.R.styleable#Transition_interpolator 203 */ 204 public Transition setInterpolator(TimeInterpolator interpolator) { 205 mInterpolator = interpolator; 206 return this; 207 } 208 209 /** 210 * Returns the interpolator set on this transition. If no interpolator has been set, 211 * the returned value will be null, indicating that resulting animators will 212 * retain their own interpolators. 213 * 214 * @return The interpolator set on this transition, if one has been set, otherwise 215 * returns null. 216 */ 217 public TimeInterpolator getInterpolator() { 218 return mInterpolator; 219 } 220 221 /** 222 * Returns the set of property names used stored in the {@link TransitionValues} 223 * object passed into {@link #captureStartValues(TransitionValues)} that 224 * this transition cares about for the purposes of canceling overlapping animations. 225 * When any transition is started on a given scene root, all transitions 226 * currently running on that same scene root are checked to see whether the 227 * properties on which they based their animations agree with the end values of 228 * the same properties in the new transition. If the end values are not equal, 229 * then the old animation is canceled since the new transition will start a new 230 * animation to these new values. If the values are equal, the old animation is 231 * allowed to continue and no new animation is started for that transition. 232 * 233 * <p>A transition does not need to override this method. However, not doing so 234 * will mean that the cancellation logic outlined in the previous paragraph 235 * will be skipped for that transition, possibly leading to artifacts as 236 * old transitions and new transitions on the same targets run in parallel, 237 * animating views toward potentially different end values.</p> 238 * 239 * @return An array of property names as described in the class documentation for 240 * {@link TransitionValues}. The default implementation returns <code>null</code>. 241 */ 242 public String[] getTransitionProperties() { 243 return null; 244 } 245 246 /** 247 * This method creates an animation that will be run for this transition 248 * given the information in the startValues and endValues structures captured 249 * earlier for the start and end scenes. Subclasses of Transition should override 250 * this method. The method should only be called by the transition system; it is 251 * not intended to be called from external classes. 252 * 253 * <p>This method is called by the transition's parent (all the way up to the 254 * topmost Transition in the hierarchy) with the sceneRoot and start/end 255 * values that the transition may need to set up initial target values 256 * and construct an appropriate animation. For example, if an overall 257 * Transition is a {@link TransitionSet} consisting of several 258 * child transitions in sequence, then some of the child transitions may 259 * want to set initial values on target views prior to the overall 260 * Transition commencing, to put them in an appropriate state for the 261 * delay between that start and the child Transition start time. For 262 * example, a transition that fades an item in may wish to set the starting 263 * alpha value to 0, to avoid it blinking in prior to the transition 264 * actually starting the animation. This is necessary because the scene 265 * change that triggers the Transition will automatically set the end-scene 266 * on all target views, so a Transition that wants to animate from a 267 * different value should set that value prior to returning from this method.</p> 268 * 269 * <p>Additionally, a Transition can perform logic to determine whether 270 * the transition needs to run on the given target and start/end values. 271 * For example, a transition that resizes objects on the screen may wish 272 * to avoid running for views which are not present in either the start 273 * or end scenes.</p> 274 * 275 * <p>If there is an animator created and returned from this method, the 276 * transition mechanism will apply any applicable duration, startDelay, 277 * and interpolator to that animation and start it. A return value of 278 * <code>null</code> indicates that no animation should run. The default 279 * implementation returns null.</p> 280 * 281 * <p>The method is called for every applicable target object, which is 282 * stored in the {@link TransitionValues#view} field.</p> 283 * 284 * 285 * @param sceneRoot The root of the transition hierarchy. 286 * @param startValues The values for a specific target in the start scene. 287 * @param endValues The values for the target in the end scene. 288 * @return A Animator to be started at the appropriate time in the 289 * overall transition for this scene change. A null value means no animation 290 * should be run. 291 */ 292 public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, 293 TransitionValues endValues) { 294 return null; 295 } 296 297 /** 298 * This method, essentially a wrapper around all calls to createAnimator for all 299 * possible target views, is called with the entire set of start/end 300 * values. The implementation in Transition iterates through these lists 301 * and calls {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)} 302 * with each set of start/end values on this transition. The 303 * TransitionSet subclass overrides this method and delegates it to 304 * each of its children in succession. 305 * 306 * @hide 307 */ 308 protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues, 309 TransitionValuesMaps endValues) { 310 if (DBG) { 311 Log.d(LOG_TAG, "createAnimators() for " + this); 312 } 313 ArrayMap<View, TransitionValues> endCopy = 314 new ArrayMap<View, TransitionValues>(endValues.viewValues); 315 SparseArray<TransitionValues> endIdCopy = 316 new SparseArray<TransitionValues>(endValues.idValues.size()); 317 for (int i = 0; i < endValues.idValues.size(); ++i) { 318 int id = endValues.idValues.keyAt(i); 319 endIdCopy.put(id, endValues.idValues.valueAt(i)); 320 } 321 LongSparseArray<TransitionValues> endItemIdCopy = 322 new LongSparseArray<TransitionValues>(endValues.itemIdValues.size()); 323 for (int i = 0; i < endValues.itemIdValues.size(); ++i) { 324 long id = endValues.itemIdValues.keyAt(i); 325 endItemIdCopy.put(id, endValues.itemIdValues.valueAt(i)); 326 } 327 // Walk through the start values, playing everything we find 328 // Remove from the end set as we go 329 ArrayList<TransitionValues> startValuesList = new ArrayList<TransitionValues>(); 330 ArrayList<TransitionValues> endValuesList = new ArrayList<TransitionValues>(); 331 for (View view : startValues.viewValues.keySet()) { 332 TransitionValues start = null; 333 TransitionValues end = null; 334 boolean isInListView = false; 335 if (view.getParent() instanceof ListView) { 336 isInListView = true; 337 } 338 if (!isInListView) { 339 int id = view.getId(); 340 start = startValues.viewValues.get(view) != null ? 341 startValues.viewValues.get(view) : startValues.idValues.get(id); 342 if (endValues.viewValues.get(view) != null) { 343 end = endValues.viewValues.get(view); 344 endCopy.remove(view); 345 } else { 346 end = endValues.idValues.get(id); 347 View removeView = null; 348 for (View viewToRemove : endCopy.keySet()) { 349 if (viewToRemove.getId() == id) { 350 removeView = viewToRemove; 351 } 352 } 353 if (removeView != null) { 354 endCopy.remove(removeView); 355 } 356 } 357 endIdCopy.remove(id); 358 if (isValidTarget(view, id)) { 359 startValuesList.add(start); 360 endValuesList.add(end); 361 } 362 } else { 363 ListView parent = (ListView) view.getParent(); 364 if (parent.getAdapter().hasStableIds()) { 365 int position = parent.getPositionForView(view); 366 long itemId = parent.getItemIdAtPosition(position); 367 start = startValues.itemIdValues.get(itemId); 368 endItemIdCopy.remove(itemId); 369 // TODO: deal with targetIDs for itemIDs for ListView items 370 startValuesList.add(start); 371 endValuesList.add(end); 372 } 373 } 374 } 375 int startItemIdCopySize = startValues.itemIdValues.size(); 376 for (int i = 0; i < startItemIdCopySize; ++i) { 377 long id = startValues.itemIdValues.keyAt(i); 378 if (isValidTarget(null, id)) { 379 TransitionValues start = startValues.itemIdValues.get(id); 380 TransitionValues end = endValues.itemIdValues.get(id); 381 endItemIdCopy.remove(id); 382 startValuesList.add(start); 383 endValuesList.add(end); 384 } 385 } 386 // Now walk through the remains of the end set 387 for (View view : endCopy.keySet()) { 388 int id = view.getId(); 389 if (isValidTarget(view, id)) { 390 TransitionValues start = startValues.viewValues.get(view) != null ? 391 startValues.viewValues.get(view) : startValues.idValues.get(id); 392 TransitionValues end = endCopy.get(view); 393 endIdCopy.remove(id); 394 startValuesList.add(start); 395 endValuesList.add(end); 396 } 397 } 398 int endIdCopySize = endIdCopy.size(); 399 for (int i = 0; i < endIdCopySize; ++i) { 400 int id = endIdCopy.keyAt(i); 401 if (isValidTarget(null, id)) { 402 TransitionValues start = startValues.idValues.get(id); 403 TransitionValues end = endIdCopy.get(id); 404 startValuesList.add(start); 405 endValuesList.add(end); 406 } 407 } 408 int endItemIdCopySize = endItemIdCopy.size(); 409 for (int i = 0; i < endItemIdCopySize; ++i) { 410 long id = endItemIdCopy.keyAt(i); 411 // TODO: Deal with targetIDs and itemIDs 412 TransitionValues start = startValues.itemIdValues.get(id); 413 TransitionValues end = endItemIdCopy.get(id); 414 startValuesList.add(start); 415 endValuesList.add(end); 416 } 417 ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); 418 for (int i = 0; i < startValuesList.size(); ++i) { 419 TransitionValues start = startValuesList.get(i); 420 TransitionValues end = endValuesList.get(i); 421 // Only bother trying to animate with values that differ between start/end 422 if (start != null || end != null) { 423 if (start == null || !start.equals(end)) { 424 if (DBG) { 425 View view = (end != null) ? end.view : start.view; 426 Log.d(LOG_TAG, " differing start/end values for view " + 427 view); 428 if (start == null || end == null) { 429 if (start == null) { 430 Log.d(LOG_TAG, " " + ((start == null) ? 431 "start null, end non-null" : "start non-null, end null")); 432 } 433 } else { 434 for (String key : start.values.keySet()) { 435 Object startValue = start.values.get(key); 436 Object endValue = end.values.get(key); 437 if (startValue != endValue && !startValue.equals(endValue)) { 438 Log.d(LOG_TAG, " " + key + ": start(" + startValue + 439 "), end(" + endValue +")"); 440 } 441 } 442 } 443 } 444 // TODO: what to do about targetIds and itemIds? 445 Animator animator = createAnimator(sceneRoot, start, end); 446 if (animator != null) { 447 // Save animation info for future cancellation purposes 448 View view = null; 449 TransitionValues infoValues = null; 450 if (end != null) { 451 view = end.view; 452 String[] properties = getTransitionProperties(); 453 if (view != null && properties != null && properties.length > 0) { 454 infoValues = new TransitionValues(); 455 infoValues.view = view; 456 TransitionValues newValues = endValues.viewValues.get(view); 457 if (newValues != null) { 458 for (int j = 0; j < properties.length; ++j) { 459 infoValues.values.put(properties[j], 460 newValues.values.get(properties[j])); 461 } 462 } 463 int numExistingAnims = runningAnimators.size(); 464 for (int j = 0; j < numExistingAnims; ++j) { 465 Animator anim = runningAnimators.keyAt(j); 466 AnimationInfo info = runningAnimators.get(anim); 467 if (info.values != null && info.view == view && 468 ((info.name == null && getName() == null) || 469 info.name.equals(getName()))) { 470 if (info.values.equals(infoValues)) { 471 // Favor the old animator 472 animator = null; 473 break; 474 } 475 } 476 } 477 } 478 } else { 479 view = (start != null) ? start.view : null; 480 } 481 if (animator != null) { 482 AnimationInfo info = new AnimationInfo(view, getName(), infoValues); 483 runningAnimators.put(animator, info); 484 mAnimators.add(animator); 485 } 486 } 487 } 488 } 489 } 490 } 491 492 /** 493 * Internal utility method for checking whether a given view/id 494 * is valid for this transition, where "valid" means that either 495 * the Transition has no target/targetId list (the default, in which 496 * cause the transition should act on all views in the hiearchy), or 497 * the given view is in the target list or the view id is in the 498 * targetId list. If the target parameter is null, then the target list 499 * is not checked (this is in the case of ListView items, where the 500 * views are ignored and only the ids are used). 501 */ 502 boolean isValidTarget(View target, long targetId) { 503 if (mTargetIds.size() == 0 && mTargets.size() == 0) { 504 return true; 505 } 506 if (mTargetIds.size() > 0) { 507 for (int i = 0; i < mTargetIds.size(); ++i) { 508 if (mTargetIds.get(i) == targetId) { 509 return true; 510 } 511 } 512 } 513 if (target != null && mTargets.size() > 0) { 514 for (int i = 0; i < mTargets.size(); ++i) { 515 if (mTargets.get(i) == target) { 516 return true; 517 } 518 } 519 } 520 return false; 521 } 522 523 private static ArrayMap<Animator, AnimationInfo> getRunningAnimators() { 524 ArrayMap<Animator, AnimationInfo> runningAnimators = sRunningAnimators.get(); 525 if (runningAnimators == null) { 526 runningAnimators = new ArrayMap<Animator, AnimationInfo>(); 527 sRunningAnimators.set(runningAnimators); 528 } 529 return runningAnimators; 530 } 531 532 /** 533 * This is called internally once all animations have been set up by the 534 * transition hierarchy. \ 535 * 536 * @hide 537 */ 538 protected void runAnimators() { 539 if (DBG) { 540 Log.d(LOG_TAG, "runAnimators() on " + this); 541 } 542 start(); 543 ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); 544 // Now start every Animator that was previously created for this transition 545 for (Animator anim : mAnimators) { 546 if (DBG) { 547 Log.d(LOG_TAG, " anim: " + anim); 548 } 549 if (runningAnimators.containsKey(anim)) { 550 start(); 551 runAnimator(anim, runningAnimators); 552 } 553 } 554 mAnimators.clear(); 555 end(); 556 } 557 558 private void runAnimator(Animator animator, 559 final ArrayMap<Animator, AnimationInfo> runningAnimators) { 560 if (animator != null) { 561 // TODO: could be a single listener instance for all of them since it uses the param 562 animator.addListener(new AnimatorListenerAdapter() { 563 @Override 564 public void onAnimationStart(Animator animation) { 565 mCurrentAnimators.add(animation); 566 } 567 @Override 568 public void onAnimationEnd(Animator animation) { 569 runningAnimators.remove(animation); 570 mCurrentAnimators.remove(animation); 571 } 572 }); 573 animate(animator); 574 } 575 } 576 577 /** 578 * Captures the values in the start scene for the properties that this 579 * transition monitors. These values are then passed as the startValues 580 * structure in a later call to 581 * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}. 582 * The main concern for an implementation is what the 583 * properties are that the transition cares about and what the values are 584 * for all of those properties. The start and end values will be compared 585 * later during the 586 * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)} 587 * method to determine what, if any, animations, should be run. 588 * 589 * <p>Subclasses must implement this method. The method should only be called by the 590 * transition system; it is not intended to be called from external classes.</p> 591 * 592 * @param transitionValues The holder for any values that the Transition 593 * wishes to store. Values are stored in the <code>values</code> field 594 * of this TransitionValues object and are keyed from 595 * a String value. For example, to store a view's rotation value, 596 * a transition might call 597 * <code>transitionValues.values.put("appname:transitionname:rotation", 598 * view.getRotation())</code>. The target view will already be stored in 599 * the transitionValues structure when this method is called. 600 * 601 * @see #captureEndValues(TransitionValues) 602 * @see #createAnimator(ViewGroup, TransitionValues, TransitionValues) 603 */ 604 public abstract void captureStartValues(TransitionValues transitionValues); 605 606 /** 607 * Captures the values in the end scene for the properties that this 608 * transition monitors. These values are then passed as the endValues 609 * structure in a later call to 610 * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}. 611 * The main concern for an implementation is what the 612 * properties are that the transition cares about and what the values are 613 * for all of those properties. The start and end values will be compared 614 * later during the 615 * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)} 616 * method to determine what, if any, animations, should be run. 617 * 618 * <p>Subclasses must implement this method. The method should only be called by the 619 * transition system; it is not intended to be called from external classes.</p> 620 * 621 * @param transitionValues The holder for any values that the Transition 622 * wishes to store. Values are stored in the <code>values</code> field 623 * of this TransitionValues object and are keyed from 624 * a String value. For example, to store a view's rotation value, 625 * a transition might call 626 * <code>transitionValues.values.put("appname:transitionname:rotation", 627 * view.getRotation())</code>. The target view will already be stored in 628 * the transitionValues structure when this method is called. 629 * 630 * @see #captureStartValues(TransitionValues) 631 * @see #createAnimator(ViewGroup, TransitionValues, TransitionValues) 632 */ 633 public abstract void captureEndValues(TransitionValues transitionValues); 634 635 /** 636 * Adds the id of a target view that this Transition is interested in 637 * animating. By default, there are no targetIds, and a Transition will 638 * listen for changes on every view in the hierarchy below the sceneRoot 639 * of the Scene being transitioned into. Setting targetIds constrains 640 * the Transition to only listen for, and act on, views with these IDs. 641 * Views with different IDs, or no IDs whatsoever, will be ignored. 642 * 643 * <p>Note that using ids to specify targets implies that ids should be unique 644 * within the view hierarchy underneat the scene root.</p> 645 * 646 * @see View#getId() 647 * @param targetId The id of a target view, must be a positive number. 648 * @return The Transition to which the targetId is added. 649 * Returning the same object makes it easier to chain calls during 650 * construction, such as 651 * <code>transitionSet.addTransitions(new Fade()).addTargetId(someId);</code> 652 */ 653 public Transition addTargetId(int targetId) { 654 if (targetId > 0) { 655 mTargetIds.add(targetId); 656 } 657 return this; 658 } 659 660 /** 661 * Removes the given targetId from the list of ids that this Transition 662 * is interested in animating. 663 * 664 * @param targetId The id of a target view, must be a positive number. 665 * @return The Transition from which the targetId is removed. 666 * Returning the same object makes it easier to chain calls during 667 * construction, such as 668 * <code>transitionSet.addTransitions(new Fade()).removeTargetId(someId);</code> 669 */ 670 public Transition removeTargetId(int targetId) { 671 if (targetId > 0) { 672 mTargetIds.remove(targetId); 673 } 674 return this; 675 } 676 677 /** 678 * Sets the target view instances that this Transition is interested in 679 * animating. By default, there are no targets, and a Transition will 680 * listen for changes on every view in the hierarchy below the sceneRoot 681 * of the Scene being transitioned into. Setting targets constrains 682 * the Transition to only listen for, and act on, these views. 683 * All other views will be ignored. 684 * 685 * <p>The target list is like the {@link #addTargetId(int) targetId} 686 * list except this list specifies the actual View instances, not the ids 687 * of the views. This is an important distinction when scene changes involve 688 * view hierarchies which have been inflated separately; different views may 689 * share the same id but not actually be the same instance. If the transition 690 * should treat those views as the same, then {@link #addTargetId(int)} should be used 691 * instead of {@link #addTarget(View)}. If, on the other hand, scene changes involve 692 * changes all within the same view hierarchy, among views which do not 693 * necessarily have ids set on them, then the target list of views may be more 694 * convenient.</p> 695 * 696 * @see #addTargetId(int) 697 * @param target A View on which the Transition will act, must be non-null. 698 * @return The Transition to which the target is added. 699 * Returning the same object makes it easier to chain calls during 700 * construction, such as 701 * <code>transitionSet.addTransitions(new Fade()).addTarget(someView);</code> 702 */ 703 public Transition addTarget(View target) { 704 mTargets.add(target); 705 return this; 706 } 707 708 /** 709 * Removes the given target from the list of targets that this Transition 710 * is interested in animating. 711 * 712 * @param target The target view, must be non-null. 713 * @return Transition The Transition from which the target is removed. 714 * Returning the same object makes it easier to chain calls during 715 * construction, such as 716 * <code>transitionSet.addTransitions(new Fade()).removeTarget(someView);</code> 717 */ 718 public Transition removeTarget(View target) { 719 if (target != null) { 720 mTargets.remove(target); 721 } 722 return this; 723 } 724 725 /** 726 * Returns the array of target IDs that this transition limits itself to 727 * tracking and animating. If the array is null for both this method and 728 * {@link #getTargets()}, then this transition is 729 * not limited to specific views, and will handle changes to any views 730 * in the hierarchy of a scene change. 731 * 732 * @return the list of target IDs 733 */ 734 public List<Integer> getTargetIds() { 735 return mTargetIds; 736 } 737 738 /** 739 * Returns the array of target views that this transition limits itself to 740 * tracking and animating. If the array is null for both this method and 741 * {@link #getTargetIds()}, then this transition is 742 * not limited to specific views, and will handle changes to any views 743 * in the hierarchy of a scene change. 744 * 745 * @return the list of target views 746 */ 747 public List<View> getTargets() { 748 return mTargets; 749 } 750 751 /** 752 * Recursive method that captures values for the given view and the 753 * hierarchy underneath it. 754 * @param sceneRoot The root of the view hierarchy being captured 755 * @param start true if this capture is happening before the scene change, 756 * false otherwise 757 */ 758 void captureValues(ViewGroup sceneRoot, boolean start) { 759 if (start) { 760 mStartValues.viewValues.clear(); 761 mStartValues.idValues.clear(); 762 mStartValues.itemIdValues.clear(); 763 } else { 764 mEndValues.viewValues.clear(); 765 mEndValues.idValues.clear(); 766 mEndValues.itemIdValues.clear(); 767 } 768 if (mTargetIds.size() > 0 || mTargets.size() > 0) { 769 if (mTargetIds.size() > 0) { 770 for (int i = 0; i < mTargetIds.size(); ++i) { 771 int id = mTargetIds.get(i); 772 View view = sceneRoot.findViewById(id); 773 if (view != null) { 774 TransitionValues values = new TransitionValues(); 775 values.view = view; 776 if (start) { 777 captureStartValues(values); 778 } else { 779 captureEndValues(values); 780 } 781 if (start) { 782 mStartValues.viewValues.put(view, values); 783 if (id >= 0) { 784 mStartValues.idValues.put(id, values); 785 } 786 } else { 787 mEndValues.viewValues.put(view, values); 788 if (id >= 0) { 789 mEndValues.idValues.put(id, values); 790 } 791 } 792 } 793 } 794 } 795 if (mTargets.size() > 0) { 796 for (int i = 0; i < mTargets.size(); ++i) { 797 View view = mTargets.get(i); 798 if (view != null) { 799 TransitionValues values = new TransitionValues(); 800 values.view = view; 801 if (start) { 802 captureStartValues(values); 803 } else { 804 captureEndValues(values); 805 } 806 if (start) { 807 mStartValues.viewValues.put(view, values); 808 } else { 809 mEndValues.viewValues.put(view, values); 810 } 811 } 812 } 813 } 814 } else { 815 captureHierarchy(sceneRoot, start); 816 } 817 } 818 819 /** 820 * Recursive method which captures values for an entire view hierarchy, 821 * starting at some root view. Transitions without targetIDs will use this 822 * method to capture values for all possible views. 823 * 824 * @param view The view for which to capture values. Children of this View 825 * will also be captured, recursively down to the leaf nodes. 826 * @param start true if values are being captured in the start scene, false 827 * otherwise. 828 */ 829 private void captureHierarchy(View view, boolean start) { 830 if (view == null) { 831 return; 832 } 833 boolean isListViewItem = false; 834 if (view.getParent() instanceof ListView) { 835 isListViewItem = true; 836 } 837 if (isListViewItem && !((ListView) view.getParent()).getAdapter().hasStableIds()) { 838 // ignore listview children unless we can track them with stable IDs 839 return; 840 } 841 long id; 842 if (!isListViewItem) { 843 id = view.getId(); 844 } else { 845 ListView listview = (ListView) view.getParent(); 846 int position = listview.getPositionForView(view); 847 id = listview.getItemIdAtPosition(position); 848 view.setHasTransientState(true); 849 } 850 TransitionValues values = new TransitionValues(); 851 values.view = view; 852 captureStartValues(values); 853 if (start) { 854 if (!isListViewItem) { 855 mStartValues.viewValues.put(view, values); 856 if (id >= 0) { 857 mStartValues.idValues.put((int) id, values); 858 } 859 } else { 860 mStartValues.itemIdValues.put(id, values); 861 } 862 } else { 863 if (!isListViewItem) { 864 mEndValues.viewValues.put(view, values); 865 if (id >= 0) { 866 mEndValues.idValues.put((int) id, values); 867 } 868 } else { 869 mEndValues.itemIdValues.put(id, values); 870 } 871 } 872 if (view instanceof ViewGroup) { 873 ViewGroup parent = (ViewGroup) view; 874 for (int i = 0; i < parent.getChildCount(); ++i) { 875 captureHierarchy(parent.getChildAt(i), start); 876 } 877 } 878 } 879 880 /** 881 * This method can be called by transitions to get the TransitionValues for 882 * any particular view during the transition-playing process. This might be 883 * necessary, for example, to query the before/after state of related views 884 * for a given transition. 885 */ 886 public TransitionValues getTransitionValues(View view, boolean start) { 887 if (mParent != null) { 888 return mParent.getTransitionValues(view, start); 889 } 890 TransitionValuesMaps valuesMaps = start ? mStartValues : mEndValues; 891 TransitionValues values = valuesMaps.viewValues.get(view); 892 if (values == null) { 893 int id = view.getId(); 894 if (id >= 0) { 895 values = valuesMaps.idValues.get(id); 896 } 897 if (values == null && view.getParent() instanceof ListView) { 898 ListView listview = (ListView) view.getParent(); 899 int position = listview.getPositionForView(view); 900 long itemId = listview.getItemIdAtPosition(position); 901 values = valuesMaps.itemIdValues.get(itemId); 902 } 903 // TODO: Doesn't handle the case where a view was parented to a 904 // ListView (with an itemId), but no longer is 905 } 906 return values; 907 } 908 909 /** 910 * Pauses this transition, sending out calls to {@link 911 * TransitionListener#onTransitionPause(Transition)} to all listeners 912 * and pausing all running animators started by this transition. 913 * 914 * @hide 915 */ 916 public void pause() { 917 ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); 918 int numOldAnims = runningAnimators.size(); 919 for (int i = numOldAnims - 1; i >= 0; i--) { 920 Animator anim = runningAnimators.keyAt(i); 921 anim.pause(); 922 } 923 if (mListeners != null && mListeners.size() > 0) { 924 ArrayList<TransitionListener> tmpListeners = 925 (ArrayList<TransitionListener>) mListeners.clone(); 926 int numListeners = tmpListeners.size(); 927 for (int i = 0; i < numListeners; ++i) { 928 tmpListeners.get(i).onTransitionPause(this); 929 } 930 } 931 mPaused = true; 932 } 933 934 /** 935 * Resumes this transition, sending out calls to {@link 936 * TransitionListener#onTransitionPause(Transition)} to all listeners 937 * and pausing all running animators started by this transition. 938 * 939 * @hide 940 */ 941 public void resume() { 942 if (mPaused) { 943 ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); 944 int numOldAnims = runningAnimators.size(); 945 for (int i = numOldAnims - 1; i >= 0; i--) { 946 Animator anim = runningAnimators.keyAt(i); 947 anim.resume(); 948 } 949 if (mListeners != null && mListeners.size() > 0) { 950 ArrayList<TransitionListener> tmpListeners = 951 (ArrayList<TransitionListener>) mListeners.clone(); 952 int numListeners = tmpListeners.size(); 953 for (int i = 0; i < numListeners; ++i) { 954 tmpListeners.get(i).onTransitionResume(this); 955 } 956 } 957 mPaused = false; 958 } 959 } 960 961 /** 962 * Called by TransitionManager to play the transition. This calls 963 * createAnimators() to set things up and create all of the animations and then 964 * runAnimations() to actually start the animations. 965 */ 966 void playTransition(ViewGroup sceneRoot) { 967 ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); 968 int numOldAnims = runningAnimators.size(); 969 for (int i = numOldAnims - 1; i >= 0; i--) { 970 Animator anim = runningAnimators.keyAt(i); 971 if (anim != null) { 972 AnimationInfo oldInfo = runningAnimators.get(anim); 973 if (oldInfo != null) { 974 boolean cancel = false; 975 TransitionValues oldValues = oldInfo.values; 976 View oldView = oldInfo.view; 977 TransitionValues newValues = mEndValues.viewValues != null ? 978 mEndValues.viewValues.get(oldView) : null; 979 if (oldValues != null) { 980 // if oldValues null, then transition didn't care to stash values, 981 // and won't get canceled 982 if (newValues == null) { 983 cancel = true; 984 } else { 985 for (String key : oldValues.values.keySet()) { 986 Object oldValue = oldValues.values.get(key); 987 Object newValue = newValues.values.get(key); 988 if (oldValue != null && newValue != null && 989 !oldValue.equals(newValue)) { 990 cancel = true; 991 if (DBG) { 992 Log.d(LOG_TAG, "Transition.playTransition: " + 993 "oldValue != newValue for " + key + 994 ": old, new = " + oldValue + ", " + newValue); 995 } 996 break; 997 } 998 } 999 } 1000 } 1001 if (cancel) { 1002 if (anim.isRunning() || anim.isStarted()) { 1003 if (DBG) { 1004 Log.d(LOG_TAG, "Canceling anim " + anim); 1005 } 1006 anim.cancel(); 1007 } else { 1008 if (DBG) { 1009 Log.d(LOG_TAG, "removing anim from info list: " + anim); 1010 } 1011 runningAnimators.remove(anim); 1012 } 1013 } 1014 } 1015 } 1016 } 1017 1018 createAnimators(sceneRoot, mStartValues, mEndValues); 1019 runAnimators(); 1020 } 1021 1022 /** 1023 * This is a utility method used by subclasses to handle standard parts of 1024 * setting up and running an Animator: it sets the {@link #getDuration() 1025 * duration} and the {@link #getStartDelay() startDelay}, starts the 1026 * animation, and, when the animator ends, calls {@link #end()}. 1027 * 1028 * @param animator The Animator to be run during this transition. 1029 * 1030 * @hide 1031 */ 1032 protected void animate(Animator animator) { 1033 // TODO: maybe pass auto-end as a boolean parameter? 1034 if (animator == null) { 1035 end(); 1036 } else { 1037 if (getDuration() >= 0) { 1038 animator.setDuration(getDuration()); 1039 } 1040 if (getStartDelay() >= 0) { 1041 animator.setStartDelay(getStartDelay()); 1042 } 1043 if (getInterpolator() != null) { 1044 animator.setInterpolator(getInterpolator()); 1045 } 1046 animator.addListener(new AnimatorListenerAdapter() { 1047 @Override 1048 public void onAnimationEnd(Animator animation) { 1049 end(); 1050 animation.removeListener(this); 1051 } 1052 }); 1053 animator.start(); 1054 } 1055 } 1056 1057 /** 1058 * This method is called automatically by the transition and 1059 * TransitionSet classes prior to a Transition subclass starting; 1060 * subclasses should not need to call it directly. 1061 * 1062 * @hide 1063 */ 1064 protected void start() { 1065 if (mNumInstances == 0) { 1066 if (mListeners != null && mListeners.size() > 0) { 1067 ArrayList<TransitionListener> tmpListeners = 1068 (ArrayList<TransitionListener>) mListeners.clone(); 1069 int numListeners = tmpListeners.size(); 1070 for (int i = 0; i < numListeners; ++i) { 1071 tmpListeners.get(i).onTransitionStart(this); 1072 } 1073 } 1074 } 1075 mNumInstances++; 1076 } 1077 1078 /** 1079 * This method is called automatically by the Transition and 1080 * TransitionSet classes when a transition finishes, either because 1081 * a transition did nothing (returned a null Animator from 1082 * {@link Transition#createAnimator(ViewGroup, TransitionValues, 1083 * TransitionValues)}) or because the transition returned a valid 1084 * Animator and end() was called in the onAnimationEnd() 1085 * callback of the AnimatorListener. 1086 * 1087 * @hide 1088 */ 1089 protected void end() { 1090 --mNumInstances; 1091 if (mNumInstances == 0) { 1092 if (mListeners != null && mListeners.size() > 0) { 1093 ArrayList<TransitionListener> tmpListeners = 1094 (ArrayList<TransitionListener>) mListeners.clone(); 1095 int numListeners = tmpListeners.size(); 1096 for (int i = 0; i < numListeners; ++i) { 1097 tmpListeners.get(i).onTransitionEnd(this); 1098 } 1099 } 1100 for (int i = 0; i < mStartValues.itemIdValues.size(); ++i) { 1101 TransitionValues tv = mStartValues.itemIdValues.valueAt(i); 1102 View v = tv.view; 1103 if (v.hasTransientState()) { 1104 v.setHasTransientState(false); 1105 } 1106 } 1107 for (int i = 0; i < mEndValues.itemIdValues.size(); ++i) { 1108 TransitionValues tv = mEndValues.itemIdValues.valueAt(i); 1109 View v = tv.view; 1110 if (v.hasTransientState()) { 1111 v.setHasTransientState(false); 1112 } 1113 } 1114 } 1115 } 1116 1117 /** 1118 * This method cancels a transition that is currently running. 1119 * 1120 * @hide 1121 */ 1122 protected void cancel() { 1123 int numAnimators = mCurrentAnimators.size(); 1124 for (int i = numAnimators - 1; i >= 0; i--) { 1125 Animator animator = mCurrentAnimators.get(i); 1126 animator.cancel(); 1127 } 1128 if (mListeners != null && mListeners.size() > 0) { 1129 ArrayList<TransitionListener> tmpListeners = 1130 (ArrayList<TransitionListener>) mListeners.clone(); 1131 int numListeners = tmpListeners.size(); 1132 for (int i = 0; i < numListeners; ++i) { 1133 tmpListeners.get(i).onTransitionCancel(this); 1134 } 1135 } 1136 } 1137 1138 /** 1139 * Adds a listener to the set of listeners that are sent events through the 1140 * life of an animation, such as start, repeat, and end. 1141 * 1142 * @param listener the listener to be added to the current set of listeners 1143 * for this animation. 1144 * @return This transition object. 1145 */ 1146 public Transition addListener(TransitionListener listener) { 1147 if (mListeners == null) { 1148 mListeners = new ArrayList<TransitionListener>(); 1149 } 1150 mListeners.add(listener); 1151 return this; 1152 } 1153 1154 /** 1155 * Removes a listener from the set listening to this animation. 1156 * 1157 * @param listener the listener to be removed from the current set of 1158 * listeners for this transition. 1159 * @return This transition object. 1160 */ 1161 public Transition removeListener(TransitionListener listener) { 1162 if (mListeners == null) { 1163 return this; 1164 } 1165 mListeners.remove(listener); 1166 if (mListeners.size() == 0) { 1167 mListeners = null; 1168 } 1169 return this; 1170 } 1171 1172 Transition setSceneRoot(ViewGroup sceneRoot) { 1173 mSceneRoot = sceneRoot; 1174 return this; 1175 } 1176 1177 @Override 1178 public String toString() { 1179 return toString(""); 1180 } 1181 1182 @Override 1183 public Transition clone() { 1184 Transition clone = null; 1185 try { 1186 clone = (Transition) super.clone(); 1187 clone.mAnimators = new ArrayList<Animator>(); 1188 } catch (CloneNotSupportedException e) {} 1189 1190 return clone; 1191 } 1192 1193 /** 1194 * Returns the name of this Transition. This name is used internally to distinguish 1195 * between different transitions to determine when interrupting transitions overlap. 1196 * For example, a ChangeBounds running on the same target view as another ChangeBounds 1197 * should determine whether the old transition is animating to different end values 1198 * and should be canceled in favor of the new transition. 1199 * 1200 * <p>By default, a Transition's name is simply the value of {@link Class#getName()}, 1201 * but subclasses are free to override and return something different.</p> 1202 * 1203 * @return The name of this transition. 1204 */ 1205 public String getName() { 1206 return mName; 1207 } 1208 1209 String toString(String indent) { 1210 String result = indent + getClass().getSimpleName() + "@" + 1211 Integer.toHexString(hashCode()) + ": "; 1212 if (mDuration != -1) { 1213 result += "dur(" + mDuration + ") "; 1214 } 1215 if (mStartDelay != -1) { 1216 result += "dly(" + mStartDelay + ") "; 1217 } 1218 if (mInterpolator != null) { 1219 result += "interp(" + mInterpolator + ") "; 1220 } 1221 if (mTargetIds.size() > 0 || mTargets.size() > 0) { 1222 result += "tgts("; 1223 if (mTargetIds.size() > 0) { 1224 for (int i = 0; i < mTargetIds.size(); ++i) { 1225 if (i > 0) { 1226 result += ", "; 1227 } 1228 result += mTargetIds.get(i); 1229 } 1230 } 1231 if (mTargets.size() > 0) { 1232 for (int i = 0; i < mTargets.size(); ++i) { 1233 if (i > 0) { 1234 result += ", "; 1235 } 1236 result += mTargets.get(i); 1237 } 1238 } 1239 result += ")"; 1240 } 1241 return result; 1242 } 1243 1244 /** 1245 * A transition listener receives notifications from a transition. 1246 * Notifications indicate transition lifecycle events. 1247 */ 1248 public static interface TransitionListener { 1249 /** 1250 * Notification about the start of the transition. 1251 * 1252 * @param transition The started transition. 1253 */ 1254 void onTransitionStart(Transition transition); 1255 1256 /** 1257 * Notification about the end of the transition. Canceled transitions 1258 * will always notify listeners of both the cancellation and end 1259 * events. That is, {@link #onTransitionEnd(Transition)} is always called, 1260 * regardless of whether the transition was canceled or played 1261 * through to completion. 1262 * 1263 * @param transition The transition which reached its end. 1264 */ 1265 void onTransitionEnd(Transition transition); 1266 1267 /** 1268 * Notification about the cancellation of the transition. 1269 * Note that cancel may be called by a parent {@link TransitionSet} on 1270 * a child transition which has not yet started. This allows the child 1271 * transition to restore state on target objects which was set at 1272 * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues) 1273 * createAnimator()} time. 1274 * 1275 * @param transition The transition which was canceled. 1276 */ 1277 void onTransitionCancel(Transition transition); 1278 1279 /** 1280 * Notification when a transition is paused. 1281 * Note that createAnimator() may be called by a parent {@link TransitionSet} on 1282 * a child transition which has not yet started. This allows the child 1283 * transition to restore state on target objects which was set at 1284 * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues) 1285 * createAnimator()} time. 1286 * 1287 * @param transition The transition which was paused. 1288 */ 1289 void onTransitionPause(Transition transition); 1290 1291 /** 1292 * Notification when a transition is resumed. 1293 * Note that resume() may be called by a parent {@link TransitionSet} on 1294 * a child transition which has not yet started. This allows the child 1295 * transition to restore state which may have changed in an earlier call 1296 * to {@link #onTransitionPause(Transition)}. 1297 * 1298 * @param transition The transition which was resumed. 1299 */ 1300 void onTransitionResume(Transition transition); 1301 } 1302 1303 /** 1304 * Utility adapter class to avoid having to override all three methods 1305 * whenever someone just wants to listen for a single event. 1306 * 1307 * @hide 1308 * */ 1309 public static class TransitionListenerAdapter implements TransitionListener { 1310 @Override 1311 public void onTransitionStart(Transition transition) { 1312 } 1313 1314 @Override 1315 public void onTransitionEnd(Transition transition) { 1316 } 1317 1318 @Override 1319 public void onTransitionCancel(Transition transition) { 1320 } 1321 1322 @Override 1323 public void onTransitionPause(Transition transition) { 1324 } 1325 1326 @Override 1327 public void onTransitionResume(Transition transition) { 1328 } 1329 } 1330 1331 /** 1332 * Holds information about each animator used when a new transition starts 1333 * while other transitions are still running to determine whether a running 1334 * animation should be canceled or a new animation noop'd. The structure holds 1335 * information about the state that an animation is going to, to be compared to 1336 * end state of a new animation. 1337 */ 1338 private static class AnimationInfo { 1339 View view; 1340 String name; 1341 TransitionValues values; 1342 1343 AnimationInfo(View view, String name, TransitionValues values) { 1344 this.view = view; 1345 this.name = name; 1346 this.values = values; 1347 } 1348 } 1349} 1350