| package com.airbnb.lottie; |
| |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| 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.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.v4.util.LongSparseArray; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| class LayerView extends AnimatableLayer { |
| private static final int SAVE_FLAGS = Canvas.CLIP_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG; |
| private MaskKeyframeAnimation mask; |
| private LayerView matteLayer; |
| |
| private final PorterDuffXfermode DST_OUT = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); |
| private final PorterDuffXfermode DST_IN = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); |
| private final RectF rect = new RectF(); |
| private final List<LayerView> transformLayers = new ArrayList<>(); |
| private final Paint mainCanvasPaint = new Paint(); |
| private final Paint mattePaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Paint imagePaint = |
| new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); |
| |
| private final Layer layerModel; |
| private final LottieComposition composition; |
| private final CanvasPool canvasPool; |
| |
| @Nullable private LayerView parentLayer; |
| /** |
| * The width and height of the precomp that this was added to. |
| * This differs from the LayerModel precompWidth and height which will be set if this is |
| * the precomp layer itself. |
| */ |
| private int precompWidth; |
| private int precompHeight; |
| |
| LayerView(Layer layerModel, LottieComposition composition, Callback callback, CanvasPool canvasPool) { |
| super(callback); |
| this.layerModel = layerModel; |
| this.composition = composition; |
| this.canvasPool = canvasPool; |
| setBounds(composition.getBounds()); |
| |
| if (layerModel.getMatteType() == Layer.MatteType.Invert) { |
| mattePaint.setXfermode(DST_OUT); |
| } else { |
| mattePaint.setXfermode(DST_IN); |
| } |
| |
| setupForModel(); |
| } |
| |
| private void setupForModel() { |
| setBackgroundColor(layerModel.getSolidColor()); |
| setBounds(0, 0, layerModel.getSolidWidth(), layerModel.getSolidHeight()); |
| |
| setTransform(layerModel.getTransform().createAnimation()); |
| setupInOutAnimations(); |
| |
| switch (layerModel.getLayerType()) { |
| case Shape: |
| setupShapeLayer(); |
| break; |
| case PreComp: |
| setupPreCompLayer(); |
| break; |
| } |
| |
| if (layerModel.getMasks() != null && !layerModel.getMasks().isEmpty()) { |
| setMask(new MaskKeyframeAnimation(layerModel.getMasks())); |
| } |
| |
| LongSparseArray<LayerView> layerMap = new LongSparseArray<>(); |
| |
| for (AnimatableLayer layer : layers) { |
| if (layer instanceof LayerView) { |
| layerMap.put(((LayerView) layer).getId(), ((LayerView) layer)); |
| LayerView matteLayer = ((LayerView) layer).matteLayer; |
| if (matteLayer != null) { |
| layerMap.put(matteLayer.getId(), matteLayer); |
| } |
| } |
| } |
| |
| for (AnimatableLayer layer : layers) { |
| if (!(layer instanceof LayerView)) { |
| continue; |
| } |
| long parentId = ((LayerView) layer).getLayerModel().getParentId(); |
| LayerView parentLayer = layerMap.get(parentId); |
| if (parentLayer != null) { |
| ((LayerView) layer).setParentLayer(parentLayer); |
| } |
| |
| LayerView matteLayer = ((LayerView) layer).matteLayer; |
| if (matteLayer != null) { |
| parentId = matteLayer.getLayerModel().getParentId(); |
| parentLayer = layerMap.get(parentId); |
| if (parentLayer != null) { |
| matteLayer.setParentLayer(parentLayer); |
| } |
| } |
| |
| } |
| } |
| |
| private void setupShapeLayer() { |
| List<Object> reversedItems = new ArrayList<>(layerModel.getShapes()); |
| Collections.reverse(reversedItems); |
| AnimatableTransform currentTransform = null; |
| ShapeTrimPath currentTrim = null; |
| ShapeFill currentFill = null; |
| ShapeStroke currentStroke = null; |
| |
| for (int i = 0; i < reversedItems.size(); i++) { |
| Object item = reversedItems.get(i); |
| if (item instanceof ShapeGroup) { |
| GroupLayerView groupLayer = new GroupLayerView((ShapeGroup) item, currentFill, |
| currentStroke, currentTrim, currentTransform, getCallback()); |
| addLayer(groupLayer); |
| } else if (item instanceof AnimatableTransform) { |
| currentTransform = (AnimatableTransform) item; |
| } else if (item instanceof ShapeFill) { |
| currentFill = (ShapeFill) item; |
| } else if (item instanceof ShapeTrimPath) { |
| currentTrim = (ShapeTrimPath) item; |
| } else if (item instanceof ShapeStroke) { |
| currentStroke = (ShapeStroke) item; |
| } else if (item instanceof ShapePath) { |
| ShapePath shapePath = (ShapePath) item; |
| ShapeLayerView shapeLayer = |
| new ShapeLayerView(shapePath, currentFill, currentStroke, currentTrim, |
| AnimatableTransform.Factory.newInstance(composition), getCallback()); |
| addLayer(shapeLayer); |
| } else if (item instanceof RectangleShape) { |
| RectangleShape shapeRect = (RectangleShape) item; |
| RectLayer shapeLayer = |
| new RectLayer(shapeRect, currentFill, currentStroke, currentTrim, |
| AnimatableTransform.Factory.newInstance(composition), getCallback()); |
| addLayer(shapeLayer); |
| } else if (item instanceof CircleShape) { |
| CircleShape shapeCircle = (CircleShape) item; |
| EllipseLayer shapeLayer = |
| new EllipseLayer(shapeCircle, currentFill, currentStroke, currentTrim, |
| AnimatableTransform.Factory.newInstance(composition), getCallback()); |
| addLayer(shapeLayer); |
| } else if (item instanceof PolystarShape) { |
| PolystarShape polystarShape = (PolystarShape) item; |
| PolystarLayer shapeLayer = new PolystarLayer(polystarShape, currentFill, currentStroke, |
| currentTrim, AnimatableTransform.Factory.newInstance(composition), getCallback()); |
| addLayer(shapeLayer); |
| } |
| } |
| } |
| |
| private void setupPreCompLayer() { |
| List<Layer> precompLayers = composition.getPrecomps(layerModel.getRefId()); |
| if (precompLayers == null) { |
| return; |
| } |
| LayerView mattedLayer = null; |
| for (int i = precompLayers.size() - 1; i >= 0; i--) { |
| Layer layer = precompLayers.get(i); |
| LayerView layerView = |
| new LayerView(layer, composition, getCallback(), canvasPool); |
| layerView.setPrecompSize(layerModel.getPreCompWidth(), layerModel.getPreCompHeight()); |
| if (mattedLayer != null) { |
| mattedLayer.setMatteLayer(layerView); |
| mattedLayer = null; |
| } else { |
| addLayer(layerView); |
| if (layer.getMatteType() == Layer.MatteType.Add) { |
| mattedLayer = layerView; |
| } else if (layer.getMatteType() == Layer.MatteType.Invert) { |
| mattedLayer = layerView; |
| } |
| } |
| } |
| } |
| |
| private void setupInOutAnimations() { |
| if (!layerModel.getInOutKeyframes().isEmpty()) { |
| FloatKeyframeAnimation inOutAnimation = |
| new FloatKeyframeAnimation(layerModel.getInOutKeyframes()); |
| inOutAnimation.setIsDiscrete(); |
| inOutAnimation.addUpdateListener(new KeyframeAnimation.AnimationListener<Float>() { |
| @Override public void onValueChanged(Float value) { |
| setVisible(value == 1f, false); |
| } |
| }); |
| setVisible(inOutAnimation.getValue() == 1f, false); |
| addAnimation(inOutAnimation); |
| } else { |
| setVisible(true, false); |
| } |
| } |
| |
| Layer getLayerModel() { |
| return layerModel; |
| } |
| |
| void setParentLayer(@Nullable LayerView parentLayer) { |
| this.parentLayer = parentLayer; |
| } |
| |
| @Nullable |
| private LayerView getParentLayer() { |
| return parentLayer; |
| } |
| |
| private void setPrecompSize(int width, int height) { |
| precompWidth = width; |
| precompHeight = height; |
| } |
| |
| private void setMask(MaskKeyframeAnimation mask) { |
| this.mask = mask; |
| for (BaseKeyframeAnimation<?, Path> animation : mask.getMaskAnimations()) { |
| addAnimation(animation); |
| animation.addUpdateListener(pathChangedListener); |
| } |
| } |
| |
| void setMatteLayer(LayerView matteLayer) { |
| this.matteLayer = matteLayer; |
| } |
| |
| @Override public void draw(@NonNull Canvas canvas) { |
| if (!isVisible() || mainCanvasPaint.getAlpha() == 0) { |
| return; |
| } |
| |
| // Make a list of all parent layers. |
| transformLayers.clear(); |
| LayerView parent = parentLayer; |
| while (parent != null) { |
| transformLayers.add(parent); |
| parent = parent.getParentLayer(); |
| } |
| |
| if (!hasMasks() && !hasMatte()) { |
| int mainCanvasCount = saveCanvas(canvas); |
| if (precompWidth != 0 || precompHeight != 0) { |
| canvas.clipRect(0, 0, precompWidth, precompHeight); |
| } |
| // Now apply the parent transformations from the top down. |
| for (int i = transformLayers.size() - 1; i >= 0; i--) { |
| LayerView layer = transformLayers.get(i); |
| applyTransformForLayer(canvas, layer); |
| } |
| drawImageIfNeeded(canvas); |
| super.draw(canvas); |
| canvas.restoreToCount(mainCanvasCount); |
| return; |
| } |
| |
| |
| BitmapCanvas bitmapCanvas = |
| canvasPool.acquire(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888); |
| |
| // Now apply the parent transformations from the top down. |
| bitmapCanvas.save(); |
| drawImageIfNeeded(bitmapCanvas); |
| for (int i = transformLayers.size() - 1; i >= 0; i--) { |
| LayerView layer = transformLayers.get(i); |
| applyTransformForLayer(bitmapCanvas, layer); |
| } |
| super.draw(bitmapCanvas); |
| |
| rect.set(0, 0, canvas.getWidth(), canvas.getHeight()); |
| if (hasMasks()) { |
| List<Mask> masks = mask.getMasks(); |
| List<BaseKeyframeAnimation<?, Path>> maskAnimations = mask.getMaskAnimations(); |
| for (int i = 0; i < masks.size(); i++) { |
| applyMask(bitmapCanvas, masks.get(i), maskAnimations.get(i)); |
| } |
| } |
| bitmapCanvas.restore(); |
| |
| if (hasMatte()) { |
| bitmapCanvas.saveLayer(rect, mattePaint, SAVE_FLAGS); |
| matteLayer.draw(bitmapCanvas); |
| bitmapCanvas.restore(); |
| } |
| |
| if (precompWidth != 0 || precompHeight != 0) { |
| canvas.clipRect(0, 0, precompWidth, precompHeight); |
| } |
| canvas.drawBitmap(bitmapCanvas.getBitmap(), 0, 0, null); |
| canvasPool.release(bitmapCanvas); |
| } |
| |
| private void applyMask(BitmapCanvas canvas, Mask mask, |
| BaseKeyframeAnimation<?, Path> maskAnimation) { |
| switch (mask.getMaskMode()) { |
| case MaskModeSubtract: |
| maskPaint.setXfermode(DST_OUT); |
| break; |
| case MaskModeAdd: |
| default: |
| maskPaint.setXfermode(DST_IN); |
| } |
| |
| canvas.saveLayer(rect, maskPaint, SAVE_FLAGS); |
| for (int i = transformLayers.size() - 1; i >= 0; i--) { |
| LayerView layer = transformLayers.get(i); |
| applyTransformForLayer(canvas, layer); |
| } |
| applyTransformForLayer(canvas, this); |
| canvas.drawPath(maskAnimation.getValue(), mainCanvasPaint); |
| canvas.restore(); |
| } |
| |
| private void drawImageIfNeeded(Canvas canvas) { |
| if (!composition.hasImages()) { |
| return; |
| } |
| String refId = layerModel.getRefId(); |
| Bitmap bitmap = getLottieDrawable().getImageAsset(refId); |
| if (bitmap == null) { |
| return; |
| } |
| |
| canvas.save(); |
| applyTransformForLayer(canvas, this); |
| canvas.drawBitmap(bitmap, 0, 0 ,imagePaint); |
| canvas.restore(); |
| } |
| |
| boolean hasMatte() { |
| return matteLayer != null; |
| } |
| |
| boolean hasMasks() { |
| return mask != null && !mask.getMaskAnimations().isEmpty(); |
| } |
| |
| @Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { |
| // TODO: use this. |
| // progress -= layerModel.getStartProgress(); |
| progress *= layerModel.getTimeStretch(); |
| |
| |
| super.setProgress(progress); |
| if (matteLayer != null) { |
| matteLayer.setProgress(progress); |
| } |
| } |
| |
| long getId() { |
| return layerModel.getId(); |
| } |
| |
| @Override public String toString() { |
| return layerModel.toString(); |
| } |
| } |