• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.airbnb.lottie.utils;
2 
3 import android.animation.ValueAnimator;
4 import android.view.Choreographer;
5 
6 import androidx.annotation.FloatRange;
7 import androidx.annotation.MainThread;
8 import androidx.annotation.Nullable;
9 import androidx.annotation.VisibleForTesting;
10 
11 import com.airbnb.lottie.L;
12 import com.airbnb.lottie.LottieComposition;
13 
14 /**
15  * This is a slightly modified {@link ValueAnimator} that allows us to update start and end values
16  * easily optimizing for the fact that we know that it's a value animator with 2 floats.
17  */
18 public class LottieValueAnimator extends BaseLottieAnimator implements Choreographer.FrameCallback {
19 
20 
21   private float speed = 1f;
22   private boolean speedReversedForRepeatMode = false;
23   private long lastFrameTimeNs = 0;
24   private float frame = 0;
25   private int repeatCount = 0;
26   private float minFrame = Integer.MIN_VALUE;
27   private float maxFrame = Integer.MAX_VALUE;
28   @Nullable private LottieComposition composition;
29   @VisibleForTesting protected boolean running = false;
30 
LottieValueAnimator()31   public LottieValueAnimator() {
32   }
33 
34   /**
35    * Returns a float representing the current value of the animation from 0 to 1
36    * regardless of the animation speed, direction, or min and max frames.
37    */
getAnimatedValue()38   @Override public Object getAnimatedValue() {
39     return getAnimatedValueAbsolute();
40   }
41 
42   /**
43    * Returns the current value of the animation from 0 to 1 regardless
44    * of the animation speed, direction, or min and max frames.
45    */
getAnimatedValueAbsolute()46   @FloatRange(from = 0f, to = 1f) public float getAnimatedValueAbsolute() {
47     if (composition == null) {
48       return 0;
49     }
50     return (frame - composition.getStartFrame()) / (composition.getEndFrame() - composition.getStartFrame());
51 
52   }
53 
54   /**
55    * Returns the current value of the currently playing animation taking into
56    * account direction, min and max frames.
57    */
getAnimatedFraction()58   @Override @FloatRange(from = 0f, to = 1f) public float getAnimatedFraction() {
59     if (composition == null) {
60       return 0;
61     }
62     if (isReversed()) {
63       return (getMaxFrame() - frame) / (getMaxFrame() - getMinFrame());
64     } else {
65       return (frame - getMinFrame()) / (getMaxFrame() - getMinFrame());
66     }
67   }
68 
getDuration()69   @Override public long getDuration() {
70     return composition == null ? 0 : (long) composition.getDuration();
71   }
72 
getFrame()73   public float getFrame() {
74     return frame;
75   }
76 
isRunning()77   @Override public boolean isRunning() {
78     return running;
79   }
80 
doFrame(long frameTimeNanos)81   @Override public void doFrame(long frameTimeNanos) {
82     postFrameCallback();
83     if (composition == null || !isRunning()) {
84       return;
85     }
86 
87     L.beginSection("LottieValueAnimator#doFrame");
88     long timeSinceFrame = lastFrameTimeNs == 0 ? 0 : frameTimeNanos - lastFrameTimeNs;
89     float frameDuration = getFrameDurationNs();
90     float dFrames = timeSinceFrame / frameDuration;
91 
92     frame += isReversed() ? -dFrames : dFrames;
93     boolean ended = !MiscUtils.contains(frame, getMinFrame(), getMaxFrame());
94     frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());
95 
96     lastFrameTimeNs = frameTimeNanos;
97 
98     notifyUpdate();
99     if (ended) {
100       if (getRepeatCount() != INFINITE && repeatCount >= getRepeatCount()) {
101         frame = speed < 0 ? getMinFrame() : getMaxFrame();
102         removeFrameCallback();
103         notifyEnd(isReversed());
104       } else {
105         notifyRepeat();
106         repeatCount++;
107         if (getRepeatMode() == REVERSE) {
108           speedReversedForRepeatMode = !speedReversedForRepeatMode;
109           reverseAnimationSpeed();
110         } else {
111           frame = isReversed() ? getMaxFrame() : getMinFrame();
112         }
113         lastFrameTimeNs = frameTimeNanos;
114       }
115     }
116 
117     verifyFrame();
118     L.endSection("LottieValueAnimator#doFrame");
119   }
120 
121   private float getFrameDurationNs() {
122     if (composition == null) {
123       return Float.MAX_VALUE;
124     }
125     return Utils.SECOND_IN_NANOS / composition.getFrameRate() / Math.abs(speed);
126   }
127 
128   public void clearComposition() {
129     this.composition = null;
130     minFrame = Integer.MIN_VALUE;
131     maxFrame = Integer.MAX_VALUE;
132   }
133 
134   public void setComposition(LottieComposition composition) {
135     // Because the initial composition is loaded async, the first min/max frame may be set
136     boolean keepMinAndMaxFrames = this.composition == null;
137     this.composition = composition;
138 
139     if (keepMinAndMaxFrames) {
140       setMinAndMaxFrames(
141           Math.max(this.minFrame, composition.getStartFrame()),
142           Math.min(this.maxFrame, composition.getEndFrame())
143       );
144     } else {
145       setMinAndMaxFrames((int) composition.getStartFrame(), (int) composition.getEndFrame());
146     }
147     float frame = this.frame;
148     this.frame = 0f;
149     setFrame((int) frame);
150     notifyUpdate();
151   }
152 
153   public void setFrame(float frame) {
154     if (this.frame == frame) {
155       return;
156     }
157     this.frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());
158     lastFrameTimeNs = 0;
159     notifyUpdate();
160   }
161 
162   public void setMinFrame(int minFrame) {
163     setMinAndMaxFrames(minFrame, (int) maxFrame);
164   }
165 
166   public void setMaxFrame(float maxFrame) {
167     setMinAndMaxFrames(minFrame, maxFrame);
168   }
169 
170   public void setMinAndMaxFrames(float minFrame, float maxFrame) {
171     if (minFrame > maxFrame) {
172       throw new IllegalArgumentException(String.format("minFrame (%s) must be <= maxFrame (%s)", minFrame, maxFrame));
173     }
174     float compositionMinFrame = composition == null ? -Float.MAX_VALUE : composition.getStartFrame();
175     float compositionMaxFrame = composition == null ? Float.MAX_VALUE : composition.getEndFrame();
176     float newMinFrame = MiscUtils.clamp(minFrame, compositionMinFrame, compositionMaxFrame);
177     float newMaxFrame = MiscUtils.clamp(maxFrame, compositionMinFrame, compositionMaxFrame);
178     if (newMinFrame != this.minFrame || newMaxFrame != this.maxFrame) {
179       this.minFrame = newMinFrame;
180       this.maxFrame = newMaxFrame;
181       setFrame((int) MiscUtils.clamp(frame, newMinFrame, newMaxFrame));
182     }
183   }
184 
reverseAnimationSpeed()185   public void reverseAnimationSpeed() {
186     setSpeed(-getSpeed());
187   }
188 
setSpeed(float speed)189   public void setSpeed(float speed) {
190     this.speed = speed;
191   }
192 
193   /**
194    * Returns the current speed. This will be affected by repeat mode REVERSE.
195    */
getSpeed()196   public float getSpeed() {
197     return speed;
198   }
199 
setRepeatMode(int value)200   @Override public void setRepeatMode(int value) {
201     super.setRepeatMode(value);
202     if (value != REVERSE && speedReversedForRepeatMode) {
203       speedReversedForRepeatMode = false;
204       reverseAnimationSpeed();
205     }
206   }
207 
208   @MainThread
playAnimation()209   public void playAnimation() {
210     running = true;
211     notifyStart(isReversed());
212     setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
213     lastFrameTimeNs = 0;
214     repeatCount = 0;
215     postFrameCallback();
216   }
217 
218   @MainThread
endAnimation()219   public void endAnimation() {
220     removeFrameCallback();
221     notifyEnd(isReversed());
222   }
223 
224   @MainThread
pauseAnimation()225   public void pauseAnimation() {
226     removeFrameCallback();
227   }
228 
229   @MainThread
resumeAnimation()230   public void resumeAnimation() {
231     running = true;
232     postFrameCallback();
233     lastFrameTimeNs = 0;
234     if (isReversed() && getFrame() == getMinFrame()) {
235       frame = getMaxFrame();
236     } else if (!isReversed() && getFrame() == getMaxFrame()) {
237       frame = getMinFrame();
238     }
239   }
240 
241   @MainThread
cancel()242   @Override public void cancel() {
243     notifyCancel();
244     removeFrameCallback();
245   }
246 
isReversed()247   private boolean isReversed() {
248     return getSpeed() < 0;
249   }
250 
getMinFrame()251   public float getMinFrame() {
252     if (composition == null) {
253       return 0;
254     }
255     return minFrame == Integer.MIN_VALUE ? composition.getStartFrame() : minFrame;
256   }
257 
getMaxFrame()258   public float getMaxFrame() {
259     if (composition == null) {
260       return 0;
261     }
262     return maxFrame == Integer.MAX_VALUE ? composition.getEndFrame() : maxFrame;
263   }
264 
notifyCancel()265   @Override void notifyCancel() {
266     super.notifyCancel();
267     notifyEnd(isReversed());
268   }
269 
postFrameCallback()270   protected void postFrameCallback() {
271     if (isRunning()) {
272       removeFrameCallback(false);
273       Choreographer.getInstance().postFrameCallback(this);
274     }
275   }
276 
277   @MainThread
removeFrameCallback()278   protected void removeFrameCallback() {
279     this.removeFrameCallback(true);
280   }
281 
282   @MainThread
removeFrameCallback(boolean stopRunning)283   protected void removeFrameCallback(boolean stopRunning) {
284     Choreographer.getInstance().removeFrameCallback(this);
285     if (stopRunning) {
286       running = false;
287     }
288   }
289 
verifyFrame()290   private void verifyFrame() {
291     if (composition == null) {
292       return;
293     }
294     if (frame < minFrame || frame > maxFrame) {
295       throw new IllegalStateException(String.format("Frame must be [%f,%f]. It is %f", minFrame, maxFrame, frame));
296     }
297   }
298 }
299