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