Compare commits

..

2 Commits

Author SHA1 Message Date
548c77dc45 Add FrameRate and improve performance (#93)
- Run the animation at the exported frame rate
- Wrap the animation in a RepaintBoundary
- Don't paint during "static" periods
2020-08-04 22:02:02 +02:00
45a4c0b981 Remove direct dependency on dart:io (#90) 2020-07-26 23:09:58 +02:00
39 changed files with 480 additions and 290 deletions

View File

@ -1,3 +1,17 @@
## [0.6.0]
- Runs the animation at the frame rate specified in the json file (ie. An animation encoded with a 20 FPS will only
be paint 20 times per seconds even though the AnimationController will invalidate the widget 60 times per seconds).
A new property `frameRate` allows to opt-out this behavior and have the widget to repaint at the device frame rate
(`FrameRate.max`).
- Automatically add a `RepaintBoundary` around the widget. Since `Lottie` animations are generally complex to paint, a
`RepaintBoundary` will separate the animation with the rest of the app and improve performance. A new property `addRepaintBoundary`
allows to opt-out this behavior.
- Fix a bug where we would call `markNeedPaint` when the animation was not changing. This removes unnecessary paints in
animations with static periods.
## [0.5.1]
- Remove direct dependencies on dart:io to support Flutter Web
## [0.5.0]
- Support loading animation from network in a web app
- Fix a couple of bugs with the web dev compiler

View File

@ -17,7 +17,7 @@ This example shows how to display a Lottie animation in the simplest way.
The `Lottie` widget will load the json file and run the animation indefinitely.
```dart
import 'example/lib/examples/main.dart';
import 'example/lib/main.dart';
```
### Specify a custom `AnimationController`

View File

@ -1,27 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ListView(
children: [
// Load a Lottie file from your assets
Lottie.asset('assets/LottieLogo1.json'),
// Load a Lottie file from a remote url
Lottie.network(
'https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json'),
// Load an animation and its images from a zip file
Lottie.asset('assets/lottiefiles/angel.zip'),
],
),
),
);
}
}

View File

