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