Compare commits

...

5 Commits

10 changed files with 518 additions and 57 deletions

View File

@ -1,3 +1,8 @@
## [0.2.2] - 2020-02-21
- Add a [repeat] parameter to specify if the automatic animation should loop.
- Add the [animate], [reverse], [repeat] properties on `LottieBuilder`
- Fix bug with `onLoaded` callback when the `LottieProvider` is changed
## [0.2.1] - 2020-02-11
- Fix a big bug in the path transformation code. A lot more animations look correct now.

View File

@ -0,0 +1,11 @@
final files = [
'assets/lottiefiles/cubo_livre.json',
'assets/lottiefiles/slack_app_loader.json',
'assets/lottiefiles/walking.json',
'assets/lottiefiles/jojo_the_bird.json',
'assets/lottiefiles/bitcoin_to_the_moon.json',
'assets/lottiefiles/splashy_loader.json',
'assets/lottiefiles/uk.json',
'assets/lottiefiles/yoga_carpet.json',
'assets/lottiefiles/books.json',
];

View File

@ -87,7 +87,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.2.0+1"
version: "0.2.1"
matcher:
dependency: transitive
description:

View File

@ -6,21 +6,12 @@ import '../lottie.dart';
import 'lottie_builder.dart';
import 'providers/load_image.dart';
/// A widget to display a loaded [LottieComposition].
/// The [controller] property allows to specify a custom AnimationController that
/// will drive the animation. If [controller] is null, the animation will play
/// automatically and the behavior could be adjusted with the properties [animate],
/// [repeat] and [reverse].
class Lottie extends StatefulWidget {
final LottieComposition composition;
/// The animation controller to animate the Lottie animation.
/// If null, a controller is automatically created by this class and is configured
/// with the properties [animate], [reverse]
final AnimationController controller;
/// If no controller is specified, use this values to automatically plays the
/// Lottie animation.
final bool animate, reverse;
final double width, height;
final AlignmentGeometry alignment;
final BoxFit fit;
Lottie({
Key key,
@required this.composition,
@ -34,10 +25,14 @@ class Lottie extends StatefulWidget {
bool reverse,
}) : animate = animate ?? true,
reverse = reverse ?? false,
repeat = repeat ?? true,
super(key: key);
static LottieBuilder asset(String name,
{AnimationController controller,
{Animation<double> controller,
bool animate,
bool repeat,
bool reverse,
void Function(LottieComposition) onLoaded,
LottieImageProviderFactory imageProviderFactory,
Key key,
@ -51,6 +46,9 @@ class Lottie extends StatefulWidget {
LottieBuilder.asset(
name,
controller: controller,
animate: animate,
repeat: repeat,
reverse: reverse,
imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded,
key: key,
@ -65,7 +63,10 @@ class Lottie extends StatefulWidget {
static LottieBuilder file(
File file, {
AnimationController controller,
Animation<double> controller,
bool animate,
bool repeat,
bool reverse,
LottieImageProviderFactory imageProviderFactory,
void Function(LottieComposition) onLoaded,
Key key,
@ -78,6 +79,9 @@ class Lottie extends StatefulWidget {
LottieBuilder.file(
file,
controller: controller,
animate: animate,
repeat: repeat,
reverse: reverse,
imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded,
key: key,
@ -90,7 +94,10 @@ class Lottie extends StatefulWidget {
static LottieBuilder memory(
Uint8List bytes, {
AnimationController controller,
Animation<double> controller,
bool animate,
bool repeat,
bool reverse,
LottieImageProviderFactory imageProviderFactory,
void Function(LottieComposition) onLoaded,
Key key,
@ -103,6 +110,9 @@ class Lottie extends StatefulWidget {
LottieBuilder.memory(
bytes,
controller: controller,
animate: animate,
repeat: repeat,
reverse: reverse,
imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded,
key: key,
@ -115,7 +125,10 @@ class Lottie extends StatefulWidget {
static LottieBuilder network(
String url, {
AnimationController controller,
Animation<double> controller,
bool animate,
bool repeat,
bool reverse,
LottieImageProviderFactory imageProviderFactory,
void Function(LottieComposition) onLoaded,
Key key,
@ -128,6 +141,9 @@ class Lottie extends StatefulWidget {
LottieBuilder.network(
url,
controller: controller,
animate: animate,
repeat: repeat,
reverse: reverse,
imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded,
key: key,
@ -138,6 +154,67 @@ class Lottie extends StatefulWidget {
alignment: alignment,
);
/// The Lottie composition to animate.
/// It could be parsed asynchronously with `LottieComposition.fromBytes`.
final LottieComposition composition;
/// The animation controller to animate the Lottie animation.
/// If null, a controller is automatically created by this class and is configured
/// with the properties [animate], [reverse]
final Animation<double> controller;
/// If no controller is specified, this value indicate whether or not the
/// Lottie animation should be played automatically (default to true).
/// If there is an animation controller specified, this property has no effect.
///
/// See [repeat] to control whether the animation should repeat.
final bool animate;
/// Specify that the automatic animation should repeat in a loop (default to true).
/// The property has no effect if [animate] is false or [controller] is not null.
final bool repeat;
/// Specify that the automatic animation should repeat in a loop in a "reverse"
/// mode (go from start to end and then continuously from end to start).
/// It default to false.
/// The property has no effect if [animate] is false, [repeat] is false or [controller] is not null.
final bool reverse;
/// If non-null, require the Lottie composition to have this width.
///
/// If null, the composition will pick a size that best preserves its intrinsic
/// aspect ratio.
final double width;
/// If non-null, require the Lottie composition to have this height.
///
/// If null, the composition will pick a size that best preserves its intrinsic
/// aspect ratio.
final double height;
/// How to inscribe the Lottie composition into the space allocated during layout.
final BoxFit fit;
/// How to align the composition within its bounds.
///
/// The alignment aligns the given position in the image to the given position
/// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
/// -1.0) aligns the image to the top-left corner of its layout bounds, while a
/// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
/// image with the bottom right corner of its layout bounds. Similarly, an
/// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
/// middle of the bottom edge of its layout bounds.
///
/// Defaults to [Alignment.center].
///
/// See also:
///
/// * [Alignment], a class with convenient constants typically used to
/// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction.
final AlignmentGeometry alignment;
@override
_LottieState createState() => _LottieState();
}
@ -167,8 +244,12 @@ class _LottieState extends State<Lottie> with TickerProviderStateMixin {
void _updateAutoAnimation() {
_autoAnimation.stop();
if (widget.animate) {
_autoAnimation.repeat(reverse: widget.reverse);
if (widget.animate && widget.controller == null) {
if (widget.repeat) {
_autoAnimation.repeat(reverse: widget.reverse);
} else {
_autoAnimation.forward();
}
}
}

View File

@ -25,18 +25,21 @@ typedef LottieFrameBuilder = Widget Function(
/// Several constructors are provided for the various ways that a Lottie file
/// can be provided:
///
/// * [new Lottie], for obtaining an image from a [LottieProvider].
/// * [new Lottie], for obtaining a composition from a [LottieProvider].
/// * [new Lottie.asset], for obtaining a Lottie file from an [AssetBundle]
/// using a key.
/// * [new Image.network], for obtaining a lottie file from a URL.
/// * [new Image.file], for obtaining a lottie file from a [File].
/// * [new Image.memory], for obtaining a lottie file from a [Uint8List].
/// * [new Lottie.network], for obtaining a lottie file from a URL.
/// * [new Lottie.file], for obtaining a lottie file from a [File].
/// * [new Lottie.memory], for obtaining a lottie file from a [Uint8List].
///
class LottieBuilder extends StatefulWidget {
const LottieBuilder({
Key key,
@required this.lottie,
this.controller,
this.animate,
this.reverse,
this.repeat,
this.onLoaded,
this.frameBuilder,
this.width,
@ -51,6 +54,9 @@ class LottieBuilder extends StatefulWidget {
String src, {
Map<String, String> headers,
this.controller,
this.animate,
this.reverse,
this.repeat,
LottieImageProviderFactory imageProviderFactory,
this.onLoaded,
Key key,
@ -78,6 +84,9 @@ class LottieBuilder extends StatefulWidget {
LottieBuilder.file(
File file, {
this.controller,
this.animate,
this.reverse,
this.repeat,
LottieImageProviderFactory imageProviderFactory,
this.onLoaded,
Key key,
@ -92,6 +101,9 @@ class LottieBuilder extends StatefulWidget {
LottieBuilder.asset(
String name, {
this.controller,
this.animate,
this.reverse,
this.repeat,
LottieImageProviderFactory imageProviderFactory,
this.onLoaded,
Key key,
@ -112,6 +124,9 @@ class LottieBuilder extends StatefulWidget {
LottieBuilder.memory(
Uint8List bytes, {
this.controller,
this.animate,
this.reverse,
this.repeat,
LottieImageProviderFactory imageProviderFactory,
this.onLoaded,
Key key,
@ -125,6 +140,7 @@ class LottieBuilder extends StatefulWidget {
super(key: key);
/// The lottie animation to display.
/// Example of providers: [AssetLottie], [NetworkLottie], [FileLottie], [MemoryLottie]
final LottieProvider lottie;
/// A callback called when the LottieComposition has been loaded.
@ -135,7 +151,24 @@ class LottieBuilder extends StatefulWidget {
/// The animation controller of the Lottie animation.
/// The animated value will be mapped to the `progress` property of the
/// Lottie animation.
final AnimationController controller;
final Animation<double> controller;
/// If no controller is specified, this value indicate whether or not the
/// Lottie animation should be played automatically (default to true).
/// If there is an animation controller specified, this property has no effect.
///
/// See [repeat] to control whether the animation should repeat.
final bool animate;
/// Specify that the automatic animation should repeat in a loop (default to true).
/// The property has no effect if [animate] is false or [controller] is not null.
final bool repeat;
/// Specify that the automatic animation should repeat in a loop in a "reverse"
/// mode (go from start to end and then continuously from end to start).
/// It default to false.
/// The property has no effect if [animate] is false, [repeat] is false or [controller] is not null.
final bool reverse;
/// A builder function responsible for creating the widget that represents
/// this lottie animation.
@ -143,10 +176,10 @@ class LottieBuilder extends StatefulWidget {
/// If this is null, this widget will display a lottie animation that is painted as
/// soon as it is available (and will appear to "pop" in
/// if it becomes available asynchronously). Callers might use this builder to
/// add effects to the image (such as fading the image in when it becomes
/// available) or to display a placeholder widget while the image is loading.
/// add effects to the animation (such as fading the animation in when it becomes
/// available) or to display a placeholder widget while the animation is loading.
///
/// To have finer-grained control over the way that an image's loading
/// To have finer-grained control over the way that an animation's loading
/// progress is communicated to the user, see [loadingBuilder].
///
/// {@template lottie.chainedBuildersExample}
@ -177,7 +210,7 @@ class LottieBuilder extends StatefulWidget {
/// {@tool snippet --template=stateless_widget_material}
///
/// The following sample demonstrates how to use this builder to implement an
/// image that fades in once it's been loaded.
/// animation that fades in once it's been loaded.
///
/// This sample contains a limited subset of the functionality that the
/// [FadeInImage] widget provides out of the box.
@ -219,9 +252,9 @@ class LottieBuilder extends StatefulWidget {
///
/// It is strongly recommended that either both the [width] and the [height]
/// be specified, or that the widget be placed in a context that sets tight
/// layout constraints, so that the image does not change size as it loads.
/// Consider using [fit] to adapt the image's rendering to fit the given width
/// and height if the exact image dimensions are not known in advance.
/// layout constraints, so that the animation does not change size as it loads.
/// Consider using [fit] to adapt the animation's rendering to fit the given width
/// and height if the exact animation dimensions are not known in advance.
final double width;
/// If non-null, require the lottie animation to have this height.
@ -231,28 +264,28 @@ class LottieBuilder extends StatefulWidget {
///
/// It is strongly recommended that either both the [width] and the [height]
/// be specified, or that the widget be placed in a context that sets tight
/// layout constraints, so that the image does not change size as it loads.
/// Consider using [fit] to adapt the image's rendering to fit the given width
/// and height if the exact image dimensions are not known in advance.
/// layout constraints, so that the animation does not change size as it loads.
/// Consider using [fit] to adapt the animation's rendering to fit the given width
/// and height if the exact animation dimensions are not known in advance.
final double height;
/// How to inscribe the image into the space allocated during layout.
/// How to inscribe the animation into the space allocated during layout.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
final BoxFit fit;
/// How to align the image within its bounds.
/// How to align the animation within its bounds.
///
/// The alignment aligns the given position in the image to the given position
/// The alignment aligns the given position in the animation to the given position
/// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
/// -1.0) aligns the image to the top-left corner of its layout bounds, while an
/// -1.0) aligns the animation to the top-left corner of its layout bounds, while an
/// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
/// image with the bottom right corner of its layout bounds. Similarly, an
/// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
/// animation with the bottom right corner of its layout bounds. Similarly, an
/// alignment of (0.0, 1.0) aligns the bottom middle of the animation with the
/// middle of the bottom edge of its layout bounds.
///
/// To display a subpart of an image, consider using a [CustomPainter] and
/// To display a subpart of an animation, consider using a [CustomPainter] and
/// [Canvas.drawImageRect].
///
/// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
@ -288,13 +321,12 @@ class LottieBuilder extends StatefulWidget {
class _LottieBuilderState extends State<LottieBuilder> {
Future<LottieComposition> _loadingFuture;
bool _calledLoadedCallback = false;
@override
void initState() {
super.initState();
_loadingFuture = widget.lottie.load();
_load();
}
@override
@ -302,11 +334,21 @@ class _LottieBuilderState extends State<LottieBuilder> {
super.didUpdateWidget(oldWidget);
if (oldWidget.lottie != widget.lottie) {
_loadingFuture = widget.lottie.load();
_calledLoadedCallback = false;
_load();
}
}
void _load() {
var provider = widget.lottie;
_loadingFuture = widget.lottie.load().then((composition) {
if (mounted && widget.onLoaded != null && widget.lottie == provider) {
widget.onLoaded(composition);
}
return composition;
});
}
@override
Widget build(BuildContext context) {
return FutureBuilder<LottieComposition>(
@ -319,16 +361,13 @@ class _LottieBuilderState extends State<LottieBuilder> {
}
var composition = snapshot.data;
if (composition != null && !_calledLoadedCallback) {
_calledLoadedCallback = true;
if (widget.onLoaded != null) {
widget.onLoaded(composition);
}
}
Widget result = Lottie(
composition: composition,
controller: widget.controller,
animate: widget.animate,
reverse: widget.reverse,
repeat: widget.repeat,
width: widget.width,
height: widget.height,
fit: widget.fit,

View File

@ -20,7 +20,8 @@ class MaskParser {
var mode = reader.nextName();
switch (mode) {
case 'mode':
switch (reader.nextString()) {
var modeName = reader.nextString();
switch (modeName) {
case 'a':
maskMode = MaskMode.maskModeAdd;
break;
@ -36,7 +37,7 @@ class MaskParser {
maskMode = MaskMode.maskModeIntersect;
break;
default:
logger.warning('Unknown mask mode $mode. Defaulting to Add.');
logger.warning('Unknown mask mode $modeName. Defaulting to Add.');
maskMode = MaskMode.maskModeAdd;
}
break;

View File

@ -46,6 +46,10 @@ class LottieCache {
_cache.remove(_cache.keys.first);
}
}
void clear() {
_cache.clear();
}
}
final sharedLottieCache = LottieCache();

View File

@ -144,6 +144,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
mockito:
dependency: "direct dev"
description:
name: mockito
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.1"
node_interop:
dependency: transitive
description:

View File

@ -1,6 +1,6 @@
name: lottie
description: Render After Effects animations natively on Flutter. This package is a pure Dart implementation of a Lottie player.
version: 0.2.1
version: 0.2.2
homepage: https://github.com/xvrh/lottie-flutter
environment:
@ -19,9 +19,10 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
yaml:
analyzer:
dart_style:
mockito: ^4.0.0
yaml:
dependency_overrides:
pedantic: ^1.9.0

312
test/lottie_test.dart Normal file
View File

@ -0,0 +1,312 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
import 'package:lottie/src/providers/lottie_provider.dart';
import 'package:mockito/mockito.dart';
void main() {
tearDown(() {
sharedLottieCache.clear();
});
testWidgets('Should settle if no animation', (tester) async {
var data = File('example/assets/HamburgerArrow.json').readAsBytesSync();
var composition = await LottieComposition.fromBytes(data);
await tester.pumpWidget(Lottie(
composition: composition,
animate: false,
));
await tester.pumpAndSettle();
});
testWidgets('onLoaded called with the correct composition', (tester) async {
LottieComposition composition;
var file = MockFile();
var data = File('example/assets/HamburgerArrow.json').readAsBytesSync();
when(file.readAsBytes()).thenAnswer((value) => Future.value(data));
await tester.pumpWidget(LottieBuilder.file(
file,
onLoaded: (c) {
composition = c;
},
));
await tester.pump();
expect(composition, isNotNull);
expect(composition.endFrame, 179.99);
});
testWidgets('onLoaded called when remplacing the widget animation',
(tester) async {
var mockAsset = MockAssetBundle();
ByteData read(String path) =>
File(path).readAsBytesSync().buffer.asByteData();
var hamburgerData =
Future.value(read('example/assets/HamburgerArrow.json'));
var androidData = Future.value(read('example/assets/AndroidWave.json'));
when(mockAsset.load('hamburger.json')).thenAnswer((_) => hamburgerData);
when(mockAsset.load('android.json')).thenAnswer((_) => androidData);
var animation = AnimationController(vsync: tester);
LottieComposition composition;
await tester.pumpWidget(
Lottie.asset(
'hamburger.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
},
),
);
await tester.pump();
var widgetFinder = find.byType(Lottie);
expect(widgetFinder, findsOneWidget);
expect(composition, isNotNull);
expect(composition.duration, Duration(seconds: 6));
composition = null;
await tester.pumpWidget(
Lottie.asset(
'android.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
},
),
);
await tester.pump();
expect(composition, isNotNull);
expect(composition.duration, Duration(seconds: 2, milliseconds: 50));
});
testWidgets('onLoaded data race 1', (tester) async {
var mockAsset = MockAssetBundle();
ByteData read(String path) =>
File(path).readAsBytesSync().buffer.asByteData();
var hamburgerCompleter = Completer<ByteData>();
var androidCompleter = Completer<ByteData>();
var hamburgerData = read('example/assets/HamburgerArrow.json');
var androidData = read('example/assets/AndroidWave.json');
when(mockAsset.load('hamburger.json'))
.thenAnswer((_) => hamburgerCompleter.future);
when(mockAsset.load('android.json'))
.thenAnswer((_) => androidCompleter.future);
var animation = AnimationController(vsync: tester);
var onLoadedCount = 0;
LottieComposition composition;
await tester.pumpWidget(
Lottie.asset(
'hamburger.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
++onLoadedCount;
},
),
);
await tester.pump();
expect(
find.byWidgetPredicate((w) => w is RawLottie && w.composition == null),
findsOneWidget);
expect(composition, isNull);
expect(onLoadedCount, 0);
await tester.pumpWidget(
Lottie.asset(
'android.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
++onLoadedCount;
},
),
);
await tester.pump();
expect(composition, isNull);
expect(onLoadedCount, 0);
hamburgerCompleter.complete(hamburgerData);
await tester.pump();
expect(composition, isNull);
expect(onLoadedCount, 0);
androidCompleter.complete(androidData);
await tester.pump();
expect(composition.duration, Duration(seconds: 2, milliseconds: 50));
expect(onLoadedCount, 1);
});
testWidgets('onLoaded data race 2', (tester) async {
var mockAsset = MockAssetBundle();
ByteData read(String path) =>
File(path).readAsBytesSync().buffer.asByteData();
var hamburgerCompleter = Completer<ByteData>();
var androidCompleter = Completer<ByteData>();
var hamburgerData = read('example/assets/HamburgerArrow.json');
var androidData = read('example/assets/AndroidWave.json');
when(mockAsset.load('hamburger.json'))
.thenAnswer((_) => hamburgerCompleter.future);
when(mockAsset.load('android.json'))
.thenAnswer((_) => androidCompleter.future);
var animation = AnimationController(vsync: tester);
var onLoadedCount = 0;
LottieComposition composition;
await tester.pumpWidget(
Lottie.asset(
'hamburger.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
++onLoadedCount;
},
),
);
await tester.pump();
expect(
find.byWidgetPredicate((w) => w is RawLottie && w.composition == null),
findsOneWidget);
expect(composition, isNull);
expect(onLoadedCount, 0);
await tester.pumpWidget(
Lottie.asset(
'android.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
++onLoadedCount;
},
),
);
await tester.pump();
expect(composition, isNull);
expect(onLoadedCount, 0);
androidCompleter.complete(androidData);
await tester.pump();
expect(composition.duration, Duration(seconds: 2, milliseconds: 50));
expect(onLoadedCount, 1);
hamburgerCompleter.complete(hamburgerData);
await tester.pump();
expect(composition.duration, Duration(seconds: 2, milliseconds: 50));
expect(onLoadedCount, 1);
});
testWidgets('Should auto animate', (tester) async {
var composition = await LottieComposition.fromBytes(
File('example/assets/HamburgerArrow.json').readAsBytesSync());
await tester.pumpWidget(Lottie(
composition: composition,
));
await tester.pump();
var lottie =
tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect((lottie.listenable as AnimationController).duration,
Duration(seconds: 6));
expect((lottie.listenable as AnimationController).isAnimating, true);
await tester.pumpWidget(Lottie(
composition: composition,
animate: false,
));
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect((lottie.listenable as AnimationController).duration,
Duration(seconds: 6));
expect((lottie.listenable as AnimationController).isAnimating, false);
await tester.pumpWidget(Lottie(
composition: composition,
));
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect((lottie.listenable as AnimationController).duration,
Duration(seconds: 6));
var animationController =
AnimationController(vsync: tester, duration: Duration(seconds: 2));
await tester.pumpWidget(Lottie(
composition: composition,
controller: animationController.view,
));
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect((lottie.listenable as AnimationController).duration,
Duration(seconds: 2));
await tester.pumpWidget(Lottie(
composition: composition,
controller: animationController.view,
animate: false,
));
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect((lottie.listenable as AnimationController).duration,
Duration(seconds: 2));
await tester.pumpWidget(Lottie(
composition: composition,
animate: false,
));
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect((lottie.listenable as AnimationController).duration,
Duration(seconds: 6));
expect((lottie.listenable as AnimationController).isAnimating, false);
});
}
class MockFile extends Mock implements File {}
class MockAssetBundle extends Mock implements AssetBundle {}