@ -1,157 +1,24 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:lottie/lottie.dart';
import 'src/all_files.g.dart';
void main() {
Logger.root
..level = Level.ALL
..onRecord.listen(print);
Lottie.traceEnabled = true;
runApp(App());
}
void main() => runApp(MyApp());
class App extends StatelessWidget {
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//showPerformanceOverlay: true,
home: Scaffold(
appBar: AppBar(
title: Text('Lottie Flutter'),
),
body: GridView.builder(
itemCount: files.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
itemBuilder: (context, index) {
var assetName = files[index];
return GestureDetector(
child: _Item(
child: Lottie.asset(
assetName,
frameBuilder: (context, child, composition) {
return AnimatedOpacity(
child: child,
opacity: composition == null ? 0 : 1,
duration: const Duration(seconds: 1),
curve: Curves.easeOut,
);
},
),
),
onTap: () {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (context) => Detail(assetName)));
},
);
},
),
),
);
}
}
class _Item extends StatelessWidget {
final Widget child;
const _Item({Key key, this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(10)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
offset: Offset(2, 2),
blurRadius: 5)
]),
child: child,
),
);
}
}
class Detail extends StatefulWidget {
final String assetName;
const Detail(this.assetName, {Key key}) : super(key: key);
@override
_DetailState createState() => _DetailState();
}
class _DetailState extends State<Detail> with TickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${widget.assetName}'),
),
body: SingleChildScrollView(
child: Column(
body: ListView(
children: [
Center(
child: Lottie.asset(
widget.assetName,
controller: _controller,
onLoaded: (composition) {
_controller.duration = composition.duration;
_controller.repeat();
},
),
),
AnimatedBuilder(
animation: _controller,
builder: (context, _) => Row(
children: <Widget>[
Expanded(
child: Slider(
value: _controller.value,
onChanged: (newValue) {
_controller.value = newValue;
},
),
),
IconButton(
icon: Icon(_controller.isAnimating
? Icons.stop
: Icons.play_arrow),
onPressed: () {
setState(() {
if (_controller.isAnimating) {
_controller.stop();
} else {
_controller.repeat();
}
});
},
),
],
),
),
// Load a Lottie file from your assets
Lottie.asset('assets/LottieLogo1.json'),
// Load a Lottie file from a remote url
Lottie.network(
'https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json'),
// Load an animation and its images from a zip file
Lottie.asset('assets/lottiefiles/angel.zip'),
],
),
),

160
example/lib/main_app.dart Normal file
View File

@ -0,0 +1,160 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:lottie/lottie.dart';
import 'src/all_files.g.dart';
void main() {
Logger.root
..level = Level.ALL
..onRecord.listen(print);
Lottie.traceEnabled = true;
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//showPerformanceOverlay: true,
home: Scaffold(
appBar: AppBar(
title: Text('Lottie Flutter'),
),
body: GridView.builder(
itemCount: files.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
itemBuilder: (context, index) {
var assetName = files[index];
return GestureDetector(
child: _Item(
child: Lottie.asset(
assetName,
frameBuilder: (context, child, composition) {
return AnimatedOpacity(
child: child,
opacity: composition == null ? 0 : 1,
duration: const Duration(seconds: 1),
curve: Curves.easeOut,
);
},
),
),
onTap: () {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (context) => Detail(assetName)));
},
);
},
),
),
);
}
}
class _Item extends StatelessWidget {
final Widget child;
const _Item({Key key, this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(10)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
offset: Offset(2, 2),
blurRadius: 5)
]),
child: child,
),
);
}
}
class Detail extends StatefulWidget {
final String assetName;
const Detail(this.assetName, {Key key}) : super(key: key);
@override
_DetailState createState() => _DetailState();
}
class _DetailState extends State<Detail> with TickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${widget.assetName}'),
),
body: SingleChildScrollView(
child: Column(
children: [
Center(
child: Lottie.asset(
widget.assetName,
controller: _controller,
onLoaded: (composition) {
_controller.duration = composition.duration;
_controller.repeat();
},
),
),
AnimatedBuilder(
animation: _controller,
builder: (context, _) => Row(
children: <Widget>[
Expanded(
child: Slider(
value: _controller.value,
onChanged: (newValue) {
_controller.value = newValue;
},
),
),
IconButton(
icon: Icon(_controller.isAnimating
? Icons.stop
: Icons.play_arrow),
onPressed: () {
setState(() {
if (_controller.isAnimating) {
_controller.stop();
} else {
_controller.repeat();
}
});
},
),
],
),
),
],
),
),
);
}
}

View File

