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