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