Compare commits

..

6 Commits

25 changed files with 551 additions and 99 deletions

View File

@ -1,3 +1,11 @@
## [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.
## [0.2.0+1] - 2020-02-04 ## [0.2.0+1] - 2020-02-04
- Improve readme - Improve readme
- (internal) Add golden tests - (internal) Add golden tests

View File

@ -8,7 +8,7 @@ if (localPropertiesFile.exists()) {
def flutterRoot = localProperties.getProperty('flutter.sdk') def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) { if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") throw new Exception("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
} }
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@ -37,8 +37,7 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.github.xvrh.lottie.sample"
applicationId "com.example.example"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 28 targetSdkVersion 28
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example"> package="com.github.xvrh.lottie.sample">
<!-- Flutter needs it to communicate with the running application <!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example"> package="com.github.xvrh.lottie.sample">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that <!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method. calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide In most cases you can leave this as-is, but you if you want to provide

View File

@ -1,4 +1,4 @@
package com.example.example package com.github.xvrh.lottie.sample
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example"> package="com.github.xvrh.lottie.sample">
<!-- Flutter needs it to communicate with the running application <!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->

View File

@ -322,7 +322,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_BUNDLE_IDENTIFIER = com.github.xvrh.lottie.sample;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -457,7 +457,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_BUNDLE_IDENTIFIER = com.github.xvrh.lottie.sample;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -485,7 +485,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_BUNDLE_IDENTIFIER = com.github.xvrh.lottie.sample;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

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

@ -8,7 +8,7 @@
PRODUCT_NAME = example PRODUCT_NAME = example
// The application's bundle identifier // The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.example PRODUCT_BUNDLE_IDENTIFIER = com.github.xvrh.lottie.sample
// The copyright displayed in application information // The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2020 com.example. All rights reserved. PRODUCT_COPYRIGHT = Copyright © 2020 com.example. All rights reserved.

View File

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

View File

