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