| package com.airbnb.lottie; |
| |
| import android.graphics.Canvas; |
| import android.graphics.Matrix; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffXfermode; |
| import android.graphics.RectF; |
| import android.support.annotation.FloatRange; |
| import android.support.annotation.Nullable; |
| import android.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| abstract class BaseLayer implements DrawingContent, BaseKeyframeAnimation.AnimationListener { |
| private static final int SAVE_FLAGS = Canvas.CLIP_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG | |
| Canvas.MATRIX_SAVE_FLAG; |
| |
| static BaseLayer forModel( |
| Layer layerModel, LottieDrawable drawable, LottieComposition composition) { |
| switch (layerModel.getLayerType()) { |
| case Shape: |
| return new ShapeLayer(drawable, layerModel); |
| case PreComp: |
| return new CompositionLayer(drawable, layerModel, |
| composition.getPrecomps(layerModel.getRefId()), composition); |
| case Solid: |
| return new SolidLayer(drawable, layerModel); |
| case Image: |
| return new ImageLayer(drawable, layerModel); |
| case Null: |
| return new NullLayer(drawable, layerModel); |
| case Text: |
| case Unknown: |
| default: |
| // Do nothing |
| Log.w(L.TAG, "Unknown layer type " + layerModel.getLayerType()); |
| return new NullLayer(drawable, layerModel); |
| } |
| } |
| |
| private final Path path = new Path(); |
| private final Matrix matrix = new Matrix(); |
| private final Paint contentPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Paint mattePaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Paint clearPaint = new Paint(); |
| private final RectF rect = new RectF(); |
| final LottieDrawable lottieDrawable; |
| final Layer layerModel; |
| @Nullable private MaskKeyframeAnimation mask; |
| @Nullable private BaseLayer matteLayer; |
| @Nullable private BaseLayer parentLayer; |
| private List<BaseLayer> parentLayers; |
| |
| private final List<BaseKeyframeAnimation<?, ?>> animations = new ArrayList<>(); |
| final TransformKeyframeAnimation transform; |
| private boolean visible = true; |
| |
| BaseLayer(LottieDrawable lottieDrawable, Layer layerModel) { |
| this.lottieDrawable = lottieDrawable; |
| this.layerModel = layerModel; |
| clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); |
| maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); |
| if (layerModel.getMatteType() == Layer.MatteType.Invert) { |
| mattePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); |
| } else { |
| mattePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); |
| } |
| |
| this.transform = layerModel.getTransform().createAnimation(); |
| transform.addListener(this); |
| transform.addAnimationsToLayer(this); |
| |
| if (layerModel.getMasks() != null && !layerModel.getMasks().isEmpty()) { |
| this.mask = new MaskKeyframeAnimation(layerModel.getMasks()); |
| for (BaseKeyframeAnimation<?, Path> animation : mask.getMaskAnimations()) { |
| addAnimation(animation); |
| animation.addUpdateListener(this); |
| } |
| } |
| setupInOutAnimations(); |
| } |
| |
| @Override public void onValueChanged() { |
| invalidateSelf(); |
| } |
| |
| Layer getLayerModel() { |
| return layerModel; |
| } |
| |
| void setMatteLayer(@Nullable BaseLayer matteLayer) { |
| this.matteLayer = matteLayer; |
| } |
| |
| boolean hasMatteOnThisLayer() { |
| return matteLayer != null; |
| } |
| |
| void setParentLayer(@Nullable BaseLayer parentLayer) { |
| this.parentLayer = parentLayer; |
| } |
| |
| private void setupInOutAnimations() { |
| if (!layerModel.getInOutKeyframes().isEmpty()) { |
| final FloatKeyframeAnimation inOutAnimation = |
| new FloatKeyframeAnimation(layerModel.getInOutKeyframes()); |
| inOutAnimation.setIsDiscrete(); |
| inOutAnimation.addUpdateListener(new BaseKeyframeAnimation.AnimationListener() { |
| @Override public void onValueChanged() { |
| setVisible(inOutAnimation.getValue() == 1f); |
| } |
| }); |
| setVisible(inOutAnimation.getValue() == 1f); |
| addAnimation(inOutAnimation); |
| } else { |
| setVisible(true); |
| } |
| } |
| |
| private void invalidateSelf() { |
| lottieDrawable.invalidateSelf(); |
| } |
| |
| void addAnimation(BaseKeyframeAnimation<?, ?> newAnimation) { |
| if (!(newAnimation instanceof StaticKeyframeAnimation)) { |
| animations.add(newAnimation); |
| } |
| } |
| |
| @Override |
| public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) { |
| if (!visible) { |
| return; |
| } |
| buildParentLayerListIfNeeded(); |
| matrix.reset(); |
| matrix.set(parentMatrix); |
| for (int i = parentLayers.size() - 1; i >= 0; i--) { |
| matrix.preConcat(parentLayers.get(i).transform.getMatrix()); |
| } |
| matrix.preConcat(transform.getMatrix()); |
| int alpha = (int) |
| ((parentAlpha / 255f * (float) transform.getOpacity().getValue() / 100f) * 255); |
| if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) { |
| drawLayer(canvas, matrix, alpha); |
| return; |
| } |
| |
| // TODO: make sure this is the right clip. |
| rect.set(canvas.getClipBounds()); |
| canvas.saveLayer(rect, contentPaint, Canvas.ALL_SAVE_FLAG); |
| // Clear the off screen buffer. This is necessary for some phones. |
| canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), clearPaint); |
| drawLayer(canvas, matrix, alpha); |
| |
| if (hasMasksOnThisLayer()) { |
| applyMasks(canvas, matrix); |
| } |
| |
| if (hasMatteOnThisLayer()) { |
| canvas.saveLayer(rect, mattePaint, SAVE_FLAGS); |
| canvas.drawRect(rect, clearPaint); |
| //noinspection ConstantConditions |
| matteLayer.draw(canvas, parentMatrix, alpha); |
| canvas.restore(); |
| } |
| |
| canvas.restore(); |
| } |
| |
| abstract void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha); |
| |
| private void applyMasks(Canvas canvas, Matrix matrix) { |
| canvas.saveLayer(rect, maskPaint, SAVE_FLAGS); |
| canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), clearPaint); |
| |
| //noinspection ConstantConditions |
| int size = mask.getMasks().size(); |
| for (int i = 0; i < size; i++) { |
| Mask mask = this.mask.getMasks().get(i); |
| BaseKeyframeAnimation<?, Path> maskAnimation = this.mask.getMaskAnimations().get(i); |
| Path maskPath = maskAnimation.getValue(); |
| path.set(maskPath); |
| path.transform(matrix); |
| |
| switch (mask.getMaskMode()) { |
| case MaskModeSubtract: |
| maskPath.setFillType(Path.FillType.INVERSE_WINDING); |
| break; |
| case MaskModeAdd: |
| default: |
| maskPath.setFillType(Path.FillType.WINDING); |
| } |
| canvas.drawPath(path, contentPaint); |
| } |
| canvas.restore(); |
| } |
| |
| boolean hasMasksOnThisLayer() { |
| return mask != null && !mask.getMaskAnimations().isEmpty(); |
| } |
| |
| private void setVisible(boolean visible) { |
| if (visible != this.visible) { |
| this.visible = visible; |
| invalidateSelf(); |
| } |
| } |
| |
| void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { |
| if (matteLayer != null) { |
| matteLayer.setProgress(progress); |
| } |
| for (int i = 0; i < animations.size(); i++) { |
| animations.get(i).setProgress(progress); |
| } |
| } |
| |
| private void buildParentLayerListIfNeeded() { |
| if (parentLayers != null) { |
| return; |
| } |
| if (parentLayer == null) { |
| parentLayers = Collections.emptyList(); |
| return; |
| } |
| |
| parentLayers = new ArrayList<>(); |
| BaseLayer layer = parentLayer; |
| while (layer != null) { |
| parentLayers.add(layer); |
| layer = layer.parentLayer; |
| } |
| } |
| |
| @Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) { |
| // Do nothing |
| } |
| } |