@ -182,8 +182,8 @@ abstract class BaseStrokeContent
var currentLength = 0.0; var currentLength = 0.0;
for (var j = pathGroup.paths.length - 1; j >= 0; j--) { for (var j = pathGroup.paths.length - 1; j >= 0; j--) {
_trimPathPath.set(pathGroup.paths[j].getPath()); _trimPathPath
_trimPathPath.transform(parentMatrix.storage); .set(pathGroup.paths[j].getPath().transform(parentMatrix.storage));
var pathMetrics = _trimPathPath.computeMetrics().toList(); var pathMetrics = _trimPathPath.computeMetrics().toList();
var length = pathMetrics.first.length; var length = pathMetrics.first.length;
if (endLength > totalLength && if (endLength > totalLength &&

View File

@ -87,7 +87,7 @@ class MergePathsContent implements PathContent, GreedyContent {
var pathList = content.getPathList(); var pathList = content.getPathList();
for (var j = pathList.length - 1; j >= 0; j--) { for (var j = pathList.length - 1; j >= 0; j--) {
var path = pathList[j].getPath(); var path = pathList[j].getPath();
path.transform(content.getTransformationMatrix().storage); path = path.transform(content.getTransformationMatrix().storage);
_remainderPath.addPath(path, Offset.zero); _remainderPath.addPath(path, Offset.zero);
} }
} else { } else {
@ -100,7 +100,7 @@ class MergePathsContent implements PathContent, GreedyContent {
var pathList = lastContent.getPathList(); var pathList = lastContent.getPathList();
for (var j = 0; j < pathList.length; j++) { for (var j = 0; j < pathList.length; j++) {
var path = pathList[j].getPath(); var path = pathList[j].getPath();
path.transform(lastContent.getTransformationMatrix().storage); path = path.transform(lastContent.getTransformationMatrix().storage);
_firstPath.addPath(path, Offset.zero); _firstPath.addPath(path, Offset.zero);
} }
} else { } else {

View File

@ -6,21 +6,12 @@ import '../lottie.dart';
import 'lottie_builder.dart'; import 'lottie_builder.dart';
import 'providers/load_image.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 { 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({ Lottie({
Key key, Key key,
@required this.composition, @required this.composition,
@ -34,10 +25,14 @@ class Lottie extends StatefulWidget {
bool reverse, bool reverse,
}) : animate = animate ?? true, }) : animate = animate ?? true,
reverse = reverse ?? false, reverse = reverse ?? false,
repeat = repeat ?? true,
super(key: key); super(key: key);
static LottieBuilder asset(String name, static LottieBuilder asset(String name,
{AnimationController controller, {Animation<double> controller,
bool animate,
bool repeat,
bool reverse,
void Function(LottieComposition) onLoaded, void Function(LottieComposition) onLoaded,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
Key key, Key key,
@ -51,6 +46,9 @@ class Lottie extends StatefulWidget {
LottieBuilder.asset( LottieBuilder.asset(
name, name,
controller: controller, controller: controller,
animate: animate,
repeat: repeat,
reverse: reverse,
imageProviderFactory: imageProviderFactory, imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded, onLoaded: onLoaded,
key: key, key: key,
@ -65,7 +63,10 @@ class Lottie extends StatefulWidget {
static LottieBuilder file( static LottieBuilder file(
File file, { File file, {
AnimationController controller, Animation<double> controller,
bool animate,
bool repeat,
bool reverse,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
void Function(LottieComposition) onLoaded, void Function(LottieComposition) onLoaded,
Key key, Key key,
@ -78,6 +79,9 @@ class Lottie extends StatefulWidget {
LottieBuilder.file( LottieBuilder.file(
file, file,
controller: controller, controller: controller,
animate: animate,
repeat: repeat,
reverse: reverse,
imageProviderFactory: imageProviderFactory, imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded, onLoaded: onLoaded,
key: key, key: key,
@ -90,7 +94,10 @@ class Lottie extends StatefulWidget {
static LottieBuilder memory( static LottieBuilder memory(
Uint8List bytes, { Uint8List bytes, {
AnimationController controller, Animation<double> controller,
bool animate,
bool repeat,
bool reverse,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
void Function(LottieComposition) onLoaded, void Function(LottieComposition) onLoaded,
Key key, Key key,
@ -103,6 +110,9 @@ class Lottie extends StatefulWidget {
LottieBuilder.memory( LottieBuilder.memory(
bytes, bytes,
controller: controller, controller: controller,
animate: animate,
repeat: repeat,
reverse: reverse,
imageProviderFactory: imageProviderFactory, imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded, onLoaded: onLoaded,
key: key, key: key,
@ -115,7 +125,10 @@ class Lottie extends StatefulWidget {
static LottieBuilder network( static LottieBuilder network(
String url, { String url, {
AnimationController controller, Animation<double> controller,
bool animate,
bool repeat,
bool reverse,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
void Function(LottieComposition) onLoaded, void Function(LottieComposition) onLoaded,
Key key, Key key,
@ -128,6 +141,9 @@ class Lottie extends StatefulWidget {
LottieBuilder.network( LottieBuilder.network(
url, url,
controller: controller, controller: controller,
animate: animate,
repeat: repeat,
reverse: reverse,
imageProviderFactory: imageProviderFactory, imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded, onLoaded: onLoaded,
key: key, key: key,
@ -138,6 +154,67 @@ class Lottie extends StatefulWidget {
alignment: alignment, 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 @override
_LottieState createState() => _LottieState(); _LottieState createState() => _LottieState();
} }
@ -167,8 +244,12 @@ class _LottieState extends State<Lottie> with TickerProviderStateMixin {
void _updateAutoAnimation() { void _updateAutoAnimation() {
_autoAnimation.stop(); _autoAnimation.stop();
if (widget.animate) { if (widget.animate && widget.controller == null) {
_autoAnimation.repeat(reverse: widget.reverse); 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 /// Several constructors are provided for the various ways that a Lottie file
/// can be provided: /// 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] /// * [new Lottie.asset], for obtaining a Lottie file from an [AssetBundle]
/// using a key. /// using a key.
/// * [new Image.network], for obtaining a lottie file from a URL. /// * [new Lottie.network], for obtaining a lottie file from a URL.
/// * [new Image.file], for obtaining a lottie file from a [File]. /// * [new Lottie.file], for obtaining a lottie file from a [File].
/// * [new Image.memory], for obtaining a lottie file from a [Uint8List]. /// * [new Lottie.memory], for obtaining a lottie file from a [Uint8List].
/// ///
class LottieBuilder extends StatefulWidget { class LottieBuilder extends StatefulWidget {
const LottieBuilder({ const LottieBuilder({
Key key, Key key,
@required this.lottie, @required this.lottie,
this.controller, this.controller,
this.animate,
this.reverse,
this.repeat,
this.onLoaded, this.onLoaded,
this.frameBuilder, this.frameBuilder,
this.width, this.width,
@ -51,6 +54,9 @@ class LottieBuilder extends StatefulWidget {
String src, { String src, {
Map<String, String> headers, Map<String, String> headers,
this.controller, this.controller,
this.animate,
this.reverse,
this.repeat,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
this.onLoaded, this.onLoaded,
Key key, Key key,
@ -78,6 +84,9 @@ class LottieBuilder extends StatefulWidget {
LottieBuilder.file( LottieBuilder.file(
File file, { File file, {
this.controller, this.controller,
this.animate,
this.reverse,
this.repeat,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
this.onLoaded, this.onLoaded,
Key key, Key key,
@ -92,6 +101,9 @@ class LottieBuilder extends StatefulWidget {
LottieBuilder.asset( LottieBuilder.asset(
String name, { String name, {
this.controller, this.controller,
this.animate,
this.reverse,
this.repeat,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
this.onLoaded, this.onLoaded,
Key key, Key key,
@ -112,6 +124,9 @@ class LottieBuilder extends StatefulWidget {
LottieBuilder.memory( LottieBuilder.memory(
Uint8List bytes, { Uint8List bytes, {
this.controller, this.controller,
this.animate,
this.reverse,
this.repeat,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
this.onLoaded, this.onLoaded,
Key key, Key key,
@ -125,6 +140,7 @@ class LottieBuilder extends StatefulWidget {
super(key: key); super(key: key);
/// The lottie animation to display. /// The lottie animation to display.
/// Example of providers: [AssetLottie], [NetworkLottie], [FileLottie], [MemoryLottie]
final LottieProvider lottie; final LottieProvider lottie;
/// A callback called when the LottieComposition has been loaded. /// 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 animation controller of the Lottie animation.
/// The animated value will be mapped to the `progress` property of the /// The animated value will be mapped to the `progress` property of the
/// Lottie animation. /// 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 /// A builder function responsible for creating the widget that represents
/// this lottie animation. /// 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 /// 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 /// soon as it is available (and will appear to "pop" in
/// if it becomes available asynchronously). Callers might use this builder to /// 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 /// add effects to the animation (such as fading the animation in when it becomes
/// available) or to display a placeholder widget while the image is loading. /// 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]. /// progress is communicated to the user, see [loadingBuilder].
/// ///
/// {@template lottie.chainedBuildersExample} /// {@template lottie.chainedBuildersExample}
@ -177,7 +210,7 @@ class LottieBuilder extends StatefulWidget {
/// {@tool snippet --template=stateless_widget_material} /// {@tool snippet --template=stateless_widget_material}
/// ///
/// The following sample demonstrates how to use this builder to implement an /// 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 /// This sample contains a limited subset of the functionality that the
/// [FadeInImage] widget provides out of the box. /// [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] /// 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 /// 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. /// layout constraints, so that the animation does not change size as it loads.
/// Consider using [fit] to adapt the image's rendering to fit the given width /// Consider using [fit] to adapt the animation's rendering to fit the given width
/// and height if the exact image dimensions are not known in advance. /// and height if the exact animation dimensions are not known in advance.
final double width; final double width;
/// If non-null, require the lottie animation to have this height. /// 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] /// 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 /// 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. /// layout constraints, so that the animation does not change size as it loads.
/// Consider using [fit] to adapt the image's rendering to fit the given width /// Consider using [fit] to adapt the animation's rendering to fit the given width
/// and height if the exact image dimensions are not known in advance. /// and height if the exact animation dimensions are not known in advance.
final double height; 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 /// The default varies based on the other fields. See the discussion at
/// [paintImage]. /// [paintImage].
final BoxFit fit; 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, /// 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 /// [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 /// animation 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 /// alignment of (0.0, 1.0) aligns the bottom middle of the animation with the
/// middle of the bottom edge of its layout bounds. /// 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]. /// [Canvas.drawImageRect].
/// ///
/// If the [alignment] is [TextDirection]-dependent (i.e. if it is a /// 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> { class _LottieBuilderState extends State<LottieBuilder> {
Future<LottieComposition> _loadingFuture; Future<LottieComposition> _loadingFuture;
bool _calledLoadedCallback = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadingFuture = widget.lottie.load(); _load();
} }
@override @override
@ -302,11 +334,21 @@ class _LottieBuilderState extends State<LottieBuilder> {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.lottie != widget.lottie) { if (oldWidget.lottie != widget.lottie) {
_loadingFuture = widget.lottie.load(); _load();
_calledLoadedCallback = false;
} }
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<LottieComposition>( return FutureBuilder<LottieComposition>(
@ -319,16 +361,13 @@ class _LottieBuilderState extends State<LottieBuilder> {
} }
var composition = snapshot.data; var composition = snapshot.data;
if (composition != null && !_calledLoadedCallback) {
_calledLoadedCallback = true;
if (widget.onLoaded != null) {
widget.onLoaded(composition);
}
}
Widget result = Lottie( Widget result = Lottie(
composition: composition, composition: composition,
controller: widget.controller, controller: widget.controller,
animate: widget.animate,
reverse: widget.reverse,
repeat: widget.repeat,
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
fit: widget.fit, fit: widget.fit,

View File

@ -52,7 +52,6 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
} }
} }
final ui.Path _path = ui.Path();
final Matrix4 _matrix = Matrix4.identity(); final Matrix4 _matrix = Matrix4.identity();
final Paint _contentPaint = ui.Paint(); final Paint _contentPaint = ui.Paint();
final Paint _dstInPaint = ui.Paint()..blendMode = ui.BlendMode.dstIn; final Paint _dstInPaint = ui.Paint()..blendMode = ui.BlendMode.dstIn;
@ -279,8 +278,7 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
BaseKeyframeAnimation<dynamic, Path> maskAnimation = BaseKeyframeAnimation<dynamic, Path> maskAnimation =
_mask.maskAnimations[i]; _mask.maskAnimations[i];
var maskPath = maskAnimation.value; var maskPath = maskAnimation.value;
_path.set(maskPath); var path = maskPath.transform(matrix.storage);
_path.transform(matrix.storage);
switch (mask.maskMode) { switch (mask.maskMode) {
case MaskMode.maskModeNone: case MaskMode.maskModeNone:
@ -297,7 +295,7 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
return bounds; return bounds;
} }
var maskBounds = _path.getBounds(); var maskBounds = path.getBounds();
// As we iterate through the masks, we want to calculate the union region of the masks. // As we iterate through the masks, we want to calculate the union region of the masks.
// We initialize the rect with the first mask. If we don't call set() on the first call, // We initialize the rect with the first mask. If we don't call set() on the first call,
// the rect will always extend to (0,0). // the rect will always extend to (0,0).
@ -420,11 +418,9 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
BaseKeyframeAnimation<ShapeData, Path> maskAnimation, BaseKeyframeAnimation<ShapeData, Path> maskAnimation,
BaseKeyframeAnimation<int, int> opacityAnimation) { BaseKeyframeAnimation<int, int> opacityAnimation) {
var maskPath = maskAnimation.value; var maskPath = maskAnimation.value;
_path var path = maskPath.transform(matrix.storage);
..set(maskPath)
..transform(matrix.storage);
_contentPaint.setAlpha((opacityAnimation.value * 2.55).round()); _contentPaint.setAlpha((opacityAnimation.value * 2.55).round());
canvas.drawPath(_path, _contentPaint); canvas.drawPath(path, _contentPaint);
} }
void _applyInvertedAddMask( void _applyInvertedAddMask(
@ -437,11 +433,9 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
canvas.saveLayer(bounds, _contentPaint); canvas.saveLayer(bounds, _contentPaint);
canvas.drawRect(bounds, _contentPaint); canvas.drawRect(bounds, _contentPaint);
var maskPath = maskAnimation.value; var maskPath = maskAnimation.value;
_path var path = maskPath.transform(matrix.storage);
..set(maskPath)
..transform(matrix.storage);
_contentPaint.setAlpha((opacityAnimation.value * 2.55).round()); _contentPaint.setAlpha((opacityAnimation.value * 2.55).round());
canvas.drawPath(_path, _dstOutPaint); canvas.drawPath(path, _dstOutPaint);
canvas.restore(); canvas.restore();
} }
@ -452,10 +446,8 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
BaseKeyframeAnimation<ShapeData, Path> maskAnimation, BaseKeyframeAnimation<ShapeData, Path> maskAnimation,
BaseKeyframeAnimation<int, int> opacityAnimation) { BaseKeyframeAnimation<int, int> opacityAnimation) {
var maskPath = maskAnimation.value; var maskPath = maskAnimation.value;
_path var path = maskPath.transform(matrix.storage);
..set(maskPath) canvas.drawPath(path, _dstOutPaint);
..transform(matrix.storage);
canvas.drawPath(_path, _dstOutPaint);
} }
void _applyInvertedSubtractMask( void _applyInvertedSubtractMask(
@ -470,9 +462,8 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
_dstOutPaint.setAlpha((opacityAnimation.value * 2.55).round()); _dstOutPaint.setAlpha((opacityAnimation.value * 2.55).round());
var maskPath = maskAnimation.value; var maskPath = maskAnimation.value;
_path.set(maskPath); var path = maskPath.transform(matrix.storage);
_path.transform(matrix.storage); canvas.drawPath(path, _dstOutPaint);
canvas.drawPath(_path, _dstOutPaint);
canvas.restore(); canvas.restore();
} }
@ -485,10 +476,9 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
BaseKeyframeAnimation<int, int> opacityAnimation) { BaseKeyframeAnimation<int, int> opacityAnimation) {
canvas.saveLayer(bounds, _dstInPaint); canvas.saveLayer(bounds, _dstInPaint);
var maskPath = maskAnimation.value; var maskPath = maskAnimation.value;
_path.set(maskPath); var path = maskPath.transform(matrix.storage);
_path.transform(matrix.storage);
_contentPaint.setAlpha((opacityAnimation.value * 2.55).round()); _contentPaint.setAlpha((opacityAnimation.value * 2.55).round());
canvas.drawPath(_path, _contentPaint); canvas.drawPath(path, _contentPaint);
canvas.restore(); canvas.restore();
} }
@ -503,9 +493,8 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
canvas.drawRect(bounds, _contentPaint); canvas.drawRect(bounds, _contentPaint);
_dstOutPaint.setAlpha((opacityAnimation.value * 2.55).round()); _dstOutPaint.setAlpha((opacityAnimation.value * 2.55).round());
var maskPath = maskAnimation.value; var maskPath = maskAnimation.value;
_path.set(maskPath); var path = maskPath.transform(matrix.storage);
_path.transform(matrix.storage); canvas.drawPath(path, _dstOutPaint);
canvas.drawPath(_path, _dstOutPaint);
canvas.restore(); canvas.restore();
} }

View File

@ -348,7 +348,7 @@ class TextLayer extends BaseLayer {
_matrix.translate( _matrix.translate(
0.0, -documentData.baselineShift * window.devicePixelRatio); 0.0, -documentData.baselineShift * window.devicePixelRatio);
_matrix.scale(fontScale, fontScale); _matrix.scale(fontScale, fontScale);
path.transform(_matrix.storage); path = path.transform(_matrix.storage);
if (documentData.strokeOverFill) { if (documentData.strokeOverFill) {
_drawGlyph(path, _fillPaint, canvas); _drawGlyph(path, _fillPaint, canvas);
_drawGlyph(path, _strokePaint, canvas); _drawGlyph(path, _strokePaint, canvas);

View File

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

View File

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

View File

@ -67,7 +67,7 @@ class MiscUtils {
} }
static int floorMod(double x, double y) { static int floorMod(double x, double y) {
return _floorDiv(x.round(), y.round()); return x.toInt() - y.toInt() * _floorDiv(x.toInt(), y.toInt());
} }
static int _floorDiv(int x, int y) { static int _floorDiv(int x, int y) {

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 133 KiB

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 {}