blob: cb63438708787212e8384b790728854319579b10 [file] [log] [blame]
package com.airbnb.lottie.animation.keyframe;
import android.annotation.SuppressLint;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.airbnb.lottie.L;
import com.airbnb.lottie.value.Keyframe;
import com.airbnb.lottie.value.LottieValueCallback;
import java.util.ArrayList;
import java.util.List;
/**
* @param <K> Keyframe type
* @param <A> Animation type
*/
public abstract class BaseKeyframeAnimation<K, A> {
public interface AnimationListener {
void onValueChanged();
}
// This is not a Set because we don't want to create an iterator object on every setProgress.
final List<AnimationListener> listeners = new ArrayList<>(1);
private boolean isDiscrete = false;
private final KeyframesWrapper<K> keyframesWrapper;
protected float progress = 0f;
@Nullable protected LottieValueCallback<A> valueCallback;
@Nullable private A cachedGetValue = null;
private float cachedStartDelayProgress = -1f;
private float cachedEndProgress = -1f;
BaseKeyframeAnimation(List<? extends Keyframe<K>> keyframes) {
keyframesWrapper = wrap(keyframes);
}
public void setIsDiscrete() {
isDiscrete = true;
}
public void addUpdateListener(AnimationListener listener) {
listeners.add(listener);
}
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
if (L.isTraceEnabled()) {
L.beginSection("BaseKeyframeAnimation#setProgress");
}
if (keyframesWrapper.isEmpty()) {
if (L.isTraceEnabled()) {
L.endSection("BaseKeyframeAnimation#setProgress");
}
return;
}
if (progress < getStartDelayProgress()) {
progress = getStartDelayProgress();
} else if (progress > getEndProgress()) {
progress = getEndProgress();
}
if (progress == this.progress) {
if (L.isTraceEnabled()) {
L.endSection("BaseKeyframeAnimation#setProgress");
}
return;
}
this.progress = progress;
if (keyframesWrapper.isValueChanged(progress)) {
notifyListeners();
}
if (L.isTraceEnabled()) {
L.endSection("BaseKeyframeAnimation#setProgress");
}
}
public void notifyListeners() {
if (L.isTraceEnabled()) {
L.beginSection("BaseKeyframeAnimation#notifyListeners");
}
for (int i = 0; i < listeners.size(); i++) {
listeners.get(i).onValueChanged();
}
if (L.isTraceEnabled()) {
L.endSection("BaseKeyframeAnimation#notifyListeners");
}
}
protected Keyframe<K> getCurrentKeyframe() {
if (L.isTraceEnabled()) {
L.beginSection("BaseKeyframeAnimation#getCurrentKeyframe");
}
final Keyframe<K> keyframe = keyframesWrapper.getCurrentKeyframe();
if (L.isTraceEnabled()) {
L.endSection("BaseKeyframeAnimation#getCurrentKeyframe");
}
return keyframe;
}
/**
* Returns the progress into the current keyframe between 0 and 1. This does not take into account
* any interpolation that the keyframe may have.
*/
float getLinearCurrentKeyframeProgress() {
if (isDiscrete) {
return 0f;
}
Keyframe<K> keyframe = getCurrentKeyframe();
if (keyframe.isStatic()) {
return 0f;
}
float progressIntoFrame = progress - keyframe.getStartProgress();
float keyframeProgress = keyframe.getEndProgress() - keyframe.getStartProgress();
return progressIntoFrame / keyframeProgress;
}
/**
* Takes the value of {@link #getLinearCurrentKeyframeProgress()} and interpolates it with
* the current keyframe's interpolator.
*/
protected float getInterpolatedCurrentKeyframeProgress() {
Keyframe<K> keyframe = getCurrentKeyframe();
// Keyframe should not be null here but there seems to be a Xiaomi Android 10 specific crash.
// https://github.com/airbnb/lottie-android/issues/2050
// https://github.com/airbnb/lottie-android/issues/2483
if (keyframe == null || keyframe.isStatic() || keyframe.interpolator == null) {
return 0f;
}
//noinspection ConstantConditions
return keyframe.interpolator.getInterpolation(getLinearCurrentKeyframeProgress());
}
@SuppressLint("Range")
@FloatRange(from = 0f, to = 1f)
private float getStartDelayProgress() {
if (cachedStartDelayProgress == -1f) {
cachedStartDelayProgress = keyframesWrapper.getStartDelayProgress();
}
return cachedStartDelayProgress;
}
@SuppressLint("Range")
@FloatRange(from = 0f, to = 1f)
float getEndProgress() {
if (cachedEndProgress == -1f) {
cachedEndProgress = keyframesWrapper.getEndProgress();
}
return cachedEndProgress;
}
public A getValue() {
A value;
float linearProgress = getLinearCurrentKeyframeProgress();
if (valueCallback == null && keyframesWrapper.isCachedValueEnabled(linearProgress)) {
return cachedGetValue;
}
final Keyframe<K> keyframe = getCurrentKeyframe();
if (keyframe.xInterpolator != null && keyframe.yInterpolator != null) {
float xProgress = keyframe.xInterpolator.getInterpolation(linearProgress);
float yProgress = keyframe.yInterpolator.getInterpolation(linearProgress);
value = getValue(keyframe, linearProgress, xProgress, yProgress);
} else {
float progress = getInterpolatedCurrentKeyframeProgress();
value = getValue(keyframe, progress);
}
cachedGetValue = value;
return value;
}
public float getProgress() {
return progress;
}
public void setValueCallback(@Nullable LottieValueCallback<A> valueCallback) {
if (this.valueCallback != null) {
this.valueCallback.setAnimation(null);
}
this.valueCallback = valueCallback;
if (valueCallback != null) {
valueCallback.setAnimation(this);
}
}
public boolean hasValueCallback() {
return valueCallback != null;
}
/**
* keyframeProgress will be [0, 1] unless the interpolator has overshoot in which case, this
* should be able to handle values outside of that range.
*/
abstract A getValue(Keyframe<K> keyframe, float keyframeProgress);
/**
* Similar to {@link #getValue(Keyframe, float)} but used when an animation has separate interpolators for the X and Y axis.
*/
protected A getValue(Keyframe<K> keyframe, float linearKeyframeProgress, float xKeyframeProgress, float yKeyframeProgress) {
throw new UnsupportedOperationException("This animation does not support split dimensions!");
}
private static <T> KeyframesWrapper<T> wrap(List<? extends Keyframe<T>> keyframes) {
if (keyframes.isEmpty()) {
return new EmptyKeyframeWrapper<>();
}
if (keyframes.size() == 1) {
return new SingleKeyframeWrapper<>(keyframes);
}
return new KeyframesWrapperImpl<>(keyframes);
}
private interface KeyframesWrapper<T> {
boolean isEmpty();
boolean isValueChanged(float progress);
Keyframe<T> getCurrentKeyframe();
@FloatRange(from = 0f, to = 1f)
float getStartDelayProgress();
@FloatRange(from = 0f, to = 1f)
float getEndProgress();
boolean isCachedValueEnabled(float progress);
}
private static final class EmptyKeyframeWrapper<T> implements KeyframesWrapper<T> {
@Override
public boolean isEmpty() {
return true;
}
@Override
public boolean isValueChanged(float progress) {
return false;
}
@Override
public Keyframe<T> getCurrentKeyframe() {
throw new IllegalStateException("not implemented");
}
@Override
public float getStartDelayProgress() {
return 0f;
}
@Override
public float getEndProgress() {
return 1f;
}
@Override
public boolean isCachedValueEnabled(float progress) {
throw new IllegalStateException("not implemented");
}
}
private static final class SingleKeyframeWrapper<T> implements KeyframesWrapper<T> {
@NonNull
private final Keyframe<T> keyframe;
private float cachedInterpolatedProgress = -1f;
SingleKeyframeWrapper(List<? extends Keyframe<T>> keyframes) {
this.keyframe = keyframes.get(0);
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean isValueChanged(float progress) {
return !keyframe.isStatic();
}
@Override
public Keyframe<T> getCurrentKeyframe() {
return keyframe;
}
@Override
public float getStartDelayProgress() {
return keyframe.getStartProgress();
}
@Override
public float getEndProgress() {
return keyframe.getEndProgress();
}
@Override
public boolean isCachedValueEnabled(float progress) {
if (cachedInterpolatedProgress == progress) {
return true;
}
cachedInterpolatedProgress = progress;
return false;
}
}
private static final class KeyframesWrapperImpl<T> implements KeyframesWrapper<T> {
private final List<? extends Keyframe<T>> keyframes;
@NonNull
private Keyframe<T> currentKeyframe;
private Keyframe<T> cachedCurrentKeyframe = null;
private float cachedInterpolatedProgress = -1f;
KeyframesWrapperImpl(List<? extends Keyframe<T>> keyframes) {
this.keyframes = keyframes;
currentKeyframe = findKeyframe(0);
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean isValueChanged(float progress) {
if (currentKeyframe.containsProgress(progress)) {
return !currentKeyframe.isStatic();
}
currentKeyframe = findKeyframe(progress);
return true;
}
private Keyframe<T> findKeyframe(float progress) {
Keyframe<T> keyframe = keyframes.get(keyframes.size() - 1);
if (progress >= keyframe.getStartProgress()) {
return keyframe;
}
for (int i = keyframes.size() - 2; i >= 1; i--) {
keyframe = keyframes.get(i);
if (currentKeyframe == keyframe) {
continue;
}
if (keyframe.containsProgress(progress)) {
return keyframe;
}
}
return keyframes.get(0);
}
@Override
@NonNull
public Keyframe<T> getCurrentKeyframe() {
return currentKeyframe;
}
@Override
public float getStartDelayProgress() {
return keyframes.get(0).getStartProgress();
}
@Override
public float getEndProgress() {
return keyframes.get(keyframes.size() - 1).getEndProgress();
}
@Override
public boolean isCachedValueEnabled(float progress) {
if (cachedCurrentKeyframe == currentKeyframe
&& cachedInterpolatedProgress == progress) {
return true;
}
cachedCurrentKeyframe = currentKeyframe;
cachedInterpolatedProgress = progress;
return false;
}
}
}