blob: e493288432e1814c5016d182913fd2c1a5e59ef4 [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.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
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.view.ViewGroup;
import android.view.ViewParent;
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.Font;
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.LottieThreadFactory;
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;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 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);
}
/**
* Internal record keeping of the desired play state when {@link #isVisible()} transitions to or is false.
* <p>
* If the animation was playing when it becomes invisible or play/pause is called on it while it is invisible, it will
* store the state and then take the appropriate action when the drawable becomes visible again.
*/
private enum OnVisibleAction {
NONE,
PLAY,
RESUME,
}
private LottieComposition composition;
private final LottieValueAnimator animator = new LottieValueAnimator();
// Call animationsEnabled() instead of using these fields directly.
private boolean systemAnimationsEnabled = true;
private boolean ignoreSystemAnimationsDisabled = false;
private boolean safeMode = false;
private OnVisibleAction onVisibleAction = OnVisibleAction.NONE;
private final ArrayList<LazyCompositionTask> lazyCompositionTasks = new ArrayList<>();
/**
* ImageAssetManager created automatically by Lottie for views.
*/
@Nullable
private ImageAssetManager imageAssetManager;
@Nullable
private String imageAssetsFolder;
@Nullable
private ImageAssetDelegate imageAssetDelegate;
@Nullable
private FontAssetManager fontAssetManager;
@Nullable
private Map<String, Typeface> fontMap;
/**
* Will be set if manually overridden by {@link #setDefaultFontFileExtension(String)}.
* This must be stored as a field in case it is set before the font asset delegate
* has been created.
*/
@Nullable String defaultFontFileExtension;
@Nullable
FontAssetDelegate fontAssetDelegate;
@Nullable
TextDelegate textDelegate;
private boolean enableMergePaths;
private boolean maintainOriginalImageBounds = false;
private boolean clipToCompositionBounds = true;
@Nullable
private CompositionLayer compositionLayer;
private int alpha = 255;
private boolean performanceTrackingEnabled;
private boolean outlineMasksAndMattes;
private boolean isApplyingOpacityToLayersEnabled;
private RenderMode renderMode = RenderMode.AUTOMATIC;
/**
* The actual render mode derived from {@link #renderMode}.
*/
private boolean useSoftwareRendering = false;
private final Matrix renderingMatrix = new Matrix();
private Bitmap softwareRenderingBitmap;
private Canvas softwareRenderingCanvas;
private Rect canvasClipBounds;
private RectF canvasClipBoundsRectF;
private Paint softwareRenderingPaint;
private Rect softwareRenderingSrcBoundsRect;
private Rect softwareRenderingDstBoundsRect;
private RectF softwareRenderingDstBoundsRectF;
private RectF softwareRenderingTransformedBounds;
private Matrix softwareRenderingOriginalCanvasMatrix;
private Matrix softwareRenderingOriginalCanvasMatrixInverse;
private AsyncUpdates asyncUpdates = AsyncUpdates.AUTOMATIC;
private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = animation -> {
if (getAsyncUpdatesEnabled()) {
// Render a new frame.
// If draw is called while lastDrawnProgress is still recent enough, it will
// draw straight away and then enqueue a background setProgress immediately after draw
// finishes.
invalidateSelf();
} else if (compositionLayer != null) {
compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
}
};
/**
* Ensures that setProgress and draw will never happen at the same time on different threads.
* If that were to happen, parts of the animation may be on one frame while other parts would
* be on another.
*/
private final Semaphore setProgressDrawLock = new Semaphore(1);
/**
* The executor that {@link AsyncUpdates} will be run on.
* <p/>
* Defaults to a core size of 0 so that when no animations are playing, there will be no
* idle cores consuming resources.
* <p/>
* Allows up to two active threads so that if there are many animations, they can all work in parallel.
* Two was arbitrarily chosen but should be sufficient for most uses cases. In the case of a single
* animation, this should never exceed one.
* <p/>
* Each thread will timeout after 35ms which gives it enough time to persist for one frame, one dropped frame
* and a few extra ms just in case.
*/
private static final Executor setProgressExecutor = new ThreadPoolExecutor(0, 2, 35, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(), new LottieThreadFactory());
private final Runnable updateProgressRunnable = () -> {
CompositionLayer compositionLayer = this.compositionLayer;
if (compositionLayer == null) {
return;
}
try {
setProgressDrawLock.acquire();
compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
} catch (InterruptedException e) {
// Do nothing.
} finally {
setProgressDrawLock.release();
}
};
private float lastDrawnProgress = -Float.MAX_VALUE;
private static final float MAX_DELTA_MS_ASYNC_SET_PROGRESS = 3 / 60f * 1000;
/**
* 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;
}
/**
* Sets whether or not Lottie should clip to the original animation composition bounds.
* <p>
* Defaults to true.
*/
public void setClipToCompositionBounds(boolean clipToCompositionBounds) {
if (clipToCompositionBounds != this.clipToCompositionBounds) {
this.clipToCompositionBounds = clipToCompositionBounds;
CompositionLayer compositionLayer = this.compositionLayer;
if (compositionLayer != null) {
compositionLayer.setClipToCompositionBounds(clipToCompositionBounds);
}
invalidateSelf();
}
}
/**
* Gets whether or not Lottie should clip to the original animation composition bounds.
* <p>
* Defaults to true.
*/
public boolean getClipToCompositionBounds() {
return clipToCompositionBounds;
}
/**
* 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;
}
/**
* When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
* When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
* <p>
* Defaults to false.
*/
public void setMaintainOriginalImageBounds(boolean maintainOriginalImageBounds) {
this.maintainOriginalImageBounds = maintainOriginalImageBounds;
}
/**
* When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
* When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
* <p>
* Defaults to false.
*/
public boolean getMaintainOriginalImageBounds() {
return maintainOriginalImageBounds;
}
/**
* 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());
// 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);
computeRenderMode();
// 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;
}
/**
* Call this to set whether or not to render with hardware or software acceleration.
* Lottie defaults to Automatic which will use hardware acceleration unless:
* 1) There are dash paths and the device is pre-Pie.
* 2) There are more than 4 masks and mattes and the device is pre-Pie.
* Hardware acceleration is generally faster for those devices unless
* there are many large mattes and masks in which case there is a lot
* of GPU uploadTexture thrashing which makes it much slower.
* <p>
* In most cases, hardware rendering will be faster, even if you have mattes and masks.
* However, if you have multiple mattes and masks (especially large ones), you
* should test both render modes. You should also test on pre-Pie and Pie+ devices
* because the underlying rendering engine changed significantly.
*
* @see <a href="https://developer.android.com/guide/topics/graphics/hardware-accel#unsupported">Android Hardware Acceleration</a>
*/
public void setRenderMode(RenderMode renderMode) {
this.renderMode = renderMode;
computeRenderMode();
}
/**
* Returns the current value of {@link AsyncUpdates}. Refer to the docs for {@link AsyncUpdates} for more info.
*/
public AsyncUpdates getAsyncUpdates() {
return asyncUpdates;
}
/**
* Similar to {@link #getAsyncUpdates()} except it returns the actual
* boolean value for whether async updates are enabled or not.
* This is useful when the mode is automatic and you want to know
* whether automatic is defaulting to enabled or not.
*/
public boolean getAsyncUpdatesEnabled() {
return asyncUpdates == AsyncUpdates.ENABLED;
}
/**
* **Note: this API is experimental and may changed.**
* <p/>
* Sets the current value for {@link AsyncUpdates}. Refer to the docs for {@link AsyncUpdates} for more info.
*/
public void setAsyncUpdates(AsyncUpdates asyncUpdates) {
this.asyncUpdates = asyncUpdates;
}
/**
* Returns the actual render mode being used. It will always be {@link RenderMode#HARDWARE} or {@link RenderMode#SOFTWARE}.
* When the render mode is set to AUTOMATIC, the value will be derived from {@link RenderMode#useSoftwareRendering(int, boolean, int)}.
*/
public RenderMode getRenderMode() {
return useSoftwareRendering ? RenderMode.SOFTWARE : RenderMode.HARDWARE;
}
private void computeRenderMode() {
LottieComposition composition = this.composition;
if (composition == null) {
return;
}
useSoftwareRendering = renderMode.useSoftwareRendering(
Build.VERSION.SDK_INT, composition.hasDashPattern(), composition.getMaskAndMatteCount());
}
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);
}
compositionLayer.setClipToCompositionBounds(clipToCompositionBounds);
}
public void clearComposition() {
if (animator.isRunning()) {
animator.cancel();
if (!isVisible()) {
onVisibleAction = OnVisibleAction.NONE;
}
}
composition = null;
compositionLayer = null;
imageAssetManager = null;
lastDrawnProgress = -Float.MAX_VALUE;
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;
}
/**
* Helper for the async execution path to potentially call setProgress
* before drawing if the current progress has drifted sufficiently far
* from the last set progress.
*
* @see AsyncUpdates
* @see #setAsyncUpdates(AsyncUpdates)
*/
private boolean shouldSetProgressBeforeDrawing() {
LottieComposition composition = this.composition;
if (composition == null) {
return false;
}
float lastDrawnProgress = this.lastDrawnProgress;
float currentProgress = animator.getAnimatedValueAbsolute();
this.lastDrawnProgress = currentProgress;
float duration = composition.getDuration();
float deltaProgress = Math.abs(currentProgress - lastDrawnProgress);
float deltaMs = deltaProgress * duration;
return deltaMs >= MAX_DELTA_MS_ASYNC_SET_PROGRESS;
}
@Override
public void draw(@NonNull Canvas canvas) {
CompositionLayer compositionLayer = this.compositionLayer;
if (compositionLayer == null) {
return;
}
boolean asyncUpdatesEnabled = getAsyncUpdatesEnabled();
try {
if (asyncUpdatesEnabled) {
setProgressDrawLock.acquire();
}
L.beginSection("Drawable#draw");
if (asyncUpdatesEnabled && shouldSetProgressBeforeDrawing()) {
setProgress(animator.getAnimatedValueAbsolute());
}
if (safeMode) {
try {
if (useSoftwareRendering) {
renderAndDrawAsBitmap(canvas, compositionLayer);
} else {
drawDirectlyToCanvas(canvas);
}
} catch (Throwable e) {
Logger.error("Lottie crashed in draw!", e);
}
} else {
if (useSoftwareRendering) {
renderAndDrawAsBitmap(canvas, compositionLayer);
} else {
drawDirectlyToCanvas(canvas);
}
}
isDirty = false;
} catch (InterruptedException e) {
// Do nothing.
} finally {
L.endSection("Drawable#draw");
if (asyncUpdatesEnabled) {
setProgressDrawLock.release();
if (compositionLayer.getProgress() != animator.getAnimatedValueAbsolute()) {
setProgressExecutor.execute(updateProgressRunnable);
}
}
}
}
/**
* To be used by lottie-compose only.
*/
@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;
}
boolean asyncUpdatesEnabled = getAsyncUpdatesEnabled();
try {
if (asyncUpdatesEnabled) {
setProgressDrawLock.acquire();
if (shouldSetProgressBeforeDrawing()) {
setProgress(animator.getAnimatedValueAbsolute());
}
}
if (useSoftwareRendering) {
canvas.save();
canvas.concat(matrix);
renderAndDrawAsBitmap(canvas, compositionLayer);
canvas.restore();
} else {
compositionLayer.draw(canvas, matrix, alpha);
}
isDirty = false;
} catch (InterruptedException e) {
// Do nothing.
} finally {
if (asyncUpdatesEnabled) {
setProgressDrawLock.release();
if (compositionLayer.getProgress() != animator.getAnimatedValueAbsolute()) {
setProgressExecutor.execute(updateProgressRunnable);
}
}
}
}
// <editor-fold desc="animator">
@MainThread
@Override
public void start() {
Callback callback = getCallback();
if (callback instanceof View && ((View) callback).isInEditMode()) {
// Don't auto play when in edit mode.
return;
}
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;
}
computeRenderMode();
if (animationsEnabled() || getRepeatCount() == 0) {
if (isVisible()) {
animator.playAnimation();
onVisibleAction = OnVisibleAction.NONE;
} else {
onVisibleAction = OnVisibleAction.PLAY;
}
}
if (!animationsEnabled()) {
setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
animator.endAnimation();
if (!isVisible()) {
onVisibleAction = OnVisibleAction.NONE;
}
}
}
@MainThread
public void endAnimation() {
lazyCompositionTasks.clear();
animator.endAnimation();
if (!isVisible()) {
onVisibleAction = OnVisibleAction.NONE;
}
}
/**
* 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;
}
computeRenderMode();
if (animationsEnabled() || getRepeatCount() == 0) {
if (isVisible()) {
animator.resumeAnimation();
onVisibleAction = OnVisibleAction.NONE;
} else {
onVisibleAction = OnVisibleAction.RESUME;
}
}
if (!animationsEnabled()) {
setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
animator.endAnimation();
if (!isVisible()) {
onVisibleAction = OnVisibleAction.NONE;
}
}
}
/**
* 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;
}
animator.setMaxFrame(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();
}
@SuppressWarnings("unused")
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();
}
boolean isAnimatingOrWillAnimateOnVisible() {
if (isVisible()) {
return animator.isRunning();
} else {
return onVisibleAction == OnVisibleAction.PLAY || onVisibleAction == OnVisibleAction.RESUME;
}
}
private boolean animationsEnabled() {
return systemAnimationsEnabled || ignoreSystemAnimationsDisabled;
}
/**
* Tell Lottie that system animations are disabled. When using {@link LottieAnimationView} or Compose {@code LottieAnimation}, this is done
* automatically. However, if you are using LottieDrawable on its own, you should set this to false when
* {@link com.airbnb.lottie.utils.Utils#getAnimationScale(Context)} is 0.
*/
public 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;
}
/**
* Lottie files can specify a target frame rate. By default, Lottie ignores it and re-renders
* on every frame. If that behavior is undesirable, you can set this to true to use the composition
* frame rate instead.
* <p>
* Note: composition frame rates are usually lower than display frame rates
* so this will likely make your animation feel janky. However, it may be desirable
* for specific situations such as pixel art that are intended to have low frame rates.
*/
public void setUseCompositionFrameRate(boolean useCompositionFrameRate) {
animator.setUseCompositionFrameRate(useCompositionFrameRate);
}
/**
* 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 <a href="http://airbnb.io/lottie">http://airbnb.io/lottie</a> 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);
}
}
/**
* Set a map from font name keys to Typefaces.
* The keys can be in the form:
* * fontFamily
* * fontFamily-fontStyle
* * fontName
* All 3 are defined as fName, fFamily, and fStyle in the Lottie file.
* <p>
* If you change a value in fontMap, create a new map or call
* {@link #invalidateSelf()}. Setting the same map again will noop.
*/
public void setFontMap(@Nullable Map<String, Typeface> fontMap) {
if (fontMap == this.fontMap) {
return;
}
this.fontMap = fontMap;
invalidateSelf();
}
public void setTextDelegate(@SuppressWarnings("NullableProblems") TextDelegate textDelegate) {
this.textDelegate = textDelegate;
}
@Nullable
public TextDelegate getTextDelegate() {
return textDelegate;
}
public boolean useTextGlyphs() {
return fontMap == null && textDelegate == null && composition.getCharacters().size() > 0;
}
public LottieComposition getComposition() {
return composition;
}
public void cancelAnimation() {
lazyCompositionTasks.clear();
animator.cancel();
if (!isVisible()) {
onVisibleAction = OnVisibleAction.NONE;
}
}
public void pauseAnimation() {
lazyCompositionTasks.clear();
animator.pauseAnimation();
if (!isVisible()) {
onVisibleAction = OnVisibleAction.NONE;
}
}
@FloatRange(from = 0f, to = 1f)
public float getProgress() {
return animator.getAnimatedValueAbsolute();
}
@Override
public int getIntrinsicWidth() {
return composition == null ? -1 : composition.getBounds().width();
}
@Override
public int getIntrinsicHeight() {
return composition == null ? -1 : composition.getBounds().height();
}
/**
* 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<>() {
@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;
}
/**
* @deprecated use {@link #getBitmapForId(String)}.
*/
@Nullable
@Deprecated
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;
}
/**
* Returns the bitmap that will be rendered for the given id in the Lottie animation file.
* The id is the asset reference id stored in the "id" property of each object in the "assets" array.
* <p>
* The returned bitmap could be from:
* * Embedded in the animation file as a base64 string.
* * In the same directory as the animation file.
* * In the same zip file as the animation file.
* * Returned from an {@link ImageAssetDelegate}.
* or null if the image doesn't exist from any of those places.
*/
@Nullable
public Bitmap getBitmapForId(String id) {
ImageAssetManager assetManager = getImageAssetManager();
if (assetManager != null) {
return assetManager.bitmapForId(id);
}
return null;
}
/**
* Returns the {@link LottieImageAsset} that will be rendered for the given id in the Lottie animation file.
* The id is the asset reference id stored in the "id" property of each object in the "assets" array.
* <p>
* The returned bitmap could be from:
* * Embedded in the animation file as a base64 string.
* * In the same directory as the animation file.
* * In the same zip file as the animation file.
* * Returned from an {@link ImageAssetDelegate}.
* or null if the image doesn't exist from any of those places.
*/
@Nullable
public LottieImageAsset getLottieImageAssetForId(String id) {
LottieComposition composition = this.composition;
if (composition == null) {
return null;
}
return composition.getImages().get(id);
}
private ImageAssetManager getImageAssetManager() {
if (imageAssetManager != null && !imageAssetManager.hasSameContext(getContext())) {
imageAssetManager = null;
}
if (imageAssetManager == null) {
imageAssetManager = new ImageAssetManager(getCallback(),
imageAssetsFolder, imageAssetDelegate, composition.getImages());
}
return imageAssetManager;
}
@Nullable
@RestrictTo(RestrictTo.Scope.LIBRARY)
public Typeface getTypeface(Font font) {
Map<String, Typeface> fontMap = this.fontMap;
if (fontMap != null) {
String key = font.getFamily();
if (fontMap.containsKey(key)) {
return fontMap.get(key);
}
key = font.getName();
if (fontMap.containsKey(key)) {
return fontMap.get(key);
}
key = font.getFamily() + "-" + font.getStyle();
if (fontMap.containsKey(key)) {
return fontMap.get(key);
}
}
FontAssetManager assetManager = getFontAssetManager();
if (assetManager != null) {
return assetManager.getTypeface(font);
}
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);
String defaultExtension = this.defaultFontFileExtension;
if (defaultExtension != null) {
fontAssetManager.setDefaultFontFileExtension(defaultFontFileExtension);
}
}
return fontAssetManager;
}
/**
* By default, Lottie will look in src/assets/fonts/FONT_NAME.ttf
* where FONT_NAME is the fFamily specified in your Lottie file.
* If your fonts have a different extension, you can override the
* default here.
* <p>
* Alternatively, you can use {@link #setFontAssetDelegate(FontAssetDelegate)}
* for more control.
*
* @see #setFontAssetDelegate(FontAssetDelegate)
*/
public void setDefaultFontFileExtension(String extension) {
defaultFontFileExtension = extension;
FontAssetManager fam = getFontAssetManager();
if (fam != null) {
fam.setDefaultFontFileExtension(extension);
}
}
@Nullable
private Context getContext() {
Callback callback = getCallback();
if (callback == null) {
return null;
}
if (callback instanceof View) {
return ((View) callback).getContext();
}
return null;
}
@Override public boolean setVisible(boolean visible, boolean restart) {
// Sometimes, setVisible(false) gets called twice in a row. If we don't check wasNotVisibleAlready, we could
// wind up clearing the onVisibleAction value for the second call.
boolean wasNotVisibleAlready = !isVisible();
boolean ret = super.setVisible(visible, restart);
if (visible) {
if (onVisibleAction == OnVisibleAction.PLAY) {
playAnimation();
} else if (onVisibleAction == OnVisibleAction.RESUME) {
resumeAnimation();
}
} else {
if (animator.isRunning()) {
pauseAnimation();
onVisibleAction = OnVisibleAction.RESUME;
} else if (!wasNotVisibleAlready) {
onVisibleAction = OnVisibleAction.NONE;
}
}
return ret;
}
/**
* 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);
}
/**
* Hardware accelerated render path.
*/
private void drawDirectlyToCanvas(Canvas canvas) {
CompositionLayer compositionLayer = this.compositionLayer;
LottieComposition composition = this.composition;
if (compositionLayer == null || composition == null) {
return;
}
renderingMatrix.reset();
Rect bounds = getBounds();
if (!bounds.isEmpty()) {
// 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.preScale(scaleX, scaleY);
renderingMatrix.preTranslate(bounds.left, bounds.top);
}
compositionLayer.draw(canvas, renderingMatrix, alpha);
}
/**
* Software accelerated render path.
* <p>
* This draws the animation to an internally managed bitmap and then draws the bitmap to the original canvas.
*
* @see LottieAnimationView#setRenderMode(RenderMode)
*/
private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compositionLayer) {
if (composition == null || compositionLayer == null) {
return;
}
ensureSoftwareRenderingObjectsInitialized();
//noinspection deprecation
originalCanvas.getMatrix(softwareRenderingOriginalCanvasMatrix);
// Get the canvas clip bounds and map it to the coordinate space of canvas with it's current transform.
originalCanvas.getClipBounds(canvasClipBounds);
convertRect(canvasClipBounds, canvasClipBoundsRectF);
softwareRenderingOriginalCanvasMatrix.mapRect(canvasClipBoundsRectF);
convertRect(canvasClipBoundsRectF, canvasClipBounds);
if (clipToCompositionBounds) {
// Start with the intrinsic bounds. This will later be unioned with the clip bounds to find the
// smallest possible render area.
softwareRenderingTransformedBounds.set(0f, 0f, getIntrinsicWidth(), getIntrinsicHeight());
} else {
// Calculate the full bounds of the animation.
compositionLayer.getBounds(softwareRenderingTransformedBounds, null, false);
}
// Transform the animation bounds to the bounds that they will render to on the canvas.
softwareRenderingOriginalCanvasMatrix.mapRect(softwareRenderingTransformedBounds);
// 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.
Rect bounds = getBounds();
float scaleX = bounds.width() / (float) getIntrinsicWidth();
float scaleY = bounds.height() / (float) getIntrinsicHeight();
scaleRect(softwareRenderingTransformedBounds, scaleX, scaleY);
if (!ignoreCanvasClipBounds()) {
softwareRenderingTransformedBounds.intersect(canvasClipBounds.left, canvasClipBounds.top, canvasClipBounds.right, canvasClipBounds.bottom);
}
int renderWidth = (int) Math.ceil(softwareRenderingTransformedBounds.width());
int renderHeight = (int) Math.ceil(softwareRenderingTransformedBounds.height());
if (renderWidth == 0 || renderHeight == 0) {
return;
}
ensureSoftwareRenderingBitmap(renderWidth, renderHeight);
if (isDirty) {
renderingMatrix.set(softwareRenderingOriginalCanvasMatrix);
renderingMatrix.preScale(scaleX, scaleY);
// 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);
softwareRenderingBitmap.eraseColor(0);
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.
softwareRenderingOriginalCanvasMatrix.invert(softwareRenderingOriginalCanvasMatrixInverse);
softwareRenderingOriginalCanvasMatrixInverse.mapRect(softwareRenderingDstBoundsRectF, softwareRenderingTransformedBounds);
convertRect(softwareRenderingDstBoundsRectF, softwareRenderingDstBoundsRect);
}
softwareRenderingSrcBoundsRect.set(0, 0, renderWidth, renderHeight);
originalCanvas.drawBitmap(softwareRenderingBitmap, softwareRenderingSrcBoundsRect, softwareRenderingDstBoundsRect, softwareRenderingPaint);
}
private void ensureSoftwareRenderingObjectsInitialized() {
if (softwareRenderingCanvas != null) {
return;
}
softwareRenderingCanvas = new Canvas();
softwareRenderingTransformedBounds = new RectF();
softwareRenderingOriginalCanvasMatrix = new Matrix();
softwareRenderingOriginalCanvasMatrixInverse = new Matrix();
canvasClipBounds = new Rect();
canvasClipBoundsRectF = new RectF();
softwareRenderingPaint = new LPaint();
softwareRenderingSrcBoundsRect = new Rect();
softwareRenderingDstBoundsRect = new Rect();
softwareRenderingDstBoundsRectF = new RectF();
}
private void ensureSoftwareRenderingBitmap(int renderWidth, int renderHeight) {
if (softwareRenderingBitmap == null ||
softwareRenderingBitmap.getWidth() < renderWidth ||
softwareRenderingBitmap.getHeight() < renderHeight) {
// The bitmap is larger. We need to create a new one.
softwareRenderingBitmap = Bitmap.createBitmap(renderWidth, renderHeight, Bitmap.Config.ARGB_8888);
softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
isDirty = true;
} else if (softwareRenderingBitmap.getWidth() > renderWidth || softwareRenderingBitmap.getHeight() > renderHeight) {
// The bitmap is smaller. Take subset of the original.
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)
);
}
/**
* Convert a Rect to a RectF
*/
private void convertRect(Rect src, RectF dst) {
dst.set(
src.left,
src.top,
src.right,
src.bottom);
}
private void scaleRect(RectF rect, float scaleX, float scaleY) {
rect.set(
rect.left * scaleX,
rect.top * scaleY,
rect.right * scaleX,
rect.bottom * scaleY
);
}
/**
* When a View's parent has clipChildren set to false, it doesn't affect the clipBound
* of its child canvases so we should explicitly check for it and draw the full animation
* bounds instead.
*/
private boolean ignoreCanvasClipBounds() {
Callback callback = getCallback();
if (!(callback instanceof View)) {
// If the callback isn't a view then respect the canvas's clip bounds.
return false;
}
ViewParent parent = ((View) callback).getParent();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && parent instanceof ViewGroup) {
return !((ViewGroup) parent).getClipChildren();
}
// Unlikely to ever happen. If the callback is a View, its parent should be a ViewGroup.
return false;
}
}