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