blob: db5381f5f99109e6b769f3bc3b9a4d481f72a8c9 [file] [log] [blame]
package com.airbnb.lottie;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Typeface;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.View;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.airbnb.lottie.manager.FontAssetManager;
import com.airbnb.lottie.manager.ImageAssetManager;
import com.airbnb.lottie.model.KeyPath;
import com.airbnb.lottie.model.Marker;
import com.airbnb.lottie.model.layer.CompositionLayer;
import com.airbnb.lottie.parser.LayerParser;
import com.airbnb.lottie.utils.Logger;
import com.airbnb.lottie.utils.LottieValueAnimator;
import com.airbnb.lottie.utils.MiscUtils;
import com.airbnb.lottie.value.LottieFrameInfo;
import com.airbnb.lottie.value.LottieValueCallback;
import com.airbnb.lottie.value.SimpleLottieValueCallback;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* This can be used to show an lottie animation in any place that would normally take a drawable.
*
* @see <a href="http://airbnb.io/lottie">Full Documentation</a>
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class LottieDrawable extends Drawable implements Drawable.Callback, Animatable {
private static final String TAG = LottieDrawable.class.getSimpleName();
private interface LazyCompositionTask {
void run(LottieComposition composition);
}
private final Matrix matrix = new Matrix();
private LottieComposition composition;
private final LottieValueAnimator animator = new LottieValueAnimator();
private float scale = 1f;
private boolean systemAnimationsEnabled = true;
private final Set<ColorFilterData> colorFilterData = new HashSet<>();
private final ArrayList<LazyCompositionTask> lazyCompositionTasks = new ArrayList<>();
private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (compositionLayer != null) {
compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
}
}
};
@Nullable
private ImageAssetManager imageAssetManager;
@Nullable
private String imageAssetsFolder;
@Nullable
private ImageAssetDelegate imageAssetDelegate;
@Nullable
private FontAssetManager fontAssetManager;
@Nullable
FontAssetDelegate fontAssetDelegate;
@Nullable
TextDelegate textDelegate;
private boolean enableMergePaths;
@Nullable
private CompositionLayer compositionLayer;
private int alpha = 255;
private boolean performanceTrackingEnabled;
/**
* True if the drawable has not been drawn since the last invalidateSelf.
* We can do this to prevent things like bounds from getting recalculated
* many times.
*/
private boolean isDirty = false;
@IntDef({RESTART, REVERSE})
@Retention(RetentionPolicy.SOURCE)
public @interface RepeatMode {
}
/**
* When the animation reaches the end and <code>repeatCount</code> is INFINITE
* or a positive value, the animation restarts from the beginning.
*/
public static final int RESTART = ValueAnimator.RESTART;
/**
* When the animation reaches the end and <code>repeatCount</code> is INFINITE
* or a positive value, the animation reverses direction on every iteration.
*/
public static final int REVERSE = ValueAnimator.REVERSE;
/**
* This value used used with the {@link #setRepeatCount(int)} property to repeat
* the animation indefinitely.
*/
public static final int INFINITE = ValueAnimator.INFINITE;
public LottieDrawable() {
animator.addUpdateListener(progressUpdateListener);
}
/**
* Returns whether or not any layers in this composition has masks.
*/
public boolean hasMasks() {
return compositionLayer != null && compositionLayer.hasMasks();
}
/**
* Returns whether or not any layers in this composition has a matte layer.
*/
public boolean hasMatte() {
return compositionLayer != null && compositionLayer.hasMatte();
}
public boolean enableMergePathsForKitKatAndAbove() {
return enableMergePaths;
}
/**
* Enable this to get merge path support for devices running KitKat (19) and above.
* <p>
* Merge paths currently don't work if the the operand shape is entirely contained within the
* first shape. If you need to cut out one shape from another shape, use an even-odd fill type
* instead of using merge paths.
*/
public void enableMergePathsForKitKatAndAbove(boolean enable) {
if (enableMergePaths == enable) {
return;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
Logger.warning("Merge paths are not supported pre-Kit Kat.");
return;
}
enableMergePaths = enable;
if (composition != null) {
buildCompositionLayer();
}
}
public boolean isMergePathsEnabledForKitKatAndAbove() {
return enableMergePaths;
}
/**
* If you use image assets, you must explicitly specify the folder in assets/ in which they are
* located because bodymovin uses the name filenames across all compositions (img_#).
* Do NOT rename the images themselves.
* <p>
* If your images are located in src/main/assets/airbnb_loader/ then call
* `setImageAssetsFolder("airbnb_loader/");`.
* <p>
* <p>
* If you use LottieDrawable directly, you MUST call {@link #recycleBitmaps()} when you
* are done. Calling {@link #recycleBitmaps()} doesn't have to be final and {@link LottieDrawable}
* will recreate the bitmaps if needed but they will leak if you don't recycle them.
* <p>
* Be wary if you are using many images, however. Lottie is designed to work with vector shapes
* from After Effects. If your images look like they could be represented with vector shapes,
* see if it is possible to convert them to shape layers and re-export your animation. Check
* the documentation at http://airbnb.io/lottie for more information about importing shapes from
* Sketch or Illustrator to avoid this.
*/
public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {
this.imageAssetsFolder = imageAssetsFolder;
}
@Nullable
public String getImageAssetsFolder() {
return imageAssetsFolder;
}
/**
* Create a composition with {@link LottieCompositionFactory}
*
* @return True if the composition is different from the previously set composition, false otherwise.
*/
public boolean setComposition(LottieComposition composition) {
if (this.composition == composition) {
return false;
}
isDirty = false;
clearComposition();
this.composition = composition;
buildCompositionLayer();
animator.setComposition(composition);
setProgress(animator.getAnimatedFraction());
setScale(scale);
updateBounds();
// We copy the tasks to a new ArrayList so that if this method is called from multiple threads,
// then there won't be two iterators iterating and removing at the same time.
Iterator<LazyCompositionTask> it = new ArrayList<>(lazyCompositionTasks).iterator();
while (it.hasNext()) {
LazyCompositionTask t = it.next();
t.run(composition);
it.remove();
}
lazyCompositionTasks.clear();
composition.setPerformanceTrackingEnabled(performanceTrackingEnabled);
return true;
}
public void setPerformanceTrackingEnabled(boolean enabled) {
performanceTrackingEnabled = enabled;
if (composition != null) {
composition.setPerformanceTrackingEnabled(enabled);
}
}
@Nullable
public PerformanceTracker getPerformanceTracker() {
if (composition != null) {
return composition.getPerformanceTracker();
}
return null;
}
private void buildCompositionLayer() {
compositionLayer = new CompositionLayer(
this, LayerParser.parse(composition), composition.getLayers(), composition);
}
public void clearComposition() {
if (animator.isRunning()) {
animator.cancel();
}
composition = null;
compositionLayer = null;
imageAssetManager = null;
animator.clearComposition();
invalidateSelf();
}
@Override
public void invalidateSelf() {
if (isDirty) {
return;
}
isDirty = true;
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
this.alpha = alpha;
invalidateSelf();
}
@Override
public int getAlpha() {
return alpha;
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
Logger.warning("Use addColorFilter instead.");
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public void draw(@NonNull Canvas canvas) {
isDirty = false;
L.beginSection("Drawable#draw");
if (compositionLayer == null) {
return;
}
float scale = this.scale;
float extraScale = 1f;
float maxScale = getMaxScale(canvas);
if (scale > maxScale) {
scale = maxScale;
extraScale = this.scale / scale;
}
int saveCount = -1;
if (extraScale > 1) {
// This is a bit tricky...
// We can't draw on a canvas larger than ViewConfiguration.get(context).getScaledMaximumDrawingCacheSize()
// which works out to be roughly the size of the screen because Android can't generate a
// bitmap large enough to render to.
// As a result, we cap the scale such that it will never be wider/taller than the screen
// and then only render in the top left corner of the canvas. We then use extraScale
// to scale up the rest of the scale. However, since we rendered the animation to the top
// left corner, we need to scale up and translate the canvas to zoom in on the top left
// corner.
saveCount = canvas.save();
float halfWidth = composition.getBounds().width() / 2f;
float halfHeight = composition.getBounds().height() / 2f;
float scaledHalfWidth = halfWidth * scale;
float scaledHalfHeight = halfHeight * scale;
canvas.translate(
getScale() * halfWidth - scaledHalfWidth,
getScale() * halfHeight - scaledHalfHeight);
canvas.scale(extraScale, extraScale, scaledHalfWidth, scaledHalfHeight);
}
matrix.reset();
matrix.preScale(scale, scale);
compositionLayer.draw(canvas, matrix, alpha);
L.endSection("Drawable#draw");
if (saveCount > 0) {
canvas.restoreToCount(saveCount);
}
}
// <editor-fold desc="animator">
@MainThread
@Override
public void start() {
playAnimation();
}
@MainThread
@Override
public void stop() {
endAnimation();
}
@Override
public boolean isRunning() {
return isAnimating();
}
/**
* Plays the animation from the beginning. If speed is < 0, it will start at the end
* and play towards the beginning
*/
@MainThread
public void playAnimation() {
if (compositionLayer == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
playAnimation();
}
});
return;
}
if (systemAnimationsEnabled || getRepeatCount() == 0) {
animator.playAnimation();
}
if (!systemAnimationsEnabled) {
setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
}
}
@MainThread
public void endAnimation() {
lazyCompositionTasks.clear();
animator.endAnimation();
}
/**
* Continues playing the animation from its current position. If speed < 0, it will play backwards
* from the current position.
*/
@MainThread
public void resumeAnimation() {
if (compositionLayer == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
resumeAnimation();
}
});
return;
}
if (systemAnimationsEnabled) {
animator.resumeAnimation();
}
}
/**
* Sets the minimum frame that the animation will start from when playing or looping.
*/
public void setMinFrame(final int minFrame) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setMinFrame(minFrame);
}
});
return;
}
animator.setMinFrame(minFrame);
}
/**
* Returns the minimum frame set by {@link #setMinFrame(int)} or {@link #setMinProgress(float)}
*/
public float getMinFrame() {
return animator.getMinFrame();
}
/**
* Sets the minimum progress that the animation will start from when playing or looping.
*/
public void setMinProgress(final float minProgress) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setMinProgress(minProgress);
}
});
return;
}
setMinFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), minProgress));
}
/**
* Sets the maximum frame that the animation will end at when playing or looping.
*/
public void setMaxFrame(final int maxFrame) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setMaxFrame(maxFrame);
}
});
return;
}
animator.setMaxFrame(maxFrame + 0.99f);
}
/**
* Returns the maximum frame set by {@link #setMaxFrame(int)} or {@link #setMaxProgress(float)}
*/
public float getMaxFrame() {
return animator.getMaxFrame();
}
/**
* Sets the maximum progress that the animation will end at when playing or looping.
*/
public void setMaxProgress(@FloatRange(from = 0f, to = 1f) final float maxProgress) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setMaxProgress(maxProgress);
}
});
return;
}
setMaxFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), maxProgress));
}
/**
* Sets the minimum frame to the start time of the specified marker.
* @throws IllegalArgumentException if the marker is not found.
*/
public void setMinFrame(final String markerName) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setMinFrame(markerName);
}
});
return;
}
Marker marker = composition.getMarker(markerName);
if (marker == null) {
throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
}
setMinFrame((int) marker.startFrame);
}
/**
* Sets the maximum frame to the start time + duration of the specified marker.
* @throws IllegalArgumentException if the marker is not found.
*/
public void setMaxFrame(final String markerName) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setMaxFrame(markerName);
}
});
return;
}
Marker marker = composition.getMarker(markerName);
if (marker == null) {
throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
}
setMaxFrame((int) (marker.startFrame + marker.durationFrames));
}
/**
* Sets the minimum and maximum frame to the start time and start time + duration
* of the specified marker.
* @throws IllegalArgumentException if the marker is not found.
*/
public void setMinAndMaxFrame(final String markerName) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setMinAndMaxFrame(markerName);
}
});
return;
}
Marker marker = composition.getMarker(markerName);
if (marker == null) {
throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
}
int startFrame = (int) marker.startFrame;
setMinAndMaxFrame(startFrame, startFrame + (int) marker.durationFrames);
}
/**
* @see #setMinFrame(int)
* @see #setMaxFrame(int)
*/
public void setMinAndMaxFrame(final int minFrame, final int maxFrame) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setMinAndMaxFrame(minFrame, maxFrame);
}
});
return;
}
// Adding 0.99 ensures that the maxFrame itself gets played.
animator.setMinAndMaxFrames(minFrame, maxFrame + 0.99f);
}
/**
* @see #setMinProgress(float)
* @see #setMaxProgress(float)
*/
public void setMinAndMaxProgress(
@FloatRange(from = 0f, to = 1f) final float minProgress,
@FloatRange(from = 0f, to = 1f) final float maxProgress) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setMinAndMaxProgress(minProgress, maxProgress);
}
});
return;
}
setMinAndMaxFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), minProgress),
(int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), maxProgress));
}
/**
* Reverses the current animation speed. This does NOT play the animation.
*
* @see #setSpeed(float)
* @see #playAnimation()
* @see #resumeAnimation()
*/
public void reverseAnimationSpeed() {
animator.reverseAnimationSpeed();
}
/**
* Sets the playback speed. If speed < 0, the animation will play backwards.
*/
public void setSpeed(float speed) {
animator.setSpeed(speed);
}
/**
* Returns the current playback speed. This will be < 0 if the animation is playing backwards.
*/
public float getSpeed() {
return animator.getSpeed();
}
public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
animator.addUpdateListener(updateListener);
}
public void removeAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
animator.removeUpdateListener(updateListener);
}
public void removeAllUpdateListeners() {
animator.removeAllUpdateListeners();
animator.addUpdateListener(progressUpdateListener);
}
public void addAnimatorListener(Animator.AnimatorListener listener) {
animator.addListener(listener);
}
public void removeAnimatorListener(Animator.AnimatorListener listener) {
animator.removeListener(listener);
}
public void removeAllAnimatorListeners() {
animator.removeAllListeners();
}
/**
* Sets the progress to the specified frame.
* If the composition isn't set yet, the progress will be set to the frame when
* it is.
*/
public void setFrame(final int frame) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setFrame(frame);
}
});
return;
}
animator.setFrame(frame);
}
/**
* Get the currently rendered frame.
*/
public int getFrame() {
return (int) animator.getFrame();
}
public void setProgress(@FloatRange(from = 0f, to = 1f) final float progress) {
if (composition == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
setProgress(progress);
}
});
return;
}
L.beginSection("Drawable#setProgress");
animator.setFrame(MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), progress));
L.endSection("Drawable#setProgress");
}
/**
* @see #setRepeatCount(int)
*/
@Deprecated
public void loop(boolean loop) {
animator.setRepeatCount(loop ? ValueAnimator.INFINITE : 0);
}
/**
* Defines what this animation should do when it reaches the end. This
* setting is applied only when the repeat count is either greater than
* 0 or {@link #INFINITE}. Defaults to {@link #RESTART}.
*
* @param mode {@link #RESTART} or {@link #REVERSE}
*/
public void setRepeatMode(@RepeatMode int mode) {
animator.setRepeatMode(mode);
}
/**
* Defines what this animation should do when it reaches the end.
*
* @return either one of {@link #REVERSE} or {@link #RESTART}
*/
@RepeatMode
public int getRepeatMode() {
return animator.getRepeatMode();
}
/**
* Sets how many times the animation should be repeated. If the repeat
* count is 0, the animation is never repeated. If the repeat count is
* greater than 0 or {@link #INFINITE}, the repeat mode will be taken
* into account. The repeat count is 0 by default.
*
* @param count the number of times the animation should be repeated
*/
public void setRepeatCount(int count) {
animator.setRepeatCount(count);
}
/**
* Defines how many times the animation should repeat. The default value
* is 0.
*
* @return the number of times the animation should repeat, or {@link #INFINITE}
*/
public int getRepeatCount() {
return animator.getRepeatCount();
}
public boolean isLooping() {
return animator.getRepeatCount() == ValueAnimator.INFINITE;
}
public boolean isAnimating() {
return animator.isRunning();
}
void setSystemAnimationsAreEnabled(Boolean areEnabled) {
systemAnimationsEnabled = areEnabled;
}
// </editor-fold>
/**
* Set the scale on the current composition. The only cost of this function is re-rendering the
* current frame so you may call it frequent to scale something up or down.
* <p>
* The smaller the animation is, the better the performance will be. You may find that scaling an
* animation down then rendering it in a larger ImageView and letting ImageView scale it back up
* with a scaleType such as centerInside will yield better performance with little perceivable
* quality loss.
* <p>
* You can also use a fixed view width/height in conjunction with the normal ImageView
* scaleTypes centerCrop and centerInside.
*/
public void setScale(float scale) {
this.scale = scale;
updateBounds();
}
/**
* Use this if you can't bundle images with your app. This may be useful if you download the
* animations from the network or have the images saved to an SD Card. In that case, Lottie
* will defer the loading of the bitmap to this delegate.
* <p>
* Be wary if you are using many images, however. Lottie is designed to work with vector shapes
* from After Effects. If your images look like they could be represented with vector shapes,
* see if it is possible to convert them to shape layers and re-export your animation. Check
* the documentation at http://airbnb.io/lottie for more information about importing shapes from
* Sketch or Illustrator to avoid this.
*/
public void setImageAssetDelegate(
@SuppressWarnings("NullableProblems") ImageAssetDelegate assetDelegate) {
this.imageAssetDelegate = assetDelegate;
if (imageAssetManager != null) {
imageAssetManager.setDelegate(assetDelegate);
}
}
/**
* Use this to manually set fonts.
*/
public void setFontAssetDelegate(
@SuppressWarnings("NullableProblems") FontAssetDelegate assetDelegate) {
this.fontAssetDelegate = assetDelegate;
if (fontAssetManager != null) {
fontAssetManager.setDelegate(assetDelegate);
}
}
public void setTextDelegate(@SuppressWarnings("NullableProblems") TextDelegate textDelegate) {
this.textDelegate = textDelegate;
}
@Nullable
public TextDelegate getTextDelegate() {
return textDelegate;
}
public boolean useTextGlyphs() {
return textDelegate == null && composition.getCharacters().size() > 0;
}
public float getScale() {
return scale;
}
public LottieComposition getComposition() {
return composition;
}
private void updateBounds() {
if (composition == null) {
return;
}
float scale = getScale();
setBounds(0, 0, (int) (composition.getBounds().width() * scale),
(int) (composition.getBounds().height() * scale));
}
public void cancelAnimation() {
lazyCompositionTasks.clear();
animator.cancel();
}
public void pauseAnimation() {
lazyCompositionTasks.clear();
animator.pauseAnimation();
}
@FloatRange(from = 0f, to = 1f)
public float getProgress() {
return animator.getAnimatedValueAbsolute();
}
@Override
public int getIntrinsicWidth() {
return composition == null ? -1 : (int) (composition.getBounds().width() * getScale());
}
@Override
public int getIntrinsicHeight() {
return composition == null ? -1 : (int) (composition.getBounds().height() * getScale());
}
/**
* Takes a {@link KeyPath}, potentially with wildcards or globstars and resolve it to a list of
* zero or more actual {@link KeyPath Keypaths} that exist in the current animation.
* <p>
* If you want to set value callbacks for any of these values, it is recommend to use the
* returned {@link KeyPath} objects because they will be internally resolved to their content
* and won't trigger a tree walk of the animation contents when applied.
*/
public List<KeyPath> resolveKeyPath(KeyPath keyPath) {
if (compositionLayer == null) {
Logger.warning("Cannot resolve KeyPath. Composition is not set yet.");
return Collections.emptyList();
}
List<KeyPath> keyPaths = new ArrayList<>();
compositionLayer.resolveKeyPath(keyPath, 0, keyPaths, new KeyPath());
return keyPaths;
}
/**
* Add an property callback for the specified {@link KeyPath}. This {@link KeyPath} can resolve
* to multiple contents. In that case, the callbacks's value will apply to all of them.
* <p>
* Internally, this will check if the {@link KeyPath} has already been resolved with
* {@link #resolveKeyPath(KeyPath)} and will resolve it if it hasn't.
*/
public <T> void addValueCallback(
final KeyPath keyPath, final T property, final LottieValueCallback<T> callback) {
if (compositionLayer == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
addValueCallback(keyPath, property, callback);
}
});
return;
}
boolean invalidate;
if (keyPath.getResolvedElement() != null) {
keyPath.getResolvedElement().addValueCallback(property, callback);
invalidate = true;
} else {
List<KeyPath> elements = resolveKeyPath(keyPath);
for (int i = 0; i < elements.size(); i++) {
//noinspection ConstantConditions
elements.get(i).getResolvedElement().addValueCallback(property, callback);
}
invalidate = !elements.isEmpty();
}
if (invalidate) {
invalidateSelf();
if (property == LottieProperty.TIME_REMAP) {
// Time remapping values are read in setProgress. In order for the new value
// to apply, we have to re-set the progress with the current progress so that the
// time remapping can be reapplied.
setProgress(getProgress());
}
}
}
/**
* Overload of {@link #addValueCallback(KeyPath, Object, LottieValueCallback)} that takes an interface. This allows you to use a single abstract
* method code block in Kotlin such as:
* drawable.addValueCallback(yourKeyPath, LottieProperty.COLOR) { yourColor }
*/
public <T> void addValueCallback(KeyPath keyPath, T property,
final SimpleLottieValueCallback<T> callback) {
addValueCallback(keyPath, property, new LottieValueCallback<T>() {
@Override
public T getValue(LottieFrameInfo<T> frameInfo) {
return callback.getValue(frameInfo);
}
});
}
/**
* Allows you to modify or clear a bitmap that was loaded for an image either automatically
* through {@link #setImagesAssetsFolder(String)} or with an {@link ImageAssetDelegate}.
*
* @return the previous Bitmap or null.
*/
@Nullable
public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) {
ImageAssetManager bm = getImageAssetManager();
if (bm == null) {
Logger.warning("Cannot update bitmap. Most likely the drawable is not added to a View " +
"which prevents Lottie from getting a Context.");
return null;
}
Bitmap ret = bm.updateBitmap(id, bitmap);
invalidateSelf();
return ret;
}
@Nullable
public Bitmap getImageAsset(String id) {
ImageAssetManager bm = getImageAssetManager();
if (bm != null) {
return bm.bitmapForId(id);
}
return null;
}
private ImageAssetManager getImageAssetManager() {
if (getCallback() == null) {
// We can't get a bitmap since we can't get a Context from the callback.
return null;
}
if (imageAssetManager != null && !imageAssetManager.hasSameContext(getContext())) {
imageAssetManager = null;
}
if (imageAssetManager == null) {
imageAssetManager = new ImageAssetManager(getCallback(),
imageAssetsFolder, imageAssetDelegate, composition.getImages());
}
return imageAssetManager;
}
@Nullable
public Typeface getTypeface(String fontFamily, String style) {
FontAssetManager assetManager = getFontAssetManager();
if (assetManager != null) {
return assetManager.getTypeface(fontFamily, style);
}
return null;
}
private FontAssetManager getFontAssetManager() {
if (getCallback() == null) {
// We can't get a bitmap since we can't get a Context from the callback.
return null;
}
if (fontAssetManager == null) {
fontAssetManager = new FontAssetManager(getCallback(), fontAssetDelegate);
}
return fontAssetManager;
}
@Nullable
private Context getContext() {
Callback callback = getCallback();
if (callback == null) {
return null;
}
if (callback instanceof View) {
return ((View) callback).getContext();
}
return null;
}
/**
* These Drawable.Callback methods proxy the calls so that this is the drawable that is
* actually invalidated, not a child one which will not pass the view's validateDrawable check.
*/
@Override
public void invalidateDrawable(@NonNull Drawable who) {
Callback callback = getCallback();
if (callback == null) {
return;
}
callback.invalidateDrawable(this);
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
Callback callback = getCallback();
if (callback == null) {
return;
}
callback.scheduleDrawable(this, what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
Callback callback = getCallback();
if (callback == null) {
return;
}
callback.unscheduleDrawable(this, what);
}
/**
* If the composition is larger than the canvas, we have to use a different method to scale it up.
* See the comments in {@link #draw(Canvas)} for more info.
*/
private float getMaxScale(@NonNull Canvas canvas) {
float maxScaleX = canvas.getWidth() / (float) composition.getBounds().width();
float maxScaleY = canvas.getHeight() / (float) composition.getBounds().height();
return Math.min(maxScaleX, maxScaleY);
}
private static class ColorFilterData {
final String layerName;
@Nullable
final String contentName;
@Nullable
final ColorFilter colorFilter;
ColorFilterData(@Nullable String layerName, @Nullable String contentName,
@Nullable ColorFilter colorFilter) {
this.layerName = layerName;
this.contentName = contentName;
this.colorFilter = colorFilter;
}
@Override
public int hashCode() {
int hashCode = 17;
if (layerName != null) {
hashCode = hashCode * 31 * layerName.hashCode();
}
if (contentName != null) {
hashCode = hashCode * 31 * contentName.hashCode();
}
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ColorFilterData)) {
return false;
}
final ColorFilterData other = (ColorFilterData) obj;
return hashCode() == other.hashCode() && colorFilter == other.colorFilter;
}
}
}