@ -5,13 +5,13 @@ PODS:
- FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral/.symlinks/flutter/darwin-x64`)
- FlutterMacOS (from `Flutter/ephemeral/.symlinks/flutter/darwin-x64-profile`)
- path_provider (from `Flutter/ephemeral/.symlinks/plugins/path_provider/macos`)
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral/.symlinks/flutter/darwin-x64
:path: Flutter/ephemeral/.symlinks/flutter/darwin-x64-profile
path_provider:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider/macos
path_provider_macos:

View File

@ -21,7 +21,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
version: "2.4.2"
boolean_selector:
dependency: transitive
description:
@ -35,7 +35,7 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.1.0-nullsafety"
charcode:
dependency: transitive
description:
@ -56,7 +56,7 @@ packages:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.12"
version: "1.15.0-nullsafety"
convert:
dependency: transitive
description:
@ -143,21 +143,21 @@ packages:
path: ".."
relative: true
source: path
version: "0.4.1"
version: "0.6.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
version: "0.12.8"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
version: "1.3.0-nullsafety"
path:
dependency: transitive
description:
@ -199,7 +199,7 @@ packages:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
version: "1.9.2"
platform:
dependency: transitive
description:
@ -239,7 +239,7 @@ packages:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
version: "1.9.5"
stream_channel:
dependency: transitive
description:
@ -267,21 +267,21 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.16"
version: "0.2.17"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
version: "1.3.0-nullsafety"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.1.0-nullsafety"
xdg_directories:
dependency: transitive
description:
@ -290,5 +290,5 @@ packages:
source: hosted
version: "0.1.0"
sdks:
dart: ">=2.7.0 <3.0.0"
dart: ">=2.9.0-18.0 <2.9.0"
flutter: ">=1.12.13+hotfix.5 <2.0.0"

View File

@ -33,6 +33,7 @@ void main() {
composition: composition,
controller: animation,
delegates: LottieDelegates(values: [delegate]),
addRepaintBoundary: false,
),
);
await tester.pump();
@ -45,6 +46,7 @@ void main() {
composition: composition,
controller: animation,
delegates: LottieDelegates(values: []),
addRepaintBoundary: false,
),
);
await tester.pump();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -42,7 +42,7 @@ class _CustomerPainter extends CustomPainter {
thumbSize;
drawable
..setProgress(progress)
..setProgress(progress, frameRate: FrameRate.max)
..draw(canvas, rect);
++index;

View File

@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie_example/main.dart';
import 'package:lottie_example/main_app.dart';
void main() {
testWidgets('Main sample', (tester) async {

View File

@ -1,4 +1,5 @@
export 'src/composition.dart' show LottieComposition;
export 'src/frame_rate.dart' show FrameRate;
export 'src/lottie.dart' show Lottie;
export 'src/lottie_builder.dart' show LottieBuilder;
export 'src/lottie_delegates.dart' show LottieDelegates;

View File

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:flutter/widgets.dart';
import 'package:path/path.dart' as p;
import 'frame_rate.dart';
import 'logger.dart';
import 'lottie_image_asset.dart';
import 'model/font.dart';
@ -133,9 +134,11 @@ class LottieComposition {
Rectangle<int> get bounds => _bounds;
Duration get duration {
return Duration(milliseconds: (durationFrames / _frameRate * 1000).round());
return Duration(milliseconds: (seconds * 1000).round());
}
double get seconds => durationFrames / _frameRate;
double get startFrame => _startFrame;
double get endFrame => _endFrame;
@ -176,6 +179,24 @@ class LottieComposition {
return _endFrame - _startFrame;
}
/// Returns a "rounded" progress value according to the frameRate
double roundProgress(double progress, {@required FrameRate frameRate}) {
num fps;
if (frameRate == FrameRate.max) {
return progress;
} else if (frameRate == FrameRate.composition) {
fps = this.frameRate;
}
fps ??= frameRate.framesPerSecond;
var totalFrameCount = seconds * fps;
var frameIndex = (totalFrameCount * progress).toInt();
var roundedProgress = frameIndex / totalFrameCount;
assert(roundedProgress >= 0 && roundedProgress <= 1,
'Progress is $roundedProgress');
return roundedProgress;
}
@override
String toString() {
final sb = StringBuffer('LottieComposition:\n');

9
lib/src/frame_rate.dart Normal file
View File

@ -0,0 +1,9 @@
class FrameRate {
static final max = FrameRate._special(0);
static final composition = FrameRate._special(-1);
final double framesPerSecond;
FrameRate(this.framesPerSecond) : assert(framesPerSecond > 0);
FrameRate._special(this.framesPerSecond);
}

View File

@ -1,8 +1,8 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../lottie.dart';
import 'frame_rate.dart';
import 'l.dart';
import 'lottie_builder.dart';
import 'options.dart';
@ -23,36 +23,44 @@ class Lottie extends StatefulWidget {
this.alignment,
this.fit,
bool animate,
this.frameRate,
bool repeat,
bool reverse,
this.delegates,
this.options,
bool addRepaintBoundary,
}) : animate = animate ?? true,
reverse = reverse ?? false,
repeat = repeat ?? true,
addRepaintBoundary = addRepaintBoundary ?? true,
super(key: key);
/// Creates a widget that displays an [LottieComposition] obtained from an [AssetBundle].
static LottieBuilder asset(String name,
{Animation<double> controller,
bool animate,
bool repeat,
bool reverse,
LottieDelegates delegates,
LottieOptions options,
void Function(LottieComposition) onLoaded,
LottieImageProviderFactory imageProviderFactory,
Key key,
AssetBundle bundle,
LottieFrameBuilder frameBuilder,
double width,
double height,
BoxFit fit,
Alignment alignment,
String package}) =>
static LottieBuilder asset(
String name, {
Animation<double> controller,
bool animate,
FrameRate frameRate,
bool repeat,
bool reverse,
LottieDelegates delegates,
LottieOptions options,
void Function(LottieComposition) onLoaded,
LottieImageProviderFactory imageProviderFactory,
Key key,
AssetBundle bundle,
LottieFrameBuilder frameBuilder,
double width,
double height,
BoxFit fit,
Alignment alignment,
String package,
bool addRepaintBoundary,
}) =>
LottieBuilder.asset(
name,
controller: controller,
frameRate: frameRate,
animate: animate,
repeat: repeat,
reverse: reverse,
@ -68,12 +76,14 @@ class Lottie extends StatefulWidget {
fit: fit,
alignment: alignment,
package: package,
addRepaintBoundary: addRepaintBoundary,
);
/// Creates a widget that displays an [LottieComposition] obtained from a [File].
static LottieBuilder file(
File file, {
Object /*io.File|html.File*/ file, {
Animation<double> controller,
FrameRate frameRate,
bool animate,
bool repeat,
bool reverse,
@ -87,10 +97,12 @@ class Lottie extends StatefulWidget {
double height,
BoxFit fit,
Alignment alignment,
bool addRepaintBoundary,
}) =>
LottieBuilder.file(
file,
controller: controller,
frameRate: frameRate,
animate: animate,
repeat: repeat,
reverse: reverse,
@ -104,12 +116,14 @@ class Lottie extends StatefulWidget {
height: height,
fit: fit,
alignment: alignment,
addRepaintBoundary: addRepaintBoundary,
);
/// Creates a widget that displays an [LottieComposition] obtained from a [Uint8List].
static LottieBuilder memory(
Uint8List bytes, {
Animation<double> controller,
FrameRate frameRate,
bool animate,
bool repeat,
bool reverse,
@ -123,10 +137,12 @@ class Lottie extends StatefulWidget {
double height,
BoxFit fit,
Alignment alignment,
bool addRepaintBoundary,
}) =>
LottieBuilder.memory(
bytes,
controller: controller,
frameRate: frameRate,
animate: animate,
repeat: repeat,
reverse: reverse,
@ -140,12 +156,14 @@ class Lottie extends StatefulWidget {
height: height,
fit: fit,
alignment: alignment,
addRepaintBoundary: addRepaintBoundary,
);
/// Creates a widget that displays an [LottieComposition] obtained from the network.
static LottieBuilder network(
String url, {
Animation<double> controller,
FrameRate frameRate,
bool animate,
bool repeat,
bool reverse,
@ -159,10 +177,12 @@ class Lottie extends StatefulWidget {
double height,
BoxFit fit,
Alignment alignment,
bool addRepaintBoundary,
}) =>
LottieBuilder.network(
url,
controller: controller,
frameRate: frameRate,
animate: animate,
repeat: repeat,
reverse: reverse,
@ -176,6 +196,7 @@ class Lottie extends StatefulWidget {
height: height,
fit: fit,
alignment: alignment,
addRepaintBoundary: addRepaintBoundary,
);
/// The Lottie composition to animate.
@ -187,6 +208,14 @@ class Lottie extends StatefulWidget {
/// with the properties [animate], [reverse]
final Animation<double> controller;
/// The number of frames per second to render.
/// Use `FrameRate.composition` to use the original frame rate of the Lottie composition (default)
/// Use `FrameRate.max` to advance the animation progression at every frame.
///
/// The advantage of using a low frame rate is to preserve the device battery
/// by doing less rendering work.
final FrameRate frameRate;
/// 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.
@ -250,6 +279,13 @@ class Lottie extends StatefulWidget {
/// - enableMergePaths: Enable merge path support
final LottieOptions options;
/// Indicate to automatically add a `RepaintBoundary` widget around the animation.
/// This allows to optimize the app performance by isolating the animation in its
/// own `Layer`.
///
/// This property is `true` by default.
final bool addRepaintBoundary;
static bool get traceEnabled => L.traceEnabled;
static set traceEnabled(bool enabled) {
L.traceEnabled = enabled;
@ -304,18 +340,27 @@ class _LottieState extends State<Lottie> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
Widget child = AnimatedBuilder(
animation: _progressAnimation,
builder: (context, _) => RawLottie(
composition: widget.composition,
delegates: widget.delegates,
options: widget.options,
progress: _progressAnimation.value,
width: widget.width,
height: widget.height,
fit: widget.fit,
alignment: widget.alignment,
),
builder: (context, _) {
return RawLottie(
composition: widget.composition,
delegates: widget.delegates,
options: widget.options,
progress: _progressAnimation.value,
frameRate: widget.frameRate,
width: widget.width,
height: widget.height,
fit: widget.fit,
alignment: widget.alignment,
);
},
);
if (widget.addRepaintBoundary) {
child = RepaintBoundary(child: child);
}
return child;
}
}

View File

@ -1,10 +1,10 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../lottie.dart';
import 'frame_rate.dart';
import 'lottie.dart';
import 'providers/asset_provider.dart';
import 'providers/file_provider.dart';
@ -36,6 +36,7 @@ class LottieBuilder extends StatefulWidget {
Key key,
@required this.lottie,
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
@ -47,6 +48,7 @@ class LottieBuilder extends StatefulWidget {
this.height,
this.fit,
this.alignment,
this.addRepaintBoundary,
}) : assert(lottie != null),
super(key: key);
@ -55,6 +57,7 @@ class LottieBuilder extends StatefulWidget {
String src, {
Map<String, String> headers,
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
@ -68,6 +71,7 @@ class LottieBuilder extends StatefulWidget {
this.height,
this.fit,
this.alignment,
this.addRepaintBoundary,
}) : lottie = NetworkLottie(src,
headers: headers, imageProviderFactory: imageProviderFactory),
super(key: key);
@ -83,8 +87,9 @@ class LottieBuilder extends StatefulWidget {
/// `android.permission.READ_EXTERNAL_STORAGE` permission.
///
LottieBuilder.file(
File file, {
Object /*io.File|html.File*/ file, {
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
@ -98,6 +103,7 @@ class LottieBuilder extends StatefulWidget {
this.height,
this.fit,
this.alignment,
this.addRepaintBoundary,
}) : lottie = FileLottie(file, imageProviderFactory: imageProviderFactory),
super(key: key);
@ -105,6 +111,7 @@ class LottieBuilder extends StatefulWidget {
LottieBuilder.asset(
String name, {
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
@ -120,6 +127,7 @@ class LottieBuilder extends StatefulWidget {
this.fit,
this.alignment,
String package,
this.addRepaintBoundary,
}) : lottie = AssetLottie(name,
bundle: bundle,
package: package,
@ -130,6 +138,7 @@ class LottieBuilder extends StatefulWidget {
LottieBuilder.memory(
Uint8List bytes, {
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
@ -143,6 +152,7 @@ class LottieBuilder extends StatefulWidget {
this.height,
this.fit,
this.alignment,
this.addRepaintBoundary,
}) : lottie =
MemoryLottie(bytes, imageProviderFactory: imageProviderFactory),
super(key: key);
@ -161,6 +171,11 @@ class LottieBuilder extends StatefulWidget {
/// Lottie animation.
final Animation<double> controller;
/// The number of frames per second to render.
/// Use `FrameRate.composition` to use the original frame rate of the Lottie composition (default)
/// Use `FrameRate.max` to advance the animation progression at every frame.
final FrameRate frameRate;
/// 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.
@ -321,6 +336,13 @@ class LottieBuilder extends StatefulWidget {
/// relative to text direction.
final AlignmentGeometry alignment;
/// Indicate to automatically add a `RepaintBoundary` widget around the animation.
/// This allows to optimize the app performance by isolating the animation in its
/// own `Layer`.
///
/// This property is `true` by default.
final bool addRepaintBoundary;
@override
_LottieBuilderState createState() => _LottieBuilderState();
@ -384,6 +406,7 @@ class _LottieBuilderState extends State<LottieBuilder> {
Widget result = Lottie(
composition: composition,
controller: widget.controller,
frameRate: widget.frameRate,
animate: widget.animate,
reverse: widget.reverse,
repeat: widget.repeat,
@ -393,6 +416,7 @@ class _LottieBuilderState extends State<LottieBuilder> {
height: widget.height,
fit: widget.fit,
alignment: widget.alignment,
addRepaintBoundary: widget.addRepaintBoundary,
);
if (widget.frameBuilder != null) {

View File

@ -3,7 +3,7 @@ import 'lottie_drawable.dart';
import 'value_delegate.dart';
// TODO(xha): recognize style Bold, Medium, Regular, SemiBold, etc...
TextStyle _defaultTextStyleDelegate(LottieFontStyle font) =>
TextStyle defaultTextStyleDelegate(LottieFontStyle font) =>
TextStyle(fontFamily: font.fontFamily);
@immutable
@ -49,7 +49,7 @@ class LottieDelegates {
this.text,
TextStyle Function(LottieFontStyle) textStyle,
this.values,
}) : textStyle = textStyle ?? _defaultTextStyleDelegate;
}) : textStyle = textStyle;
@override
bool operator ==(Object other) =>

View File

@ -2,6 +2,7 @@ import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math_64.dart';
import 'composition.dart';
import 'frame_rate.dart';
import 'lottie_delegates.dart';
import 'model/key_path.dart';
import 'model/layer/composition_layer.dart';
@ -43,26 +44,32 @@ class LottieDrawable {
_isDirty = true;
}
double get progress => _progress;
double _progress = 0.0;
bool setProgress(double value) {
_isDirty = false;
_progress = value;
_compositionLayer.setProgress(value);
return _isDirty;
double get progress => _progress ?? 0.0;
double _progress;
bool setProgress(double value, {FrameRate frameRate}) {
frameRate ??= FrameRate.composition;
var roundedProgress =
composition.roundProgress(value, frameRate: frameRate);
if (roundedProgress != _progress) {
_isDirty = false;
_progress = roundedProgress;
_compositionLayer.setProgress(roundedProgress);
return _isDirty;
} else {
return false;
}
}
LottieDelegates get delegates => _delegates;
set delegates(LottieDelegates delegates) {
delegates ??= LottieDelegates();
if (_delegates != delegates) {
_delegates = delegates;
_updateValueDelegates(delegates.values);
_updateValueDelegates(delegates?.values);
}
}
bool get useTextGlyphs {
return delegates.text == null && composition.characters.isNotEmpty;
return delegates?.text == null && composition.characters.isNotEmpty;
}
ui.Image getImageAsset(String ref) {
@ -75,8 +82,8 @@ class LottieDrawable {
}
TextStyle getTextStyle(String font, String style) {
return _delegates
.textStyle(LottieFontStyle(fontFamily: font, style: style));
return (_delegates?.textStyle ?? defaultTextStyleDelegate)(
LottieFontStyle(fontFamily: font, style: style));
}
List<ValueDelegate> _valueDelegates = <ValueDelegate>[];

View File

@ -232,7 +232,7 @@ class TextLayer extends BaseLayer {
return;
}
var text = documentData.text;
var textDelegate = lottieDrawable.delegates.text;
var textDelegate = lottieDrawable.delegates?.text;
if (textDelegate != null) {
text = textDelegate(text);
}

View File

@ -1,25 +1,24 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:path/path.dart' as p;
import '../composition.dart';
import '../lottie_image_asset.dart';
import 'load_image.dart';
import 'lottie_provider.dart';
import 'provider_io.dart' if (dart.library.html) 'provider_web.dart' as io;
class FileLottie extends LottieProvider {
FileLottie(this.file, {LottieImageProviderFactory imageProviderFactory})
: super(imageProviderFactory: imageProviderFactory);
final File file;
final Object /*io.File|html.File*/ file;
@override
Future<LottieComposition> load() async {
var cacheKey = 'file-${file.path}';
var cacheKey = 'file-${io.filePath(file)}';
return sharedLottieCache.putIfAbsent(cacheKey, () async {
var bytes = await file.readAsBytes();
var bytes = await io.loadFile(file);
var composition = await LottieComposition.fromBytes(bytes,
name: p.basenameWithoutExtension(file.path));
name: p.basenameWithoutExtension(io.filePath(file)));
for (var image in composition.images.values) {
image.loadedImage ??= await _loadImage(composition, image);
@ -33,11 +32,7 @@ class FileLottie extends LottieProvider {
LottieComposition composition, LottieImageAsset lottieImage) {
var imageProvider = getImageProvider(lottieImage);
if (imageProvider == null) {
var imagePath = p.url.join(
p.dirname(file.path), lottieImage.dirName, lottieImage.fileName);
imageProvider = FileImage(File(imagePath));
}
imageProvider ??= io.loadImageForFile(file, lottieImage);
return loadImage(composition, lottieImage, imageProvider);
}
@ -52,5 +47,5 @@ class FileLottie extends LottieProvider {
int get hashCode => file.hashCode;
@override
String toString() => '$runtimeType(file: ${file.path})';
String toString() => '$runtimeType(file: ${io.filePath(file)})';
}

View File

@ -6,8 +6,7 @@ import '../composition.dart';
import '../lottie_image_asset.dart';
import 'load_image.dart';
import 'lottie_provider.dart';
import 'network_provider_io.dart'
if (dart.library.html) 'network_provider_web.dart' as network;
import 'provider_io.dart' if (dart.library.html) 'provider_web.dart' as network;
class NetworkLottie extends LottieProvider {
NetworkLottie(this.url,
@ -22,7 +21,7 @@ class NetworkLottie extends LottieProvider {
var cacheKey = 'network-$url';
return sharedLottieCache.putIfAbsent(cacheKey, () async {
var resolved = Uri.base.resolve(url);
var bytes = await network.load(resolved, headers: headers);
var bytes = await network.loadHttp(resolved, headers: headers);
var composition = await LottieComposition.fromBytes(bytes,
name: p.url.basenameWithoutExtension(url));

View File

@ -1,16 +0,0 @@
import 'dart:html';
import 'dart:typed_data';
Future<Uint8List> load(Uri uri, {Map<String, String> headers}) async {
var request = await HttpRequest.request(uri.toString(),
requestHeaders: headers, responseType: 'blob');
var reader = FileReader();
reader.readAsArrayBuffer(request.response as Blob);
await reader.onLoadEnd.first;
if (reader.readyState != FileReader.DONE) {
throw Exception('Error while reading $uri');
}
return reader.result as Uint8List;
}

View File

@ -1,10 +1,13 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:path/path.dart' as p;
import '../lottie_image_asset.dart';
final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
Future<Uint8List> load(Uri uri, {Map<String, String> headers}) async {
Future<Uint8List> loadHttp(Uri uri, {Map<String, String> headers}) async {
var request = await _sharedHttpClient.getUrl(uri);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
@ -21,3 +24,19 @@ Future<Uint8List> load(Uri uri, {Map<String, String> headers}) async {
return bytes;
}
Future<Uint8List> loadFile(Object file) {
return (file as File).readAsBytes();
}
String filePath(Object file) {
return (file as File).path;
}
ImageProvider loadImageForFile(Object file, LottieImageAsset lottieImage) {
var fileIo = file as File;
var imagePath = p.url
.join(p.dirname(fileIo.path), lottieImage.dirName, lottieImage.fileName);
return FileImage(File(imagePath));
}

View File

@ -0,0 +1,35 @@
import 'dart:html';
import 'dart:typed_data';
import 'package:flutter/rendering.dart';
import '../lottie_image_asset.dart';
Future<Uint8List> loadHttp(Uri uri, {Map<String, String> headers}) async {
var request = await HttpRequest.request(uri.toString(),
requestHeaders: headers, responseType: 'blob');
return _loadBlob(request.response as Blob);
}
Future<Uint8List> loadFile(Object file) {
return _loadBlob(file as File);
}
Future<Uint8List> _loadBlob(Blob file) async {
var reader = FileReader();
reader.readAsArrayBuffer(file);
await reader.onLoadEnd.first;
if (reader.readyState != FileReader.DONE) {
throw Exception('Error while reading blob');
}
return reader.result as Uint8List;
}
String filePath(Object file) {
return (file as File).relativePath;
}
ImageProvider loadImageForFile(Object file, LottieImageAsset lottieImage) {
return null;
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import '../lottie.dart';
import 'frame_rate.dart';
import 'lottie_drawable.dart';
import 'render_lottie.dart';
@ -15,6 +16,7 @@ class RawLottie extends LeafRenderObjectWidget {
this.delegates,
this.options,
double progress,
this.frameRate,
this.width,
this.height,
this.fit,
@ -34,6 +36,11 @@ class RawLottie extends LeafRenderObjectWidget {
/// The progress of the Lottie animation (between 0.0 and 1.0).
final double progress;
/// The number of frames per second to render.
/// Use `FrameRate.composition` to use the original frame rate of the Lottie composition (default)
/// Use `FrameRate.max` to advance the animation progression at every frame.
final FrameRate frameRate;
/// If non-null, require the Lottie composition to have this width.
///
/// If null, the composition will pick a size that best preserves its intrinsic
@ -76,6 +83,7 @@ class RawLottie extends LeafRenderObjectWidget {
delegates: delegates,
enableMergePaths: options?.enableMergePaths,
progress: progress,
frameRate: frameRate,
width: width,
height: height,
fit: fit,
@ -88,6 +96,7 @@ class RawLottie extends LeafRenderObjectWidget {
renderObject
..setComposition(composition,
progress: progress,
frameRate: frameRate,
delegates: delegates,
enableMergePaths: options?.enableMergePaths)
..width = width

View File

@ -1,6 +1,7 @@
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import '../lottie.dart';
import 'frame_rate.dart';
import 'lottie_drawable.dart';
/// A Lottie animation in the render tree.
@ -9,10 +10,11 @@ import 'lottie_drawable.dart';
/// constraints and preserves the composition's intrinsic aspect ratio.
class RenderLottie extends RenderBox {
RenderLottie({
LottieComposition composition,
@required LottieComposition composition,
LottieDelegates delegates,
bool enableMergePaths,
double progress = 0.0,
FrameRate frameRate,
double width,
double height,
BoxFit fit,
@ -21,7 +23,7 @@ class RenderLottie extends RenderBox {
assert(progress != null && progress >= 0.0 && progress <= 1.0),
_drawable = composition != null
? (LottieDrawable(composition, enableMergePaths: enableMergePaths)
..setProgress(progress)
..setProgress(progress, frameRate: frameRate)
..delegates = delegates)
: null,
_width = width,
@ -34,6 +36,7 @@ class RenderLottie extends RenderBox {
LottieDrawable _drawable;
void setComposition(LottieComposition composition,
{@required double progress,
@required FrameRate frameRate,
@required LottieDelegates delegates,
bool enableMergePaths}) {
enableMergePaths ??= false;
@ -41,9 +44,11 @@ class RenderLottie extends RenderBox {
var needsLayout = false;
var needsPaint = false;
if (composition == null) {
_drawable = null;
needsPaint = true;
needsLayout = true;
if (_drawable != null) {
_drawable = null;
needsPaint = true;
needsLayout = true;
}
} else {
if (_drawable == null ||
_drawable.composition != composition ||
@ -54,7 +59,7 @@ class RenderLottie extends RenderBox {
needsPaint = true;
}
needsPaint |= _drawable.setProgress(progress);
needsPaint |= _drawable.setProgress(progress, frameRate: frameRate);
if (_drawable.delegates != delegates) {
_drawable.delegates = delegates;

View File

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
version: "6.0.0"
analyzer:
dependency: "direct dev"
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.39.12"
version: "0.39.15"
archive:
dependency: "direct main"
description:
@ -35,7 +35,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
version: "2.4.2"
boolean_selector:
dependency: transitive
description:
@ -49,7 +49,7 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.1.0-nullsafety"
charcode:
dependency: "direct main"
description:
@ -57,6 +57,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
clock:
dependency: transitive
description:
@ -70,7 +77,7 @@ packages:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.12"
version: "1.15.0-nullsafety"
convert:
dependency: transitive
description:
@ -91,7 +98,7 @@ packages:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
version: "0.16.2"
dart_style:
dependency: "direct dev"
description:
@ -150,14 +157,14 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
version: "0.12.8"
meta:
dependency: "direct main"
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
version: "1.3.0-nullsafety"
mockito:
dependency: "direct dev"
description:
@ -199,7 +206,7 @@ packages:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
version: "1.9.2"
pub_semver:
dependency: transitive
description:
@ -225,7 +232,7 @@ packages:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
version: "1.9.5"
stream_channel:
dependency: transitive
description:
@ -253,21 +260,21 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.16"
version: "0.2.17"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
version: "1.3.0-nullsafety"
vector_math:
dependency: "direct main"
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.1.0-nullsafety"
watcher:
dependency: transitive
description:
@ -283,4 +290,4 @@ packages:
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.7.0 <3.0.0"
dart: ">=2.9.0-18.0 <2.9.0"

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.5.0
version: 0.6.0
homepage: https://github.com/xvrh/lottie-flutter
environment:

14
test/frame_rate_test.dart Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 31 KiB