blob: 5882ef20134ce4cdb574aa08693e449ebc514707 [file] [log] [blame]
package com.airbnb.lottie;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
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.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.View;
import android.widget.ImageView;
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 androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import com.airbnb.lottie.animation.LPaint;
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.Iterator;
import java.util.List;
/**
* 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"})
public class LottieDrawable extends Drawable implements Drawable.Callback, Animatable {
private interface LazyCompositionTask {
void run(LottieComposition composition);
}
private LottieComposition composition;
private final LottieValueAnimator animator = new LottieValueAnimator();
private float scale = 1f;
//Call animationsEnabled() instead of using these fields directly
private boolean systemAnimationsEnabled = true;
private boolean ignoreSystemAnimationsDisabled = false;
private boolean safeMode = false;
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());
}
}
};
/**
* ImageAssetManager created automatically by Lottie for views.
*/
@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;
private boolean outlineMasksAndMattes;
private boolean isApplyingOpacityToLayersEnabled;
private final Matrix renderingMatrix = new Matrix();
private boolean softwareRenderingEnabled = false;
private Bitmap softwareRenderingBitmap;
private final LPaint softwareRenderingClearPaint = new LPaint();
private final Canvas softwareRenderingCanvas = new Canvas();
private Paint softwareRenderingPaint;
private Rect softwareRenderingSrcBoundsRect;
private Rect softwareRenderingDstBoundsRect;
private RectF softwareRenderingDstBoundsRectF;
private RectF softwareRenderingTransformedBounds;
private Matrix softwareRenderingOriginalCanvasMatrix;
private Matrix softwareRenderingOriginalCanvasMatrixInverse;
/**
* 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>
* 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 = true;
clearComposition();
this.composition = composition;
buildCompositionLayer();
animator.setComposition(composition);
setProgress(animator.getAnimatedFraction());
setScale(scale);
// 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();
// The task should never be null but it appears to happen in rare cases. Maybe it's an oem-specific or ART bug.
// https://github.com/airbnb/lottie-android/issues/1702
if (t != null) {
t.run(composition);
}
it.remove();
}
lazyCompositionTasks.clear();
composition.setPerformanceTrackingEnabled(performanceTrackingEnabled);
// Ensure that ImageView updates the drawable width/height so it can
// properly calculate its drawable matrix.
Callback callback = getCallback();
if (callback instanceof ImageView) {
((ImageView) callback).setImageDrawable(null);
((ImageView) callback).setImageDrawable(this);
}
return true;
}
/**
* When set to true, Lottie will first render your animation to a bitmap and then draw the bitmap
* onto the original canvas.
*
* @see LottieAnimationView#setRenderMode(RenderMode)
*/
public void useSoftwareRendering(boolean softwareRenderingEnabled) {
if (this.softwareRenderingEnabled == softwareRenderingEnabled) {
return;
}
this.softwareRenderingEnabled = softwareRenderingEnabled;
invalidateSelf();
}
public void setPerformanceTrackingEnabled(boolean enabled) {
performanceTrackingEnabled = enabled;
if (composition != null) {
composition.setPerformanceTrackingEnabled(enabled);
}
}
/**
* Enable this to debug slow animations by outlining masks and mattes. The performance overhead of the masks and mattes will
* be proportional to the surface area of all of the masks/mattes combined.
* <p>
* DO NOT leave this enabled in production.
*/
public void setOutlineMasksAndMattes(boolean outline) {
if (outlineMasksAndMattes == outline) {
return;
}
outlineMasksAndMattes = outline;
if (compositionLayer != null) {
compositionLayer.setOutlineMasksAndMattes(outline);
}
}
@Nullable
public PerformanceTracker getPerformanceTracker() {
if (composition != null) {
return composition.getPerformanceTracker();
}
return null;
}
/**
* Sets whether to apply opacity to the each layer instead of shape.
* <p>
* Opacity is normally applied directly to a shape. In cases where translucent shapes overlap, applying opacity to a layer will be more accurate
* at the expense of performance.
* <p>
* The default value is false.
* <p>
* Note: This process is very expensive. The performance impact will be reduced when hardware acceleration is enabled.
*
* @see android.view.View#setLayerType(int, android.graphics.Paint)
* @see LottieAnimationView#setRenderMode(RenderMode)
*/
public void setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersEnabled) {
this.isApplyingOpacityToLayersEnabled = isApplyingOpacityToLayersEnabled;
}
/**
* This API no longer has any effect.
*/
@Deprecated
public void disableExtraScaleModeInFitXY() {
}
public boolean isApplyingOpacityToLayersEnabled() {
return isApplyingOpacityToLayersEnabled;
}
private void buildCompositionLayer() {
LottieComposition composition = this.composition;
if (composition == null) {
return;
}
compositionLayer = new CompositionLayer(
this, LayerParser.parse(composition), composition.getLayers(), composition);
if (outlineMasksAndMattes) {
compositionLayer.setOutlineMasksAndMattes(true);
}
}
public void clearComposition() {
if (animator.isRunning()) {
animator.cancel();
}
composition = null;
compositionLayer = null;
imageAssetManager = null;
animator.clearComposition();
invalidateSelf();
}
/**
* If you are experiencing a device specific crash that happens during drawing, you can set this to true
* for those devices. If set to true, draw will be wrapped with a try/catch which will cause Lottie to
* render an empty frame rather than crash your app.
* <p>
* Ideally, you will never need this and the vast majority of apps and animations won't. However, you may use
* this for very specific cases if absolutely necessary.
*/
public void setSafeMode(boolean safeMode) {
this.safeMode = safeMode;
}
@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) {
L.beginSection("Drawable#draw");
if (safeMode) {
try {
drawInternal(canvas);
} catch (Throwable e) {
Logger.error("Lottie crashed in draw!", e);
}
} else {
drawInternal(canvas);
}
isDirty = false;
L.endSection("Drawable#draw");
}
private void drawInternal(@NonNull Canvas canvas) {
if (!boundsMatchesCompositionAspectRatio()) {
drawWithNewAspectRatio(canvas);
} else {
drawWithOriginalAspectRatio(canvas);
}
}
private boolean boundsMatchesCompositionAspectRatio() {
LottieComposition composition = this.composition;
if (composition == null || getBounds().isEmpty()) {
return true;
}
return aspectRatio(getBounds()) == aspectRatio(composition.getBounds());
}
private float aspectRatio(Rect rect) {
return rect.width() / (float) rect.height();
}
// <editor-fold desc="animator">
@MainThread
@Override
public void start() {
// Don't auto play when in edit mode.
Callback callback = getCallback();
if (callback instanceof View && !((View) callback).isInEditMode()) {
playAnimation();
}
}
@MainThread
@Override
public void stop() {
endAnimation();
}
@Override
public boolean isRunning() {
return isAnimating();
}
/**
* Plays the animation from the beginning. If speed is {@literal <} 0, it will start at the end
* and play towards the beginning
*/
@MainThread
public void playAnimation() {
if (compositionLayer == null) {
lazyCompositionTasks.add(c -> playAnimation());
return;
}
if (animationsEnabled() || getRepeatCount() == 0) {
animator.playAnimation();
}
if (!animationsEnabled()) {
setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
animator.endAnimation();
}
}
@MainThread
public void endAnimation() {
lazyCompositionTasks.clear();
animator.endAnimation();
}
/**
* Continues playing the animation from its current position. If speed {@literal <} 0, it will play backwards
* from the current position.
*/
@MainThread
public void resumeAnimation() {
if (compositionLayer == null) {
lazyCompositionTasks.add(c -> resumeAnimation());
return;
}
if (animationsEnabled() || getRepeatCount() == 0) {
animator.resumeAnimation();
}
if (!animationsEnabled()) {
setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
animator.endAnimation();
}
}
/**
* 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(c -> 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(c -> 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.
* <p>
* The value will be clamped to the composition bounds. For example, setting Integer.MAX_VALUE would result in the same
* thing as composition.endFrame.
*/
public void setMaxFrame(final int maxFrame) {
if (composition == null) {
lazyCompositionTasks.add(c -> 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(c -> 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(c -> 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(c -> 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(c -> 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);
}
/**
* Sets the minimum and maximum frame to the start marker start and the maximum frame to the end marker start.
* playEndMarkerStartFrame determines whether or not to play the frame that the end marker is on. If the end marker
* represents the end of the section that you want, it should be true. If the marker represents the beginning of the
* next section, it should be false.
*
* @throws IllegalArgumentException if either marker is not found.
*/
public void setMinAndMaxFrame(final String startMarkerName, final String endMarkerName, final boolean playEndMarkerStartFrame) {
if (composition == null) {
lazyCompositionTasks.add(c -> setMinAndMaxFrame(startMarkerName, endMarkerName, playEndMarkerStartFrame));
return;
}
Marker startMarker = composition.getMarker(startMarkerName);
if (startMarker == null) {
throw new IllegalArgumentException("Cannot find marker with name " + startMarkerName + ".");
}
int startFrame = (int) startMarker.startFrame;
final Marker endMarker = composition.getMarker(endMarkerName);
if (endMarker == null) {
throw new IllegalArgumentException("Cannot find marker with name " + endMarkerName + ".");
}
int endFrame = (int) (endMarker.startFrame + (playEndMarkerStartFrame ? 1f : 0f));
setMinAndMaxFrame(startFrame, endFrame);
}
/**
* @see #setMinFrame(int)
* @see #setMaxFrame(int)
*/
public void setMinAndMaxFrame(final int minFrame, final int maxFrame) {
if (composition == null) {
lazyCompositionTasks.add(c -> 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(c -> 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 {@literal <} 0, the animation will play backwards.
*/
public void setSpeed(float speed) {
animator.setSpeed(speed);
}
/**
* Returns the current playback speed. This will be {@literal <} 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();
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void addAnimatorPauseListener(Animator.AnimatorPauseListener listener) {
animator.addPauseListener(listener);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void removeAnimatorPauseListener(Animator.AnimatorPauseListener listener) {
animator.removePauseListener(listener);
}
/**
* 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(c -> 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(c -> setProgress(progress));
return;
}
L.beginSection("Drawable#setProgress");
animator.setFrame(composition.getFrameForProgress(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}
*/
@SuppressLint("WrongConstant")
@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() {
// On some versions of Android, this is called from the LottieAnimationView constructor, before animator was created.
// https://github.com/airbnb/lottie-android/issues/1430
//noinspection ConstantConditions
if (animator == null) {
return false;
}
return animator.isRunning();
}
private boolean animationsEnabled() {
return systemAnimationsEnabled || ignoreSystemAnimationsDisabled;
}
void setSystemAnimationsAreEnabled(Boolean areEnabled) {
systemAnimationsEnabled = areEnabled;
}
// </editor-fold>
/**
* Allows ignoring system animations settings, therefore allowing animations to run even if they are disabled.
* <p>
* Defaults to false.
*
* @param ignore if true animations will run even when they are disabled in the system settings.
*/
public void setIgnoreDisabledSystemAnimations(boolean ignore) {
ignoreSystemAnimationsDisabled = ignore;
}
/**
* 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;
}
/**
* 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(ImageAssetDelegate assetDelegate) {
this.imageAssetDelegate = assetDelegate;
if (imageAssetManager != null) {
imageAssetManager.setDelegate(assetDelegate);
}
}
/**
* Use this to manually set fonts.
*/
public void setFontAssetDelegate(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;
}
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 callback'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, @Nullable final LottieValueCallback<T> callback) {
if (compositionLayer == null) {
lazyCompositionTasks.add(c -> addValueCallback(keyPath, property, callback));
return;
}
boolean invalidate;
if (keyPath == KeyPath.COMPOSITION) {
compositionLayer.addValueCallback(property, callback);
invalidate = true;
} else 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);
}
LottieImageAsset imageAsset = composition == null ? null : composition.getImages().get(id);
if (imageAsset != null) {
return imageAsset.getBitmap();
}
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);
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void draw(Canvas canvas, Matrix matrix) {
CompositionLayer compositionLayer = this.compositionLayer;
LottieComposition composition = this.composition;
if (compositionLayer == null || composition == null) {
return;
}
if (softwareRenderingEnabled) {
renderAndDrawAsBitmap(canvas, compositionLayer, matrix);
} else {
compositionLayer.draw(canvas, matrix, alpha);
}
}
private void drawWithNewAspectRatio(Canvas canvas) {
CompositionLayer compositionLayer = this.compositionLayer;
LottieComposition composition = this.composition;
if (compositionLayer == null || composition == null) {
return;
}
if (softwareRenderingEnabled) {
renderAndDrawAsBitmap(canvas, compositionLayer, null);
} else {
Rect bounds = getBounds();
// In fitXY mode, the scale doesn't take effect.
float scaleX = bounds.width() / (float) composition.getBounds().width();
float scaleY = bounds.height() / (float) composition.getBounds().height();
renderingMatrix.reset();
renderingMatrix.preScale(scaleX, scaleY);
compositionLayer.draw(canvas, renderingMatrix, alpha);
}
}
private void drawWithOriginalAspectRatio(Canvas canvas) {
CompositionLayer compositionLayer = this.compositionLayer;
LottieComposition composition = this.composition;
float scale = this.scale;
if (compositionLayer == null || composition == null) {
return;
}
if (softwareRenderingEnabled) {
renderAndDrawAsBitmap(canvas, compositionLayer, null);
} else {
renderingMatrix.reset();
renderingMatrix.preScale(scale, scale);
compositionLayer.draw(canvas, renderingMatrix, alpha);
}
}
/**
* This is the software rendering pipeline. This draws the animation to an internally managed bitmap
* and then draws the bitmap to the original canvas.
*
* @see LottieDrawable#useSoftwareRendering(boolean)
* @see LottieAnimationView#setRenderMode(RenderMode)
*/
private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compositionLayer, @Nullable Matrix parentMatrix) {
ensureSoftwareRenderingObjectsInitialized();
//noinspection deprecation
originalCanvas.getMatrix(softwareRenderingOriginalCanvasMatrix);
softwareRenderingOriginalCanvasMatrix.invert(softwareRenderingOriginalCanvasMatrixInverse);
renderingMatrix.set(softwareRenderingOriginalCanvasMatrix);
if (parentMatrix != null) {
renderingMatrix.postConcat(parentMatrix);
}
// Determine what bounds the animation will render to after taking into account the canvas and parent matrix.
softwareRenderingTransformedBounds.set(0f, 0f, getIntrinsicWidth(), getIntrinsicHeight());
renderingMatrix.mapRect(softwareRenderingTransformedBounds);
// We only need to render the portion of the animation that intersects with the canvas's bounds.
softwareRenderingTransformedBounds.intersect(0f, 0f, originalCanvas.getWidth(), originalCanvas.getHeight());
int renderWidth = (int) Math.ceil(softwareRenderingTransformedBounds.width());
int renderHeight = (int) Math.ceil(softwareRenderingTransformedBounds.height());
if (renderWidth == 0 || renderHeight == 0) {
return;
}
ensureSoftwareRenderingBitmap(renderWidth, renderHeight);
softwareRenderingSrcBoundsRect.set(0, 0, renderWidth, renderHeight);
if (isDirty) {
softwareRenderingBitmap.eraseColor(0);
renderingMatrix.preScale(scale, scale);
// The bounds are usually intrinsicWidth x intrinsicHeight. If they are different, an external source is scaling this drawable.
// This is how ImageView.ScaleType.FIT_XY works.
renderingMatrix.preScale(getBounds().width() / (float) getIntrinsicWidth(), getBounds().height() / (float) getIntrinsicHeight());
// We want to render the smallest bitmap possible. If the animation doesn't start at the top left, we translate the canvas and shrink the
// bitmap to avoid allocating and copying the empty space on the left and top. renderWidth and renderHeight take this into account.
renderingMatrix.postTranslate(-softwareRenderingTransformedBounds.left, -softwareRenderingTransformedBounds.top);
compositionLayer.draw(softwareRenderingCanvas, renderingMatrix, alpha);
// Calculate the dst bounds.
// We need to map the rendered coordinates back to the canvas's coordinates. To do so, we need to invert the transform
// of the original canvas.
// Take the bounds of the rendered animation and map them to the canvas's coordinates.
// This is similar to the src rect above but the src bound may have a left and top offset.
softwareRenderingOriginalCanvasMatrixInverse.mapRect(softwareRenderingDstBoundsRectF, softwareRenderingTransformedBounds);
convertRect(softwareRenderingDstBoundsRectF, softwareRenderingDstBoundsRect);
}
originalCanvas.drawBitmap(softwareRenderingBitmap, softwareRenderingSrcBoundsRect, softwareRenderingDstBoundsRect, softwareRenderingPaint);
}
private void ensureSoftwareRenderingObjectsInitialized() {
if (softwareRenderingPaint != null) {
return;
}
softwareRenderingPaint = new LPaint();
softwareRenderingClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
softwareRenderingClearPaint.setColor(Color.BLACK);
softwareRenderingSrcBoundsRect = new Rect();
softwareRenderingDstBoundsRect = new Rect();
softwareRenderingDstBoundsRectF = new RectF();
softwareRenderingTransformedBounds = new RectF();
softwareRenderingOriginalCanvasMatrix = new Matrix();
softwareRenderingOriginalCanvasMatrixInverse = new Matrix();
}
private void ensureSoftwareRenderingBitmap(int renderWidth, int renderHeight) {
if (softwareRenderingBitmap == null ||
softwareRenderingBitmap.getWidth() < renderWidth ||
softwareRenderingBitmap.getHeight() < renderHeight) {
softwareRenderingBitmap = Bitmap.createBitmap(renderWidth, renderHeight, Bitmap.Config.ARGB_8888);
softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
isDirty = true;
} else if (softwareRenderingBitmap.getWidth() > renderWidth || softwareRenderingBitmap.getHeight() > renderHeight) {
softwareRenderingBitmap = Bitmap.createBitmap(softwareRenderingBitmap, 0, 0, renderWidth, renderHeight);
softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
isDirty = true;
}
}
/**
* Convert a RectF to a Rect
*/
private void convertRect(RectF src, Rect dst) {
dst.set(
(int) Math.floor(src.left),
(int) Math.floor(src.top),
(int) Math.ceil(src.right),
(int) Math.ceil(src.bottom)
);
}
}