mirror of
https://github.com/xvrh/lottie-flutter.git
synced 2025-08-06 16:39:36 +08:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
42833c6429 | |||
c9a6a6a187 | |||
6eb9210952 | |||
20405b1c5f | |||
c383dc6be1 | |||
98c2efe12d |
@ -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
|
||||
- Improve readme
|
||||
- (internal) Add golden tests
|
||||
|
@ -8,7 +8,7 @@ if (localPropertiesFile.exists()) {
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
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')
|
||||
@ -37,8 +37,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.example"
|
||||
applicationId "com.github.xvrh.lottie.sample"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
In most cases you can leave this as-is, but you if you want to provide
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.example.example
|
||||
package com.github.xvrh.lottie.sample
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.embedding.android.FlutterActivity
|
@ -1,5 +1,5 @@
|
||||
<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
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
@ -322,7 +322,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.xvrh.lottie.sample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -457,7 +457,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.xvrh.lottie.sample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@ -485,7 +485,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.xvrh.lottie.sample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
11
example/lib/src/web_files.dart
Normal file
11
example/lib/src/web_files.dart
Normal 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',
|
||||
];
|
@ -8,7 +8,7 @@
|
||||
PRODUCT_NAME = example
|
||||
|
||||
// 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
|
||||
PRODUCT_COPYRIGHT = Copyright © 2020 com.example. All rights reserved.
|
||||
|
@ -87,7 +87,7 @@ packages:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.2.0+1"
|
||||
version: "0.2.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -182,8 +182,8 @@ abstract class BaseStrokeContent
|
||||
|
||||
var currentLength = 0.0;
|
||||
for (var j = pathGroup.paths.length - 1; j >= 0; j--) {
|
||||
_trimPathPath.set(pathGroup.paths[j].getPath());
|
||||
_trimPathPath.transform(parentMatrix.storage);
|
||||
_trimPathPath
|
||||
.set(pathGroup.paths[j].getPath().transform(parentMatrix.storage));
|
||||
var pathMetrics = _trimPathPath.computeMetrics().toList();
|
||||
var length = pathMetrics.first.length;
|
||||
if (endLength > totalLength &&
|
||||
|
@ -87,7 +87,7 @@ class MergePathsContent implements PathContent, GreedyContent {
|
||||
var pathList = content.getPathList();
|
||||
for (var j = pathList.length - 1; j >= 0; j--) {
|
||||
var path = pathList[j].getPath();
|
||||
path.transform(content.getTransformationMatrix().storage);
|
||||
path = path.transform(content.getTransformationMatrix().storage);
|
||||
_remainderPath.addPath(path, Offset.zero);
|
||||
}
|
||||
} else {
|
||||
@ -100,7 +100,7 @@ class MergePathsContent implements PathContent, GreedyContent {
|
||||
var pathList = lastContent.getPathList();
|
||||
for (var j = 0; j < pathList.length; j++) {
|
||||
var path = pathList[j].getPath();
|
||||
path.transform(lastContent.getTransformationMatrix().storage);
|
||||
path = path.transform(lastContent.getTransformationMatrix().storage);
|
||||
_firstPath.addPath(path, Offset.zero);
|
||||
}
|
||||
} else {
|
||||
|
@ -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) {
|
||||
if (widget.animate && widget.controller == null) {
|
||||
if (widget.repeat) {
|
||||
_autoAnimation.repeat(reverse: widget.reverse);
|
||||
} else {
|
||||
_autoAnimation.forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -52,7 +52,6 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
|
||||
}
|
||||
}
|
||||
|
||||
final ui.Path _path = ui.Path();
|
||||
final Matrix4 _matrix = Matrix4.identity();
|
||||
final Paint _contentPaint = ui.Paint();
|
||||
final Paint _dstInPaint = ui.Paint()..blendMode = ui.BlendMode.dstIn;
|
||||
@ -279,8 +278,7 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
|
||||
BaseKeyframeAnimation<dynamic, Path> maskAnimation =
|
||||
_mask.maskAnimations[i];
|
||||
var maskPath = maskAnimation.value;
|
||||
_path.set(maskPath);
|
||||
_path.transform(matrix.storage);
|
||||
var path = maskPath.transform(matrix.storage);
|
||||
|
||||
switch (mask.maskMode) {
|
||||
case MaskMode.maskModeNone:
|
||||
@ -297,7 +295,7 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
|
||||
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.
|
||||
// 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).
|
||||
@ -420,11 +418,9 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
|
||||
BaseKeyframeAnimation<ShapeData, Path> maskAnimation,
|
||||
BaseKeyframeAnimation<int, int> opacityAnimation) {
|
||||
var maskPath = maskAnimation.value;
|
||||
_path
|
||||
..set(maskPath)
|
||||
..transform(matrix.storage);
|
||||
var path = maskPath.transform(matrix.storage);
|
||||
_contentPaint.setAlpha((opacityAnimation.value * 2.55).round());
|
||||
canvas.drawPath(_path, _contentPaint);
|
||||
canvas.drawPath(path, _contentPaint);
|
||||
}
|
||||
|
||||
void _applyInvertedAddMask(
|
||||
@ -437,11 +433,9 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
|
||||
canvas.saveLayer(bounds, _contentPaint);
|
||||
canvas.drawRect(bounds, _contentPaint);
|
||||
var maskPath = maskAnimation.value;
|
||||
_path
|
||||
..set(maskPath)
|
||||
..transform(matrix.storage);
|
||||
var path = maskPath.transform(matrix.storage);
|
||||
_contentPaint.setAlpha((opacityAnimation.value * 2.55).round());
|
||||
canvas.drawPath(_path, _dstOutPaint);
|
||||
canvas.drawPath(path, _dstOutPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@ -452,10 +446,8 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
|
||||
BaseKeyframeAnimation<ShapeData, Path> maskAnimation,
|
||||
BaseKeyframeAnimation<int, int> opacityAnimation) {
|
||||
var maskPath = maskAnimation.value;
|
||||
_path
|
||||
..set(maskPath)
|
||||
..transform(matrix.storage);
|
||||
canvas.drawPath(_path, _dstOutPaint);
|
||||
var path = maskPath.transform(matrix.storage);
|
||||
canvas.drawPath(path, _dstOutPaint);
|
||||
}
|
||||
|
||||
void _applyInvertedSubtractMask(
|
||||
@ -470,9 +462,8 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
|
||||
_dstOutPaint.setAlpha((opacityAnimation.value * 2.55).round());
|
||||
|
||||
var maskPath = maskAnimation.value;
|
||||
_path.set(maskPath);
|
||||
_path.transform(matrix.storage);
|
||||
canvas.drawPath(_path, _dstOutPaint);
|
||||
var path = maskPath.transform(matrix.storage);
|
||||
canvas.drawPath(path, _dstOutPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@ -485,10 +476,9 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
|
||||
BaseKeyframeAnimation<int, int> opacityAnimation) {
|
||||
canvas.saveLayer(bounds, _dstInPaint);
|
||||
var maskPath = maskAnimation.value;
|
||||
_path.set(maskPath);
|
||||
_path.transform(matrix.storage);
|
||||
var path = maskPath.transform(matrix.storage);
|
||||
_contentPaint.setAlpha((opacityAnimation.value * 2.55).round());
|
||||
canvas.drawPath(_path, _contentPaint);
|
||||
canvas.drawPath(path, _contentPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@ -503,9 +493,8 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement {
|
||||
canvas.drawRect(bounds, _contentPaint);
|
||||
_dstOutPaint.setAlpha((opacityAnimation.value * 2.55).round());
|
||||
var maskPath = maskAnimation.value;
|
||||
_path.set(maskPath);
|
||||
_path.transform(matrix.storage);
|
||||
canvas.drawPath(_path, _dstOutPaint);
|
||||
var path = maskPath.transform(matrix.storage);
|
||||
canvas.drawPath(path, _dstOutPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
|
@ -348,7 +348,7 @@ class TextLayer extends BaseLayer {
|
||||
_matrix.translate(
|
||||
0.0, -documentData.baselineShift * window.devicePixelRatio);
|
||||
_matrix.scale(fontScale, fontScale);
|
||||
path.transform(_matrix.storage);
|
||||
path = path.transform(_matrix.storage);
|
||||
if (documentData.strokeOverFill) {
|
||||
_drawGlyph(path, _fillPaint, canvas);
|
||||
_drawGlyph(path, _strokePaint, canvas);
|
||||
|
@ -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;
|
||||
|
@ -46,6 +46,10 @@ class LottieCache {
|
||||
_cache.remove(_cache.keys.first);
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final sharedLottieCache = LottieCache();
|
||||
|
@ -67,7 +67,7 @@ class MiscUtils {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -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:
|
||||
|
@ -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.0+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
|
||||
|
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
312
test/lottie_test.dart
Normal 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 {}
|
Reference in New Issue
Block a user