/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.transition;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.Path;
import android.graphics.Rect;
import android.support.annotation.IdRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.v4.content.res.TypedArrayUtils;
import android.support.v4.util.ArrayMap;
import android.support.v4.util.LongSparseArray;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.InflateException;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.widget.ListView;
import android.widget.Spinner;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
/**
* A Transition holds information about animations that will be run on its
* targets during a scene change. Subclasses of this abstract class may
* choreograph several child transitions ({@link TransitionSet} or they may
* perform custom animations themselves. Any Transition has two main jobs:
* (1) capture property values, and (2) play animations based on changes to
* captured property values. A custom transition knows what property values
* on View objects are of interest to it, and also knows how to animate
* changes to those values. For example, the {@link Fade} transition tracks
* changes to visibility-related properties and is able to construct and run
* animations that fade items in or out based on changes to those properties.
*
*
Note: Transitions may not work correctly with either {@link SurfaceView}
* or {@link TextureView}, due to the way that these views are displayed
* on the screen. For SurfaceView, the problem is that the view is updated from
* a non-UI thread, so changes to the view due to transitions (such as moving
* and resizing the view) may be out of sync with the display inside those bounds.
* TextureView is more compatible with transitions in general, but some
* specific transitions (such as {@link Fade}) may not be compatible
* with TextureView because they rely on {@link android.view.ViewOverlay}
* functionality, which does not currently work with TextureView.
*
* Transitions can be declared in XML resource files inside the res/transition
* directory. Transition resources consist of a tag name for one of the Transition
* subclasses along with attributes to define some of the attributes of that transition.
* For example, here is a minimal resource file that declares a {@link ChangeBounds}
* transition:
*
*
* <changeBounds/>
*
*
* Note that attributes for the transition are not required, just as they are
* optional when declared in code; Transitions created from XML resources will use
* the same defaults as their code-created equivalents. Here is a slightly more
* elaborate example which declares a {@link TransitionSet} transition with
* {@link ChangeBounds} and {@link Fade} child transitions:
*
*
* <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
* android:transitionOrdering="sequential">
* <changeBounds/>
* <fade android:fadingMode="fade_out">
* <targets>
* <target android:targetId="@id/grayscaleContainer"/>
* </targets>
* </fade>
* </transitionSet>
*
*
* In this example, the transitionOrdering attribute is used on the TransitionSet
* object to change from the default {@link TransitionSet#ORDERING_TOGETHER} behavior
* to be {@link TransitionSet#ORDERING_SEQUENTIAL} instead. Also, the {@link Fade}
* transition uses a fadingMode of {@link Fade#OUT} instead of the default
* out-in behavior. Finally, note the use of the targets
sub-tag, which
* takes a set of {code target} tags, each of which lists a specific targetId
which
* this transition acts upon. Use of targets is optional, but can be used to either limit the time
* spent checking attributes on unchanging views, or limiting the types of animations run on
* specific views. In this case, we know that only the grayscaleContainer
will be
* disappearing, so we choose to limit the {@link Fade} transition to only that view.
*/
public abstract class Transition implements Cloneable {
private static final String LOG_TAG = "Transition";
static final boolean DBG = false;
/**
* With {@link #setMatchOrder(int...)}, chooses to match by View instance.
*/
public static final int MATCH_INSTANCE = 0x1;
private static final int MATCH_FIRST = MATCH_INSTANCE;
/**
* With {@link #setMatchOrder(int...)}, chooses to match by
* {@link android.view.View#getTransitionName()}. Null names will not be matched.
*/
public static final int MATCH_NAME = 0x2;
/**
* With {@link #setMatchOrder(int...)}, chooses to match by
* {@link android.view.View#getId()}. Negative IDs will not be matched.
*/
public static final int MATCH_ID = 0x3;
/**
* With {@link #setMatchOrder(int...)}, chooses to match by the {@link android.widget.Adapter}
* item id. When {@link android.widget.Adapter#hasStableIds()} returns false, no match
* will be made for items.
*/
public static final int MATCH_ITEM_ID = 0x4;
private static final int MATCH_LAST = MATCH_ITEM_ID;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef({MATCH_INSTANCE, MATCH_NAME, MATCH_ID, MATCH_ITEM_ID})
@Retention(RetentionPolicy.SOURCE)
public @interface MatchOrder {
}
private static final String MATCH_INSTANCE_STR = "instance";
private static final String MATCH_NAME_STR = "name";
private static final String MATCH_ID_STR = "id";
private static final String MATCH_ITEM_ID_STR = "itemId";
private static final int[] DEFAULT_MATCH_ORDER = {
MATCH_NAME,
MATCH_INSTANCE,
MATCH_ID,
MATCH_ITEM_ID,
};
private static final PathMotion STRAIGHT_PATH_MOTION = new PathMotion() {
@Override
public Path getPath(float startX, float startY, float endX, float endY) {
Path path = new Path();
path.moveTo(startX, startY);
path.lineTo(endX, endY);
return path;
}
};
private String mName = getClass().getName();
private long mStartDelay = -1;
long mDuration = -1;
private TimeInterpolator mInterpolator = null;
ArrayList mTargetIds = new ArrayList<>();
ArrayList mTargets = new ArrayList<>();
private ArrayList mTargetNames = null;
private ArrayList mTargetTypes = null;
private ArrayList mTargetIdExcludes = null;
private ArrayList mTargetExcludes = null;
private ArrayList mTargetTypeExcludes = null;
private ArrayList mTargetNameExcludes = null;
private ArrayList mTargetIdChildExcludes = null;
private ArrayList mTargetChildExcludes = null;
private ArrayList mTargetTypeChildExcludes = null;
private TransitionValuesMaps mStartValues = new TransitionValuesMaps();
private TransitionValuesMaps mEndValues = new TransitionValuesMaps();
TransitionSet mParent = null;
private int[] mMatchOrder = DEFAULT_MATCH_ORDER;
private ArrayList mStartValuesList; // only valid after playTransition starts
private ArrayList mEndValuesList; // only valid after playTransitions starts
// Per-animator information used for later canceling when future transitions overlap
private static ThreadLocal> sRunningAnimators =
new ThreadLocal<>();
// Scene Root is set at createAnimator() time in the cloned Transition
private ViewGroup mSceneRoot = null;
// Whether removing views from their parent is possible. This is only for views
// in the start scene, which are no longer in the view hierarchy. This property
// is determined by whether the previous Scene was created from a layout
// resource, and thus the views from the exited scene are going away anyway
// and can be removed as necessary to achieve a particular effect, such as
// removing them from parents to add them to overlays.
boolean mCanRemoveViews = false;
// Track all animators in use in case the transition gets canceled and needs to
// cancel running animators
private ArrayList mCurrentAnimators = new ArrayList<>();
// Number of per-target instances of this Transition currently running. This count is
// determined by calls to start() and end()
private int mNumInstances = 0;
// Whether this transition is currently paused, due to a call to pause()
private boolean mPaused = false;
// Whether this transition has ended. Used to avoid pause/resume on transitions
// that have completed
private boolean mEnded = false;
// The set of listeners to be sent transition lifecycle events.
private ArrayList mListeners = null;
// The set of animators collected from calls to createAnimator(),
// to be run in runAnimators()
private ArrayList mAnimators = new ArrayList<>();
// The function for calculating the Animation start delay.
TransitionPropagation mPropagation;
// The rectangular region for Transitions like Explode and TransitionPropagations
// like CircularPropagation
private EpicenterCallback mEpicenterCallback;
// For Fragment shared element transitions, linking views explicitly by mismatching
// transitionNames.
private ArrayMap mNameOverrides;
// The function used to interpolate along two-dimensional points. Typically used
// for adding curves to x/y View motion.
private PathMotion mPathMotion = STRAIGHT_PATH_MOTION;
/**
* Constructs a Transition object with no target objects. A transition with
* no targets defaults to running on all target objects in the scene hierarchy
* (if the transition is not contained in a TransitionSet), or all target
* objects passed down from its parent (if it is in a TransitionSet).
*/
public Transition() {
}
/**
* Perform inflation from XML and apply a class-specific base style from a
* theme attribute or style resource. This constructor of Transition allows
* subclasses to use their own base style when they are inflating.
*
* @param context The Context the transition is running in, through which it can
* access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the transition.
*/
public Transition(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, Styleable.TRANSITION);
XmlResourceParser parser = (XmlResourceParser) attrs;
long duration = TypedArrayUtils.getNamedInt(a, parser, "duration",
Styleable.Transition.DURATION, -1);
if (duration >= 0) {
setDuration(duration);
}
long startDelay = TypedArrayUtils.getNamedInt(a, parser, "startDelay",
Styleable.Transition.START_DELAY, -1);
if (startDelay > 0) {
setStartDelay(startDelay);
}
final int resId = TypedArrayUtils.getNamedResourceId(a, parser, "interpolator",
Styleable.Transition.INTERPOLATOR, 0);
if (resId > 0) {
setInterpolator(AnimationUtils.loadInterpolator(context, resId));
}
String matchOrder = TypedArrayUtils.getNamedString(a, parser, "matchOrder",
Styleable.Transition.MATCH_ORDER);
if (matchOrder != null) {
setMatchOrder(parseMatchOrder(matchOrder));
}
a.recycle();
}
@MatchOrder
private static int[] parseMatchOrder(String matchOrderString) {
StringTokenizer st = new StringTokenizer(matchOrderString, ",");
@MatchOrder
int[] matches = new int[st.countTokens()];
int index = 0;
while (st.hasMoreTokens()) {
String token = st.nextToken().trim();
if (MATCH_ID_STR.equalsIgnoreCase(token)) {
matches[index] = Transition.MATCH_ID;
} else if (MATCH_INSTANCE_STR.equalsIgnoreCase(token)) {
matches[index] = Transition.MATCH_INSTANCE;
} else if (MATCH_NAME_STR.equalsIgnoreCase(token)) {
matches[index] = Transition.MATCH_NAME;
} else if (MATCH_ITEM_ID_STR.equalsIgnoreCase(token)) {
matches[index] = Transition.MATCH_ITEM_ID;
} else if (token.isEmpty()) {
@MatchOrder
int[] smallerMatches = new int[matches.length - 1];
System.arraycopy(matches, 0, smallerMatches, 0, index);
matches = smallerMatches;
index--;
} else {
throw new InflateException("Unknown match type in matchOrder: '" + token + "'");
}
index++;
}
return matches;
}
/**
* Sets the duration of this transition. By default, there is no duration
* (indicated by a negative number), which means that the Animator created by
* the transition will have its own specified duration. If the duration of a
* Transition is set, that duration will override the Animator duration.
*
* @param duration The length of the animation, in milliseconds.
* @return This transition object.
*/
@NonNull
public Transition setDuration(long duration) {
mDuration = duration;
return this;
}
/**
* Returns the duration set on this transition. If no duration has been set,
* the returned value will be negative, indicating that resulting animators will
* retain their own durations.
*
* @return The duration set on this transition, in milliseconds, if one has been
* set, otherwise returns a negative number.
*/
public long getDuration() {
return mDuration;
}
/**
* Sets the startDelay of this transition. By default, there is no delay
* (indicated by a negative number), which means that the Animator created by
* the transition will have its own specified startDelay. If the delay of a
* Transition is set, that delay will override the Animator delay.
*
* @param startDelay The length of the delay, in milliseconds.
* @return This transition object.
*/
@NonNull
public Transition setStartDelay(long startDelay) {
mStartDelay = startDelay;
return this;
}
/**
* Returns the startDelay set on this transition. If no startDelay has been set,
* the returned value will be negative, indicating that resulting animators will
* retain their own startDelays.
*
* @return The startDelay set on this transition, in milliseconds, if one has
* been set, otherwise returns a negative number.
*/
public long getStartDelay() {
return mStartDelay;
}
/**
* Sets the interpolator of this transition. By default, the interpolator
* is null, which means that the Animator created by the transition
* will have its own specified interpolator. If the interpolator of a
* Transition is set, that interpolator will override the Animator interpolator.
*
* @param interpolator The time interpolator used by the transition
* @return This transition object.
*/
@NonNull
public Transition setInterpolator(@Nullable TimeInterpolator interpolator) {
mInterpolator = interpolator;
return this;
}
/**
* Returns the interpolator set on this transition. If no interpolator has been set,
* the returned value will be null, indicating that resulting animators will
* retain their own interpolators.
*
* @return The interpolator set on this transition, if one has been set, otherwise
* returns null.
*/
@Nullable
public TimeInterpolator getInterpolator() {
return mInterpolator;
}
/**
* Returns the set of property names used stored in the {@link TransitionValues}
* object passed into {@link #captureStartValues(TransitionValues)} that
* this transition cares about for the purposes of canceling overlapping animations.
* When any transition is started on a given scene root, all transitions
* currently running on that same scene root are checked to see whether the
* properties on which they based their animations agree with the end values of
* the same properties in the new transition. If the end values are not equal,
* then the old animation is canceled since the new transition will start a new
* animation to these new values. If the values are equal, the old animation is
* allowed to continue and no new animation is started for that transition.
*
* A transition does not need to override this method. However, not doing so
* will mean that the cancellation logic outlined in the previous paragraph
* will be skipped for that transition, possibly leading to artifacts as
* old transitions and new transitions on the same targets run in parallel,
* animating views toward potentially different end values.
*
* @return An array of property names as described in the class documentation for
* {@link TransitionValues}. The default implementation returns null
.
*/
@Nullable
public String[] getTransitionProperties() {
return null;
}
/**
* This method creates an animation that will be run for this transition
* given the information in the startValues and endValues structures captured
* earlier for the start and end scenes. Subclasses of Transition should override
* this method. The method should only be called by the transition system; it is
* not intended to be called from external classes.
*
* This method is called by the transition's parent (all the way up to the
* topmost Transition in the hierarchy) with the sceneRoot and start/end
* values that the transition may need to set up initial target values
* and construct an appropriate animation. For example, if an overall
* Transition is a {@link TransitionSet} consisting of several
* child transitions in sequence, then some of the child transitions may
* want to set initial values on target views prior to the overall
* Transition commencing, to put them in an appropriate state for the
* delay between that start and the child Transition start time. For
* example, a transition that fades an item in may wish to set the starting
* alpha value to 0, to avoid it blinking in prior to the transition
* actually starting the animation. This is necessary because the scene
* change that triggers the Transition will automatically set the end-scene
* on all target views, so a Transition that wants to animate from a
* different value should set that value prior to returning from this method.
*
* Additionally, a Transition can perform logic to determine whether
* the transition needs to run on the given target and start/end values.
* For example, a transition that resizes objects on the screen may wish
* to avoid running for views which are not present in either the start
* or end scenes.
*
* If there is an animator created and returned from this method, the
* transition mechanism will apply any applicable duration, startDelay,
* and interpolator to that animation and start it. A return value of
* null
indicates that no animation should run. The default
* implementation returns null.
*
* The method is called for every applicable target object, which is
* stored in the {@link TransitionValues#view} field.
*
* @param sceneRoot The root of the transition hierarchy.
* @param startValues The values for a specific target in the start scene.
* @param endValues The values for the target in the end scene.
* @return A Animator to be started at the appropriate time in the
* overall transition for this scene change. A null value means no animation
* should be run.
*/
@Nullable
public Animator createAnimator(@NonNull ViewGroup sceneRoot,
@Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
return null;
}
/**
* Sets the order in which Transition matches View start and end values.
*
* The default behavior is to match first by {@link android.view.View#getTransitionName()},
* then by View instance, then by {@link android.view.View#getId()} and finally
* by its item ID if it is in a direct child of ListView. The caller can
* choose to have only some or all of the values of {@link #MATCH_INSTANCE},
* {@link #MATCH_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}. Only
* the match algorithms supplied will be used to determine whether Views are the
* the same in both the start and end Scene. Views that do not match will be considered
* as entering or leaving the Scene.
*
*
* @param matches A list of zero or more of {@link #MATCH_INSTANCE},
* {@link #MATCH_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}.
* If none are provided, then the default match order will be set.
*/
public void setMatchOrder(@MatchOrder int... matches) {
if (matches == null || matches.length == 0) {
mMatchOrder = DEFAULT_MATCH_ORDER;
} else {
for (int i = 0; i < matches.length; i++) {
int match = matches[i];
if (!isValidMatch(match)) {
throw new IllegalArgumentException("matches contains invalid value");
}
if (alreadyContains(matches, i)) {
throw new IllegalArgumentException("matches contains a duplicate value");
}
}
mMatchOrder = matches.clone();
}
}
private static boolean isValidMatch(int match) {
return (match >= MATCH_FIRST && match <= MATCH_LAST);
}
private static boolean alreadyContains(int[] array, int searchIndex) {
int value = array[searchIndex];
for (int i = 0; i < searchIndex; i++) {
if (array[i] == value) {
return true;
}
}
return false;
}
/**
* Match start/end values by View instance. Adds matched values to mStartValuesList
* and mEndValuesList and removes them from unmatchedStart and unmatchedEnd.
*/
private void matchInstances(ArrayMap unmatchedStart,
ArrayMap unmatchedEnd) {
for (int i = unmatchedStart.size() - 1; i >= 0; i--) {
View view = unmatchedStart.keyAt(i);
if (view != null && isValidTarget(view)) {
TransitionValues end = unmatchedEnd.remove(view);
if (end != null && end.view != null && isValidTarget(end.view)) {
TransitionValues start = unmatchedStart.removeAt(i);
mStartValuesList.add(start);
mEndValuesList.add(end);
}
}
}
}
/**
* Match start/end values by Adapter item ID. Adds matched values to mStartValuesList
* and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
* startItemIds and endItemIds as a guide for which Views have unique item IDs.
*/
private void matchItemIds(ArrayMap unmatchedStart,
ArrayMap unmatchedEnd,
LongSparseArray startItemIds, LongSparseArray endItemIds) {
int numStartIds = startItemIds.size();
for (int i = 0; i < numStartIds; i++) {
View startView = startItemIds.valueAt(i);
if (startView != null && isValidTarget(startView)) {
View endView = endItemIds.get(startItemIds.keyAt(i));
if (endView != null && isValidTarget(endView)) {
TransitionValues startValues = unmatchedStart.get(startView);
TransitionValues endValues = unmatchedEnd.get(endView);
if (startValues != null && endValues != null) {
mStartValuesList.add(startValues);
mEndValuesList.add(endValues);
unmatchedStart.remove(startView);
unmatchedEnd.remove(endView);
}
}
}
}
}
/**
* Match start/end values by Adapter view ID. Adds matched values to mStartValuesList
* and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
* startIds and endIds as a guide for which Views have unique IDs.
*/
private void matchIds(ArrayMap unmatchedStart,
ArrayMap unmatchedEnd,
SparseArray startIds, SparseArray endIds) {
int numStartIds = startIds.size();
for (int i = 0; i < numStartIds; i++) {
View startView = startIds.valueAt(i);
if (startView != null && isValidTarget(startView)) {
View endView = endIds.get(startIds.keyAt(i));
if (endView != null && isValidTarget(endView)) {
TransitionValues startValues = unmatchedStart.get(startView);
TransitionValues endValues = unmatchedEnd.get(endView);
if (startValues != null && endValues != null) {
mStartValuesList.add(startValues);
mEndValuesList.add(endValues);
unmatchedStart.remove(startView);
unmatchedEnd.remove(endView);
}
}
}
}
}
/**
* Match start/end values by Adapter transitionName. Adds matched values to mStartValuesList
* and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
* startNames and endNames as a guide for which Views have unique transitionNames.
*/
private void matchNames(ArrayMap unmatchedStart,
ArrayMap unmatchedEnd,
ArrayMap startNames, ArrayMap endNames) {
int numStartNames = startNames.size();
for (int i = 0; i < numStartNames; i++) {
View startView = startNames.valueAt(i);
if (startView != null && isValidTarget(startView)) {
View endView = endNames.get(startNames.keyAt(i));
if (endView != null && isValidTarget(endView)) {
TransitionValues startValues = unmatchedStart.get(startView);
TransitionValues endValues = unmatchedEnd.get(endView);
if (startValues != null && endValues != null) {
mStartValuesList.add(startValues);
mEndValuesList.add(endValues);
unmatchedStart.remove(startView);
unmatchedEnd.remove(endView);
}
}
}
}
}
/**
* Adds all values from unmatchedStart and unmatchedEnd to mStartValuesList and mEndValuesList,
* assuming that there is no match between values in the list.
*/
private void addUnmatched(ArrayMap unmatchedStart,
ArrayMap unmatchedEnd) {
// Views that only exist in the start Scene
for (int i = 0; i < unmatchedStart.size(); i++) {
final TransitionValues start = unmatchedStart.valueAt(i);
if (isValidTarget(start.view)) {
mStartValuesList.add(start);
mEndValuesList.add(null);
}
}
// Views that only exist in the end Scene
for (int i = 0; i < unmatchedEnd.size(); i++) {
final TransitionValues end = unmatchedEnd.valueAt(i);
if (isValidTarget(end.view)) {
mEndValuesList.add(end);
mStartValuesList.add(null);
}
}
}
private void matchStartAndEnd(TransitionValuesMaps startValues,
TransitionValuesMaps endValues) {
ArrayMap unmatchedStart = new ArrayMap<>(startValues.mViewValues);
ArrayMap unmatchedEnd = new ArrayMap<>(endValues.mViewValues);
for (int i = 0; i < mMatchOrder.length; i++) {
switch (mMatchOrder[i]) {
case MATCH_INSTANCE:
matchInstances(unmatchedStart, unmatchedEnd);
break;
case MATCH_NAME:
matchNames(unmatchedStart, unmatchedEnd,
startValues.mNameValues, endValues.mNameValues);
break;
case MATCH_ID:
matchIds(unmatchedStart, unmatchedEnd,
startValues.mIdValues, endValues.mIdValues);
break;
case MATCH_ITEM_ID:
matchItemIds(unmatchedStart, unmatchedEnd,
startValues.mItemIdValues, endValues.mItemIdValues);
break;
}
}
addUnmatched(unmatchedStart, unmatchedEnd);
}
/**
* This method, essentially a wrapper around all calls to createAnimator for all
* possible target views, is called with the entire set of start/end
* values. The implementation in Transition iterates through these lists
* and calls {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
* with each set of start/end values on this transition. The
* TransitionSet subclass overrides this method and delegates it to
* each of its children in succession.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues,
TransitionValuesMaps endValues, ArrayList startValuesList,
ArrayList endValuesList) {
if (DBG) {
Log.d(LOG_TAG, "createAnimators() for " + this);
}
ArrayMap runningAnimators = getRunningAnimators();
long minStartDelay = Long.MAX_VALUE;
SparseIntArray startDelays = new SparseIntArray();
int startValuesListCount = startValuesList.size();
for (int i = 0; i < startValuesListCount; ++i) {
TransitionValues start = startValuesList.get(i);
TransitionValues end = endValuesList.get(i);
if (start != null && !start.mTargetedTransitions.contains(this)) {
start = null;
}
if (end != null && !end.mTargetedTransitions.contains(this)) {
end = null;
}
if (start == null && end == null) {
continue;
}
// Only bother trying to animate with values that differ between start/end
boolean isChanged = start == null || end == null || isTransitionRequired(start, end);
if (isChanged) {
if (DBG) {
View view = (end != null) ? end.view : start.view;
Log.d(LOG_TAG, " differing start/end values for view " + view);
if (start == null || end == null) {
Log.d(LOG_TAG, " " + ((start == null)
? "start null, end non-null" : "start non-null, end null"));
} else {
for (String key : start.values.keySet()) {
Object startValue = start.values.get(key);
Object endValue = end.values.get(key);
if (startValue != endValue && !startValue.equals(endValue)) {
Log.d(LOG_TAG, " " + key + ": start(" + startValue
+ "), end(" + endValue + ")");
}
}
}
}
// TODO: what to do about targetIds and itemIds?
Animator animator = createAnimator(sceneRoot, start, end);
if (animator != null) {
// Save animation info for future cancellation purposes
View view;
TransitionValues infoValues = null;
if (end != null) {
view = end.view;
String[] properties = getTransitionProperties();
if (view != null && properties != null && properties.length > 0) {
infoValues = new TransitionValues();
infoValues.view = view;
TransitionValues newValues = endValues.mViewValues.get(view);
if (newValues != null) {
for (int j = 0; j < properties.length; ++j) {
infoValues.values.put(properties[j],
newValues.values.get(properties[j]));
}
}
int numExistingAnims = runningAnimators.size();
for (int j = 0; j < numExistingAnims; ++j) {
Animator anim = runningAnimators.keyAt(j);
AnimationInfo info = runningAnimators.get(anim);
if (info.mValues != null && info.mView == view
&& info.mName.equals(getName())) {
if (info.mValues.equals(infoValues)) {
// Favor the old animator
animator = null;
break;
}
}
}
}
} else {
view = start.view;
}
if (animator != null) {
if (mPropagation != null) {
long delay = mPropagation.getStartDelay(sceneRoot, this, start, end);
startDelays.put(mAnimators.size(), (int) delay);
minStartDelay = Math.min(delay, minStartDelay);
}
AnimationInfo info = new AnimationInfo(view, getName(), this,
ViewUtils.getWindowId(sceneRoot), infoValues);
runningAnimators.put(animator, info);
mAnimators.add(animator);
}
}
}
}
if (minStartDelay != 0) {
for (int i = 0; i < startDelays.size(); i++) {
int index = startDelays.keyAt(i);
Animator animator = mAnimators.get(index);
long delay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay();
animator.setStartDelay(delay);
}
}
}
/**
* Internal utility method for checking whether a given view/id
* is valid for this transition, where "valid" means that either
* the Transition has no target/targetId list (the default, in which
* cause the transition should act on all views in the hiearchy), or
* the given view is in the target list or the view id is in the
* targetId list. If the target parameter is null, then the target list
* is not checked (this is in the case of ListView items, where the
* views are ignored and only the ids are used).
*/
boolean isValidTarget(View target) {
int targetId = target.getId();
if (mTargetIdExcludes != null && mTargetIdExcludes.contains(targetId)) {
return false;
}
if (mTargetExcludes != null && mTargetExcludes.contains(target)) {
return false;
}
if (mTargetTypeExcludes != null) {
int numTypes = mTargetTypeExcludes.size();
for (int i = 0; i < numTypes; ++i) {
Class type = mTargetTypeExcludes.get(i);
if (type.isInstance(target)) {
return false;
}
}
}
if (mTargetNameExcludes != null && ViewCompat.getTransitionName(target) != null) {
if (mTargetNameExcludes.contains(ViewCompat.getTransitionName(target))) {
return false;
}
}
if (mTargetIds.size() == 0 && mTargets.size() == 0
&& (mTargetTypes == null || mTargetTypes.isEmpty())
&& (mTargetNames == null || mTargetNames.isEmpty())) {
return true;
}
if (mTargetIds.contains(targetId) || mTargets.contains(target)) {
return true;
}
if (mTargetNames != null && mTargetNames.contains(ViewCompat.getTransitionName(target))) {
return true;
}
if (mTargetTypes != null) {
for (int i = 0; i < mTargetTypes.size(); ++i) {
if (mTargetTypes.get(i).isInstance(target)) {
return true;
}
}
}
return false;
}
private static ArrayMap getRunningAnimators() {
ArrayMap runningAnimators = sRunningAnimators.get();
if (runningAnimators == null) {
runningAnimators = new ArrayMap<>();
sRunningAnimators.set(runningAnimators);
}
return runningAnimators;
}
/**
* This is called internally once all animations have been set up by the
* transition hierarchy. \
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected void runAnimators() {
if (DBG) {
Log.d(LOG_TAG, "runAnimators() on " + this);
}
start();
ArrayMap runningAnimators = getRunningAnimators();
// Now start every Animator that was previously created for this transition
for (Animator anim : mAnimators) {
if (DBG) {
Log.d(LOG_TAG, " anim: " + anim);
}
if (runningAnimators.containsKey(anim)) {
start();
runAnimator(anim, runningAnimators);
}
}
mAnimators.clear();
end();
}
private void runAnimator(Animator animator,
final ArrayMap runningAnimators) {
if (animator != null) {
// TODO: could be a single listener instance for all of them since it uses the param
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mCurrentAnimators.add(animation);
}
@Override
public void onAnimationEnd(Animator animation) {
runningAnimators.remove(animation);
mCurrentAnimators.remove(animation);
}
});
animate(animator);
}
}
/**
* Captures the values in the start scene for the properties that this
* transition monitors. These values are then passed as the startValues
* structure in a later call to
* {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}.
* The main concern for an implementation is what the
* properties are that the transition cares about and what the values are
* for all of those properties. The start and end values will be compared
* later during the
* {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
* method to determine what, if any, animations, should be run.
*
* Subclasses must implement this method. The method should only be called by the
* transition system; it is not intended to be called from external classes.
*
* @param transitionValues The holder for any values that the Transition
* wishes to store. Values are stored in the values
field
* of this TransitionValues object and are keyed from
* a String value. For example, to store a view's rotation value,
* a transition might call
* transitionValues.values.put("appname:transitionname:rotation",
* view.getRotation())
. The target view will already be stored
* in
* the transitionValues structure when this method is called.
* @see #captureEndValues(TransitionValues)
* @see #createAnimator(ViewGroup, TransitionValues, TransitionValues)
*/
public abstract void captureStartValues(@NonNull TransitionValues transitionValues);
/**
* Captures the values in the end scene for the properties that this
* transition monitors. These values are then passed as the endValues
* structure in a later call to
* {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}.
* The main concern for an implementation is what the
* properties are that the transition cares about and what the values are
* for all of those properties. The start and end values will be compared
* later during the
* {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
* method to determine what, if any, animations, should be run.
*
* Subclasses must implement this method. The method should only be called by the
* transition system; it is not intended to be called from external classes.
*
* @param transitionValues The holder for any values that the Transition
* wishes to store. Values are stored in the values
field
* of this TransitionValues object and are keyed from
* a String value. For example, to store a view's rotation value,
* a transition might call
* transitionValues.values.put("appname:transitionname:rotation",
* view.getRotation())
. The target view will already be stored
* in
* the transitionValues structure when this method is called.
* @see #captureStartValues(TransitionValues)
* @see #createAnimator(ViewGroup, TransitionValues, TransitionValues)
*/
public abstract void captureEndValues(@NonNull TransitionValues transitionValues);
/**
* Sets the target view instances that this Transition is interested in
* animating. By default, there are no targets, and a Transition will
* listen for changes on every view in the hierarchy below the sceneRoot
* of the Scene being transitioned into. Setting targets constrains
* the Transition to only listen for, and act on, these views.
* All other views will be ignored.
*
* The target list is like the {@link #addTarget(int) targetId}
* list except this list specifies the actual View instances, not the ids
* of the views. This is an important distinction when scene changes involve
* view hierarchies which have been inflated separately; different views may
* share the same id but not actually be the same instance. If the transition
* should treat those views as the same, then {@link #addTarget(int)} should be used
* instead of {@link #addTarget(View)}. If, on the other hand, scene changes involve
* changes all within the same view hierarchy, among views which do not
* necessarily have ids set on them, then the target list of views may be more
* convenient.
*
* @param target A View on which the Transition will act, must be non-null.
* @return The Transition to which the target is added.
* Returning the same object makes it easier to chain calls during
* construction, such as
* transitionSet.addTransitions(new Fade()).addTarget(someView);
* @see #addTarget(int)
*/
@NonNull
public Transition addTarget(@NonNull View target) {
mTargets.add(target);
return this;
}
/**
* Adds the id of a target view that this Transition is interested in
* animating. By default, there are no targetIds, and a Transition will
* listen for changes on every view in the hierarchy below the sceneRoot
* of the Scene being transitioned into. Setting targetIds constrains
* the Transition to only listen for, and act on, views with these IDs.
* Views with different IDs, or no IDs whatsoever, will be ignored.
*
* Note that using ids to specify targets implies that ids should be unique
* within the view hierarchy underneath the scene root.
*
* @param targetId The id of a target view, must be a positive number.
* @return The Transition to which the targetId is added.
* Returning the same object makes it easier to chain calls during
* construction, such as
* transitionSet.addTransitions(new Fade()).addTarget(someId);
* @see View#getId()
*/
@NonNull
public Transition addTarget(@IdRes int targetId) {
if (targetId > 0) {
mTargetIds.add(targetId);
}
return this;
}
/**
* Adds the transitionName of a target view that this Transition is interested in
* animating. By default, there are no targetNames, and a Transition will
* listen for changes on every view in the hierarchy below the sceneRoot
* of the Scene being transitioned into. Setting targetNames constrains
* the Transition to only listen for, and act on, views with these transitionNames.
* Views with different transitionNames, or no transitionName whatsoever, will be ignored.
*
* Note that transitionNames should be unique within the view hierarchy.
*
* @param targetName The transitionName of a target view, must be non-null.
* @return The Transition to which the target transitionName is added.
* Returning the same object makes it easier to chain calls during
* construction, such as
* transitionSet.addTransitions(new Fade()).addTarget(someName);
* @see ViewCompat#getTransitionName(View)
*/
@NonNull
public Transition addTarget(@NonNull String targetName) {
if (mTargetNames == null) {
mTargetNames = new ArrayList<>();
}
mTargetNames.add(targetName);
return this;
}
/**
* Adds the Class of a target view that this Transition is interested in
* animating. By default, there are no targetTypes, and a Transition will
* listen for changes on every view in the hierarchy below the sceneRoot
* of the Scene being transitioned into. Setting targetTypes constrains
* the Transition to only listen for, and act on, views with these classes.
* Views with different classes will be ignored.
*
* Note that any View that can be cast to targetType will be included, so
* if targetType is View.class
, all Views will be included.
*
* @param targetType The type to include when running this transition.
* @return The Transition to which the target class was added.
* Returning the same object makes it easier to chain calls during
* construction, such as
* transitionSet.addTransitions(new Fade()).addTarget(ImageView.class);
* @see #addTarget(int)
* @see #addTarget(android.view.View)
* @see #excludeTarget(Class, boolean)
* @see #excludeChildren(Class, boolean)
*/
@NonNull
public Transition addTarget(@NonNull Class targetType) {
if (mTargetTypes == null) {
mTargetTypes = new ArrayList<>();
}
mTargetTypes.add(targetType);
return this;
}
/**
* Removes the given target from the list of targets that this Transition
* is interested in animating.
*
* @param target The target view, must be non-null.
* @return Transition The Transition from which the target is removed.
* Returning the same object makes it easier to chain calls during
* construction, such as
* transitionSet.addTransitions(new Fade()).removeTarget(someView);
*/
@NonNull
public Transition removeTarget(@NonNull View target) {
mTargets.remove(target);
return this;
}
/**
* Removes the given targetId from the list of ids that this Transition
* is interested in animating.
*
* @param targetId The id of a target view, must be a positive number.
* @return The Transition from which the targetId is removed.
* Returning the same object makes it easier to chain calls during
* construction, such as
* transitionSet.addTransitions(new Fade()).removeTargetId(someId);
*/
@NonNull
public Transition removeTarget(@IdRes int targetId) {
if (targetId > 0) {
mTargetIds.remove((Integer) targetId);
}
return this;
}
/**
* Removes the given targetName from the list of transitionNames that this Transition
* is interested in animating.
*
* @param targetName The transitionName of a target view, must not be null.
* @return The Transition from which the targetName is removed.
* Returning the same object makes it easier to chain calls during
* construction, such as
* transitionSet.addTransitions(new Fade()).removeTargetName(someName);
*/
@NonNull
public Transition removeTarget(@NonNull String targetName) {
if (mTargetNames != null) {
mTargetNames.remove(targetName);
}
return this;
}
/**
* Removes the given target from the list of targets that this Transition
* is interested in animating.
*
* @param target The type of the target view, must be non-null.
* @return Transition The Transition from which the target is removed.
* Returning the same object makes it easier to chain calls during
* construction, such as
* transitionSet.addTransitions(new Fade()).removeTarget(someType);
*/
@NonNull
public Transition removeTarget(@NonNull Class target) {
if (mTargetTypes != null) {
mTargetTypes.remove(target);
}
return this;
}
/**
* Utility method to manage the boilerplate code that is the same whether we
* are excluding targets or their children.
*/
private static ArrayList excludeObject(ArrayList list, T target, boolean exclude) {
if (target != null) {
if (exclude) {
list = ArrayListManager.add(list, target);
} else {
list = ArrayListManager.remove(list, target);
}
}
return list;
}
/**
* Whether to add the given target to the list of targets to exclude from this
* transition. The exclude
parameter specifies whether the target
* should be added to or removed from the excluded list.
*
* Excluding targets is a general mechanism for allowing transitions to run on
* a view hierarchy while skipping target views that should not be part of
* the transition. For example, you may want to avoid animating children
* of a specific ListView or Spinner. Views can be excluded either by their
* id, or by their instance reference, or by the Class of that view
* (eg, {@link Spinner}).
*
* @param target The target to ignore when running this transition.
* @param exclude Whether to add the target to or remove the target from the
* current list of excluded targets.
* @return This transition object.
* @see #excludeChildren(View, boolean)
* @see #excludeTarget(int, boolean)
* @see #excludeTarget(Class, boolean)
*/
@NonNull
public Transition excludeTarget(@NonNull View target, boolean exclude) {
mTargetExcludes = excludeView(mTargetExcludes, target, exclude);
return this;
}
/**
* Whether to add the given id to the list of target ids to exclude from this
* transition. The exclude
parameter specifies whether the target
* should be added to or removed from the excluded list.
*
* Excluding targets is a general mechanism for allowing transitions to run on
* a view hierarchy while skipping target views that should not be part of
* the transition. For example, you may want to avoid animating children
* of a specific ListView or Spinner. Views can be excluded either by their
* id, or by their instance reference, or by the Class of that view
* (eg, {@link Spinner}).
*
* @param targetId The id of a target to ignore when running this transition.
* @param exclude Whether to add the target to or remove the target from the
* current list of excluded targets.
* @return This transition object.
* @see #excludeChildren(int, boolean)
* @see #excludeTarget(View, boolean)
* @see #excludeTarget(Class, boolean)
*/
@NonNull
public Transition excludeTarget(@IdRes int targetId, boolean exclude) {
mTargetIdExcludes = excludeId(mTargetIdExcludes, targetId, exclude);
return this;
}
/**
* Whether to add the given transitionName to the list of target transitionNames to exclude
* from this transition. The exclude
parameter specifies whether the target
* should be added to or removed from the excluded list.
*
* Excluding targets is a general mechanism for allowing transitions to run on
* a view hierarchy while skipping target views that should not be part of
* the transition. For example, you may want to avoid animating children
* of a specific ListView or Spinner. Views can be excluded by their
* id, their instance reference, their transitionName, or by the Class of that view
* (eg, {@link Spinner}).
*
* @param targetName The name of a target to ignore when running this transition.
* @param exclude Whether to add the target to or remove the target from the
* current list of excluded targets.
* @return This transition object.
* @see #excludeTarget(View, boolean)
* @see #excludeTarget(int, boolean)
* @see #excludeTarget(Class, boolean)
*/
@NonNull
public Transition excludeTarget(@NonNull String targetName, boolean exclude) {
mTargetNameExcludes = excludeObject(mTargetNameExcludes, targetName, exclude);
return this;
}
/**
* Whether to add the children of given target to the list of target children
* to exclude from this transition. The exclude
parameter specifies
* whether the target should be added to or removed from the excluded list.
*
* Excluding targets is a general mechanism for allowing transitions to run on
* a view hierarchy while skipping target views that should not be part of
* the transition. For example, you may want to avoid animating children
* of a specific ListView or Spinner. Views can be excluded either by their
* id, or by their instance reference, or by the Class of that view
* (eg, {@link Spinner}).
*
* @param target The target to ignore when running this transition.
* @param exclude Whether to add the target to or remove the target from the
* current list of excluded targets.
* @return This transition object.
* @see #excludeTarget(View, boolean)
* @see #excludeChildren(int, boolean)
* @see #excludeChildren(Class, boolean)
*/
@NonNull
public Transition excludeChildren(@NonNull View target, boolean exclude) {
mTargetChildExcludes = excludeView(mTargetChildExcludes, target, exclude);
return this;
}
/**
* Whether to add the children of the given id to the list of targets to exclude
* from this transition. The exclude
parameter specifies whether
* the children of the target should be added to or removed from the excluded list.
* Excluding children in this way provides a simple mechanism for excluding all
* children of specific targets, rather than individually excluding each
* child individually.
*
* Excluding targets is a general mechanism for allowing transitions to run on
* a view hierarchy while skipping target views that should not be part of
* the transition. For example, you may want to avoid animating children
* of a specific ListView or Spinner. Views can be excluded either by their
* id, or by their instance reference, or by the Class of that view
* (eg, {@link Spinner}).
*
* @param targetId The id of a target whose children should be ignored when running
* this transition.
* @param exclude Whether to add the target to or remove the target from the
* current list of excluded-child targets.
* @return This transition object.
* @see #excludeTarget(int, boolean)
* @see #excludeChildren(View, boolean)
* @see #excludeChildren(Class, boolean)
*/
@NonNull
public Transition excludeChildren(@IdRes int targetId, boolean exclude) {
mTargetIdChildExcludes = excludeId(mTargetIdChildExcludes, targetId, exclude);
return this;
}
/**
* Utility method to manage the boilerplate code that is the same whether we
* are excluding targets or their children.
*/
private ArrayList excludeId(ArrayList list, int targetId, boolean exclude) {
if (targetId > 0) {
if (exclude) {
list = ArrayListManager.add(list, targetId);
} else {
list = ArrayListManager.remove(list, targetId);
}
}
return list;
}
/**
* Utility method to manage the boilerplate code that is the same whether we
* are excluding targets or their children.
*/
private ArrayList excludeView(ArrayList list, View target, boolean exclude) {
if (target != null) {
if (exclude) {
list = ArrayListManager.add(list, target);
} else {
list = ArrayListManager.remove(list, target);
}
}
return list;
}
/**
* Whether to add the given type to the list of types to exclude from this
* transition. The exclude
parameter specifies whether the target
* type should be added to or removed from the excluded list.
*
* Excluding targets is a general mechanism for allowing transitions to run on
* a view hierarchy while skipping target views that should not be part of
* the transition. For example, you may want to avoid animating children
* of a specific ListView or Spinner. Views can be excluded either by their
* id, or by their instance reference, or by the Class of that view
* (eg, {@link Spinner}).
*
* @param type The type to ignore when running this transition.
* @param exclude Whether to add the target type to or remove it from the
* current list of excluded target types.
* @return This transition object.
* @see #excludeChildren(Class, boolean)
* @see #excludeTarget(int, boolean)
* @see #excludeTarget(View, boolean)
*/
@NonNull
public Transition excludeTarget(@NonNull Class type, boolean exclude) {
mTargetTypeExcludes = excludeType(mTargetTypeExcludes, type, exclude);
return this;
}
/**
* Whether to add the given type to the list of types whose children should
* be excluded from this transition. The exclude
parameter
* specifies whether the target type should be added to or removed from
* the excluded list.
*
* Excluding targets is a general mechanism for allowing transitions to run on
* a view hierarchy while skipping target views that should not be part of
* the transition. For example, you may want to avoid animating children
* of a specific ListView or Spinner. Views can be excluded either by their
* id, or by their instance reference, or by the Class of that view
* (eg, {@link Spinner}).
*
* @param type The type to ignore when running this transition.
* @param exclude Whether to add the target type to or remove it from the
* current list of excluded target types.
* @return This transition object.
* @see #excludeTarget(Class, boolean)
* @see #excludeChildren(int, boolean)
* @see #excludeChildren(View, boolean)
*/
@NonNull
public Transition excludeChildren(@NonNull Class type, boolean exclude) {
mTargetTypeChildExcludes = excludeType(mTargetTypeChildExcludes, type, exclude);
return this;
}
/**
* Utility method to manage the boilerplate code that is the same whether we
* are excluding targets or their children.
*/
private ArrayList excludeType(ArrayList list, Class type, boolean exclude) {
if (type != null) {
if (exclude) {
list = ArrayListManager.add(list, type);
} else {
list = ArrayListManager.remove(list, type);
}
}
return list;
}
/**
* Returns the array of target IDs that this transition limits itself to
* tracking and animating. If the array is null for both this method and
* {@link #getTargets()}, then this transition is
* not limited to specific views, and will handle changes to any views
* in the hierarchy of a scene change.
*
* @return the list of target IDs
*/
@NonNull
public List getTargetIds() {
return mTargetIds;
}
/**
* Returns the array of target views that this transition limits itself to
* tracking and animating. If the array is null for both this method and
* {@link #getTargetIds()}, then this transition is
* not limited to specific views, and will handle changes to any views
* in the hierarchy of a scene change.
*
* @return the list of target views
*/
@NonNull
public List getTargets() {
return mTargets;
}
/**
* Returns the list of target transitionNames that this transition limits itself to
* tracking and animating. If the list is null or empty for
* {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetNames()}, and
* {@link #getTargetTypes()} then this transition is
* not limited to specific views, and will handle changes to any views
* in the hierarchy of a scene change.
*
* @return the list of target transitionNames
*/
@Nullable
public List getTargetNames() {
return mTargetNames;
}
/**
* Returns the list of target transitionNames that this transition limits itself to
* tracking and animating. If the list is null or empty for
* {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetNames()}, and
* {@link #getTargetTypes()} then this transition is
* not limited to specific views, and will handle changes to any views
* in the hierarchy of a scene change.
*
* @return the list of target Types
*/
@Nullable
public List getTargetTypes() {
return mTargetTypes;
}
/**
* Recursive method that captures values for the given view and the
* hierarchy underneath it.
*
* @param sceneRoot The root of the view hierarchy being captured
* @param start true if this capture is happening before the scene change,
* false otherwise
*/
void captureValues(ViewGroup sceneRoot, boolean start) {
clearValues(start);
if ((mTargetIds.size() > 0 || mTargets.size() > 0)
&& (mTargetNames == null || mTargetNames.isEmpty())
&& (mTargetTypes == null || mTargetTypes.isEmpty())) {
for (int i = 0; i < mTargetIds.size(); ++i) {
int id = mTargetIds.get(i);
View view = sceneRoot.findViewById(id);
if (view != null) {
TransitionValues values = new TransitionValues();
values.view = view;
if (start) {
captureStartValues(values);
} else {
captureEndValues(values);
}
values.mTargetedTransitions.add(this);
capturePropagationValues(values);
if (start) {
addViewValues(mStartValues, view, values);
} else {
addViewValues(mEndValues, view, values);
}
}
}
for (int i = 0; i < mTargets.size(); ++i) {
View view = mTargets.get(i);
TransitionValues values = new TransitionValues();
values.view = view;
if (start) {
captureStartValues(values);
} else {
captureEndValues(values);
}
values.mTargetedTransitions.add(this);
capturePropagationValues(values);
if (start) {
addViewValues(mStartValues, view, values);
} else {
addViewValues(mEndValues, view, values);
}
}
} else {
captureHierarchy(sceneRoot, start);
}
if (!start && mNameOverrides != null) {
int numOverrides = mNameOverrides.size();
ArrayList overriddenViews = new ArrayList<>(numOverrides);
for (int i = 0; i < numOverrides; i++) {
String fromName = mNameOverrides.keyAt(i);
overriddenViews.add(mStartValues.mNameValues.remove(fromName));
}
for (int i = 0; i < numOverrides; i++) {
View view = overriddenViews.get(i);
if (view != null) {
String toName = mNameOverrides.valueAt(i);
mStartValues.mNameValues.put(toName, view);
}
}
}
}
private static void addViewValues(TransitionValuesMaps transitionValuesMaps,
View view, TransitionValues transitionValues) {
transitionValuesMaps.mViewValues.put(view, transitionValues);
int id = view.getId();
if (id >= 0) {
if (transitionValuesMaps.mIdValues.indexOfKey(id) >= 0) {
// Duplicate IDs cannot match by ID.
transitionValuesMaps.mIdValues.put(id, null);
} else {
transitionValuesMaps.mIdValues.put(id, view);
}
}
String name = ViewCompat.getTransitionName(view);
if (name != null) {
if (transitionValuesMaps.mNameValues.containsKey(name)) {
// Duplicate transitionNames: cannot match by transitionName.
transitionValuesMaps.mNameValues.put(name, null);
} else {
transitionValuesMaps.mNameValues.put(name, view);
}
}
if (view.getParent() instanceof ListView) {
ListView listview = (ListView) view.getParent();
if (listview.getAdapter().hasStableIds()) {
int position = listview.getPositionForView(view);
long itemId = listview.getItemIdAtPosition(position);
if (transitionValuesMaps.mItemIdValues.indexOfKey(itemId) >= 0) {
// Duplicate item IDs: cannot match by item ID.
View alreadyMatched = transitionValuesMaps.mItemIdValues.get(itemId);
if (alreadyMatched != null) {
ViewCompat.setHasTransientState(alreadyMatched, false);
transitionValuesMaps.mItemIdValues.put(itemId, null);
}
} else {
ViewCompat.setHasTransientState(view, true);
transitionValuesMaps.mItemIdValues.put(itemId, view);
}
}
}
}
/**
* Clear valuesMaps for specified start/end state
*
* @param start true if the start values should be cleared, false otherwise
*/
void clearValues(boolean start) {
if (start) {
mStartValues.mViewValues.clear();
mStartValues.mIdValues.clear();
mStartValues.mItemIdValues.clear();
} else {
mEndValues.mViewValues.clear();
mEndValues.mIdValues.clear();
mEndValues.mItemIdValues.clear();
}
}
/**
* Recursive method which captures values for an entire view hierarchy,
* starting at some root view. Transitions without targetIDs will use this
* method to capture values for all possible views.
*
* @param view The view for which to capture values. Children of this View
* will also be captured, recursively down to the leaf nodes.
* @param start true if values are being captured in the start scene, false
* otherwise.
*/
private void captureHierarchy(View view, boolean start) {
if (view == null) {
return;
}
int id = view.getId();
if (mTargetIdExcludes != null && mTargetIdExcludes.contains(id)) {
return;
}
if (mTargetExcludes != null && mTargetExcludes.contains(view)) {
return;
}
if (mTargetTypeExcludes != null) {
int numTypes = mTargetTypeExcludes.size();
for (int i = 0; i < numTypes; ++i) {
if (mTargetTypeExcludes.get(i).isInstance(view)) {
return;
}
}
}
if (view.getParent() instanceof ViewGroup) {
TransitionValues values = new TransitionValues();
values.view = view;
if (start) {
captureStartValues(values);
} else {
captureEndValues(values);
}
values.mTargetedTransitions.add(this);
capturePropagationValues(values);
if (start) {
addViewValues(mStartValues, view, values);
} else {
addViewValues(mEndValues, view, values);
}
}
if (view instanceof ViewGroup) {
// Don't traverse child hierarchy if there are any child-excludes on this view
if (mTargetIdChildExcludes != null && mTargetIdChildExcludes.contains(id)) {
return;
}
if (mTargetChildExcludes != null && mTargetChildExcludes.contains(view)) {
return;
}
if (mTargetTypeChildExcludes != null) {
int numTypes = mTargetTypeChildExcludes.size();
for (int i = 0; i < numTypes; ++i) {
if (mTargetTypeChildExcludes.get(i).isInstance(view)) {
return;
}
}
}
ViewGroup parent = (ViewGroup) view;
for (int i = 0; i < parent.getChildCount(); ++i) {
captureHierarchy(parent.getChildAt(i), start);
}
}
}
/**
* This method can be called by transitions to get the TransitionValues for
* any particular view during the transition-playing process. This might be
* necessary, for example, to query the before/after state of related views
* for a given transition.
*/
@Nullable
public TransitionValues getTransitionValues(@NonNull View view, boolean start) {
if (mParent != null) {
return mParent.getTransitionValues(view, start);
}
TransitionValuesMaps valuesMaps = start ? mStartValues : mEndValues;
return valuesMaps.mViewValues.get(view);
}
/**
* Find the matched start or end value for a given View. This is only valid
* after playTransition starts. For example, it will be valid in
* {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)}, but not
* in {@link #captureStartValues(TransitionValues)}.
*
* @param view The view to find the match for.
* @param viewInStart Is View from the start values or end values.
* @return The matching TransitionValues for view in either start or end values, depending
* on viewInStart or null if there is no match for the given view.
*/
TransitionValues getMatchedTransitionValues(View view, boolean viewInStart) {
if (mParent != null) {
return mParent.getMatchedTransitionValues(view, viewInStart);
}
ArrayList lookIn = viewInStart ? mStartValuesList : mEndValuesList;
if (lookIn == null) {
return null;
}
int count = lookIn.size();
int index = -1;
for (int i = 0; i < count; i++) {
TransitionValues values = lookIn.get(i);
if (values == null) {
return null;
}
if (values.view == view) {
index = i;
break;
}
}
TransitionValues values = null;
if (index >= 0) {
ArrayList matchIn = viewInStart ? mEndValuesList : mStartValuesList;
values = matchIn.get(index);
}
return values;
}
/**
* Pauses this transition, sending out calls to {@link
* TransitionListener#onTransitionPause(Transition)} to all listeners
* and pausing all running animators started by this transition.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void pause(View sceneRoot) {
if (!mEnded) {
ArrayMap runningAnimators = getRunningAnimators();
int numOldAnims = runningAnimators.size();
WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
for (int i = numOldAnims - 1; i >= 0; i--) {
AnimationInfo info = runningAnimators.valueAt(i);
if (info.mView != null && windowId.equals(info.mWindowId)) {
Animator anim = runningAnimators.keyAt(i);
AnimatorUtils.pause(anim);
}
}
if (mListeners != null && mListeners.size() > 0) {
@SuppressWarnings("unchecked") ArrayList tmpListeners =
(ArrayList) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onTransitionPause(this);
}
}
mPaused = true;
}
}
/**
* Resumes this transition, sending out calls to {@link
* TransitionListener#onTransitionPause(Transition)} to all listeners
* and pausing all running animators started by this transition.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void resume(View sceneRoot) {
if (mPaused) {
if (!mEnded) {
ArrayMap runningAnimators = getRunningAnimators();
int numOldAnims = runningAnimators.size();
WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
for (int i = numOldAnims - 1; i >= 0; i--) {
AnimationInfo info = runningAnimators.valueAt(i);
if (info.mView != null && windowId.equals(info.mWindowId)) {
Animator anim = runningAnimators.keyAt(i);
AnimatorUtils.resume(anim);
}
}
if (mListeners != null && mListeners.size() > 0) {
@SuppressWarnings("unchecked") ArrayList tmpListeners =
(ArrayList) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onTransitionResume(this);
}
}
}
mPaused = false;
}
}
/**
* Called by TransitionManager to play the transition. This calls
* createAnimators() to set things up and create all of the animations and then
* runAnimations() to actually start the animations.
*/
void playTransition(ViewGroup sceneRoot) {
mStartValuesList = new ArrayList<>();
mEndValuesList = new ArrayList<>();
matchStartAndEnd(mStartValues, mEndValues);
ArrayMap runningAnimators = getRunningAnimators();
int numOldAnims = runningAnimators.size();
WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
for (int i = numOldAnims - 1; i >= 0; i--) {
Animator anim = runningAnimators.keyAt(i);
if (anim != null) {
AnimationInfo oldInfo = runningAnimators.get(anim);
if (oldInfo != null && oldInfo.mView != null
&& windowId.equals(oldInfo.mWindowId)) {
TransitionValues oldValues = oldInfo.mValues;
View oldView = oldInfo.mView;
TransitionValues startValues = getTransitionValues(oldView, true);
TransitionValues endValues = getMatchedTransitionValues(oldView, true);
boolean cancel = (startValues != null || endValues != null)
&& oldInfo.mTransition.isTransitionRequired(oldValues, endValues);
if (cancel) {
if (anim.isRunning() || anim.isStarted()) {
if (DBG) {
Log.d(LOG_TAG, "Canceling anim " + anim);
}
anim.cancel();
} else {
if (DBG) {
Log.d(LOG_TAG, "removing anim from info list: " + anim);
}
runningAnimators.remove(anim);
}
}
}
}
}
createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList);
runAnimators();
}
/**
* Returns whether or not the transition should create an Animator, based on the values
* captured during {@link #captureStartValues(TransitionValues)} and
* {@link #captureEndValues(TransitionValues)}. The default implementation compares the
* property values returned from {@link #getTransitionProperties()}, or all property values if
* {@code getTransitionProperties()} returns null. Subclasses may override this method to
* provide logic more specific to the transition implementation.
*
* @param startValues the values from captureStartValues, This may be {@code null} if the
* View did not exist in the start state.
* @param endValues the values from captureEndValues. This may be {@code null} if the View
* did not exist in the end state.
*/
public boolean isTransitionRequired(@Nullable TransitionValues startValues,
@Nullable TransitionValues endValues) {
boolean valuesChanged = false;
// if startValues null, then transition didn't care to stash values,
// and won't get canceled
if (startValues != null && endValues != null) {
String[] properties = getTransitionProperties();
if (properties != null) {
for (String property : properties) {
if (isValueChanged(startValues, endValues, property)) {
valuesChanged = true;
break;
}
}
} else {
for (String key : startValues.values.keySet()) {
if (isValueChanged(startValues, endValues, key)) {
valuesChanged = true;
break;
}
}
}
}
return valuesChanged;
}
private static boolean isValueChanged(TransitionValues oldValues, TransitionValues newValues,
String key) {
Object oldValue = oldValues.values.get(key);
Object newValue = newValues.values.get(key);
boolean changed;
if (oldValue == null && newValue == null) {
// both are null
changed = false;
} else if (oldValue == null || newValue == null) {
// one is null
changed = true;
} else {
// neither is null
changed = !oldValue.equals(newValue);
}
if (DBG && changed) {
Log.d(LOG_TAG, "Transition.playTransition: "
+ "oldValue != newValue for " + key
+ ": old, new = " + oldValue + ", " + newValue);
}
return changed;
}
/**
* This is a utility method used by subclasses to handle standard parts of
* setting up and running an Animator: it sets the {@link #getDuration()
* duration} and the {@link #getStartDelay() startDelay}, starts the
* animation, and, when the animator ends, calls {@link #end()}.
*
* @param animator The Animator to be run during this transition.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected void animate(Animator animator) {
// TODO: maybe pass auto-end as a boolean parameter?
if (animator == null) {
end();
} else {
if (getDuration() >= 0) {
animator.setDuration(getDuration());
}
if (getStartDelay() >= 0) {
animator.setStartDelay(getStartDelay());
}
if (getInterpolator() != null) {
animator.setInterpolator(getInterpolator());
}
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
end();
animation.removeListener(this);
}
});
animator.start();
}
}
/**
* This method is called automatically by the transition and
* TransitionSet classes prior to a Transition subclass starting;
* subclasses should not need to call it directly.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected void start() {
if (mNumInstances == 0) {
if (mListeners != null && mListeners.size() > 0) {
@SuppressWarnings("unchecked") ArrayList tmpListeners =
(ArrayList) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onTransitionStart(this);
}
}
mEnded = false;
}
mNumInstances++;
}
/**
* This method is called automatically by the Transition and
* TransitionSet classes when a transition finishes, either because
* a transition did nothing (returned a null Animator from
* {@link Transition#createAnimator(ViewGroup, TransitionValues,
* TransitionValues)}) or because the transition returned a valid
* Animator and end() was called in the onAnimationEnd()
* callback of the AnimatorListener.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected void end() {
--mNumInstances;
if (mNumInstances == 0) {
if (mListeners != null && mListeners.size() > 0) {
@SuppressWarnings("unchecked") ArrayList tmpListeners =
(ArrayList) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onTransitionEnd(this);
}
}
for (int i = 0; i < mStartValues.mItemIdValues.size(); ++i) {
View view = mStartValues.mItemIdValues.valueAt(i);
if (view != null) {
ViewCompat.setHasTransientState(view, false);
}
}
for (int i = 0; i < mEndValues.mItemIdValues.size(); ++i) {
View view = mEndValues.mItemIdValues.valueAt(i);
if (view != null) {
ViewCompat.setHasTransientState(view, false);
}
}
mEnded = true;
}
}
/**
* Force the transition to move to its end state, ending all the animators.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
void forceToEnd(ViewGroup sceneRoot) {
ArrayMap runningAnimators = getRunningAnimators();
int numOldAnims = runningAnimators.size();
if (sceneRoot != null) {
WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
for (int i = numOldAnims - 1; i >= 0; i--) {
AnimationInfo info = runningAnimators.valueAt(i);
if (info.mView != null && windowId != null && windowId.equals(info.mWindowId)) {
Animator anim = runningAnimators.keyAt(i);
anim.end();
}
}
}
}
/**
* This method cancels a transition that is currently running.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected void cancel() {
int numAnimators = mCurrentAnimators.size();
for (int i = numAnimators - 1; i >= 0; i--) {
Animator animator = mCurrentAnimators.get(i);
animator.cancel();
}
if (mListeners != null && mListeners.size() > 0) {
@SuppressWarnings("unchecked") ArrayList tmpListeners =
(ArrayList) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onTransitionCancel(this);
}
}
}
/**
* Adds a listener to the set of listeners that are sent events through the
* life of an animation, such as start, repeat, and end.
*
* @param listener the listener to be added to the current set of listeners
* for this animation.
* @return This transition object.
*/
@NonNull
public Transition addListener(@NonNull TransitionListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<>();
}
mListeners.add(listener);
return this;
}
/**
* Removes a listener from the set listening to this animation.
*
* @param listener the listener to be removed from the current set of
* listeners for this transition.
* @return This transition object.
*/
@NonNull
public Transition removeListener(@NonNull TransitionListener listener) {
if (mListeners == null) {
return this;
}
mListeners.remove(listener);
if (mListeners.size() == 0) {
mListeners = null;
}
return this;
}
/**
* Sets the algorithm used to calculate two-dimensional interpolation.
*
* Transitions such as {@link android.transition.ChangeBounds} move Views, typically
* in a straight path between the start and end positions. Applications that desire to
* have these motions move in a curve can change how Views interpolate in two dimensions
* by extending PathMotion and implementing
* {@link android.transition.PathMotion#getPath(float, float, float, float)}.
*
*
* @param pathMotion Algorithm object to use for determining how to interpolate in two
* dimensions. If null, a straight-path algorithm will be used.
* @see android.transition.ArcMotion
* @see PatternPathMotion
* @see android.transition.PathMotion
*/
public void setPathMotion(@Nullable PathMotion pathMotion) {
if (pathMotion == null) {
mPathMotion = STRAIGHT_PATH_MOTION;
} else {
mPathMotion = pathMotion;
}
}
/**
* Returns the algorithm object used to interpolate along two dimensions. This is typically
* used to determine the View motion between two points.
*
* @return The algorithm object used to interpolate along two dimensions.
* @see android.transition.ArcMotion
* @see PatternPathMotion
* @see android.transition.PathMotion
*/
@NonNull
public PathMotion getPathMotion() {
return mPathMotion;
}
/**
* Sets the callback to use to find the epicenter of a Transition. A null value indicates
* that there is no epicenter in the Transition and onGetEpicenter() will return null.
* Transitions like {@link android.transition.Explode} use a point or Rect to orient
* the direction of travel. This is called the epicenter of the Transition and is
* typically centered on a touched View. The
* {@link android.transition.Transition.EpicenterCallback} allows a Transition to
* dynamically retrieve the epicenter during a Transition.
*
* @param epicenterCallback The callback to use to find the epicenter of the Transition.
*/
public void setEpicenterCallback(@Nullable EpicenterCallback epicenterCallback) {
mEpicenterCallback = epicenterCallback;
}
/**
* Returns the callback used to find the epicenter of the Transition.
* Transitions like {@link android.transition.Explode} use a point or Rect to orient
* the direction of travel. This is called the epicenter of the Transition and is
* typically centered on a touched View. The
* {@link android.transition.Transition.EpicenterCallback} allows a Transition to
* dynamically retrieve the epicenter during a Transition.
*
* @return the callback used to find the epicenter of the Transition.
*/
@Nullable
public EpicenterCallback getEpicenterCallback() {
return mEpicenterCallback;
}
/**
* Returns the epicenter as specified by the
* {@link android.transition.Transition.EpicenterCallback} or null if no callback exists.
*
* @return the epicenter as specified by the
* {@link android.transition.Transition.EpicenterCallback} or null if no callback exists.
* @see #setEpicenterCallback(EpicenterCallback)
*/
@Nullable
public Rect getEpicenter() {
if (mEpicenterCallback == null) {
return null;
}
return mEpicenterCallback.onGetEpicenter(this);
}
/**
* Sets the method for determining Animator start delays.
* When a Transition affects several Views like {@link android.transition.Explode} or
* {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect
* such that the Animator start delay depends on position of the View. The
* TransitionPropagation specifies how the start delays are calculated.
*
* @param transitionPropagation The class used to determine the start delay of
* Animators created by this Transition. A null value
* indicates that no delay should be used.
*/
public void setPropagation(@Nullable TransitionPropagation transitionPropagation) {
mPropagation = transitionPropagation;
}
/**
* Returns the {@link android.transition.TransitionPropagation} used to calculate Animator
* start
* delays.
* When a Transition affects several Views like {@link android.transition.Explode} or
* {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect
* such that the Animator start delay depends on position of the View. The
* TransitionPropagation specifies how the start delays are calculated.
*
* @return the {@link android.transition.TransitionPropagation} used to calculate Animator start
* delays. This is null by default.
*/
@Nullable
public TransitionPropagation getPropagation() {
return mPropagation;
}
/**
* Captures TransitionPropagation values for the given view and the
* hierarchy underneath it.
*/
void capturePropagationValues(TransitionValues transitionValues) {
if (mPropagation != null && !transitionValues.values.isEmpty()) {
String[] propertyNames = mPropagation.getPropagationProperties();
if (propertyNames == null) {
return;
}
boolean containsAll = true;
for (int i = 0; i < propertyNames.length; i++) {
if (!transitionValues.values.containsKey(propertyNames[i])) {
containsAll = false;
break;
}
}
if (!containsAll) {
mPropagation.captureValues(transitionValues);
}
}
}
Transition setSceneRoot(ViewGroup sceneRoot) {
mSceneRoot = sceneRoot;
return this;
}
void setCanRemoveViews(boolean canRemoveViews) {
mCanRemoveViews = canRemoveViews;
}
@Override
public String toString() {
return toString("");
}
@Override
public Transition clone() {
try {
Transition clone = (Transition) super.clone();
clone.mAnimators = new ArrayList<>();
clone.mStartValues = new TransitionValuesMaps();
clone.mEndValues = new TransitionValuesMaps();
clone.mStartValuesList = null;
clone.mEndValuesList = null;
return clone;
} catch (CloneNotSupportedException e) {
return null;
}
}
/**
* Returns the name of this Transition. This name is used internally to distinguish
* between different transitions to determine when interrupting transitions overlap.
* For example, a ChangeBounds running on the same target view as another ChangeBounds
* should determine whether the old transition is animating to different end values
* and should be canceled in favor of the new transition.
*
* By default, a Transition's name is simply the value of {@link Class#getName()},
* but subclasses are free to override and return something different.
*
* @return The name of this transition.
*/
@NonNull
public String getName() {
return mName;
}
String toString(String indent) {
String result = indent + getClass().getSimpleName() + "@"
+ Integer.toHexString(hashCode()) + ": ";
if (mDuration != -1) {
result += "dur(" + mDuration + ") ";
}
if (mStartDelay != -1) {
result += "dly(" + mStartDelay + ") ";
}
if (mInterpolator != null) {
result += "interp(" + mInterpolator + ") ";
}
if (mTargetIds.size() > 0 || mTargets.size() > 0) {
result += "tgts(";
if (mTargetIds.size() > 0) {
for (int i = 0; i < mTargetIds.size(); ++i) {
if (i > 0) {
result += ", ";
}
result += mTargetIds.get(i);
}
}
if (mTargets.size() > 0) {
for (int i = 0; i < mTargets.size(); ++i) {
if (i > 0) {
result += ", ";
}
result += mTargets.get(i);
}
}
result += ")";
}
return result;
}
/**
* A transition listener receives notifications from a transition.
* Notifications indicate transition lifecycle events.
*/
public interface TransitionListener {
/**
* Notification about the start of the transition.
*
* @param transition The started transition.
*/
void onTransitionStart(@NonNull Transition transition);
/**
* Notification about the end of the transition. Canceled transitions
* will always notify listeners of both the cancellation and end
* events. That is, {@link #onTransitionEnd(Transition)} is always called,
* regardless of whether the transition was canceled or played
* through to completion.
*
* @param transition The transition which reached its end.
*/
void onTransitionEnd(@NonNull Transition transition);
/**
* Notification about the cancellation of the transition.
* Note that cancel may be called by a parent {@link TransitionSet} on
* a child transition which has not yet started. This allows the child
* transition to restore state on target objects which was set at
* {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)
* createAnimator()} time.
*
* @param transition The transition which was canceled.
*/
void onTransitionCancel(@NonNull Transition transition);
/**
* Notification when a transition is paused.
* Note that createAnimator() may be called by a parent {@link TransitionSet} on
* a child transition which has not yet started. This allows the child
* transition to restore state on target objects which was set at
* {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)
* createAnimator()} time.
*
* @param transition The transition which was paused.
*/
void onTransitionPause(@NonNull Transition transition);
/**
* Notification when a transition is resumed.
* Note that resume() may be called by a parent {@link TransitionSet} on
* a child transition which has not yet started. This allows the child
* transition to restore state which may have changed in an earlier call
* to {@link #onTransitionPause(Transition)}.
*
* @param transition The transition which was resumed.
*/
void onTransitionResume(@NonNull Transition transition);
}
/**
* Holds information about each animator used when a new transition starts
* while other transitions are still running to determine whether a running
* animation should be canceled or a new animation noop'd. The structure holds
* information about the state that an animation is going to, to be compared to
* end state of a new animation.
*/
private static class AnimationInfo {
View mView;
String mName;
TransitionValues mValues;
WindowIdImpl mWindowId;
Transition mTransition;
AnimationInfo(View view, String name, Transition transition, WindowIdImpl windowId,
TransitionValues values) {
mView = view;
mName = name;
mValues = values;
mWindowId = windowId;
mTransition = transition;
}
}
/**
* Utility class for managing typed ArrayLists efficiently. In particular, this
* can be useful for lists that we don't expect to be used often (eg, the exclude
* lists), so we'd like to keep them nulled out by default. This causes the code to
* become tedious, with constant null checks, code to allocate when necessary,
* and code to null out the reference when the list is empty. This class encapsulates
* all of that functionality into simple add()/remove() methods which perform the
* necessary checks, allocation/null-out as appropriate, and return the
* resulting list.
*/
private static class ArrayListManager {
/**
* Add the specified item to the list, returning the resulting list.
* The returned list can either the be same list passed in or, if that
* list was null, the new list that was created.
*
* Note that the list holds unique items; if the item already exists in the
* list, the list is not modified.
*/
static ArrayList add(ArrayList list, T item) {
if (list == null) {
list = new ArrayList<>();
}
if (!list.contains(item)) {
list.add(item);
}
return list;
}
/**
* Remove the specified item from the list, returning the resulting list.
* The returned list can either the be same list passed in or, if that
* list becomes empty as a result of the remove(), the new list was created.
*/
static ArrayList remove(ArrayList list, T item) {
if (list != null) {
list.remove(item);
if (list.isEmpty()) {
list = null;
}
}
return list;
}
}
/**
* Class to get the epicenter of Transition. Use
* {@link #setEpicenterCallback(EpicenterCallback)} to set the callback used to calculate the
* epicenter of the Transition. Override {@link #getEpicenter()} to return the rectangular
* region in screen coordinates of the epicenter of the transition.
*
* @see #setEpicenterCallback(EpicenterCallback)
*/
public abstract static class EpicenterCallback {
/**
* Implementers must override to return the epicenter of the Transition in screen
* coordinates. Transitions like {@link android.transition.Explode} depend upon
* an epicenter for the Transition. In Explode, Views move toward or away from the
* center of the epicenter Rect along the vector between the epicenter and the center
* of the View appearing and disappearing. Some Transitions, such as
* {@link android.transition.Fade} pay no attention to the epicenter.
*
* @param transition The transition for which the epicenter applies.
* @return The Rect region of the epicenter of transition
or null if
* there is no epicenter.
*/
public abstract Rect onGetEpicenter(@NonNull Transition transition);
}
}