update lottie
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,6 @@ import 'content.dart';
|
|||
|
||||
abstract class ShapeModifierContent extends Content {
|
||||
ShapeData modifyShape(ShapeData shapeData);
|
||||
|
||||
void addUpdateListener(void Function() listener);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>?);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
19
lottie/lib/src/model/animatable/animatable_text_style.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
2
lottie/lib/src/model/content/text_range_units.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/// `index` is named [charIndex] because every Dart enum already defines an `index` getter.
|
||||
enum TextRangeUnits { percent, charIndex }
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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})';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
|
|
@ -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'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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'),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
@ -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: '🔥💪💯'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -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')),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
|
@ -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)),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 101 KiB |