update lottie

This commit is contained in:
otsmr 2026-07-15 19:04:30 +02:00
parent 72d9bd6320
commit d99dba6a5e
586 changed files with 728 additions and 5161 deletions

View file

@ -9,7 +9,7 @@ hashlib: bc9c2f8dd7bbc72f47ccab0ce1111d40259c49bc
hashlib_codecs: 2a966c37c3b9b1f5541ae88e99ab34acf3fc968b
introduction_screen: 4a90e557630b28834479ed9c64a9d2d0185d8e48
libsignal_protocol_dart: c95a1586057022acdbb9c76b1692d94cc549bcc7
lottie: 4f1a5a52bdf1e1c1e12fa97c96174dcb05419e19
lottie: 127bc29f2c6bd8b32ec4064a09e54e6b31cd0a88
mutex: 84ca903a3ac863735e3228c75a212133621f680f
no_screenshot: fbfa2ed7ec4db782797fa6a7de8f207a2cba00bb
optional: 71c638891ce4f2aff35c7387727989f31f9d877d

View file

@ -149,7 +149,7 @@ abstract class BaseStrokeContent
var alpha = ((parentAlpha / 255.0 * _opacityAnimation.value / 100.0) * 255)
.round();
paint.setAlpha(alpha.clamp(0, 255));
paint.strokeWidth = _widthAnimation.value * parentMatrix.getScale();
paint.strokeWidth = _widthAnimation.value;
if (paint.strokeWidth <= 0) {
// Android draws a hairline stroke for 0, After Effects doesn't.
L.endSection('StrokeContent#draw');
@ -172,20 +172,19 @@ abstract class BaseStrokeContent
_blurMaskFilterRadius = blurRadius;
}
// Concat the parent matrix instead of scaling the stroke width, so skew affects the stroke.
canvas.save();
canvas.transform(parentMatrix.storage);
for (var i = 0; i < _pathGroups.length; i++) {
var pathGroup = _pathGroups[i];
if (pathGroup.trimPath != null) {
_applyTrimPath(canvas, pathGroup, parentMatrix);
_applyTrimPath(canvas, pathGroup);
} else {
L.beginSection('StrokeContent#buildPath');
_path.reset();
for (var j = pathGroup.paths.length - 1; j >= 0; j--) {
_path.addPath(
pathGroup.paths[j].getPath(),
Offset.zero,
matrix4: parentMatrix.storage,
);
_path.addPath(pathGroup.paths[j].getPath(), Offset.zero);
}
L.endSection('StrokeContent#buildPath');
L.beginSection('StrokeContent#drawPath');
@ -193,18 +192,15 @@ abstract class BaseStrokeContent
if (dropShadow != null) {
dropShadow.draw(canvas, _path);
}
canvas.drawPath(_withDashPattern(_path, parentMatrix), paint);
canvas.drawPath(_withDashPattern(_path), paint);
L.endSection('StrokeContent#drawPath');
}
}
canvas.restore();
L.endSection('StrokeContent#draw');
}
void _applyTrimPath(
Canvas canvas,
_PathGroup pathGroup,
Matrix4 parentMatrix,
) {
void _applyTrimPath(Canvas canvas, _PathGroup pathGroup) {
L.beginSection('StrokeContent#applyTrimPath');
var trimPath = pathGroup.trimPath;
if (trimPath == null) {
@ -213,11 +209,7 @@ abstract class BaseStrokeContent
}
_path.reset();
for (var j = pathGroup.paths.length - 1; j >= 0; j--) {
_path.addPath(
pathGroup.paths[j].getPath(),
Offset.zero,
matrix4: parentMatrix.storage,
);
_path.addPath(pathGroup.paths[j].getPath(), Offset.zero);
}
var animStartValue = trimPath.start.value / 100;
var animEndValue = trimPath.end.value / 100;
@ -242,9 +234,7 @@ abstract class BaseStrokeContent
var currentLength = 0.0;
for (var j = pathGroup.paths.length - 1; j >= 0; j--) {
_trimPathPath.set(
pathGroup.paths[j].getPath().transform(parentMatrix.storage),
);
_trimPathPath.set(pathGroup.paths[j].getPath());
var pathMetrics = _trimPathPath.computeMetrics().toList();
var length = pathMetrics.isNotEmpty ? pathMetrics.first.length : 0;
if (endLength > totalLength &&
@ -260,13 +250,13 @@ abstract class BaseStrokeContent
}
var endValue = min((endLength - totalLength) / length, 1).toDouble();
Utils.applyTrimPathIfNeeded(_trimPathPath, startValue, endValue, 0.0);
canvas.drawPath(_withDashPattern(_trimPathPath, parentMatrix), paint);
canvas.drawPath(_withDashPattern(_trimPathPath), paint);
} else if (currentLength + length < startLength ||
currentLength > endLength) {
// Do nothing
} else if (currentLength + length <= endLength &&
startLength < currentLength) {
canvas.drawPath(_withDashPattern(_trimPathPath, parentMatrix), paint);
canvas.drawPath(_withDashPattern(_trimPathPath), paint);
} else {
double startValue;
if (startLength < currentLength) {
@ -281,7 +271,7 @@ abstract class BaseStrokeContent
endValue = (endLength - currentLength) / length;
}
Utils.applyTrimPathIfNeeded(_trimPathPath, startValue, endValue, 0);
canvas.drawPath(_withDashPattern(_trimPathPath, parentMatrix), paint);
canvas.drawPath(_withDashPattern(_trimPathPath), paint);
}
currentLength += length;
}
@ -312,14 +302,13 @@ abstract class BaseStrokeContent
return bounds;
}
Path _withDashPattern(Path path, Matrix4 parentMatrix) {
Path _withDashPattern(Path path) {
L.beginSection('StrokeContent#applyDashPattern');
if (_dashPatternAnimations.isEmpty) {
L.endSection('StrokeContent#applyDashPattern');
return path;
}
var scale = parentMatrix.getScale();
for (var i = 0; i < _dashPatternAnimations.length; i++) {
_dashPatternValues[i] = _dashPatternAnimations[i].value;
// If the value of the dash pattern or gap is too small, the number of individual sections
@ -335,12 +324,11 @@ abstract class BaseStrokeContent
_dashPatternValues[i] = 0.1;
}
}
_dashPatternValues[i] *= scale;
}
var offset = _dashPatternOffsetAnimation == null
? 0.0
: _dashPatternOffsetAnimation.value * scale;
: _dashPatternOffsetAnimation.value;
var newPath = dashPath(path, intervals: _dashPatternValues, phase: offset);
L.endSection('StrokeContent#applyDashPattern');

View file

