blob: 6451c1ae08981b943ba354b337c7d1e21cfb6cb1 [file] [log] [blame]
package com.airbnb.lottie.utils;
import android.animation.ValueAnimator;
import android.view.Choreographer;
import androidx.annotation.FloatRange;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.airbnb.lottie.L;
import com.airbnb.lottie.LottieComposition;
/**
* This is a slightly modified {@link ValueAnimator} that allows us to update start and end values
* easily optimizing for the fact that we know that it's a value animator with 2 floats.
*/
public class LottieValueAnimator extends BaseLottieAnimator implements Choreographer.FrameCallback {
private float speed = 1f;
private boolean speedReversedForRepeatMode = false;
private long lastFrameTimeNs = 0;
private float frame = 0;
private int repeatCount = 0;
private float minFrame = Integer.MIN_VALUE;
private float maxFrame = Integer.MAX_VALUE;
@Nullable private LottieComposition composition;
@VisibleForTesting protected boolean running = false;
public LottieValueAnimator() {
}
/**
* Returns a float representing the current value of the animation from 0 to 1
* regardless of the animation speed, direction, or min and max frames.
*/
@Override public Object getAnimatedValue() {
return getAnimatedValueAbsolute();
}
/**
* Returns the current value of the animation from 0 to 1 regardless
* of the animation speed, direction, or min and max frames.
*/
@FloatRange(from = 0f, to = 1f) public float getAnimatedValueAbsolute() {
if (composition == null) {
return 0;
}
return (frame - composition.getStartFrame()) / (composition.getEndFrame() - composition.getStartFrame());
}
/**
* Returns the current value of the currently playing animation taking into
* account direction, min and max frames.
*/
@Override @FloatRange(from = 0f, to = 1f) public float getAnimatedFraction() {
if (composition == null) {
return 0;
}
if (isReversed()) {
return (getMaxFrame() - frame) / (getMaxFrame() - getMinFrame());
} else {
return (frame - getMinFrame()) / (getMaxFrame() - getMinFrame());
}
}
@Override public long getDuration() {
return composition == null ? 0 : (long) composition.getDuration();
}
public float getFrame() {
return frame;
}
@Override public boolean isRunning() {
return running;
}
@Override public void doFrame(long frameTimeNanos) {
postFrameCallback();
if (composition == null || !isRunning()) {
return;
}
L.beginSection("LottieValueAnimator#doFrame");
long timeSinceFrame = lastFrameTimeNs == 0 ? 0 : frameTimeNanos - lastFrameTimeNs;
float frameDuration = getFrameDurationNs();
float dFrames = timeSinceFrame / frameDuration;
frame += isReversed() ? -dFrames : dFrames;
boolean ended = !MiscUtils.contains(frame, getMinFrame(), getMaxFrame());
frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());
lastFrameTimeNs = frameTimeNanos;
notifyUpdate();
if (ended) {
if (getRepeatCount() != INFINITE && repeatCount >= getRepeatCount()) {
frame = speed < 0 ? getMinFrame() : getMaxFrame();
removeFrameCallback();
notifyEnd(isReversed());
} else {
notifyRepeat();
repeatCount++;
if (getRepeatMode() == REVERSE) {
speedReversedForRepeatMode = !speedReversedForRepeatMode;
reverseAnimationSpeed();
} else {
frame = isReversed() ? getMaxFrame() : getMinFrame();
}
lastFrameTimeNs = frameTimeNanos;
}
}
verifyFrame();
L.endSection("LottieValueAnimator#doFrame");
}
private float getFrameDurationNs() {
if (composition == null) {
return Float.MAX_VALUE;
}
return Utils.SECOND_IN_NANOS / composition.getFrameRate() / Math.abs(speed);
}
public void clearComposition() {
this.composition = null;
minFrame = Integer.MIN_VALUE;
maxFrame = Integer.MAX_VALUE;
}
public void setComposition(LottieComposition composition) {
// Because the initial composition is loaded async, the first min/max frame may be set
boolean keepMinAndMaxFrames = this.composition == null;
this.composition = composition;
if (keepMinAndMaxFrames) {
setMinAndMaxFrames(
(int) Math.max(this.minFrame, composition.getStartFrame()),
(int) Math.min(this.maxFrame, composition.getEndFrame())
);
} else {
setMinAndMaxFrames((int) composition.getStartFrame(), (int) composition.getEndFrame());
}
float frame = this.frame;
this.frame = 0f;
setFrame((int) frame);
notifyUpdate();
}
public void setFrame(float frame) {
if (this.frame == frame) {
return;
}
this.frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());
lastFrameTimeNs = 0;
notifyUpdate();
}
public void setMinFrame(int minFrame) {
setMinAndMaxFrames(minFrame, (int) maxFrame);
}
public void setMaxFrame(float maxFrame) {
setMinAndMaxFrames(minFrame, maxFrame);
}
public void setMinAndMaxFrames(float minFrame, float maxFrame) {
if (minFrame > maxFrame) {
throw new IllegalArgumentException(String.format("minFrame (%s) must be <= maxFrame (%s)", minFrame, maxFrame));
}
float compositionMinFrame = composition == null ? -Float.MAX_VALUE : composition.getStartFrame();
float compositionMaxFrame = composition == null ? Float.MAX_VALUE : composition.getEndFrame();
float newMinFrame = MiscUtils.clamp(minFrame, compositionMinFrame, compositionMaxFrame);
float newMaxFrame = MiscUtils.clamp(maxFrame, compositionMinFrame, compositionMaxFrame);
if (newMinFrame != this.minFrame || newMaxFrame != this.maxFrame) {
this.minFrame = newMinFrame;
this.maxFrame = newMaxFrame;
setFrame((int) MiscUtils.clamp(frame, newMinFrame, newMaxFrame));
}
}
public void reverseAnimationSpeed() {
setSpeed(-getSpeed());
}
public void setSpeed(float speed) {
this.speed = speed;
}
/**
* Returns the current speed. This will be affected by repeat mode REVERSE.
*/
public float getSpeed() {
return speed;
}
@Override public void setRepeatMode(int value) {
super.setRepeatMode(value);
if (value != REVERSE && speedReversedForRepeatMode) {
speedReversedForRepeatMode = false;
reverseAnimationSpeed();
}
}
@MainThread
public void playAnimation() {
running = true;
notifyStart(isReversed());
setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
lastFrameTimeNs = 0;
repeatCount = 0;
postFrameCallback();
}
@MainThread
public void endAnimation() {
removeFrameCallback();
notifyEnd(isReversed());
}
@MainThread
public void pauseAnimation() {
removeFrameCallback();
}
@MainThread
public void resumeAnimation() {
running = true;
postFrameCallback();
lastFrameTimeNs = 0;
if (isReversed() && getFrame() == getMinFrame()) {
frame = getMaxFrame();
} else if (!isReversed() && getFrame() == getMaxFrame()) {
frame = getMinFrame();
}
}
@MainThread
@Override public void cancel() {
notifyCancel();
removeFrameCallback();
}
private boolean isReversed() {
return getSpeed() < 0;
}
public float getMinFrame() {
if (composition == null) {
return 0;
}
return minFrame == Integer.MIN_VALUE ? composition.getStartFrame() : minFrame;
}
public float getMaxFrame() {
if (composition == null) {
return 0;
}
return maxFrame == Integer.MAX_VALUE ? composition.getEndFrame() : maxFrame;
}
@Override void notifyCancel() {
super.notifyCancel();
notifyEnd(isReversed());
}
protected void postFrameCallback() {
if (isRunning()) {
removeFrameCallback(false);
Choreographer.getInstance().postFrameCallback(this);
}
}
@MainThread
protected void removeFrameCallback() {
this.removeFrameCallback(true);
}
@MainThread
protected void removeFrameCallback(boolean stopRunning) {
Choreographer.getInstance().removeFrameCallback(this);
if (stopRunning) {
running = false;
}
}
private void verifyFrame() {
if (composition == null) {
return;
}
if (frame < minFrame || frame > maxFrame) {
throw new IllegalStateException(String.format("Frame must be [%f,%f]. It is %f", minFrame, maxFrame, frame));
}
}
}