• 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     L.beginSection("LottieValueAnimator#doFrame");
94     long timeSinceFrame = lastFrameTimeNs == 0 ? 0 : frameTimeNanos - lastFrameTimeNs;
95     float frameDuration = getFrameDurationNs();
96     float dFrames = timeSinceFrame / frameDuration;
97 
98     float newFrameRaw = frameRaw + (isReversed() ? -dFrames : dFrames);
99     boolean ended = !MiscUtils.contains(newFrameRaw, getMinFrame(), getMaxFrame());
100     float previousFrameRaw = frameRaw;
101     frameRaw = MiscUtils.clamp(newFrameRaw, getMinFrame(), getMaxFrame());
102     frame = useCompositionFrameRate ? (float) Math.floor(frameRaw) : frameRaw;
103 
104     lastFrameTimeNs = frameTimeNanos;
105 
106     if (!useCompositionFrameRate || frameRaw != previousFrameRaw) {
107       notifyUpdate();
108     }
109     if (ended) {
110       if (getRepeatCount() != INFINITE && repeatCount >= getRepeatCount()) {
111         frameRaw = speed < 0 ? getMinFrame() : getMaxFrame();
112         frame = frameRaw;
113         removeFrameCallback();
114         notifyEnd(isReversed());
115       } else {
116         notifyRepeat();
117         repeatCount++;
118         if (getRepeatMode() == REVERSE) {
119           speedReversedForRepeatMode = !speedReversedForRepeatMode;
120           reverseAnimationSpeed();
121         } else {
122           frameRaw = isReversed() ? getMaxFrame() : getMinFrame();
123           frame = frameRaw;
124         }
125         lastFrameTimeNs = frameTimeNanos;
126       }
127     }
128 
129     verifyFrame();
130     L.endSection("LottieValueAnimator#doFrame");
131   }
132 
133   private float getFrameDurationNs() {
134     if (composition == null) {
135       return Float.MAX_VALUE;
136     }
137     return Utils.SECOND_IN_NANOS / composition.getFrameRate() / Math.abs(speed);
138   }
139 
140   public void clearComposition() {
141     this.composition = null;
142     minFrame = Integer.MIN_VALUE;
143     maxFrame = Integer.MAX_VALUE;
144   }
145 
146   public void setComposition(LottieComposition composition) {
147     // Because the initial composition is loaded async, the first min/max frame may be set
148     boolean keepMinAndMaxFrames = this.composition == null;
149     this.composition = composition;
150 
151     if (keepMinAndMaxFrames) {
152       setMinAndMaxFrames(
153           Math.max(this.minFrame, composition.getStartFrame()),
154           Math.min(this.maxFrame, composition.getEndFrame())
155       );
156     } else {
157       setMinAndMaxFrames((int) composition.getStartFrame(), (int) composition.getEndFrame());
158     }
159     float frame = this.frame;
160     this.frame = 0f;
161     this.frameRaw = 0f;
162     setFrame((int) frame);
163     notifyUpdate();
164   }
165 
166   public void setFrame(float frame) {
167     if (this.frameRaw == frame) {
168       return;
169     }
170     this.frameRaw = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());
171     this.frame = useCompositionFrameRate ? ((float) Math.floor(frameRaw)) : frameRaw;
172     lastFrameTimeNs = 0;
173     notifyUpdate();
174   }
175 
176   public void setMinFrame(int minFrame) {
177     setMinAndMaxFrames(minFrame, (int) maxFrame);
178   }
179 
180   public void setMaxFrame(float maxFrame) {
181     setMinAndMaxFrames(minFrame, maxFrame);
182   }
183 
184   public void setMinAndMaxFrames(float minFrame, float maxFrame) {
185     if (minFrame > maxFrame) {
186       throw new IllegalArgumentException(String.format("minFrame (%s) must be <= maxFrame (%s)", minFrame, maxFrame));
187     }
188     float compositionMinFrame = composition == null ? -Float.MAX_VALUE : composition.getStartFrame();
189     float compositionMaxFrame = composition == null ? Float.MAX_VALUE : composition.getEndFrame();
190     float newMinFrame = MiscUtils.clamp(minFrame, compositionMinFrame, compositionMaxFrame);
191     float newMaxFrame = MiscUtils.clamp(maxFrame, compositionMinFrame, compositionMaxFrame);
192     if (newMinFrame != this.minFrame || newMaxFrame != this.maxFrame) {
193       this.minFrame = newMinFrame;
194       this.maxFrame = newMaxFrame;
195       setFrame((int) MiscUtils.clamp(frame, newMinFrame, newMaxFrame));
196     }
197   }
198 
reverseAnimationSpeed()199   public void reverseAnimationSpeed() {
200     setSpeed(-getSpeed());
201   }
202 
setSpeed(float speed)203   public void setSpeed(float speed) {
204     this.speed = speed;
205   }
206 
207   /**
208    * Returns the current speed. This will be affected by repeat mode REVERSE.
209    */
getSpeed()210   public float getSpeed() {
211     return speed;
212   }
213 
setRepeatMode(int value)214   @Override public void setRepeatMode(int value) {
215     super.setRepeatMode(value);
216     if (value != REVERSE && speedReversedForRepeatMode) {
217       speedReversedForRepeatMode = false;
218       reverseAnimationSpeed();
219     }
220   }
221 
222   @MainThread
playAnimation()223   public void playAnimation() {
224     running = true;
225     notifyStart(isReversed());
226     setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
227     lastFrameTimeNs = 0;
228     repeatCount = 0;
229     postFrameCallback();
230   }
231 
232   @MainThread
endAnimation()233   public void endAnimation() {
234     removeFrameCallback();
235     notifyEnd(isReversed());
236   }
237 
238   @MainThread
pauseAnimation()239   public void pauseAnimation() {
240     removeFrameCallback();
241     notifyPause();
242   }
243 
244   @MainThread
resumeAnimation()245   public void resumeAnimation() {
246     running = true;
247     postFrameCallback();
248     lastFrameTimeNs = 0;
249     if (isReversed() && getFrame() == getMinFrame()) {
250       setFrame(getMaxFrame());
251     } else if (!isReversed() && getFrame() == getMaxFrame()) {
252       setFrame(getMinFrame());
253     }
254     notifyResume();
255   }
256 
257   @MainThread
cancel()258   @Override public void cancel() {
259     notifyCancel();
260     removeFrameCallback();
261   }
262 
isReversed()263   private boolean isReversed() {
264     return getSpeed() < 0;
265   }
266 
getMinFrame()267   public float getMinFrame() {
268     if (composition == null) {
269       return 0;
270     }
271     return minFrame == Integer.MIN_VALUE ? composition.getStartFrame() : minFrame;
272   }
273 
getMaxFrame()274   public float getMaxFrame() {
275     if (composition == null) {
276       return 0;
277     }
278     return maxFrame == Integer.MAX_VALUE ? composition.getEndFrame() : maxFrame;
279   }
280 
notifyCancel()281   @Override void notifyCancel() {
282     super.notifyCancel();
283     notifyEnd(isReversed());
284   }
285 
postFrameCallback()286   protected void postFrameCallback() {
287     if (isRunning()) {
288       removeFrameCallback(false);
289       Choreographer.getInstance().postFrameCallback(this);
290     }
291   }
292 
293   @MainThread
removeFrameCallback()294   protected void removeFrameCallback() {
295     this.removeFrameCallback(true);
296   }
297 
298   @MainThread
removeFrameCallback(boolean stopRunning)299   protected void removeFrameCallback(boolean stopRunning) {
300     Choreographer.getInstance().removeFrameCallback(this);
301     if (stopRunning) {
302       running = false;
303     }
304   }
305 
verifyFrame()306   private void verifyFrame() {
307     if (composition == null) {
308       return;
309     }
310     if (frame < minFrame || frame > maxFrame) {
311       throw new IllegalStateException(String.format("Frame must be [%f,%f]. It is %f", minFrame, maxFrame, frame));
312     }
313   }
314 }
315