@ -73,8 +73,8 @@ class ContentGroup implements DrawingContent, PathContent, KeyPathElement {
this.name,
this._contents,
AnimatableTransform? transform, {
required bool hidden,
}) : _hidden = hidden {
required this._hidden,
}) {
if (transform != null) {
_transformAnimation = transform.createAnimation()
..addAnimationsToLayer(layer)

View file

@ -22,9 +22,7 @@ import 'path_content.dart';
class FillContent implements DrawingContent, KeyPathElementContent {
final Path _path = Path();
final BaseLayer layer;
@override
final String? name;
final bool _hidden;
final ShapeFill _fill;
final List<PathContent> _paths = <PathContent>[];
late final BaseKeyframeAnimation<Color, Color> _colorAnimation;
late final BaseKeyframeAnimation<int, int> _opacityAnimation;
@ -34,9 +32,10 @@ class FillContent implements DrawingContent, KeyPathElementContent {
double _blurMaskFilterRadius = 0;
DropShadowKeyframeAnimation? dropShadowAnimation;
FillContent(this.lottieDrawable, this.layer, ShapeFill fill)
: name = fill.name,
_hidden = fill.hidden {
@override
String? get name => _fill.name;
FillContent(this.lottieDrawable, this.layer, this._fill) {
var blurEffect = layer.blurEffect;
if (blurEffect != null) {
_blurAnimation = blurEffect.blurriness.createAnimation()
@ -52,16 +51,14 @@ class FillContent implements DrawingContent, KeyPathElementContent {
);
}
if (fill.color == null || fill.opacity == null) {
if (_fill.color == null || _fill.opacity == null) {
return;
}
_path.fillType = fill.fillType;
_colorAnimation = fill.color!.createAnimation();
_colorAnimation = _fill.color!.createAnimation();
_colorAnimation.addUpdateListener(onValueChanged);
layer.addAnimation(_colorAnimation);
_opacityAnimation = fill.opacity!.createAnimation();
_opacityAnimation = _fill.opacity!.createAnimation();
_opacityAnimation.addUpdateListener(onValueChanged);
layer.addAnimation(_opacityAnimation);
}
@ -82,7 +79,7 @@ class FillContent implements DrawingContent, KeyPathElementContent {
@override
void draw(Canvas canvas, Matrix4 parentMatrix, {required int parentAlpha}) {
if (_hidden) {
if (_fill.hidden) {
return;
}
L.beginSection('FillContent#draw');
@ -112,6 +109,7 @@ class FillContent implements DrawingContent, KeyPathElementContent {
}
_path.reset();
_path.fillType = _fill.fillType;
for (var i = 0; i < _paths.length; i++) {
_path.addPath(_paths[i].getPath(), Offset.zero);
}
@ -131,6 +129,7 @@ class FillContent implements DrawingContent, KeyPathElementContent {
@override
Rect getBounds(Matrix4 parentMatrix, {required bool applyParents}) {
_path.reset();
_path.fillType = _fill.fillType;
for (var i = 0; i < _paths.length; i++) {
_path.addPath(
_paths[i].getPath(),

View file

@ -52,7 +52,6 @@ class GradientFillContent implements DrawingContent, KeyPathElementContent {
_opacityAnimation = _fill.opacity.createAnimation(),
_startPointAnimation = _fill.startPoint.createAnimation(),
_endPointAnimation = _fill.endPoint.createAnimation() {
_path.fillType = _fill.fillType;
_colorAnimation.addUpdateListener(invalidate);
layer.addAnimation(_colorAnimation);
@ -105,6 +104,7 @@ class GradientFillContent implements DrawingContent, KeyPathElementContent {
}
L.beginSection('GradientFillContent#draw');
_path.reset();
_path.fillType = _fill.fillType;
for (var i = 0; i < _paths.length; i++) {
_path.addPath(_paths[i].getPath(), Offset.zero);
}
@ -155,6 +155,7 @@ class GradientFillContent implements DrawingContent, KeyPathElementContent {
@override
Rect getBounds(Matrix4 parentMatrix, {required bool applyParents}) {
_path.reset();
_path.fillType = _fill.fillType;
for (var i = 0; i < _paths.length; i++) {
_path.addPath(
_paths[i].getPath(),

View file

@ -73,9 +73,9 @@ class GradientStrokeContent extends BaseStrokeContent {
Gradient gradient;
if (_type == GradientType.linear) {
gradient = _getLinearGradient(parentMatrix);
gradient = _getLinearGradient();
} else {
gradient = _getRadialGradient(parentMatrix);
gradient = _getRadialGradient();
}
paint.shader = gradient;
@ -83,8 +83,9 @@ class GradientStrokeContent extends BaseStrokeContent {
super.draw(canvas, parentMatrix, parentAlpha: parentAlpha);
}
Gradient _getLinearGradient(Matrix4 parentMatrix) {
var gradientHash = _getGradientHash(parentMatrix);
// Built in local space; BaseStrokeContent.draw concats the parent matrix, so don't reapply it.
Gradient _getLinearGradient() {
var gradientHash = _getGradientHash();
var gradient = _linearGradientCache[gradientHash];
if (gradient != null) {
return gradient;
@ -95,22 +96,15 @@ class GradientStrokeContent extends BaseStrokeContent {
var colors = _applyDynamicColorsIfNeeded(gradientColor.colors);
var positions = gradientColor.positions;
gradient = Gradient.linear(
startPoint,
endPoint,
colors,
positions,
TileMode.clamp,
parentMatrix.storage,
);
gradient = Gradient.linear(startPoint, endPoint, colors, positions);
if (gradientHash != null) {
_linearGradientCache[gradientHash] = gradient;
}
return gradient;
}
Gradient _getRadialGradient(Matrix4 parentMatrix) {
var gradientHash = _getGradientHash(parentMatrix);
Gradient _getRadialGradient() {
var gradientHash = _getGradientHash();
var gradient = _radialGradientCache[gradientHash];
if (gradient != null) {
return gradient;
@ -125,14 +119,7 @@ class GradientStrokeContent extends BaseStrokeContent {
var x1 = endPoint.dx;
var y1 = endPoint.dy;
var radius = hypot(x1 - x0, y1 - y0).toDouble();
gradient = Gradient.radial(
startPoint,
radius,
colors,
positions,
TileMode.clamp,
parentMatrix.storage,
);
gradient = Gradient.radial(startPoint, radius, colors, positions);
if (gradientHash != null) {
_radialGradientCache[gradientHash] = gradient;
}
@ -142,7 +129,7 @@ class GradientStrokeContent extends BaseStrokeContent {
//TODO(xha): cache the shader based on the input parameters and not the animation
// progress.
// At first, log when there is too many cache miss.
int? _getGradientHash(Matrix4 parentMatrix) {
int? _getGradientHash() {
// Don't cache gradient if ValueDelegate.gradient is used
if (_colorCallbackAnimation != null) return null;
@ -160,7 +147,6 @@ class GradientStrokeContent extends BaseStrokeContent {
if (colorProgress != 0) {
hash = hash * 31 * colorProgress;
}
hash *= 31 * parentMatrix.hashCode;
return hash;
}

View file

@ -24,6 +24,7 @@ class PolystarContent implements PathContent, KeyPathElementContent {
static const _polystarMagicNumber = .47829;
static const _polygonMagicNumber = .25;
final _path = Path();
final _lastSegmentPath = Path();
final LottieDrawable lottieDrawable;
final PolystarShape _polystarShape;
@ -275,15 +276,50 @@ class PolystarContent implements PathContent, KeyPathElementContent {
var cp1y = radius * roundedness * _polygonMagicNumber * cp1Dy;
var cp2x = radius * roundedness * _polygonMagicNumber * cp2Dx;
var cp2y = radius * roundedness * _polygonMagicNumber * cp2Dy;
_path.cubicTo(
previousX - cp1x,
previousY - cp1y,
x + cp2x,
y + cp2y,
x,
y,
);
if (i == numPoints - 1) {
// When there is a huge stroke, it will flash if the path ends where it starts.
// We want the final bezier curve to end *slightly* before the start.
// The close() call at the end will complete the polystar.
// https://github.com/airbnb/lottie-android/issues/2329
_lastSegmentPath.reset();
_lastSegmentPath.moveTo(previousX, previousY);
_lastSegmentPath.cubicTo(
previousX - cp1x,
previousY - cp1y,
x + cp2x,
y + cp2y,
x,
y,
);
var metric = _lastSegmentPath.computeMetrics().first;
var position = metric
.getTangentForOffset(metric.length * 0.9999)!
.position;
_path.cubicTo(
previousX - cp1x,
previousY - cp1y,
x + cp2x,
y + cp2y,
position.dx,
position.dy,
);
} else {
_path.cubicTo(
previousX - cp1x,
previousY - cp1y,
x + cp2x,
y + cp2y,
x,
y,
);
}
} else {
if (i == numPoints - 1) {
// When there is a huge stroke, it will flash if the path ends where it starts.
// The close() call should make the path effectively equivalent.
// https://github.com/airbnb/lottie-android/issues/2329
continue;
}
_path.lineTo(x, y);
}

View file

@ -42,6 +42,11 @@ class RoundedCornersContent implements ShapeModifierContent {
// Do nothing.
}
@override
void addUpdateListener(void Function() listener) {
roundedCorners.addUpdateListener(listener);
}
/// Rounded corner algorithm:
/// Iterate through each vertex.
/// If a vertex is a sharp corner, it rounds it.

View file

@ -1,17 +1,22 @@
import 'dart:ui';
import '../../lottie_drawable.dart';
import '../../lottie_property.dart';
import '../../model/content/shape_path.dart';
import '../../model/content/shape_trim_path.dart';
import '../../model/key_path.dart';
import '../../model/layer/base_layer.dart';
import '../../utils.dart';
import '../../utils/misc.dart';
import '../../value/lottie_value_callback.dart';
import '../keyframe/shape_keyframe_animation.dart';
import 'compound_trim_path_content.dart';
import 'content.dart';
import 'key_path_element_content.dart';
import 'path_content.dart';
import 'shape_modifier_content.dart';
import 'trim_path_content.dart';
class ShapeContent implements PathContent {
class ShapeContent implements PathContent, KeyPathElementContent {
final _path = Path();
final ShapePath _shape;
@ -46,6 +51,7 @@ class ShapeContent implements PathContent {
trimPath.addListener(_invalidate);
} else if (content is ShapeModifierContent) {
shapeModifierContents ??= [];
content.addUpdateListener(_invalidate);
shapeModifierContents.add(content);
}
}
@ -57,7 +63,7 @@ class ShapeContent implements PathContent {
@override
Path getPath() {
if (_isPathValid) {
if (_isPathValid && !_shapeAnimation.hasValueCallback) {
return _path;
}
@ -76,4 +82,27 @@ class ShapeContent implements PathContent {
_isPathValid = true;
return _path;
}
@override
void resolveKeyPath(
KeyPath keyPath,
int depth,
List<KeyPath> accumulator,
KeyPath currentPartialKeyPath,
) {
MiscUtils.resolveKeyPath(
keyPath,
depth,
accumulator,
currentPartialKeyPath,
this,
);
}
@override
void addValueCallback<T>(T property, LottieValueCallback<T>? callback) {
if (property == LottieProperty.path) {
_shapeAnimation.setValueCallback(callback as LottieValueCallback<Path>?);
}
}
}

View file

@ -3,4 +3,6 @@ import 'content.dart';
abstract class ShapeModifierContent extends Content {
ShapeData modifyShape(ShapeData shapeData);
void addUpdateListener(void Function() listener);
}

View file

@ -107,7 +107,8 @@ abstract class BaseKeyframeAnimation<K extends Object, A extends Object?> {
var linearProgress = getLinearCurrentKeyframeProgress();
if (valueCallback == null &&
_keyframesWrapper.isCachedValueEnabled(linearProgress)) {
_keyframesWrapper.isCachedValueEnabled(linearProgress) &&
!skipCache) {
return _cachedGetValue!;
}
@ -150,6 +151,11 @@ abstract class BaseKeyframeAnimation<K extends Object, A extends Object?> {
}
}
bool get hasValueCallback => valueCallback != null;
@protected
bool get skipCache => false;
/// keyframeProgress will be [0, 1] unless the interpolator has overshoot in which case, this
/// should be able to handle values outside of that range.
A getValue(Keyframe<K> keyframe, double keyframeProgress);

View file

@ -9,17 +9,19 @@ class ShapeKeyframeAnimation extends BaseKeyframeAnimation<ShapeData, Path> {
final ShapeData _tempShapeData = ShapeData.empty();
final Path _tempPath = Path();
List<ShapeModifierContent>? _shapeModifiers;
Path? _valueCallbackStartPath;
Path? _valueCallbackEndPath;
ShapeKeyframeAnimation(super.keyframes);
@override
Path getValue(Keyframe<ShapeData> keyframe, double keyframeProgress) {
var startShapeData = keyframe.startValue!;
var endShapeData = keyframe.endValue ?? startShapeData;
var endShapeData = keyframe.endValue;
_tempShapeData.interpolateBetween(
startShapeData,
endShapeData,
endShapeData ?? startShapeData,
keyframeProgress,
);
var modifiedShapeData = _tempShapeData;
@ -30,10 +32,39 @@ class ShapeKeyframeAnimation extends BaseKeyframeAnimation<ShapeData, Path> {
}
}
MiscUtils.getPathFromData(modifiedShapeData, _tempPath);
var valueCallback = this.valueCallback;
if (valueCallback != null) {
var startPath = _valueCallbackStartPath ??= Path();
var endPath = _valueCallbackEndPath ??= Path();
MiscUtils.getPathFromData(startShapeData, startPath);
if (endShapeData != null) {
MiscUtils.getPathFromData(endShapeData, endPath);
}
return valueCallback.getValueInternal(
keyframe.startFrame,
keyframe.endFrame,
startPath,
endShapeData == null ? startPath : endPath,
keyframeProgress,
getLinearCurrentKeyframeProgress(),
progress,
) ??
_tempPath;
}
return _tempPath;
}
void setShapeModifiers(List<ShapeModifierContent>? shapeModifiers) {
_shapeModifiers = shapeModifiers;
}
@override
bool get skipCache {
// If there are shape modifiers but no animation on the shape itself, the shape animation
// will think nothing changed and will keep returning its cached value.
var shapeModifiers = _shapeModifiers;
return shapeModifiers != null && shapeModifiers.isNotEmpty;
}
}

View file

@ -26,6 +26,8 @@ class TransformKeyframeAnimation {
_position = animatableTransform.position?.createAnimation(),
_scale = animatableTransform.scale?.createAnimation(),
_rotation = animatableTransform.rotation?.createAnimation(),
_rotationX = animatableTransform.rotationX?.createAnimation(),
_rotationY = animatableTransform.rotationY?.createAnimation(),
_autoOrient = animatableTransform.isAutoOrient,
_skew = animatableTransform.skew?.createAnimation(),
_skewAngle = animatableTransform.skewAngle?.createAnimation(),
@ -42,6 +44,8 @@ class TransformKeyframeAnimation {
BaseKeyframeAnimation<Offset, Offset>? _position;
BaseKeyframeAnimation<Offset, Offset>? _scale;
BaseKeyframeAnimation<double, double>? _rotation;
BaseKeyframeAnimation<double, double>? _rotationX;
BaseKeyframeAnimation<double, double>? _rotationY;
DoubleKeyframeAnimation? _skew;
DoubleKeyframeAnimation? _skewAngle;
@ -65,6 +69,8 @@ class TransformKeyframeAnimation {
layer.addAnimation(_position);
layer.addAnimation(_scale);
layer.addAnimation(_rotation);
layer.addAnimation(_rotationX);
layer.addAnimation(_rotationY);
layer.addAnimation(_skew);
layer.addAnimation(_skewAngle);
}
@ -77,6 +83,8 @@ class TransformKeyframeAnimation {
_position?.addUpdateListener(listener);
_scale?.addUpdateListener(listener);
_rotation?.addUpdateListener(listener);
_rotationX?.addUpdateListener(listener);
_rotationY?.addUpdateListener(listener);
_skew?.addUpdateListener(listener);
_skewAngle?.addUpdateListener(listener);
}
@ -89,6 +97,8 @@ class TransformKeyframeAnimation {
_position?.setProgress(progress);
_scale?.setProgress(progress);
_rotation?.setProgress(progress);
_rotationX?.setProgress(progress);
_rotationY?.setProgress(progress);
_skew?.setProgress(progress);
_skewAngle?.setProgress(progress);
}
@ -132,6 +142,20 @@ class TransformKeyframeAnimation {
}
}
// Approximate X/Y rotation as a 2D foreshortening: scale the perpendicular axis by cos(angle).
if (_rotationY != null) {
final rotationY = _rotationY!.value;
if (rotationY != 0) {
_matrix.scaleByDouble(cos(radians(rotationY)), 1, 1, 1);
}
}
if (_rotationX != null) {
final rotationX = _rotationX!.value;
if (rotationX != 0) {
_matrix.scaleByDouble(1, cos(radians(rotationX)), 1, 1);
}
}
if (_skew != null) {
final mCos = _skewAngle == null
? 0.0
@ -258,6 +282,19 @@ class TransformKeyframeAnimation {
);
}
if (_rotationY != null) {
final rotationY = _rotationY!.value * amount;
if (rotationY != 0) {
_matrix.scaleByDouble(cos(radians(rotationY)), 1, 1, 1);
}
}
if (_rotationX != null) {
final rotationX = _rotationX!.value * amount;
if (rotationX != 0) {
_matrix.scaleByDouble(1, cos(radians(rotationX)), 1, 1);
}
}
return _matrix;
}
@ -329,6 +366,24 @@ class TransformKeyframeAnimation {
} else {
_endOpacity!.setValueCallback(callback as LottieValueCallback<double>?);
}
} else if (property == LottieProperty.transformRotationX) {
if (_rotationX == null) {
_rotationX = ValueCallbackKeyframeAnimation(
callback as LottieValueCallback<double>?,
0.0,
);
} else {
_rotationX!.setValueCallback(callback as LottieValueCallback<double>?);
}
} else if (property == LottieProperty.transformRotationY) {
if (_rotationY == null) {
_rotationY = ValueCallbackKeyframeAnimation(
callback as LottieValueCallback<double>?,
0.0,
);
} else {
_rotationY!.setValueCallback(callback as LottieValueCallback<double>?);
}
} else if (property == LottieProperty.transformSkew) {
_skew ??= DoubleKeyframeAnimation([Keyframe.nonAnimated(0.0)]);
_skew!.setValueCallback(callback as LottieValueCallback<double>?);

View file

@ -77,14 +77,21 @@ class LottieComposition {
if (filePicker != null) {
jsonFile = filePicker(archive.files);
}
jsonFile ??= archive.files.firstWhere((e) => e.name.endsWith('.json'));
jsonFile ??= archive.files.firstWhereOrNull(
(e) => e.name.startsWith('animations/') && e.name.endsWith('.json'),
);
jsonFile ??= archive.files.firstWhere(
(e) =>
e.name.endsWith('.json') && p.basename(e.name) != 'manifest.json',
);
var composition = parseJsonBytes(jsonFile.content);
for (var image in composition.images.values) {
var imagePath = p.posix.join(image.dirName, image.fileName);
var found = archive.files.firstWhereOrNull(
(f) => f.name.toLowerCase() == imagePath.toLowerCase(),
(f) =>
_normalizeArchivePath(f.name) == _normalizeArchivePath(imagePath),
);
ImageProvider? provider;
@ -119,6 +126,14 @@ class LottieComposition {
return null;
}
static String _normalizeArchivePath(String path) {
var normalized = p.posix.normalize(path).toLowerCase();
if (normalized.startsWith('/')) {
normalized = normalized.substring(1);
}
return normalized;
}
static Future<LottieComposition?> decodeGZip(List<int> bytes) async {
if (bytes[0] == 31 && bytes[1] == 139) {
var decodedBytes = GZipDecoder().decodeBytes(bytes);

View file

@ -389,6 +389,12 @@ class Lottie extends StatefulWidget {
class _LottieState extends State<Lottie> with TickerProviderStateMixin {
late AnimationController _autoAnimation;
/// The last frame we rebuilt for, expressed as the frame-rate-rounded
/// progress. The vsync ticker notifies us every frame, but we only rebuild
/// when this value actually changes, so build/paint run at the composition's
/// frame rate instead of the display refresh rate.
double? _renderedProgress;
@override
void initState() {
super.initState();
@ -397,6 +403,7 @@ class _LottieState extends State<Lottie> with TickerProviderStateMixin {
vsync: this,
duration: widget.composition?.duration ?? const Duration(seconds: 1),
);
_progressAnimation.addListener(_onProgressChanged);
_updateAutoAnimation();
}
@ -404,6 +411,13 @@ class _LottieState extends State<Lottie> with TickerProviderStateMixin {
void didUpdateWidget(Lottie oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
(oldWidget.controller ?? _autoAnimation).removeListener(
_onProgressChanged,
);
_progressAnimation.addListener(_onProgressChanged);
}
_autoAnimation.duration =
widget.composition?.duration ?? const Duration(seconds: 1);
_updateAutoAnimation();
@ -421,8 +435,25 @@ class _LottieState extends State<Lottie> with TickerProviderStateMixin {
}
}
/// Called on every vsync tick. Quantizes the raw progress to the target frame
/// rate and only triggers a rebuild when the resulting frame changes.
void _onProgressChanged() {
var rounded =
widget.composition?.roundProgress(
_progressAnimation.value,
frameRate: _frameRate,
) ??
_progressAnimation.value;
if (rounded != _renderedProgress) {
setState(() => _renderedProgress = rounded);
}
}
FrameRate get _frameRate => widget.frameRate ?? FrameRate.composition;
@override
void dispose() {
_progressAnimation.removeListener(_onProgressChanged);
_autoAnimation.dispose();
super.dispose();
}
@ -432,23 +463,18 @@ class _LottieState extends State<Lottie> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
Widget child = AnimatedBuilder(
animation: _progressAnimation,
builder: (context, _) {
return RawLottie(
composition: widget.composition,
delegates: widget.delegates,
options: widget.options,
progress: _progressAnimation.value,
frameRate: widget.frameRate,
width: widget.width,
height: widget.height,
fit: widget.fit,
alignment: widget.alignment,
filterQuality: widget.filterQuality,
renderCache: widget.renderCache,
);
},
Widget child = RawLottie(
composition: widget.composition,
delegates: widget.delegates,
options: widget.options,
progress: _progressAnimation.value,
frameRate: widget.frameRate,
width: widget.width,
height: widget.height,
fit: widget.fit,
alignment: widget.alignment,
filterQuality: widget.filterQuality,
renderCache: widget.renderCache,
);
if (widget.addRepaintBoundary) {

View file

@ -222,8 +222,6 @@ class LottieDrawable {
var destinationRect = destinationPosition & destinationSize;
var sourceRect = alignment.inscribe(sourceSize, Offset.zero & inputSize);
_matrix.setIdentity();
var cacheUsed = false;
if (renderCache != null) {
var progressForCache = _progressAliases[progress] ?? progress;
@ -243,12 +241,7 @@ class LottieDrawable {
if (!cacheUsed) {
canvas.save();
canvas.translate(destinationRect.left, destinationRect.top);
_matrix.scaleByDouble(
destinationSize.width / sourceRect.width,
destinationSize.height / sourceRect.height,
destinationSize.width / sourceRect.width,
1,
);
_matrix.setSourceToDestinationScale(sourceRect, destinationSize);
_compositionLayer.draw(canvas, _matrix, parentAlpha: 255);
canvas.restore();
}

View file

@ -83,6 +83,12 @@ abstract class LottieProperty {
/// In degrees */
static const double transformRotation = 12.0;
/// In degrees (3D X-axis rotation) */
static const double transformRotationX = 30.0;
/// In degrees (3D Y-axis rotation) */
static const double transformRotationY = 31.0;
/// 0-85 */
static const double transformSkew = 13.0;
@ -141,6 +147,9 @@ abstract class LottieProperty {
static final List<Color> gradientColor = [];
/// Replace the outline of a shape (`ShapeContent`) at runtime.
static final Path path = Path();
/// Replace the text for a text layer.
static const text = 'dynamic_text';
}

View file

@ -1,16 +1,9 @@
import 'animatable_color_value.dart';
import 'animatable_double_value.dart';
import 'animatable_text_range_selector.dart';
import 'animatable_text_style.dart';
class AnimatableTextProperties {
final AnimatableColorValue? color;
final AnimatableColorValue? stroke;
final AnimatableDoubleValue? strokeWidth;
final AnimatableDoubleValue? tracking;
final AnimatableTextStyle? textStyle;
final AnimatableTextRangeSelector? rangeSelector;
AnimatableTextProperties({
this.color,
this.stroke,
this.strokeWidth,
this.tracking,
});
AnimatableTextProperties({this.textStyle, this.rangeSelector});
}

View file

@ -0,0 +1,17 @@
import '../content/text_range_units.dart';
import 'animatable_integer_value.dart';
/// Defines an animated range of text that should have an [AnimatableTextStyle] applied to it.
class AnimatableTextRangeSelector {
final AnimatableIntegerValue? start;
final AnimatableIntegerValue? end;
final AnimatableIntegerValue? offset;
final TextRangeUnits units;
AnimatableTextRangeSelector({
this.start,
this.end,
this.offset,
required this.units,
});
}

View file

@ -0,0 +1,19 @@
import 'animatable_color_value.dart';
import 'animatable_double_value.dart';
import 'animatable_integer_value.dart';
class AnimatableTextStyle {
final AnimatableColorValue? color;
final AnimatableColorValue? stroke;
final AnimatableDoubleValue? strokeWidth;
final AnimatableDoubleValue? tracking;
final AnimatableIntegerValue? opacity;
AnimatableTextStyle({
this.color,
this.stroke,
this.strokeWidth,
this.tracking,
this.opacity,
});
}

View file

@ -20,6 +20,11 @@ class AnimatableTransform implements ModifierContent, ContentModel {
final AnimatableDoubleValue? rotation;
// 3D rotation. [rotation] is the Z rotation.
final AnimatableDoubleValue? rotationX;
final AnimatableDoubleValue? rotationY;
final AnimatableIntegerValue? opacity;
final AnimatableDoubleValue? skew;
@ -39,6 +44,8 @@ class AnimatableTransform implements ModifierContent, ContentModel {
this.position,
this.scale,
this.rotation,
this.rotationX,
this.rotationY,
this.opacity,
this.skew,
this.skewAngle,

View file

@ -0,0 +1,2 @@
/// `index` is named [charIndex] because every Dart enum already defines an `index` getter.
enum TextRangeUnits { percent, charIndex }

View file

@ -246,7 +246,7 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
canvas.saveLayer(bounds, _mattePaint);
L.endSection('Layer#saveLayer');
_clearCanvas(canvas, bounds);
_matteLayer!.draw(canvas, parentMatrix, parentAlpha: alpha);
_matteLayer!.draw(canvas, parentMatrix, parentAlpha: parentAlpha);
L.beginSection('Layer#restoreLayer');
canvas.restore();
L.endSection('Layer#restoreLayer');

View file

@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import '../../animation/content/content_group.dart';
import '../../animation/keyframe/base_keyframe_animation.dart';
@ -9,6 +10,7 @@ import '../../lottie_property.dart';
import '../../utils.dart';
import '../../utils/characters.dart';
import '../../value/lottie_value_callback.dart';
import '../content/text_range_units.dart';
import '../document_data.dart';
import '../font.dart';
import '../font_character.dart';
@ -48,6 +50,16 @@ class TextLayer extends BaseLayer {
BaseKeyframeAnimation<double, double>? _textSizeCallbackAnimation;
BaseKeyframeAnimation<int, int>? _opacityAnimation;
BaseKeyframeAnimation<int, int>? _textRangeStartAnimation;
BaseKeyframeAnimation<int, int>? _textRangeEndAnimation;
BaseKeyframeAnimation<int, int>? _textRangeOffsetAnimation;
TextRangeUnits _textRangeUnits = TextRangeUnits.charIndex;
TextLayer(LottieDrawable lottieDrawable, Layer layerModel)
: _composition = layerModel.composition,
_textAnimation = layerModel.text!.createAnimation(),
@ -56,29 +68,59 @@ class TextLayer extends BaseLayer {
addAnimation(_textAnimation);
var textProperties = layerModel.textProperties;
if (textProperties != null && textProperties.color != null) {
_colorAnimation = textProperties.color!.createAnimation()
var textStyle = textProperties?.textStyle;
if (textStyle != null && textStyle.color != null) {
_colorAnimation = textStyle.color!.createAnimation()
..addUpdateListener(invalidateSelf);
addAnimation(_colorAnimation);
}
if (textProperties != null && textProperties.stroke != null) {
_strokeColorAnimation = textProperties.stroke!.createAnimation()
if (textStyle != null && textStyle.stroke != null) {
_strokeColorAnimation = textStyle.stroke!.createAnimation()
..addUpdateListener(invalidateSelf);
addAnimation(_strokeColorAnimation);
}
if (textProperties != null && textProperties.strokeWidth != null) {
_strokeWidthAnimation = textProperties.strokeWidth!.createAnimation()
if (textStyle != null && textStyle.strokeWidth != null) {
_strokeWidthAnimation = textStyle.strokeWidth!.createAnimation()
..addUpdateListener(invalidateSelf);
addAnimation(_strokeWidthAnimation);
}
if (textProperties != null && textProperties.tracking != null) {
_trackingAnimation = textProperties.tracking!.createAnimation()
if (textStyle != null && textStyle.tracking != null) {
_trackingAnimation = textStyle.tracking!.createAnimation()
..addUpdateListener(invalidateSelf);
addAnimation(_trackingAnimation);
}
if (textStyle != null && textStyle.opacity != null) {
_opacityAnimation = textStyle.opacity!.createAnimation()
..addUpdateListener(invalidateSelf);
addAnimation(_opacityAnimation);
}
var rangeSelector = textProperties?.rangeSelector;
if (rangeSelector != null && rangeSelector.start != null) {
_textRangeStartAnimation = rangeSelector.start!.createAnimation()
..addUpdateListener(invalidateSelf);
addAnimation(_textRangeStartAnimation);
}
if (rangeSelector != null && rangeSelector.end != null) {
_textRangeEndAnimation = rangeSelector.end!.createAnimation()
..addUpdateListener(invalidateSelf);
addAnimation(_textRangeEndAnimation);
}
if (rangeSelector != null && rangeSelector.offset != null) {
_textRangeOffsetAnimation = rangeSelector.offset!.createAnimation()
..addUpdateListener(invalidateSelf);
addAnimation(_textRangeOffsetAnimation);
}
if (rangeSelector != null) {
_textRangeUnits = rangeSelector.units;
}
}
@override
@ -107,24 +149,41 @@ class TextLayer extends BaseLayer {
canvas.save();
canvas.transform(parentMatrix.storage);
_configurePaint(documentData, parentAlpha);
_configurePaint(documentData, parentAlpha, 0);
if (lottieDrawable.useTextGlyphs) {
_drawTextWithGlyphs(documentData, parentMatrix, font, canvas);
_drawTextWithGlyphs(
documentData,
parentMatrix,
font,
canvas,
parentAlpha,
);
} else {
_drawTextWithFont(documentData, font, canvas);
_drawTextWithFont(documentData, font, canvas, parentAlpha);
}
canvas.restore();
}
void _configurePaint(DocumentData documentData, int parentAlpha) {
/// Configures the [_fillPaint] and [_strokePaint] used for drawing based on the
/// currently active text ranges.
///
/// [parentAlpha] is a value from 0 to 255 indicating the alpha of the parented layer.
void _configurePaint(
DocumentData documentData,
int parentAlpha,
int indexInDocument,
) {
Color fillPaintColor;
if (_colorCallbackAnimation != null) {
// dynamic property takes priority
fillPaintColor = _colorCallbackAnimation!.value;
} else if (_colorAnimation != null) {
} else if (_colorAnimation != null &&
_isIndexInRangeSelection(indexInDocument)) {
fillPaintColor = _colorAnimation!.value;
} else {
// fall back to the document color
fillPaintColor = documentData.color;
}
_fillPaint.color = fillPaintColor.withValues(alpha: _fillPaint.color.a);
@ -132,7 +191,8 @@ class TextLayer extends BaseLayer {
Color strokePaintColor;
if (_strokeColorCallbackAnimation != null) {
strokePaintColor = _strokeColorCallbackAnimation!.value;
} else if (_strokeColorAnimation != null) {
} else if (_strokeColorAnimation != null &&
_isIndexInRangeSelection(indexInDocument)) {
strokePaintColor = _strokeColorAnimation!.value;
} else {
strokePaintColor = documentData.strokeColor;
@ -141,25 +201,78 @@ class TextLayer extends BaseLayer {
alpha: _strokePaint.color.a,
);
var opacity = transform.opacity?.value ?? 100;
var alpha = opacity * 255 / 100 * parentAlpha ~/ 255;
// These opacity values are in the range 0 to 100.
var transformOpacity = transform.opacity?.value ?? 100;
var textRangeOpacity =
_opacityAnimation != null && _isIndexInRangeSelection(indexInDocument)
? _opacityAnimation!.value
: 100;
// This alpha value needs to be in the range 0 to 255 to be applied to the Paint
// instances. We map the layer transform's opacity into that range and multiply it by
// the fractional opacity of the text range and the parent.
var alpha =
(transformOpacity *
255 /
100 *
(textRangeOpacity / 100) *
parentAlpha /
255)
.round();
_fillPaint.setAlpha(alpha);
_strokePaint.setAlpha(alpha);
if (_strokeWidthCallbackAnimation != null) {
_strokePaint.strokeWidth = _strokeWidthCallbackAnimation!.value;
} else if (_strokeWidthAnimation != null) {
} else if (_strokeWidthAnimation != null &&
_isIndexInRangeSelection(indexInDocument)) {
_strokePaint.strokeWidth = _strokeWidthAnimation!.value;
} else {
_strokePaint.strokeWidth = documentData.strokeWidth;
}
}
bool _isIndexInRangeSelection(int indexInDocument) {
var textLength = _textAnimation.value.text.length;
var textRangeStartAnimation = _textRangeStartAnimation;
var textRangeEndAnimation = _textRangeEndAnimation;
if (textRangeStartAnimation != null && textRangeEndAnimation != null) {
// After Effects supports reversed text ranges where the start index is greater than
// the end index. For the purposes of determining if the given index is inside of the
// range, we take the start as the smaller value.
var rangeStart = math.min(
textRangeStartAnimation.value,
textRangeEndAnimation.value,
);
var rangeEnd = math.max(
textRangeStartAnimation.value,
textRangeEndAnimation.value,
);
var textRangeOffsetAnimation = _textRangeOffsetAnimation;
if (textRangeOffsetAnimation != null) {
var offset = textRangeOffsetAnimation.value;
rangeStart += offset;
rangeEnd += offset;
}
if (_textRangeUnits == TextRangeUnits.charIndex) {
return indexInDocument >= rangeStart && indexInDocument < rangeEnd;
} else {
var currentIndexAsPercent = indexInDocument / textLength * 100;
return currentIndexAsPercent >= rangeStart &&
currentIndexAsPercent < rangeEnd;
}
}
return true;
}
void _drawTextWithGlyphs(
DocumentData documentData,
Matrix4 parentMatrix,
Font font,
Canvas canvas,
int parentAlpha,
) {
double textSize;
if (_textSizeCallbackAnimation != null) {
@ -211,6 +324,7 @@ class TextLayer extends BaseLayer {
parentScale,
fontScale,
tracking,
parentAlpha,
);
canvas.restore();
@ -226,21 +340,37 @@ class TextLayer extends BaseLayer {
double parentScale,
double fontScale,
double tracking,
int parentAlpha,
) {
var index = 0;
for (var c in text) {
var characterHash = FontCharacter.hashFor(c, font.family, font.style);
var character = _composition.characters[characterHash];
if (character == null) {
// Something is wrong. Potentially, they didn't export the text as a glyph.
index++;
continue;
}
_drawCharacterAsGlyph(character, fontScale, documentData, canvas);
_drawCharacterAsGlyph(
character,
fontScale,
documentData,
canvas,
index,
parentAlpha,
);
var tx = character.width * fontScale + tracking;
canvas.translate(tx, 0);
index++;
}
}
void _drawTextWithFont(DocumentData documentData, Font font, Canvas canvas) {
void _drawTextWithFont(
DocumentData documentData,
Font font,
Canvas canvas,
int parentAlpha,
) {
var textStyle = lottieDrawable.getTextStyle(font.family, font.style);
var text = documentData.text;
var textDelegate = lottieDrawable.delegates?.text;
@ -270,6 +400,7 @@ class TextLayer extends BaseLayer {
var textLines = _getTextLines(text);
var textLineCount = textLines.length;
var lineIndex = -1;
var characterIndexAtStartOfLine = 0;
for (var i = 0; i < textLineCount; i++) {
var textLine = textLines[i];
var boxWidth = documentData.boxSize?.dx ?? 0.0;
@ -288,7 +419,17 @@ class TextLayer extends BaseLayer {
canvas.save();
_offsetCanvas(canvas, documentData, lineIndex, line.width);
_drawFontTextLine(line.text, textStyle, documentData, canvas, tracking);
_drawFontTextLine(
line.text,
textStyle,
documentData,
canvas,
tracking,
characterIndexAtStartOfLine,
parentAlpha,
);
characterIndexAtStartOfLine += line.text.length;
canvas.restore();
}
@ -332,16 +473,28 @@ class TextLayer extends BaseLayer {
return textLinesArray.map((l) => l.characters).toList();
}
/// [characterIndexAtStartOfLine] is the index within the overall document of the character
/// at the start of the line.
void _drawFontTextLine(
Characters text,
TextStyle textStyle,
DocumentData documentData,
Canvas canvas,
double tracking,
int characterIndexAtStartOfLine,
int parentAlpha,
) {
var index = 0;
for (var char in text) {
var charString = char;
_drawCharacterFromFont(charString, textStyle, documentData, canvas);
_drawCharacterFromFont(
charString,
textStyle,
documentData,
canvas,
characterIndexAtStartOfLine + index,
parentAlpha,
);
var textPainter = TextPainter(
text: TextSpan(text: charString, style: textStyle),
textDirection: _textDirection,
@ -350,6 +503,7 @@ class TextLayer extends BaseLayer {
var charWidth = textPainter.width;
var tx = charWidth + tracking;
canvas.translate(tx, 0);
index++;
}
}
@ -463,7 +617,10 @@ class TextLayer extends BaseLayer {
double fontScale,
DocumentData documentData,
Canvas canvas,
int indexInDocument,
int parentAlpha,
) {
_configurePaint(documentData, parentAlpha, indexInDocument);
var contentGroups = _getContentsForCharacter(character);
for (var j = 0; j < contentGroups.length; j++) {
var path = contentGroups[j].getPath();
@ -496,7 +653,10 @@ class TextLayer extends BaseLayer {
TextStyle textStyle,
DocumentData documentData,
Canvas canvas,
int indexInDocument,
int parentAlpha,
) {
_configurePaint(documentData, parentAlpha, indexInDocument);
if (documentData.strokeOverFill) {
_drawCharacter(character, textStyle, _fillPaint, canvas);
_drawCharacter(character, textStyle, _strokePaint, canvas);

View file

@ -1,14 +1,35 @@
import '../composition.dart';
import '../model/animatable/animatable_color_value.dart';
import '../model/animatable/animatable_double_value.dart';
import '../model/animatable/animatable_integer_value.dart';
import '../model/animatable/animatable_text_properties.dart';
import '../model/animatable/animatable_text_range_selector.dart';
import '../model/animatable/animatable_text_style.dart';
import '../model/content/text_range_units.dart';
import '../value/keyframe.dart';
import 'animatable_value_parser.dart';
import 'moshi/json_reader.dart';
class AnimatableTextPropertiesParser {
static final JsonReaderOptions _propertiesNames = JsonReaderOptions.of(['a']);
static final JsonReaderOptions _propertiesNames = JsonReaderOptions.of([
's', // Range selector
'a', // Text style for this range
]);
static final JsonReaderOptions _animatableRangePropertiesNames =
JsonReaderOptions.of([
's', // start
'e', // end
'o', // offset
'r', // text range units (percent or index)
]);
static final JsonReaderOptions _animatablePropertiesNames =
JsonReaderOptions.of(['fc', 'sc', 'sw', 't']);
JsonReaderOptions.of([
'fc',
'sc',
'sw',
't',
'o', // opacity
]);
AnimatableTextPropertiesParser();
@ -16,27 +37,84 @@ class AnimatableTextPropertiesParser {
JsonReader reader,
LottieComposition composition,
) {
AnimatableTextProperties? anim;
AnimatableTextStyle? textStyle;
AnimatableTextRangeSelector? rangeSelector;
reader.beginObject();
while (reader.hasNext()) {
switch (reader.selectName(_propertiesNames)) {
case 0:
anim = _parseAnimatableTextProperties(reader, composition);
case 0: // Range selector
rangeSelector = _parseAnimatableTextRangeSelector(
reader,
composition,
);
case 1: // Text style for this range
textStyle = _parseAnimatableTextStyle(reader, composition);
default:
reader.skipName();
reader.skipValue();
}
}
reader.endObject();
if (anim == null) {
// Not sure if this is possible.
return AnimatableTextProperties();
}
return anim;
return AnimatableTextProperties(
textStyle: textStyle,
rangeSelector: rangeSelector,
);
}
static AnimatableTextProperties _parseAnimatableTextProperties(
static AnimatableTextRangeSelector _parseAnimatableTextRangeSelector(
JsonReader reader,
LottieComposition composition,
) {
AnimatableIntegerValue? start;
AnimatableIntegerValue? end;
AnimatableIntegerValue? offset;
var units = TextRangeUnits.charIndex;
reader.beginObject();
while (reader.hasNext()) {
switch (reader.selectName(_animatableRangePropertiesNames)) {
case 0: // start
start = AnimatableValueParser.parseInteger(reader, composition);
case 1: // end
end = AnimatableValueParser.parseInteger(reader, composition);
case 2: // offset
offset = AnimatableValueParser.parseInteger(reader, composition);
case 3: // text range units (percent or index)
var textRangeUnits = reader.nextInt();
if (textRangeUnits != 1 && textRangeUnits != 2) {
composition.addWarning(
'Unsupported text range units: $textRangeUnits',
);
units = TextRangeUnits.charIndex;
} else {
units = textRangeUnits == 1
? TextRangeUnits.percent
: TextRangeUnits.charIndex;
}
default:
reader.skipName();
reader.skipValue();
}
}
reader.endObject();
// If no start value is provided, default to a non-animated value of 0 to match
// After Effects/Bodymovin.
if (start == null && end != null) {
start = AnimatableIntegerValue.fromKeyframes([Keyframe.nonAnimated(0)]);
}
return AnimatableTextRangeSelector(
start: start,
end: end,
offset: offset,
units: units,
);
}
static AnimatableTextStyle _parseAnimatableTextStyle(
JsonReader reader,
LottieComposition composition,
) {
@ -44,6 +122,7 @@ class AnimatableTextPropertiesParser {
AnimatableColorValue? stroke;
AnimatableDoubleValue? strokeWidth;
AnimatableDoubleValue? tracking;
AnimatableIntegerValue? opacity;
reader.beginObject();
while (reader.hasNext()) {
@ -56,6 +135,8 @@ class AnimatableTextPropertiesParser {
strokeWidth = AnimatableValueParser.parseFloat(reader, composition);
case 3:
tracking = AnimatableValueParser.parseFloat(reader, composition);
case 4: // opacity
opacity = AnimatableValueParser.parseInteger(reader, composition);
default:
reader.skipName();
reader.skipValue();
@ -63,11 +144,12 @@ class AnimatableTextPropertiesParser {
}
reader.endObject();
return AnimatableTextProperties(
return AnimatableTextStyle(
color: color,
stroke: stroke,
strokeWidth: strokeWidth,
tracking: tracking,
opacity: opacity,
);
}
}

View file

@ -26,6 +26,8 @@ class AnimatableTransformParser {
'eo',
'sk',
'sa',
'rx',
'ry',
]);
static final JsonReaderOptions _animatableNames = JsonReaderOptions.of(['k']);
@ -37,6 +39,8 @@ class AnimatableTransformParser {
AnimatableValue<Offset, Offset>? position;
AnimatableScaleValue? scale;
AnimatableDoubleValue? rotation;
AnimatableDoubleValue? rotationX;
AnimatableDoubleValue? rotationY;
AnimatableIntegerValue? opacity;
AnimatableDoubleValue? startOpacity;
AnimatableDoubleValue? endOpacity;
@ -74,10 +78,7 @@ class AnimatableTransformParser {
scale = AnimatableValueParser.parseScale(reader, composition);
case 3:
case 4:
if (name == 3) {
composition.addWarning("Lottie doesn't support 3D layers.");
}
// Both "rz" (3D Z rotation) and "r" (2D rotation) feed the Z rotation.
// Sometimes split path rotation gets exported like:
// "rz": {
// "a": 1,
@ -116,6 +117,10 @@ class AnimatableTransformParser {
skew = AnimatableValueParser.parseFloat(reader, composition);
case 9:
skewAngle = AnimatableValueParser.parseFloat(reader, composition);
case 10:
rotationX = AnimatableValueParser.parseFloat(reader, composition);
case 11:
rotationY = AnimatableValueParser.parseFloat(reader, composition);
default:
reader.skipName();
reader.skipValue();
@ -134,6 +139,12 @@ class AnimatableTransformParser {
if (isRotationIdentity(rotation)) {
rotation = null;
}
if (isRotationIdentity(rotationX)) {
rotationX = null;
}
if (isRotationIdentity(rotationY)) {
rotationY = null;
}
if (isScaleIdentity(scale)) {
scale = null;
}
@ -148,6 +159,8 @@ class AnimatableTransformParser {
position: position,
scale: scale,
rotation: rotation,
rotationX: rotationX,
rotationY: rotationY,
opacity: opacity,
startOpacity: startOpacity,
endOpacity: endOpacity,

View file

@ -69,7 +69,7 @@ class FileLottie extends LottieProvider {
}
@override
int get hashCode => file.hashCode;
int get hashCode => file.path.hashCode;
@override
String toString() => '$runtimeType(file: ${file.path})';

View file

@ -35,3 +35,34 @@ class CacheKey {
String toString() =>
'CacheKey(${composition.hashCode}, $size, $config, $delegates)';
}
/// A [CacheKey] for the raster cache, where frames are rasterized and cropped
/// to [sourceRect] ahead of time. Unlike the drawing-commands cache (whose
/// cached [Picture] is recorded in full composition space and cropped fresh
/// on every draw), the same [composition]/[size] with a different
/// [sourceRect] (i.e. a different `fit`/`alignment`) must not share a cached
/// image.
@immutable
class RasterCacheKey extends CacheKey {
final Rect sourceRect;
RasterCacheKey({
required super.composition,
required super.size,
required this.sourceRect,
required super.config,
required super.delegates,
});
@override
int get hashCode => Object.hash(super.hashCode, sourceRect);
@override
bool operator ==(other) =>
other is RasterCacheKey &&
super == other &&
other.sourceRect == sourceRect;
@override
String toString() => '${super.toString()}, sourceRect: $sourceRect';
}

View file

@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:flutter/rendering.dart';
import '../lottie_drawable.dart';
import '../render_cache.dart';
import '../utils.dart';
import 'key.dart';
import 'store.dart';
@ -107,14 +108,8 @@ base class DrawingEntry extends CacheEntry<CacheKey> {
drawable.compositionLayer.draw(cacheCanvas, _matrix, parentAlpha: 255);
});
if (cachedImage != null) {
var destinationSize = destinationRect.size;
canvas.save();
canvas.translate(destinationRect.left, destinationRect.top);
canvas.scale(
destinationSize.width / sourceRect.width,
destinationSize.height / sourceRect.height,
);
canvas.applySourceToDestinationTransform(sourceRect, destinationRect);
canvas.drawPicture(cachedImage);
canvas.restore();

View file

@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:flutter/rendering.dart';
import '../lottie_drawable.dart';
import '../render_cache.dart';
import '../utils.dart';
import 'key.dart';
import 'store.dart';
@ -27,7 +28,7 @@ class RenderCacheRaster implements RenderCache {
}
class RasterAnimationCache extends AnimationCache {
final Handle<RasterEntry, CacheKey> handle;
final Handle<RasterEntry, RasterCacheKey> handle;
RasterAnimationCache(this.handle);
@ -52,9 +53,10 @@ class RasterAnimationCache extends AnimationCache {
(rect.size.height * devicePixelRatio).roundToDouble(),
);
var key = CacheKey(
var key = RasterCacheKey(
composition: drawable.composition,
size: cacheImageSize,
sourceRect: sourceRect,
config: drawable.configHash(),
delegates: drawable.delegatesHash(),
);
@ -72,7 +74,7 @@ class RasterAnimationCache extends AnimationCache {
}
}
class RasterStore extends Store<RasterEntry, CacheKey> {
class RasterStore extends Store<RasterEntry, RasterCacheKey> {
final int maxMemory;
RasterStore(this.maxMemory);
@ -88,7 +90,7 @@ class RasterStore extends Store<RasterEntry, CacheKey> {
}
@override
RasterEntry createEntry(CacheKey key) {
RasterEntry createEntry(RasterCacheKey key) {
return RasterEntry(this, key);
}
@ -97,7 +99,7 @@ class RasterStore extends Store<RasterEntry, CacheKey> {
}
}
base class RasterEntry extends CacheEntry<CacheKey> {
base class RasterEntry extends CacheEntry<RasterCacheKey> {
final RasterStore store;
final images = <double, Image>{};
int currentMemory = 0;
@ -146,13 +148,7 @@ base class RasterEntry extends CacheEntry<CacheKey> {
var cacheImageSize = key.size;
var cachedImage = imageForProgress(progress, (cacheCanvas) {
_matrix.setIdentity();
_matrix.scaleByDouble(
cacheImageSize.width / sourceSize.width,
cacheImageSize.height / sourceSize.height,
cacheImageSize.width / sourceSize.width,
1,
);
_matrix.setSourceToDestinationScale(sourceRect, cacheImageSize);
drawable.compositionLayer.draw(cacheCanvas, _matrix, parentAlpha: 255);
});
if (cachedImage != null) {

View file

@ -17,13 +17,13 @@ class RenderLottie extends RenderBox {
bool? enableApplyingOpacityToLayers,
double progress = 0.0,
FrameRate? frameRate,
double? width,
double? height,
BoxFit? fit,
AlignmentGeometry alignment = Alignment.center,
this._width,
this._height,
this._fit,
this._alignment = Alignment.center,
FilterQuality? filterQuality,
RenderCache? renderCache,
required double devicePixelRatio,
required this._devicePixelRatio,
}) : assert(progress >= 0.0 && progress <= 1.0),
assert(
renderCache == null || frameRate != FrameRate.max,
@ -39,12 +39,7 @@ class RenderLottie extends RenderBox {
enableApplyingOpacityToLayers ?? false
..filterQuality = filterQuality)
: null,
_width = width,
_height = height,
_fit = fit,
_alignment = alignment,
_renderCache = renderCache,
_devicePixelRatio = devicePixelRatio;
_renderCache = renderCache;
/// The lottie composition to display.
LottieComposition? get composition => _drawable?.composition;

View file

@ -69,6 +69,37 @@ extension Matrix4Extension on Matrix4 {
return p0.x == p1.x || p0.y == p1.y;
}
/// Sets this matrix to map `p -> (p - sourceRect.topLeft) * scale`,
/// i.e. crop to [sourceRect] and scale it to fit [destinationSize].
void setSourceToDestinationScale(Rect sourceRect, Size destinationSize) {
var scaleX = destinationSize.width / sourceRect.width;
var scaleY = destinationSize.height / sourceRect.height;
setIdentity();
translateByDouble(
-sourceRect.left * scaleX,
-sourceRect.top * scaleY,
0,
1,
);
scaleByDouble(scaleX, scaleY, scaleX, 1);
}
}
extension CanvasExtension on Canvas {
/// Applies `destinationRect.topLeft + (p - sourceRect.topLeft) * scale`
/// to subsequent drawing, i.e. positions and crops/scales a source rect
/// into a destination rect.
void applySourceToDestinationTransform(
Rect sourceRect,
Rect destinationRect,
) {
var scaleX = destinationRect.width / sourceRect.width;
var scaleY = destinationRect.height / sourceRect.height;
translate(destinationRect.left, destinationRect.top);
scale(scaleX, scaleY);
translate(-sourceRect.left, -sourceRect.top);
}
}
extension OffsetExtension on Offset {

View file

@ -394,6 +394,14 @@ class ValueDelegate<T> {
DropShadow Function(LottieFrameInfo<DropShadow>)? callback,
}) => ValueDelegate._(keyPath, LottieProperty.dropShadow, value, callback);
/// Replace the outline of a shape at runtime. The callback receives the start/end paths of
/// the underlying shape keyframe and returns the [Path] to draw.
static ValueDelegate<Path> path(
List<String> keyPath, {
Path? value,
Path Function(LottieFrameInfo<Path>)? callback,
}) => ValueDelegate._(keyPath, LottieProperty.path, value, callback);
ResolvedValueDelegate<T>? _resolved;
ResolvedValueDelegate _resolve(List<KeyPath> resolvedPaths) {
_resolved = ResolvedValueDelegate<T>(this, resolvedPaths);

View file

@ -1,15 +1,18 @@
name: lottie
description: Render After Effects animations natively on Flutter. This package is a pure Dart implementation of a Lottie player.
version: 3.3.2
version: 3.5.1
repository: https://github.com/xvrh/lottie-flutter
workspace:
- example
funding:
- https://www.buymeacoffee.com/xvrh
- https://github.com/sponsors/xvrh
environment:
sdk: '^3.9.0'
flutter: '>=3.35.0'
sdk: '^3.12.0'
flutter: '>=3.44.0'
dependencies:
archive: ^4.0.0

View file

@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/src/utils/characters.dart';
void main() {
test('TrimTrailing characters', () {
expect(
'ab c d '.characters.trimTrailing(' '.characters),
'ab c d'.characters,
);
expect(' '.characters.trimTrailing(' '.characters), ''.characters);
expect(' a '.characters.trimTrailing(' '.characters), ' a'.characters);
expect(' aa'.characters.trimTrailing('a'.characters), ' '.characters);
expect('aabcbc'.characters.trimTrailing('bc'.characters), 'aa'.characters);
expect('bcbc'.characters.trimTrailing('bc'.characters), ''.characters);
expect(''.characters.trimTrailing(' '.characters), ''.characters);
expect(''.characters.trimTrailing('bc'.characters), ''.characters);
expect(' '.characters.trimTrailing('bc'.characters), ' '.characters);
expect(' bc'.characters.trimTrailing('bc'.characters), ' '.characters);
});
test('TrimLeading characters', () {
expect(' ab '.characters.trimLeading(' '.characters), 'ab '.characters);
expect(' '.characters.trimLeading(' '.characters), ''.characters);
expect(' '.characters.trimLeading(' '.characters), ''.characters);
expect(' a'.characters.trimLeading(' '.characters), 'a'.characters);
expect('abc'.characters.trimLeading('ab'.characters), 'c'.characters);
expect('ababc'.characters.trimLeading('ab'.characters), 'c'.characters);
expect('abab'.characters.trimLeading('ab'.characters), ''.characters);
expect('ababcd'.characters.trimLeading('ab'.characters), 'cd'.characters);
expect(''.characters.trimLeading(''.characters), ''.characters);
});
test('Trim characters', () {
expect(' ab '.characters.trim(' '.characters), 'ab'.characters);
});
}

View file

@ -1,32 +0,0 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/src/composition.dart';
import 'package:lottie/src/lottie_drawable.dart';
import 'package:path/path.dart' as p;
void main() {
var assetsPath = 'example/assets';
for (var file
in Directory(assetsPath)
.listSync(recursive: true)
.whereType<File>()
.where((f) => f.path.endsWith('.json'))) {
test('Parse and draw ${p.relative(file.path, from: assetsPath)}', () async {
var composition = await LottieComposition.fromBytes(
file.readAsBytesSync(),
);
expect(composition, isNotNull);
var drawable = LottieDrawable(composition);
var recorder = PictureRecorder();
var canvas = Canvas(recorder);
for (var progress = 0; progress <= 100; progress += 20) {
drawable
..setProgress(progress / 100)
..draw(canvas, const Rect.fromLTWH(0, 0, 200, 200));
}
});
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,689 +0,0 @@
{
"v": "5.6.6",
"ip": 0,
"op": 1,
"fr": 60,
"w": 953,
"h": 272,
"layers": [
{
"ind": 2345,
"nm": "surface13687",
"ao": 0,
"ip": 0,
"op": 60,
"st": 0,
"ty": 4,
"ks": {
"ty": "tr",
"o": { "k": 100 },
"r": { "k": 0 },
"p": { "k": [0, 0] },
"a": { "k": [0, 0] },
"s": { "k": [133.33, 133.33] },
"sk": { "k": 0 },
"sa": { "k": 0 }
},
"shapes": [
{
"ty": "gr",
"hd": false,
"nm": "surface13687",
"it": [
{
"ty": "gr",
"hd": false,
"it": [
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[0, 0],
[0, 0],
[20, -0.05],
[0.73, 0.64],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[-20.01, 0.05],
[-1.14, 0.25],
[0, 0],
[0, 0]
],
"v": [
[79.56, 177.9],
[110.31, 147.43],
[163.43, 200.61],
[103.41, 200.64],
[100.84, 199.21],
[79.55, 177.9]
],
"c": true
}
}
},
{
"ty": "fl",
"o": { "k": 100 },
"c": { "k": [0.03, 0.35, 0.61, 1] }
},
{
"ty": "tr",
"o": { "k": 100 },
"r": { "k": 0 },
"p": { "k": [0, 0] },
"a": { "k": [0, 0] },
"s": { "k": [100, 100] },
"sk": { "k": 0 },
"sa": { "k": 0 },
"hd": false
}
]
},
{
"ty": "gr",
"hd": false,
"it": [
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[49.02, 147.35],
[79.55, 116.86],
[110.18, 147.3],
[110.31, 147.43],
[79.56, 177.9]
],
"c": true
}
}
},
{
"ty": "fl",
"o": { "k": 100 },
"c": { "k": [0.12, 0.74, 0.99, 1] }
},
{
"ty": "tr",
"o": { "k": 100 },
"r": { "k": 0 },
"p": { "k": [0, 0] },
"a": { "k": [0, 0] },
"s": { "k": [100, 100] },
"sk": { "k": 0 },
"sa": { "k": 0 },
"hd": false
}
]
},
{
"ty": "gr",
"hd": false,
"it": [
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[0, 0],
[0, 0],
[17.48, -0.07],
[0.15, -1.03],
[0, 0],
[-1.04, -0.16],
[0, 0],
[0, 0],
[13.93, -0.01],
[2.05, -0.36],
[0, 0],
[4.49, 0.1],
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[-17.47, 0],
[-1.34, 0.44],
[0, 0],
[0.44, 1.33],
[0, 0],
[0, 0],
[-13.93, -0.01],
[-2.07, -0.01],
[0, 0],
[-4.49, -0.18],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[249.55, 45.91],
[316.33, 45.93],
[316.32, 59.08],
[263.9, 59.1],
[263.05, 62.24],
[263.1, 97.61],
[266.25, 98.46],
[311.07, 98.44],
[311.07, 111.57],
[269.28, 111.57],
[263.07, 111.79],
[262.91, 161.71],
[249.44, 161.6],
[249.43, 48.03],
[249.55, 45.91]
],
"c": true
}
}
},
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[4.45, -0.12],
[0, 1.84],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[0, 0],
[-4.46, -0.08],
[-0.34, -1.83],
[0, 0],
[0, 0]
],
"v": [
[332.52, 45.93],
[346.23, 45.93],
[346.24, 124.5],
[346.09, 161.5],
[332.71, 161.51],
[332.51, 155.98],
[332.52, 45.93]
],
"c": true
}
}
},
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[0, 0],
[0, 0],
[-6.71, -0.54],
[0.07, -4.17],
[6.68, 0.1],
[0, 0],
[-4.17, -2.21],
[-4.51, 2.61],
[0, 0],
[5.33, 0.73],
[2.27, 7.45],
[0.03, 4.26],
[0, 0],
[0, 0],
[4.75, 0.17],
[-0.27, 4.24],
[-4.82, 0.37]
],
"o": [
[0, 0],
[0, 0],
[6.73, 0.16],
[-0.19, 4.17],
[-6.69, 0.02],
[0, 0],
[0.14, 4.29],
[4.76, 2.71],
[0, 0],
[-4.96, 2.25],
[-7.52, -1.15],
[-1.51, -4.08],
[0, 0],
[0, 0],
[-4.75, -0.25],
[0.03, -4.25],
[4.83, -0.08],
[0, 0]
],
"v": [
[466.71, 55.67],
[480.46, 55.68],
[480.49, 78.93],
[500.69, 79.16],
[500.59, 91.68],
[480.53, 91.64],
[480.48, 137.95],
[485.71, 149.63],
[500.83, 148.46],
[500.83, 161.93],
[484.87, 163.72],
[467.88, 150],
[466.69, 137.26],
[466.66, 92.16],
[466.32, 91.78],
[452.05, 91.65],
[452.16, 78.92],
[466.66, 78.77]
],
"c": true
}
}
},
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[-4.57, -0.16],
[0, 0],
[-0.41, -7.62],
[-6.51, 0.04],
[0, 0],
[6.55, 0.04],
[0, 0],
[-3.97, -2.02],
[-4.52, 2.71],
[0, 0],
[5.2, 0.6],
[3.18, 6.19],
[0.14, 3.58],
[0, 0],
[4.74, 0.02],
[0, 0],
[-4.67, -0.16],
[-0.29, 7.79]
],
"o": [
[4.57, 0.16],
[0, 0],
[0.17, 7.64],
[6.52, -0.12],
[0, 0],
[-6.55, 0.25],
[0, 0],
[-0.01, 4.1],
[4.87, 2.66],
[0, 0],
[-4.82, 2.18],
[-6.76, -0.9],
[-1.53, -3.24],
[0, 0],
[-4.75, -0.01],
[0, 0],
[4.67, 0.13],
[0.5, -7.77],
[0, 0]
],
"v": [
[524.82, 55.62],
[538.54, 55.73],
[538.86, 56.07],
[539.04, 78.99],
[558.58, 78.93],
[558.63, 91.57],
[538.94, 91.63],
[538.87, 138.74],
[544.01, 149.71],
[559.3, 148.29],
[559.29, 161.84],
[543.82, 163.83],
[527.28, 152.93],
[524.75, 142.52],
[524.75, 91.69],
[510.52, 91.69],
[510.55, 78.89],
[524.57, 79.01],
[524.82, 55.63]
],
"c": true
}
}
},
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[-9.7, -3.3],
[-3.15, -11.24],
[0.29, -6.48],
[0, 0],
[-4.83, -5.22],
[-9.97, 3.14],
[-3.59, 6.19],
[-3.96, -1.82],
[11.57, -2.22],
[8.71, 4.7],
[2.22, 12.3],
[-3.55, 8.97],
[-9.7, 4.25]
],
"o": [
[9.5, -3.94],
[11.11, 3.83],
[2.06, 6.23],
[0, 0],
[0.54, 6.95],
[6.86, 8],
[7, -2.06],
[3.98, 1.78],
[-5.41, 10.32],
[-9.5, 1.78],
[-11.31, -5.62],
[-1.61, -9.45],
[3.73, -9.85],
[0, 0]
],
"v": [
[591.59, 79.36],
[621.73, 78.65],
[644.59, 103.42],
[646.27, 122.75],
[580.98, 122.82],
[588.43, 142.08],
[617.08, 149.91],
[633.13, 136.27],
[644.93, 141.9],
[618.09, 162.64],
[589.48, 159.77],
[568.16, 130.41],
[570.61, 101.99],
[591.58, 79.36]
],
"c": false
}
}
},
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[1.61, -8.46],
[0, 0],
[4.05, 4],
[9.3, -4.04]
],
"o": [
[-7.99, 3.69],
[0, 0],
[-0.8, -5.53],
[-7.09, -7.61],
[0, 0]
],
"v": [
[596.75, 91.22],
[582.01, 111.57],
[632.08, 111.41],
[625.27, 96.27],
[596.75, 91.23]
],
"c": true
}
}
},
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[-12.7, -5.32],
[0, 0],
[5.63, -1.66],
[2.18, -7.55],
[-0.06, -5.23],
[0.44, -6.76],
[-0.5, -5.85],
[4.66, -0.16],
[0.07, 17.64],
[0, 0],
[-4.38, -0.11],
[-0.73, -4.37]
],
"o": [
[5.23, -12.7],
[0, 0],
[-5.33, -2.1],
[-7.7, 2.29],
[-1.5, 5.1],
[-0.59, 6.77],
[0.36, 5.84],
[-4.66, 0.23],
[-0.27, -17.63],
[0, 0],
[4.37, 0.12],
[0.4, 4.38],
[0, 0]
],
"v": [
[675.99, 92.31],
[709.98, 78.26],
[709.98, 93.08],
[693.04, 91.34],
[677.46, 108.05],
[676.59, 123.7],
[676.44, 144],
[676.62, 161.54],
[662.63, 161.68],
[662.52, 108.76],
[662.62, 79.07],
[675.75, 79.16],
[675.99, 92.31]
],
"c": true
}
}
},
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[-4.63, -0.52],
[0, 0],
[-4.16, -4.66],
[-7.34, 2.35],
[0.39, 10.38],
[0, 0],
[0, 0],
[0, 0],
[4.43, 0.05],
[0, 0],
[4.12, -2.27],
[9.44, 2.72],
[3.05, 5.58],
[0.21, 6.99],
[0, 0]
],
"o": [
[4.65, 0.2],
[0, 0],
[-0.05, 6.01],
[5.36, 5.94],
[9.8, -3.51],
[0, 0],
[0, 0],
[0, 0],
[-4.44, -0.07],
[0, 0],
[-2.87, 3.61],
[-8.43, 5.32],
[-6.09, -1.84],
[-3.62, -6.14],
[0, 0],
[0, 0]
],
"v": [
[365.89, 78.91],
[379.86, 79.15],
[379.79, 128.31],
[384.75, 145.54],
[406.54, 150.22],
[422.78, 125.95],
[422.93, 78.91],
[436.93, 78.94],
[436.85, 161.48],
[423.55, 161.43],
[423.5, 149.68],
[413.49, 159.21],
[384.93, 162.51],
[370.48, 150.83],
[365.73, 130.45],
[365.9, 78.91]
],
"c": true
}
}
},
{ "ty": "fl", "o": { "k": 54 }, "c": { "k": [0, 0, 0, 1] } },
{
"ty": "tr",
"o": { "k": 100 },
"r": { "k": 0 },
"p": { "k": [0, 0] },
"a": { "k": [0, 0] },
"s": { "k": [100, 100] },
"sk": { "k": 0 },
"sa": { "k": 0 },
"hd": false
}
]
},
{
"ty": "gr",
"hd": false,
"it": [
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[102.21, 3.07],
[163.46, 3],
[60.7, 105.71],
[33.92, 132.34],
[3.45, 101.83]
],
"c": true
}
}
},
{
"ty": "sh",
"ks": {
"k": {
"i": [
[0, 0],
[-1.11, 0.14],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"o": [
[0.8, -1.05],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[101.64, 94.78],
[105.01, 94.11],
[163.41, 94.12],
[110.18, 147.3],
[79.55, 116.86],
[101.64, 94.78]
],
"c": true
}
}
},
{
"ty": "fl",
"o": { "k": 100 },
"c": { "k": [0.27, 0.82, 0.99, 1] }
},
{
"ty": "tr",
"o": { "k": 100 },
"r": { "k": 0 },
"p": { "k": [0, 0] },
"a": { "k": [0, 0] },
"s": { "k": [100, 100] },
"sk": { "k": 0 },
"sa": { "k": 0 },
"hd": false
}
]
},
{
"ty": "tr",
"o": { "k": 100 },
"r": { "k": 0 },
"p": { "k": [0, 0] },
"a": { "k": [0, 0] },
"s": { "k": [100, 100] },
"sk": { "k": 0 },
"sa": { "k": 0 },
"hd": false
}
]
}
]
}
],
"meta": { "g": "LF SVG to Lottie" }
}

File diff suppressed because it is too large Load diff

View file

@ -1,39 +0,0 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'package:lottie/src/utils.dart';
import 'package:path/path.dart' as p;
void main() {
testWidgets('Dotlottie', (tester) async {
var size = const Size(500, 400);
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
var provider = FileLottie(
File('example/assets/cat.lottie'),
decoder: customDecoder,
);
await tester.runAsync(() => provider.load());
await tester.pumpWidget(LottieBuilder(lottie: provider));
await expectLater(
find.byType(Lottie),
matchesGoldenFile(p.join('goldens/dotlottie.png')),
);
});
}
Future<LottieComposition?> customDecoder(List<int> bytes) {
return LottieComposition.decodeZip(
bytes,
filePicker: (files) {
return files.firstWhereOrNull(
(f) => f.name.startsWith('animations/') && f.name.endsWith('.json'),
);
},
);
}

View file

@ -1,96 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'utils.dart';
void main() {
testWidgets('Can specify ImageProvider with zip file ', (tester) async {
var size = const Size(500, 400);
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
var callCount = 0;
ImageProvider imageProviderFactory(LottieImageAsset image) {
++callCount;
return FileImage(File('example/assets/Images/WeAccept/img_0.png'));
}
Future<LottieComposition?> decoder(List<int> bytes) {
return LottieComposition.decodeZip(
bytes,
imageProviderFactory: imageProviderFactory,
);
}
var composition = (await tester.runAsync(
() => FileLottie(
File('example/assets/spinning_carrousel.zip'),
imageProviderFactory: imageProviderFactory,
decoder: decoder,
).load(),
))!;
await tester.pumpWidget(FilmStrip(composition, size: size));
expect(callCount, 2);
await expectLater(
find.byType(FilmStrip),
matchesGoldenFile('goldens/dynamic_image/zip_with_provider.png'),
);
});
testWidgets('Can specify image delegate', (tester) async {
var size = const Size(500, 400);
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
var image = await tester.runAsync(
() async => loadImage(
FileImage(File('example/assets/Images/WeAccept/img_0.png')),
),
);
var composition = (await tester.runAsync(
() async =>
FileLottie(File('example/assets/spinning_carrousel.zip')).load(),
))!;
var delegates = LottieDelegates(
image: (composition, asset) {
return image;
},
);
await tester.pumpWidget(
FilmStrip(composition, size: size, delegates: delegates),
);
await expectLater(
find.byType(FilmStrip),
matchesGoldenFile('goldens/dynamic_image/delegate.png'),
);
});
}
Future<ui.Image?> loadImage(ImageProvider provider) {
var completer = Completer<ui.Image?>();
var imageStream = provider.resolve(ImageConfiguration.empty);
late ImageStreamListener listener;
listener = ImageStreamListener(
(image, synchronousLoaded) {
imageStream.removeListener(listener);
completer.complete(image.image);
},
onError: (dynamic e, _) {
imageStream.removeListener(listener);
completer.complete();
},
);
imageStream.addListener(listener);
return completer.future;
}

View file

@ -1,478 +0,0 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'utils.dart';
void main() {
void testGolden(
String description,
ValueDelegate delegate, {
double? progress,
String? filePath,
}) {
filePath ??= 'Tests/Shapes.json';
var screenshotName = description
.toLowerCase()
.replaceAll(RegExp('[^a-z0-9 ]'), '')
.replaceAll(' ', '_');
testWidgets(description, (tester) async {
var composition = await LottieComposition.fromBytes(
File('example/assets/$filePath').readAsBytesSync(),
);
var animation = AnimationController(
vsync: tester,
duration: composition.duration,
);
if (progress != null) {
animation.value = progress;
}
await tester.pumpWidget(
Lottie(
composition: composition,
controller: animation,
delegates: LottieDelegates(values: [delegate]),
addRepaintBoundary: false,
),
);
await tester.pump();
await expectLater(
find.byType(Lottie),
matchesGoldenFile('goldens/dynamic/$screenshotName.png'),
);
if (progress == null || progress == 0) {
await tester.pumpWidget(
Lottie(
composition: composition,
controller: animation,
delegates: const LottieDelegates(values: []),
addRepaintBoundary: false,
),
);
await tester.pump();
}
});
}
testGolden(
'Fill color (Green)',
ValueDelegate.color([
'Shape Layer 1',
'Rectangle',
'Fill 1',
], value: Colors.green),
);
testGolden(
'Fill color (Yellow)',
ValueDelegate.color([
'Shape Layer 1',
'Rectangle',
'Fill 1',
], value: Colors.yellow),
);
testGolden(
'Fill opacity',
ValueDelegate.opacity(['Shape Layer 1', 'Rectangle', 'Fill 1'], value: 50),
);
testGolden(
'Stroke color',
ValueDelegate.strokeColor([
'Shape Layer 1',
'Rectangle',
'Stroke 1',
], value: Colors.green),
);
testGolden(
'Stroke width',
ValueDelegate.strokeWidth([
'Shape Layer 1',
'Rectangle',
'Stroke 1',
], value: 50),
);
testGolden(
'Stroke opacity',
ValueDelegate.opacity([
'Shape Layer 1',
'Rectangle',
'Stroke 1',
], value: 50),
);
testGolden(
'Transform anchor point',
ValueDelegate.transformAnchorPoint([
'Shape Layer 1',
'Rectangle',
], value: const Offset(20, 20)),
);
testGolden(
'Transform position',
ValueDelegate.transformPosition([
'Shape Layer 1',
'Rectangle',
], value: const Offset(20, 20)),
);
testGolden(
'Transform position (relative)',
ValueDelegate.transformPosition([
'Shape Layer 1',
'Rectangle',
], relative: const Offset(20, 20)),
);
testGolden(
'Transform opacity',
ValueDelegate.transformOpacity(['Shape Layer 1', 'Rectangle'], value: 50),
);
testGolden(
'Transform rotation',
ValueDelegate.transformRotation(['Shape Layer 1', 'Rectangle'], value: 45),
);
testGolden(
'Transform scale',
ValueDelegate.transformScale([
'Shape Layer 1',
'Rectangle',
], value: const Offset(0.5, 0.5)),
);
testGolden(
'Rectangle corner roundedness',
ValueDelegate.cornerRadius([
'Shape Layer 1',
'Rectangle',
'Rectangle Path 1',
], value: 7),
);
testGolden(
'Rectangle position',
ValueDelegate.position([
'Shape Layer 1',
'Rectangle',
'Rectangle Path 1',
], relative: const Offset(20, 20)),
);
testGolden(
'Rectangle size',
ValueDelegate.rectangleSize([
'Shape Layer 1',
'Rectangle',
'Rectangle Path 1',
], relative: const Offset(30, 40)),
);
testGolden(
'Ellipse position',
ValueDelegate.position([
'Shape Layer 1',
'Ellipse',
'Ellipse Path 1',
], relative: const Offset(20, 20)),
);
testGolden(
'Ellipse size',
ValueDelegate.ellipseSize([
'Shape Layer 1',
'Ellipse',
'Ellipse Path 1',
], relative: const Offset(40, 60)),
);
testGolden(
'Star points',
ValueDelegate.polystarPoints([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], value: 8),
);
testGolden(
'Star rotation',
ValueDelegate.polystarRotation([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], value: 10),
);
testGolden(
'Star position',
ValueDelegate.position([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], relative: const Offset(20, 20)),
);
testGolden(
'Star inner radius',
ValueDelegate.polystarInnerRadius([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], value: 10),
);
testGolden(
'Star inner roundedness',
ValueDelegate.polystarInnerRoundedness([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], value: 100),
);
testGolden(
'Star outer radius',
ValueDelegate.polystarOuterRadius([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], value: 60),
);
testGolden(
'Star outer roundedness',
ValueDelegate.polystarOuterRoundedness([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], value: 100),
);
testGolden(
'Polygon points',
ValueDelegate.polystarPoints([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], value: 8),
);
testGolden(
'Polygon rotation',
ValueDelegate.polystarRotation([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], value: 10),
);
testGolden(
'Polygon position',
ValueDelegate.position([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], relative: const Offset(20, 20)),
);
testGolden(
'Polygon radius',
ValueDelegate.polystarOuterRadius([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], relative: 60),
);
testGolden(
'Polygon roundedness',
ValueDelegate.polystarOuterRoundedness([
'Shape Layer 1',
'Star',
'Polystar Path 1',
], value: 100),
);
testGolden(
'Repeater transform position',
ValueDelegate.transformPosition([
'Shape Layer 1',
'Repeater Shape',
'Repeater 1',
], relative: const Offset(100, 100)),
);
testGolden(
'Repeater transform start opacity',
ValueDelegate.transformStartOpacity([
'Shape Layer 1',
'Repeater Shape',
'Repeater 1',
], value: 25),
);
testGolden(
'Repeater transform end opacity',
ValueDelegate.transformEndOpacity([
'Shape Layer 1',
'Repeater Shape',
'Repeater 1',
], value: 25),
);
testGolden(
'Repeater transform rotation',
ValueDelegate.transformRotation([
'Shape Layer 1',
'Repeater Shape',
'Repeater 1',
], value: 45),
);
testGolden(
'Repeater transform scale',
ValueDelegate.transformScale([
'Shape Layer 1',
'Repeater Shape',
'Repeater 1',
], value: const Offset(2, 2)),
);
testGolden(
'Time remapping',
ValueDelegate.timeRemap(['Circle 1'], value: 1),
progress: 0.1,
);
testGolden(
'Color Filter',
ValueDelegate.colorFilter([
'**',
], value: const ColorFilter.mode(Colors.green, BlendMode.srcATop)),
);
testGolden('Null Color Filter', ValueDelegate.colorFilter(['**']));
testGolden(
'Matte property',
ValueDelegate.rectangleSize([
'Shape Layer 1',
'Rectangle 1',
'Rectangle Path 1',
], value: const Offset(50, 50)),
filePath: 'Tests/TrackMattes.json',
);
testGolden('Blur', ValueDelegate.blurRadius(['**'], value: 10));
testGolden(
'Drop shadow',
ValueDelegate.dropShadow(
['Shape Layer 1', '**'],
value: const DropShadow(
color: Colors.green,
direction: 150,
distance: 20,
radius: 10,
),
),
);
testGolden(
'Solid Color',
ValueDelegate.color(['Cyan Solid 1', '**'], value: Colors.yellow),
filePath: 'Tests/SolidLayerTransform.json',
);
for (var progress in [0.0, 0.5, 1.0]) {
testGolden(
'Opacity interpolation ($progress)',
ValueDelegate.transformOpacity(
['Shape Layer 1', 'Rectangle'],
callback: (frameInfo) => lerpDouble(
10,
100,
Curves.linear.transform(frameInfo.overallProgress),
)!.round(),
),
progress: progress,
);
}
testWidgets('warningShimmer', (tester) async {
var size = const Size(500, 400);
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
var composition = await LottieComposition.fromBytes(
File('test/data/warningShimmer.json').readAsBytesSync(),
);
var delegates = <String, List<ValueDelegate>>{
'1': [
for (var i in ['1', '2', '5'])
ValueDelegate.color(['Layer $i Outlines', '**'], value: Colors.red),
for (var i in ['3', '4'])
ValueDelegate.color([
'Layer $i Outlines',
'**',
], value: Colors.greenAccent),
],
'2': [
for (var i in ['1', '2', '5'])
ValueDelegate.color([
'Layer $i Outlines',
'Group 1',
'*',
], value: Colors.red),
for (var i in ['3', '4'])
ValueDelegate.color([
'Layer $i Outlines',
'Group 1',
'*',
], value: Colors.greenAccent),
],
'3': [
for (var i in ['1', '2', '5'])
ValueDelegate.color([
'Layer $i Outlines',
'Group 1',
'Fill 1',
], value: Colors.red),
for (var i in ['3', '4'])
ValueDelegate.color([
'Layer $i Outlines',
'Group 1',
'Fill 1',
], value: Colors.greenAccent),
],
};
for (var variant in delegates.entries) {
await tester.pumpWidget(
FilmStrip(
composition,
size: size,
delegates: LottieDelegates(values: variant.value),
),
);
await expectLater(
find.byType(FilmStrip),
matchesGoldenFile('goldens/warningShimmer_${variant.key}.png'),
);
}
});
}

View file

@ -1,40 +0,0 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'utils.dart';
void main() {
var root = 'example/assets';
testWidgets('Mirror animation', (tester) async {
var size = const Size(500, 400);
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
var composition = (await tester.runAsync(
() => FileLottie(File('$root/Tests/MatteTimeStretchScan.json')).load(),
))!;
await tester.pumpWidget(
FilmStrip(
composition,
size: size,
delegates: LottieDelegates(
values: [
ValueDelegate.transformAnchorPoint(
[],
value: Offset(composition.bounds.width.toDouble(), 0),
),
ValueDelegate.transformScale([], value: const Offset(-1, 1)),
],
),
),
);
await expectLater(
find.byType(FilmStrip),
matchesGoldenFile('goldens/mirror.png'),
);
});
}

View file

@ -1,61 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
void main() {
late LottieComposition composition;
setUpAll(() async {
composition = await LottieComposition.fromBytes(
File('example/assets/Tests/Text.json').readAsBytesSync(),
);
});
void testGolden(String description, ValueDelegate delegate) async {
var screenshotName = description
.toLowerCase()
.replaceAll(RegExp('[^a-z0-9 ]'), '')
.replaceAll(' ', '_');
testWidgets(description, (tester) async {
await tester.pumpWidget(
Lottie(
composition: composition,
delegates: LottieDelegates(values: [delegate]),
addRepaintBoundary: false,
),
);
await tester.pump();
await expectLater(
find.byType(Lottie),
matchesGoldenFile('goldens/dynamic_text/$screenshotName.png'),
);
});
}
testGolden(
'Text Fill (Blue -> Green)',
ValueDelegate.color(['Text'], callback: (_) => Colors.green),
);
testGolden(
'Text Stroke (Red -> Yellow)',
ValueDelegate.strokeColor(['Text'], callback: (_) => Colors.yellow),
);
testGolden(
'Text Stroke Width',
ValueDelegate.strokeWidth(['Text'], callback: (_) => 200),
);
testGolden(
'Text Tracking 1',
ValueDelegate.textTracking(['Text'], callback: (_) => 20),
);
testGolden(
'Text Tracking 2',
ValueDelegate.textSize(['Text'], callback: (_) => 60),
);
}

View file

@ -1,103 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'utils.dart';
void main() {
late LottieComposition composition;
setUpAll(() async {
composition = await LottieComposition.fromBytes(
File('example/assets/Tests/DynamicText.json').readAsBytesSync(),
);
});
void testGolden(String description, LottieDelegates delegates) async {
var screenshotName = description
.toLowerCase()
.replaceAll(RegExp('[^a-z0-9 ]'), '')
.replaceAll(' ', '_');
var size = const Size(500, 400);
testWidgets(description, (tester) async {
await tester.pumpWidget(
MaterialApp(
home: FilmStrip(composition, delegates: delegates, size: size),
),
);
await tester.pump();
await expectLater(
find.byType(FilmStrip),
matchesGoldenFile('goldens/dynamic_text/$screenshotName.png'),
);
});
}
testGolden(
'Dynamic text delegate',
LottieDelegates(
text: (input) => '🔥c🔥👮🏿🔥',
textStyle: (font) => const TextStyle(
fontFamily: 'Roboto',
fontFamilyFallback: ['Noto Emoji'],
),
values: const [],
),
);
testGolden(
'Dynamic Text ValueDelegate',
LottieDelegates(
values: [
ValueDelegate.text(['NAME'], value: 'Text with ValueDelegate'),
],
),
);
testGolden(
'Dynamic Text ValueDelegate overallProgress',
LottieDelegates(
values: [
ValueDelegate.text([
'NAME',
], callback: (frame) => '${frame.overallProgress}'),
],
),
);
testGolden(
'Dynamic Text ValueDelegate startValue',
LottieDelegates(
values: [
ValueDelegate.text([
'NAME',
], callback: (frame) => '${frame.startValue}!!'),
],
),
);
testGolden(
'Dynamic Text ValueDelegate endValue',
LottieDelegates(
values: [
ValueDelegate.text([
'NAME',
], callback: (frame) => '${frame.endValue}!!'),
],
),
);
testGolden(
'Dynamic Text Emoji',
LottieDelegates(
textStyle: (font) => const TextStyle(
fontFamily: 'Roboto',
fontFamilyFallback: ['Noto Emoji'],
),
values: [
ValueDelegate.text(['NAME'], value: '🔥💪💯'),
],
),
);
}

View file

@ -1,26 +0,0 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'package:path/path.dart' as p;
import 'utils.dart';
void main() {
testWidgets('Animations with stroke', (tester) async {
var size = const Size(500, 400);
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
var composition = await LottieComposition.fromBytes(
File('example/assets/17297-fireworks.json').readAsBytesSync(),
);
await tester.pumpWidget(FilmStrip(composition, size: size));
await expectLater(
find.byType(FilmStrip),
matchesGoldenFile(p.join('goldens/fireworks.png')),
);
});
}

View file

@ -1,22 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as path;
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
await loadFonts();
return testMain();
}
Future<void> loadFonts() async {
for (var file in Directory(
'example/assets/fonts',
).listSync().whereType<File>().where((f) => f.path.endsWith('.ttf'))) {
var fontLoader = FontLoader(
path.basenameWithoutExtension(file.path).replaceAll('-', ' '),
);
var future = file.readAsBytes().then((value) => value.buffer.asByteData());
fontLoader.addFont(future);
await fontLoader.load();
}
}

View file

@ -1,17 +0,0 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
void main() {
test('Frame rate round', () async {
var composition = await LottieComposition.fromBytes(
File('example/assets/LottieLogo1.json').readAsBytesSync(),
);
expect(composition.roundProgress(0, frameRate: FrameRate.composition), 0);
expect(
composition.roundProgress(0.0001, frameRate: FrameRate.composition),
0,
);
expect(composition.roundProgress(0.0001, frameRate: FrameRate.max), 0.0001);
});
}

View file

@ -1,13 +0,0 @@
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/src/utils/gamma_evaluator.dart';
void main() {
test('Evaluate for same color values', () {
for (var color = 0x000000; color <= 0xffffff; color++) {
var colorToTest = Color(0xff000000 | color);
var actual = GammaEvaluator.evaluate(0.3, colorToTest, colorToTest);
expect(actual, colorToTest);
}
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

View file

@ -1,37 +0,0 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'package:path/path.dart' as p;
import 'utils.dart';
void main() {
var root = 'example/assets';
for (var asset
in Directory(root)
.listSync(recursive: true)
.whereType<File>()
.where(
(f) => const ['.json', '.zip'].contains(p.extension(f.path)),
)) {
testWidgets('Goldens ${asset.path}', (tester) async {
var size = const Size(500, 400);
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
var composition = (await tester.runAsync(
() => FileLottie(asset).load(),
))!;
await tester.pumpWidget(FilmStrip(composition, size: size));
var folder = p.relative(asset.path, from: root);
var fileName = '${p.basenameWithoutExtension(asset.path)}.png'
.toLowerCase();
await expectLater(
find.byType(FilmStrip),
matchesGoldenFile(p.join('goldens/all', p.dirname(folder), fileName)),
);
});
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Some files were not shown because too many files have changed in this diff Show more