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