| 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.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); |
| } |
| |
| /** |
| * 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<>(); |
| 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; |
| 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; |
| |
| /** |
| * 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. |
| * |
| * 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. |
| * |
| * 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 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; |
| 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 { |
| 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; |
| L.endSection("Drawable#draw"); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| if (useSoftwareRendering) { |
| canvas.save(); |
| canvas.concat(matrix); |
| renderAndDrawAsBitmap(canvas, compositionLayer); |
| canvas.restore(); |
| } else { |
| compositionLayer.draw(canvas, matrix, alpha); |
| } |
| isDirty = false; |
| } |
| |
| // <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; |
| } |
| |
| /** |
| * 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 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<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; |
| } |
| |
| /** |
| * @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 (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; |
| } |
| |
| @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); |
| } |
| compositionLayer.draw(canvas, renderingMatrix, alpha); |
| } |
| |
| /** |
| * Software accelerated render path. |
| * |
| * 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; |
| } |
| } |