Explorar el Código

Added TrackEntry#holdPrevious to avoid dipping in higher tracks.

Also:
- Added mixingTo to make track entries a doubly linked list, removing the need for the mixingTo array.
- Renamed dip->hold, timelineData->timelineMode, timelineDipMix->timelineHoldMix ("dip" naming was bad).
NathanSweet hace 7 años
padre
commit
9b259c66e0

+ 11 - 1
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java

@@ -30,6 +30,7 @@
 
 
 package com.esotericsoftware.spine;
 package com.esotericsoftware.spine;
 
 
+import java.lang.reflect.Field;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
 import com.badlogic.gdx.Files.FileType;
 import com.badlogic.gdx.Files.FileType;
@@ -803,7 +804,8 @@ public class AnimationStateTests {
 		expected.addAll(expectedArray);
 		expected.addAll(expectedArray);
 		stateData = new AnimationStateData(skeletonData);
 		stateData = new AnimationStateData(skeletonData);
 		state = new AnimationState(stateData);
 		state = new AnimationState(stateData);
-		state.trackEntryPool = new Pool<TrackEntry>() {
+
+		Pool trackEntryPool = new Pool<TrackEntry>() {
 			public TrackEntry obtain () {
 			public TrackEntry obtain () {
 				TrackEntry entry = super.obtain();
 				TrackEntry entry = super.obtain();
 				entryCount++;
 				entryCount++;
@@ -821,6 +823,14 @@ public class AnimationStateTests {
 				super.free(entry);
 				super.free(entry);
 			}
 			}
 		};
 		};
+		try {
+			Field field = state.getClass().getDeclaredField("trackEntryPool");
+			field.setAccessible(true);
+			field.set(state, trackEntryPool);
+		} catch (Exception ex) {
+			throw new RuntimeException(ex);
+		}
+
 		time = 0;
 		time = 0;
 		fail = false;
 		fail = false;
 		log(test + ": " + description);
 		log(test + ": " + description);

+ 111 - 79
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java

@@ -38,6 +38,7 @@ import com.badlogic.gdx.utils.IntArray;
 import com.badlogic.gdx.utils.IntSet;
 import com.badlogic.gdx.utils.IntSet;
 import com.badlogic.gdx.utils.Pool;
 import com.badlogic.gdx.utils.Pool;
 import com.badlogic.gdx.utils.Pool.Poolable;
 import com.badlogic.gdx.utils.Pool.Poolable;
+
 import com.esotericsoftware.spine.Animation.AttachmentTimeline;
 import com.esotericsoftware.spine.Animation.AttachmentTimeline;
 import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
 import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
 import com.esotericsoftware.spine.Animation.MixBlend;
 import com.esotericsoftware.spine.Animation.MixBlend;
@@ -62,20 +63,20 @@ public class AnimationState {
 	/** 1) This is the first timeline to set this property.<br>
 	/** 1) This is the first timeline to set this property.<br>
 	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
 	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
 	 * 3) The next track entry after that one does not have a timeline to set this property.<br>
 	 * 3) The next track entry after that one does not have a timeline to set this property.<br>
-	 * Result: Mix from the setup pose to the timeline pose, but avoid the "dipping" problem by not using the mix percentage. This
-	 * means the timeline pose won't mix out toward the setup pose. A subsequent timeline will set this property using a mix. */
-	static private final int DIP = 2;
+	 * Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations
+	 * that key the same property. A subsequent timeline will set this property using a mix. */
+	static private final int HOLD = 2;
 	/** 1) This is the first timeline to set this property.<br>
 	/** 1) This is the first timeline to set this property.<br>
 	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
 	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
 	 * 3) The next track entry after that one does have a timeline to set this property.<br>
 	 * 3) The next track entry after that one does have a timeline to set this property.<br>
-	 * 4) timelineDipMix stores the first subsequent track entry that does not have a timeline to set this property.<br>
-	 * Result: This is the same as DIP except the mix percentage from the timelineDipMix track entry is used. This handles when
-	 * more than 2 track entries in a row have a timeline which sets the same property.<br>
-	 * Eg, A -> B -> C -> D where A, B, and C have a timeline to set the same property, but D does not. When A is applied, A's mix
-	 * percentage is not used to avoid dipping, however a later track entry (D, the first entry without a timeline which sets the
-	 * property) is actually mixing out A (which affects B and C). Without using D's mix percentage, A would be applied fully until
-	 * mixed out, causing snapping. */
-	static private final int DIP_MIX = 3;
+	 * 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property.<br>
+	 * Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than
+	 * 2 track entries in a row have a timeline that sets the same property.<br>
+	 * Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid
+	 * "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A
+	 * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into
+	 * place. */
+	static private final int HOLD_MIX = 3;
 
 
 	private AnimationStateData data;
 	private AnimationStateData data;
 	final Array<TrackEntry> tracks = new Array();
 	final Array<TrackEntry> tracks = new Array();
@@ -83,11 +84,10 @@ public class AnimationState {
 	final Array<AnimationStateListener> listeners = new Array();
 	final Array<AnimationStateListener> listeners = new Array();
 	private final EventQueue queue = new EventQueue();
 	private final EventQueue queue = new EventQueue();
 	private final IntSet propertyIDs = new IntSet();
 	private final IntSet propertyIDs = new IntSet();
-	private final Array<TrackEntry> mixingTo = new Array();
 	boolean animationsChanged;
 	boolean animationsChanged;
 	private float timeScale = 1;
 	private float timeScale = 1;
 
 
-	Pool<TrackEntry> trackEntryPool = new Pool() {
+	final Pool<TrackEntry> trackEntryPool = new Pool() {
 		protected Object newObject () {
 		protected Object newObject () {
 			return new TrackEntry();
 			return new TrackEntry();
 		}
 		}
@@ -147,6 +147,7 @@ public class AnimationState {
 				// End mixing from entries once all have completed.
 				// End mixing from entries once all have completed.
 				TrackEntry from = current.mixingFrom;
 				TrackEntry from = current.mixingFrom;
 				current.mixingFrom = null;
 				current.mixingFrom = null;
+				if (from != null) from.mixingTo = null;
 				while (from != null) {
 				while (from != null) {
 					queue.end(from);
 					queue.end(from);
 					from = from.mixingFrom;
 					from = from.mixingFrom;
@@ -174,6 +175,7 @@ public class AnimationState {
 			// Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame).
 			// Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame).
 			if (from.totalAlpha == 0 || to.mixDuration == 0) {
 			if (from.totalAlpha == 0 || to.mixDuration == 0) {
 				to.mixingFrom = from.mixingFrom;
 				to.mixingFrom = from.mixingFrom;
+				if (from.mixingFrom != null) from.mixingFrom.mixingTo = to;
 				to.interruptAlpha = from.interruptAlpha;
 				to.interruptAlpha = from.interruptAlpha;
 				queue.end(from);
 				queue.end(from);
 			}
 			}
@@ -213,11 +215,11 @@ public class AnimationState {
 			float animationLast = current.animationLast, animationTime = current.getAnimationTime();
 			float animationLast = current.animationLast, animationTime = current.getAnimationTime();
 			int timelineCount = current.animation.timelines.size;
 			int timelineCount = current.animation.timelines.size;
 			Object[] timelines = current.animation.timelines.items;
 			Object[] timelines = current.animation.timelines.items;
-			if (mix == 1 || blend == MixBlend.add) {
+			if (i == 0 && (mix == 1 || blend == MixBlend.add)) {
 				for (int ii = 0; ii < timelineCount; ii++)
 				for (int ii = 0; ii < timelineCount; ii++)
 					((Timeline)timelines[ii]).apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.in);
 					((Timeline)timelines[ii]).apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.in);
 			} else {
 			} else {
-				int[] timelineData = current.timelineData.items;
+				int[] timelineMode = current.timelineMode.items;
 
 
 				boolean firstFrame = current.timelinesRotation.size == 0;
 				boolean firstFrame = current.timelinesRotation.size == 0;
 				if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1);
 				if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1);
@@ -225,7 +227,7 @@ public class AnimationState {
 
 
 				for (int ii = 0; ii < timelineCount; ii++) {
 				for (int ii = 0; ii < timelineCount; ii++) {
 					Timeline timeline = (Timeline)timelines[ii];
 					Timeline timeline = (Timeline)timelines[ii];
-					MixBlend timelineBlend = timelineData[ii] == SUBSEQUENT ? blend : MixBlend.setup;
+					MixBlend timelineBlend = timelineMode[ii] == SUBSEQUENT ? blend : MixBlend.setup;
 					if (timeline instanceof RotateTimeline) {
 					if (timeline instanceof RotateTimeline) {
 						applyRotateTimeline(timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1,
 						applyRotateTimeline(timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1,
 							firstFrame);
 							firstFrame);
@@ -262,14 +264,14 @@ public class AnimationState {
 		float animationLast = from.animationLast, animationTime = from.getAnimationTime();
 		float animationLast = from.animationLast, animationTime = from.getAnimationTime();
 		int timelineCount = from.animation.timelines.size;
 		int timelineCount = from.animation.timelines.size;
 		Object[] timelines = from.animation.timelines.items;
 		Object[] timelines = from.animation.timelines.items;
-		float alphaDip = from.alpha * to.interruptAlpha, alphaMix = alphaDip * (1 - mix);
+		float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix);
 
 
 		if (blend == MixBlend.add) {
 		if (blend == MixBlend.add) {
 			for (int i = 0; i < timelineCount; i++)
 			for (int i = 0; i < timelineCount; i++)
 				((Timeline)timelines[i]).apply(skeleton, animationLast, animationTime, events, alphaMix, blend, MixDirection.out);
 				((Timeline)timelines[i]).apply(skeleton, animationLast, animationTime, events, alphaMix, blend, MixDirection.out);
 		} else {
 		} else {
-			int[] timelineData = from.timelineData.items;
-			Object[] timelineDipMix = from.timelineDipMix.items;
+			int[] timelineMode = from.timelineMode.items;
+			Object[] timelineHoldMix = from.timelineHoldMix.items;
 
 
 			boolean firstFrame = from.timelinesRotation.size == 0;
 			boolean firstFrame = from.timelinesRotation.size == 0;
 			if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
 			if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
@@ -280,7 +282,7 @@ public class AnimationState {
 				Timeline timeline = (Timeline)timelines[i];
 				Timeline timeline = (Timeline)timelines[i];
 				MixBlend timelineBlend;
 				MixBlend timelineBlend;
 				float alpha;
 				float alpha;
-				switch (timelineData[i]) {
+				switch (timelineMode[i]) {
 				case SUBSEQUENT:
 				case SUBSEQUENT:
 					if (!attachments && timeline instanceof AttachmentTimeline) continue;
 					if (!attachments && timeline instanceof AttachmentTimeline) continue;
 					if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
 					if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
@@ -291,14 +293,14 @@ public class AnimationState {
 					timelineBlend = MixBlend.setup;
 					timelineBlend = MixBlend.setup;
 					alpha = alphaMix;
 					alpha = alphaMix;
 					break;
 					break;
-				case DIP:
+				case HOLD:
 					timelineBlend = MixBlend.setup;
 					timelineBlend = MixBlend.setup;
-					alpha = alphaDip;
+					alpha = alphaHold;
 					break;
 					break;
 				default:
 				default:
 					timelineBlend = MixBlend.setup;
 					timelineBlend = MixBlend.setup;
-					TrackEntry dipMix = (TrackEntry)timelineDipMix[i];
-					alpha = alphaDip * Math.max(0, 1 - dipMix.mixTime / dipMix.mixDuration);
+					TrackEntry holdMix = (TrackEntry)timelineHoldMix[i];
+					alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
 					break;
 					break;
 				}
 				}
 				from.totalAlpha += alpha;
 				from.totalAlpha += alpha;
@@ -448,6 +450,7 @@ public class AnimationState {
 			if (from == null) break;
 			if (from == null) break;
 			queue.end(from);
 			queue.end(from);
 			entry.mixingFrom = null;
 			entry.mixingFrom = null;
+			entry.mixingTo = null;
 			entry = from;
 			entry = from;
 		}
 		}
 
 
@@ -463,6 +466,7 @@ public class AnimationState {
 		if (from != null) {
 		if (from != null) {
 			if (interrupt) queue.interrupt(from);
 			if (interrupt) queue.interrupt(from);
 			current.mixingFrom = from;
 			current.mixingFrom = from;
+			from.mixingTo = current;
 			current.mixTime = 0;
 			current.mixTime = 0;
 
 
 			// Store the interrupted mix percentage.
 			// Store the interrupted mix percentage.
@@ -625,6 +629,7 @@ public class AnimationState {
 		entry.trackIndex = trackIndex;
 		entry.trackIndex = trackIndex;
 		entry.animation = animation;
 		entry.animation = animation;
 		entry.loop = loop;
 		entry.loop = loop;
+		entry.holdPrevious = false;
 
 
 		entry.eventThreshold = 0;
 		entry.eventThreshold = 0;
 		entry.attachmentThreshold = 0;
 		entry.attachmentThreshold = 0;
@@ -662,15 +667,67 @@ public class AnimationState {
 		animationsChanged = false;
 		animationsChanged = false;
 
 
 		IntSet propertyIDs = this.propertyIDs;
 		IntSet propertyIDs = this.propertyIDs;
-		propertyIDs.clear();
-		Array<TrackEntry> mixingTo = this.mixingTo;
+		propertyIDs.clear(2048);
 
 
 		for (int i = 0, n = tracks.size; i < n; i++) {
 		for (int i = 0, n = tracks.size; i < n; i++) {
 			TrackEntry entry = tracks.get(i);
 			TrackEntry entry = tracks.get(i);
-			if (entry != null && (i == 0 || entry.mixBlend != MixBlend.add)) entry.setTimelineData(null, mixingTo, propertyIDs);
+			if (entry == null) continue;
+			// Move to last entry, then iterate in reverse (the order animations are applied).
+			while (entry.mixingFrom != null)
+				entry = entry.mixingFrom;
+			do {
+				if (entry.mixingTo == null || entry.mixBlend != MixBlend.add) setTimelineModes(entry);
+				entry = entry.mixingTo;
+			} while (entry != null);
 		}
 		}
 	}
 	}
 
 
+	private void setTimelineModes (TrackEntry entry) {
+		TrackEntry to = entry.mixingTo;
+		Object[] timelines = entry.animation.timelines.items;
+		int timelinesCount = entry.animation.timelines.size;
+		int[] timelineMode = entry.timelineMode.setSize(timelinesCount);
+		entry.timelineHoldMix.clear();
+		Object[] timelineHoldMix = entry.timelineHoldMix.setSize(timelinesCount);
+		IntSet propertyIDs = this.propertyIDs;
+
+		if (to != null && to.holdPrevious) {
+			for (int i = 0; i < timelinesCount; i++) {
+				propertyIDs.add(((Timeline)timelines[i]).getPropertyId());
+				timelineMode[i] = HOLD;
+			}
+			return;
+		}
+
+		outer:
+		for (int i = 0; i < timelinesCount; i++) {
+			int id = ((Timeline)timelines[i]).getPropertyId();
+			if (!propertyIDs.add(id))
+				timelineMode[i] = SUBSEQUENT;
+			else if (to == null || !hasTimeline(to, id))
+				timelineMode[i] = FIRST;
+			else {
+				for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
+					if (hasTimeline(next, id)) continue;
+					if (next.mixDuration > 0) {
+						timelineMode[i] = HOLD_MIX;
+						timelineHoldMix[i] = next;
+						continue outer;
+					}
+					break;
+				}
+				timelineMode[i] = HOLD;
+			}
+		}
+	}
+
+	private boolean hasTimeline (TrackEntry entry, int id) {
+		Object[] timelines = entry.animation.timelines.items;
+		for (int i = 0, n = entry.animation.timelines.size; i < n; i++)
+			if (((Timeline)timelines[i]).getPropertyId() == id) return true;
+		return false;
+	}
+
 	/** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */
 	/** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */
 	public TrackEntry getCurrent (int trackIndex) {
 	public TrackEntry getCurrent (int trackIndex) {
 		if (trackIndex >= tracks.size) return null;
 		if (trackIndex >= tracks.size) return null;
@@ -744,74 +801,30 @@ public class AnimationState {
 	 * References to a track entry must not be kept after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
 	 * References to a track entry must not be kept after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
 	static public class TrackEntry implements Poolable {
 	static public class TrackEntry implements Poolable {
 		Animation animation;
 		Animation animation;
-		TrackEntry next, mixingFrom;
+		TrackEntry next, mixingFrom, mixingTo;
 		AnimationStateListener listener;
 		AnimationStateListener listener;
 		int trackIndex;
 		int trackIndex;
-		boolean loop;
+		boolean loop, holdPrevious;
 		float eventThreshold, attachmentThreshold, drawOrderThreshold;
 		float eventThreshold, attachmentThreshold, drawOrderThreshold;
 		float animationStart, animationEnd, animationLast, nextAnimationLast;
 		float animationStart, animationEnd, animationLast, nextAnimationLast;
 		float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
 		float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
 		float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
 		float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
 		MixBlend mixBlend = MixBlend.replace;
 		MixBlend mixBlend = MixBlend.replace;
-		final IntArray timelineData = new IntArray();
-		final Array<TrackEntry> timelineDipMix = new Array();
+		final IntArray timelineMode = new IntArray();
+		final Array<TrackEntry> timelineHoldMix = new Array();
 		final FloatArray timelinesRotation = new FloatArray();
 		final FloatArray timelinesRotation = new FloatArray();
 
 
 		public void reset () {
 		public void reset () {
 			next = null;
 			next = null;
 			mixingFrom = null;
 			mixingFrom = null;
+			mixingTo = null;
 			animation = null;
 			animation = null;
 			listener = null;
 			listener = null;
-			timelineData.clear();
-			timelineDipMix.clear();
+			timelineMode.clear();
+			timelineHoldMix.clear();
 			timelinesRotation.clear();
 			timelinesRotation.clear();
 		}
 		}
 
 
-		/** @param to May be null. */
-		TrackEntry setTimelineData (TrackEntry to, Array<TrackEntry> mixingToArray, IntSet propertyIDs) {
-			if (to != null) mixingToArray.add(to);
-			TrackEntry lastEntry = mixingFrom != null ? mixingFrom.setTimelineData(this, mixingToArray, propertyIDs) : this;
-			if (to != null) mixingToArray.pop();
-
-			Object[] mixingTo = mixingToArray.items;
-			int mixingToLast = mixingToArray.size - 1;
-			Object[] timelines = animation.timelines.items;
-			int timelinesCount = animation.timelines.size;
-			int[] timelineData = this.timelineData.setSize(timelinesCount);
-			timelineDipMix.clear();
-			Object[] timelineDipMix = this.timelineDipMix.setSize(timelinesCount);
-			outer:
-			for (int i = 0; i < timelinesCount; i++) {
-				int id = ((Timeline)timelines[i]).getPropertyId();
-				if (!propertyIDs.add(id))
-					timelineData[i] = SUBSEQUENT;
-				else if (to == null || !to.hasTimeline(id))
-					timelineData[i] = FIRST;
-				else {
-					for (int ii = mixingToLast; ii >= 0; ii--) {
-						TrackEntry entry = (TrackEntry)mixingTo[ii];
-						if (!entry.hasTimeline(id)) {
-							if (entry.mixDuration > 0) {
-								timelineData[i] = DIP_MIX;
-								timelineDipMix[i] = entry;
-								continue outer;
-							}
-							break;
-						}
-					}
-					timelineData[i] = DIP;
-				}
-			}
-			return lastEntry;
-		}
-
-		private boolean hasTimeline (int id) {
-			Object[] timelines = animation.timelines.items;
-			for (int i = 0, n = animation.timelines.size; i < n; i++)
-				if (((Timeline)timelines[i]).getPropertyId() == id) return true;
-			return false;
-		}
-
 		/** The index of the track where this track entry is either current or queued.
 		/** The index of the track where this track entry is either current or queued.
 		 * <p>
 		 * <p>
 		 * See {@link AnimationState#getCurrent(int)}. */
 		 * See {@link AnimationState#getCurrent(int)}. */
@@ -1054,6 +1067,25 @@ public class AnimationState {
 			return mixingFrom;
 			return mixingFrom;
 		}
 		}
 
 
+		public void setHoldPrevious (boolean holdPrevious) {
+			this.holdPrevious = holdPrevious;
+		}
+
+		/** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
+		 * of being mixed out.
+		 * <p>
+		 * When mixing between animations that key the same property, if a lower track also keys that property then the value will
+		 * briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
+		 * while the second animation mixes from 0% to 100%. Setting <code>holdPrevious</code> to true applies the first animation
+		 * at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
+		 * keys the property, only when a higher track also keys the property.
+		 * <p>
+		 * Snapping will occur if <code>holdPrevious</code> is true and this animation does not key all the same properties as the
+		 * previous animation. */
+		public boolean getHoldPrevious () {
+			return holdPrevious;
+		}
+
 		/** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
 		/** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
 		 * long way around when using {@link #alpha} and starting animations on other tracks.
 		 * long way around when using {@link #alpha} and starting animations on other tracks.
 		 * <p>
 		 * <p>
@@ -1165,7 +1197,7 @@ public class AnimationState {
 		start, interrupt, end, dispose, complete, event
 		start, interrupt, end, dispose, complete, event
 	}
 	}
 
 
-	/** The interface which can be implemented to receive TrackEntry events.
+	/** The interface to implement for receiving TrackEntry events.
 	 * <p>
 	 * <p>
 	 * See TrackEntry {@link TrackEntry#setListener(AnimationStateListener)} and AnimationState
 	 * See TrackEntry {@link TrackEntry#setListener(AnimationStateListener)} and AnimationState
 	 * {@link AnimationState#addListener(AnimationStateListener)}. */
 	 * {@link AnimationState#addListener(AnimationStateListener)}. */