Expose LottieDelegates to modify animation properties at runtime (#23)

This commit is contained in:
Xavier H
2020-03-02 22:11:38 +01:00
committed by GitHub
parent 4ae257eaab
commit e89c3c4914
93 changed files with 1372 additions and 343 deletions

View File

@ -11,7 +11,7 @@ jobs:
strategy: strategy:
matrix: matrix:
flutter: ['stable', 'dev'] flutter: ['stable', 'dev']
runs-on: ubuntu-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: subosito/flutter-action@v1 - uses: subosito/flutter-action@v1
@ -19,8 +19,12 @@ jobs:
channel: ${{ matrix.flutter }} channel: ${{ matrix.flutter }}
- run: flutter doctor - run: flutter doctor
- run: flutter --version - run: flutter --version
- run: flutter pub get
working-directory: example
- run: flutter analyze - run: flutter analyze
- run: flutter test test # https://github.com/flutter/flutter/issues/20907 - run: flutter test test # https://github.com/flutter/flutter/issues/20907
- run: flutter test test
working-directory: example
- run: flutter pub run tool/prepare_submit.dart - run: flutter pub run tool/prepare_submit.dart
- name: "check for uncommitted changes" - name: "check for uncommitted changes"
run: | run: |

2
.gitignore vendored
View File

@ -4,6 +4,8 @@ _*
!.gitignore !.gitignore
!.github !.github
**/failures/*.png
*.iml *.iml
**/doc/api/ **/doc/api/
build/ build/

View File

@ -1,3 +1,8 @@
## [0.3.0] - 2020-03-02
- Add `LottieDelegates` a group of options to customize the lottie animation at runtime.
ie: Dynamically modify color, position, size, text... of every elements of the animation.
- Integrate latest changes from Lottie-android
## [0.2.2] - 2020-02-21 ## [0.2.2] - 2020-02-21
- Add a [repeat] parameter to specify if the automatic animation should loop. - Add a [repeat] parameter to specify if the automatic animation should loop.
- Add the [animate], [reverse], [repeat] properties on `LottieBuilder` - Add the [animate], [reverse], [repeat] properties on `LottieBuilder`

View File

@ -37,7 +37,7 @@ class MyApp extends StatelessWidget {
'https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json'), 'https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json'),
// Load an animation and its images from a zip file // Load an animation and its images from a zip file
Lottie.asset('assets/lottiesfiles/angel.zip'), Lottie.asset('assets/lottiefiles/angel.zip'),
], ],
), ),
), ),
@ -46,15 +46,8 @@ class MyApp extends StatelessWidget {
} }
``` ```
To load an animation from the assets folder, we need to add an `assets` section in the `pubspec.yaml`:
```yaml
flutter:
assets:
- assets/
```
### Specify a custom `AnimationController` ### Specify a custom `AnimationController`
This example shows how to have full control over the animation by providing your own `AnimationController`. This example shows how to take full control over the animation by providing your own `AnimationController`.
```dart ```dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -201,7 +194,9 @@ class _Painter extends CustomPainter {
var columns = 10; var columns = 10;
for (var i = 0; i < frameCount; i++) { for (var i = 0; i < frameCount; i++) {
var destRect = Offset(i % columns * 50.0, i ~/ 10 * 80.0) & (size / 5); var destRect = Offset(i % columns * 50.0, i ~/ 10 * 80.0) & (size / 5);
drawable.draw(canvas, destRect, progress: i / frameCount); drawable
..setProgress(i / frameCount)
..draw(canvas, destRect);
} }
} }
@ -212,14 +207,45 @@ class _Painter extends CustomPainter {
} }
```` ````
### Modify properties at runtime
This example shows how to modify some properties of the animation at runtime. Here we change the text,
the color, the opacity and the position of some layers.
For each `ValueDelegate` we can either provide a static `value` or a `callback` to compute a value for a each frame.
````dart
class _Animation extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Lottie.asset(
'assets/Tests/Shapes.json',
delegates: LottieDelegates(
text: (initialText) => translate(initialText),
values: [
ValueDelegate.color(
const ['Shape Layer 1', 'Rectangle', 'Fill 1'],
value: Colors.red,
),
ValueDelegate.opacity(
const ['Shape Layer 1', 'Rectangle'],
callback: (frameInfo) =>
(frameInfo.overallProgress * 100).round(),
),
ValueDelegate.position(
const ['Shape Layer 1', 'Rectangle'],
relative: Offset(100, 200),
),
]),
);
}
}
````
## Limitations ## Limitations
This is a new library so usability, documentation and performance are still work in progress. This is a new library so usability, documentation and performance are still work in progress.
The following features are not yet implemented: The following features are not yet implemented:
- Dash path effects - Dash path effects
- Transforms on gradients (stroke and fills) - Transforms on gradients (stroke and fills)
- Expose `Value callback` to modify dynamically some properties of the animation
- Text in animations has very basic support (unoptimized and buggy)
## Flutter Web ## Flutter Web
Run the app with `flutter run -d Chrome --dart-define=FLUTTER_WEB_USE_SKIA=true --release` Run the app with `flutter run -d Chrome --dart-define=FLUTTER_WEB_USE_SKIA=true --release`

View File

@ -20,15 +20,8 @@ The `Lottie` widget will load the json file and run the animation indefinitely.
import 'example/lib/examples/main.dart'; import 'example/lib/examples/main.dart';
``` ```
To load an animation from the assets folder, we need to add an `assets` section in the `pubspec.yaml`:
```yaml
flutter:
assets:
- assets/
```
### Specify a custom `AnimationController` ### Specify a custom `AnimationController`
This example shows how to have full control over the animation by providing your own `AnimationController`. This example shows how to take full control over the animation by providing your own `AnimationController`.
```dart ```dart
import 'example/lib/examples/animation_controller.dart'; import 'example/lib/examples/animation_controller.dart';
@ -69,14 +62,21 @@ a specific position and size.
import 'example/lib/examples/custom_draw.dart#example'; import 'example/lib/examples/custom_draw.dart#example';
```` ````
### Modify properties at runtime
This example shows how to modify some properties of the animation at runtime. Here we change the text,
the color, the opacity and the position of some layers.
For each `ValueDelegate` we can either provide a static `value` or a `callback` to compute a value for a each frame.
````dart
import 'example/lib/examples/simple_dynamic_properties.dart#example';
````
## Limitations ## Limitations
This is a new library so usability, documentation and performance are still work in progress. This is a new library so usability, documentation and performance are still work in progress.
The following features are not yet implemented: The following features are not yet implemented:
- Dash path effects - Dash path effects
- Transforms on gradients (stroke and fills) - Transforms on gradients (stroke and fills)
- Expose `Value callback` to modify dynamically some properties of the animation
- Text in animations has very basic support (unoptimized and buggy)
## Flutter Web ## Flutter Web
Run the app with `flutter run -d Chrome --dart-define=FLUTTER_WEB_USE_SKIA=true --release` Run the app with `flutter run -d Chrome --dart-define=FLUTTER_WEB_USE_SKIA=true --release`

Binary file not shown.

View File

@ -81,7 +81,9 @@ class _Painter extends CustomPainter {
var columns = 10; var columns = 10;
for (var i = 0; i < frameCount; i++) { for (var i = 0; i < frameCount; i++) {
var destRect = Offset(i % columns * 50.0, i ~/ 10 * 80.0) & (size / 5); var destRect = Offset(i % columns * 50.0, i ~/ 10 * 80.0) & (size / 5);
drawable.draw(canvas, destRect, progress: i / frameCount); drawable
..setProgress(i / frameCount)
..draw(canvas, destRect);
} }
} }

View File

@ -0,0 +1,86 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:lottie/lottie.dart';
void main() async {
runApp(App());
}
class App extends StatefulWidget {
const App({Key key}) : super(key: key);
@override
_AppState createState() => _AppState();
}
class _AppState extends State<App> with TickerProviderStateMixin {
Color _color = Colors.green;
double _opacity = 0.5;
bool _useDelegates = true;
@override
Widget build(BuildContext context) {
var valueDelegates = [
ValueDelegate.color(['Shape Layer 1', 'Rectangle', 'Fill 1'],
value: _color),
ValueDelegate.opacity(['Shape Layer 1', 'Rectangle', 'Fill 1'],
callback: (_) => (_opacity * 100).round()),
];
return MaterialApp(
color: Colors.blue,
home: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('Dynamic properties'),
),
body: ListView(
children: <Widget>[
SizedBox(
width: 300,
height: 300,
child: Lottie.asset(
'assets/Tests/Shapes.json',
delegates: LottieDelegates(
values: _useDelegates ? valueDelegates : null),
),
),
Checkbox(
value: _useDelegates,
onChanged: (newValue) {
setState(() {
_useDelegates = newValue;
});
},
),
Slider(
value: _opacity,
onChanged: (newOpacity) {
setState(() {
_opacity = newOpacity;
});
},
),
Center(
child: Container(
width: 500,
child: ColorPicker(
pickerColor: _color,
onColorChanged: (newColor) {
setState(() {
_color = newColor;
});
},
showLabel: false,
enableAlpha: false,
pickerAreaHeightPercent: 0.8,
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
void main() async {
runApp(App());
}
class App extends StatefulWidget {
const App({Key key}) : super(key: key);
@override
_AppState createState() => _AppState();
}
class _AppState extends State<App> with TickerProviderStateMixin {
TextEditingController _textController;
@override
void initState() {
super.initState();
_textController = TextEditingController(text: /*'🔥Fire🔥'*/ 'Fire');
}
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
color: Colors.blue,
home: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('Dynamic text'),
),
body: Center(
child: Column(
children: <Widget>[
SizedBox(
width: 300,
height: 300,
child: Lottie.asset(
'assets/Tests/DynamicText.json',
delegates: LottieDelegates(
text: (animationText) => _textController.text,
textStyle: (font) => TextStyle(
fontFamily: font.fontFamily,
fontStyle: FontStyle.italic),
),
),
),
Container(
width: 300,
child: CupertinoTextField(
controller: _textController,
onChanged: (newText) {
setState(() {});
},
),
)
],
),
),
),
);
}
}

View File

@ -18,7 +18,7 @@ class MyApp extends StatelessWidget {
'https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json'), 'https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json'),
// Load an animation and its images from a zip file // Load an animation and its images from a zip file
Lottie.asset('assets/lottiesfiles/angel.zip'), Lottie.asset('assets/lottiefiles/angel.zip'),
], ],
), ),
), ),

View File

@ -0,0 +1,47 @@
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: [_Animation()],
),
),
);
}
}
String translate(String input) => '**$input**';
//--- example
class _Animation extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Lottie.asset(
'assets/Tests/Shapes.json',
delegates: LottieDelegates(
text: (initialText) => translate(initialText),
values: [
ValueDelegate.color(
const ['Shape Layer 1', 'Rectangle', 'Fill 1'],
value: Colors.red,
),
ValueDelegate.opacity(
const ['Shape Layer 1', 'Rectangle'],
callback: (frameInfo) =>
(frameInfo.overallProgress * 100).round(),
),
ValueDelegate.position(
const ['Shape Layer 1', 'Rectangle'],
relative: Offset(100, 200),
),
]),
);
}
}
//---

View File

@ -66,7 +66,6 @@ class _Item extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
//border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.all(Radius.circular(10)), borderRadius: BorderRadius.all(Radius.circular(10)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(

View File

@ -29,6 +29,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -62,11 +69,25 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_colorpicker:
dependency: "direct main"
description:
name: flutter_colorpicker
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.2"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
golden_toolkit:
dependency: "direct dev"
description:
name: golden_toolkit
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
image: image:
dependency: transitive dependency: transitive
description: description:
@ -87,7 +108,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "0.2.2" version: "0.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -200,3 +221,4 @@ packages:
version: "3.5.0" version: "3.5.0"
sdks: sdks:
dart: ">=2.7.0 <3.0.0" dart: ">=2.7.0 <3.0.0"
flutter: ">=1.5.4 <2.0.0"

View File

@ -10,10 +10,12 @@ dependencies:
sdk: flutter sdk: flutter
lottie: lottie:
path: ../ path: ../
flutter_colorpicker:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
golden_toolkit:
dependency_overrides: dependency_overrides:
pedantic: ^1.9.0 pedantic: ^1.9.0
@ -34,33 +36,25 @@ flutter:
- assets/Images/ - assets/Images/
- assets/Images/WeAccept/ - assets/Images/WeAccept/
# To add assets to your package, add an assets section, like this: fonts:
# assets: - family: Comic Neue
# - images/a_dot_burr.jpeg fonts:
# - images/a_dot_ham.jpeg - asset: assets/fonts/Comic-Neue.ttf
# - family: Helvetica
# For details regarding assets in packages, see fonts:
# https://flutter.dev/assets-and-images/#from-packages - asset: assets/fonts/Helvetica.ttf
# - family: Helvetica Neue
# An image asset can refer to one or more resolution-specific "variants", see fonts:
# https://flutter.dev/assets-and-images/#resolution-aware. - asset: assets/fonts/Helvetica-Neue.ttf
- family: Open Sans
# To add custom fonts to your package, add a fonts section here, fonts:
# in this "flutter" section. Each entry in this list should have a - asset: assets/fonts/Open-Sans.ttf
# "family" key with the font family name, and a "fonts" key with a - family: PT Serif
# list giving the asset and other descriptors for the font. For fonts:
# example: - asset: assets/fonts/PT-Serif.ttf
# fonts: - family: Roboto
# - family: Schyler fonts:
# fonts: - asset: assets/fonts/Roboto.ttf
# - asset: fonts/Schyler-Regular.ttf - family: Noto Emoji
# - asset: fonts/Schyler-Italic.ttf fonts:
# style: italic - asset: assets/fonts/NotoEmoji-Regular.ttf
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

View File

@ -0,0 +1,293 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
void main() {
LottieComposition composition;
setUpAll(() async {
composition = await LottieComposition.fromBytes(
File('assets/Tests/Shapes.json').readAsBytesSync());
});
void testGolden(String description, ValueDelegate delegate,
{double progress}) async {
var screenshotName = description
.toLowerCase()
.replaceAll(RegExp('[^a-z0-9 ]'), '')
.replaceAll(' ', '_');
testWidgets(description, (tester) async {
var animation =
AnimationController(vsync: tester, duration: composition.duration);
if (progress != null) {
animation.value = progress;
}
await tester.pumpWidget(
Lottie(
composition: composition,
controller: animation,
delegates: LottieDelegates(values: [delegate]),
),
);
await tester.pump();
await expectLater(find.byType(Lottie),
matchesGoldenFile('goldens/dynamic/$screenshotName.png'));
if (progress == null || progress == 0) {
await tester.pumpWidget(
Lottie(
composition: composition,
controller: animation,
delegates: LottieDelegates(values: []),
),
);
await tester.pump();
await expectLater(find.byType(Lottie),
matchesGoldenFile('goldens/dynamic_without_delegate.png'));
}
});
}
testGolden(
'Fill color (Green)',
ValueDelegate.color(['Shape Layer 1', 'Rectangle', 'Fill 1'],
value: Colors.green),
);
testGolden(
'Fill color (Yellow)',
ValueDelegate.color(['Shape Layer 1', 'Rectangle', 'Fill 1'],
value: Colors.yellow),
);
testGolden(
'Fill opacity',
ValueDelegate.opacity(['Shape Layer 1', 'Rectangle', 'Fill 1'], value: 50),
);
testGolden(
'Stroke color',
ValueDelegate.strokeColor(['Shape Layer 1', 'Rectangle', 'Stroke 1'],
value: Colors.green),
);
testGolden(
'Stroke width',
ValueDelegate.strokeWidth(['Shape Layer 1', 'Rectangle', 'Stroke 1'],
value: 50),
);
testGolden(
'Stroke opacity',
ValueDelegate.opacity(['Shape Layer 1', 'Rectangle', 'Stroke 1'],
value: 50),
);
testGolden(
'Transform anchor point',
ValueDelegate.transformAnchorPoint(['Shape Layer 1', 'Rectangle'],
value: Offset(20, 20)),
);
testGolden(
'Transform position',
ValueDelegate.transformPosition(['Shape Layer 1', 'Rectangle'],
value: Offset(20, 20)),
);
testGolden(
'Transform position (relative)',
ValueDelegate.transformPosition(['Shape Layer 1', 'Rectangle'],
relative: Offset(20, 20)),
);
testGolden(
'Transform opacity',
ValueDelegate.transformOpacity(['Shape Layer 1', 'Rectangle'], value: 50),
);
testGolden(
'Transform rotation',
ValueDelegate.transformRotation(['Shape Layer 1', 'Rectangle'], value: 45),
);
testGolden(
'Transform scale',
ValueDelegate.transformScale(['Shape Layer 1', 'Rectangle'],
value: Offset(0.5, 0.5)),
);
testGolden(
'Rectangle corner roundedness',
ValueDelegate.cornerRadius(
['Shape Layer 1', 'Rectangle', 'Rectangle Path 1'],
value: 7),
);
testGolden(
'Rectangle position',
ValueDelegate.position(['Shape Layer 1', 'Rectangle', 'Rectangle Path 1'],
relative: Offset(20, 20)),
);
testGolden(
'Rectangle size',
ValueDelegate.rectangleSize(
['Shape Layer 1', 'Rectangle', 'Rectangle Path 1'],
relative: Offset(30, 40)),
);
testGolden(
'Ellipse position',
ValueDelegate.position(['Shape Layer 1', 'Ellipse', 'Ellipse Path 1'],
relative: Offset(20, 20)),
);
testGolden(
'Ellipse size',
ValueDelegate.ellipseSize(['Shape Layer 1', 'Ellipse', 'Ellipse Path 1'],
relative: Offset(40, 60)),
);
testGolden(
'Star points',
ValueDelegate.polystarPoints(['Shape Layer 1', 'Star', 'Polystar Path 1'],
value: 8),
);
testGolden(
'Star rotation',
ValueDelegate.polystarRotation(['Shape Layer 1', 'Star', 'Polystar Path 1'],
value: 10),
);
testGolden(
'Star position',
ValueDelegate.position(['Shape Layer 1', 'Star', 'Polystar Path 1'],
relative: Offset(20, 20)),
);
testGolden(
'Star inner radius',
ValueDelegate.polystarInnerRadius(
['Shape Layer 1', 'Star', 'Polystar Path 1'],
value: 10),
);
testGolden(
'Star inner roundedness',
ValueDelegate.polystarInnerRoundedness(
['Shape Layer 1', 'Star', 'Polystar Path 1'],
value: 100),
);
testGolden(
'Star outer radius',
ValueDelegate.polystarOuterRadius(
['Shape Layer 1', 'Star', 'Polystar Path 1'],
value: 60),
);
testGolden(
'Star outer roundedness',
ValueDelegate.polystarOuterRoundedness(
['Shape Layer 1', 'Star', 'Polystar Path 1'],
value: 100),
);
testGolden(
'Polygon points',
ValueDelegate.polystarPoints(['Shape Layer 1', 'Star', 'Polystar Path 1'],
value: 8),
);
testGolden(
'Polygon rotation',
ValueDelegate.polystarRotation(['Shape Layer 1', 'Star', 'Polystar Path 1'],
value: 10),
);
testGolden(
'Polygon position',
ValueDelegate.position(['Shape Layer 1', 'Star', 'Polystar Path 1'],
relative: Offset(20, 20)),
);
testGolden(
'Polygon radius',
ValueDelegate.polystarOuterRadius(
['Shape Layer 1', 'Star', 'Polystar Path 1'],
relative: 60),
);
testGolden(
'Polygon roundedness',
ValueDelegate.polystarOuterRoundedness(
['Shape Layer 1', 'Star', 'Polystar Path 1'],
value: 100),
);
testGolden(
'Repeater transform position',
ValueDelegate.transformPosition(
['Shape Layer 1', 'Repeater Shape', 'Repeater 1'],
relative: Offset(100, 100)),
);
testGolden(
'Repeater transform start opacity',
ValueDelegate.transformStartOpacity(
['Shape Layer 1', 'Repeater Shape', 'Repeater 1'],
value: 25),
);
testGolden(
'Repeater transform end opacity',
ValueDelegate.transformEndOpacity(
['Shape Layer 1', 'Repeater Shape', 'Repeater 1'],
value: 25),
);
testGolden(
'Repeater transform rotation',
ValueDelegate.transformRotation(
['Shape Layer 1', 'Repeater Shape', 'Repeater 1'],
value: 45),
);
testGolden(
'Repeater transform scale',
ValueDelegate.transformScale(
['Shape Layer 1', 'Repeater Shape', 'Repeater 1'],
value: Offset(2, 2)),
);
testGolden('Time remapping', ValueDelegate.timeRemap(['Circle 1'], value: 1),
progress: 0.1);
testGolden(
'Color Filter',
ValueDelegate.colorFilter(['**'],
value: ColorFilter.mode(Colors.green, BlendMode.srcATop)),
);
testGolden(
'Null Color Filter',
ValueDelegate.colorFilter(['**'], value: null),
);
for (var progress in [0.0, 0.5, 1.0]) {
testGolden(
'Opacity interpolation ($progress)',
ValueDelegate.transformOpacity(['Shape Layer 1', 'Rectangle'],
callback: (frameInfo) => lerpDouble(
10, 100, Curves.linear.transform(frameInfo.overallProgress))
.round()),
progress: progress);
}
}

View File

@ -0,0 +1,29 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
void main() {
testWidgets('Dynamic test', (tester) async {
var composition = await LottieComposition.fromBytes(
File('assets/Tests/DynamicText.json').readAsBytesSync());
await tester.pumpWidget(
MaterialApp(
home: Lottie(
composition: composition,
animate: false,
delegates: LottieDelegates(
text: (input) => '🔥c🔥👮🏿🔥',
textStyle: (font) => TextStyle(
fontFamily: 'Roboto', fontFamilyFallback: ['Noto Emoji']),
values: []),
),
),
);
await expectLater(
find.byType(Lottie), matchesGoldenFile('goldens/dynamic_text.png'));
});
}

View File

@ -0,0 +1,7 @@
import 'dart:async';
import 'package:golden_toolkit/golden_toolkit.dart';
Future<void> main(FutureOr<void> Function() testMain) async {
await loadAppFonts();
return testMain();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1,12 +1,9 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:lottie_example/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {}); testWidgets('Main sample', (tester) async {
await tester.pumpWidget(App());
await tester.pump();
});
} }

View File

@ -1,6 +1,8 @@
export 'src/composition.dart' show LottieComposition; export 'src/composition.dart' show LottieComposition;
export 'src/lottie.dart' show Lottie; export 'src/lottie.dart' show Lottie;
export 'src/lottie_builder.dart' show LottieBuilder; export 'src/lottie_builder.dart' show LottieBuilder;
export 'src/lottie_drawable.dart' show LottieDrawable; export 'src/lottie_delegates.dart' show LottieDelegates;
export 'src/lottie_drawable.dart' show LottieDrawable, LottieFontStyle;
export 'src/lottie_image_asset.dart' show LottieImageAsset; export 'src/lottie_image_asset.dart' show LottieImageAsset;
export 'src/raw_lottie.dart' show RawLottie; export 'src/raw_lottie.dart' show RawLottie;
export 'src/value_delegate.dart' show ValueDelegate;

View File

@ -132,7 +132,7 @@ class GradientFillContent implements DrawingContent, KeyPathElementContent {
} }
LinearGradient _getLinearGradient() { LinearGradient _getLinearGradient() {
var gradientHash = getGradientHash(); var gradientHash = _getGradientHash();
var gradient = _linearGradientCache[gradientHash]; var gradient = _linearGradientCache[gradientHash];
if (gradient != null) { if (gradient != null) {
return gradient; return gradient;
@ -152,7 +152,7 @@ class GradientFillContent implements DrawingContent, KeyPathElementContent {
} }
RadialGradient _getRadialGradient() { RadialGradient _getRadialGradient() {
var gradientHash = getGradientHash(); var gradientHash = _getGradientHash();
var gradient = _radialGradientCache[gradientHash]; var gradient = _radialGradientCache[gradientHash];
if (gradient != null) { if (gradient != null) {
return gradient; return gradient;
@ -176,7 +176,7 @@ class GradientFillContent implements DrawingContent, KeyPathElementContent {
return gradient; return gradient;
} }
int getGradientHash() { int _getGradientHash() {
var startPointProgress = var startPointProgress =
(_startPointAnimation.progress * _cacheSteps).round(); (_startPointAnimation.progress * _cacheSteps).round();
var endPointProgress = (_endPointAnimation.progress * _cacheSteps).round(); var endPointProgress = (_endPointAnimation.progress * _cacheSteps).round();

View File

@ -1,32 +0,0 @@
import '../../value/keyframe.dart';
import '../../value/scale_xy.dart';
import 'keyframe_animation.dart';
class ScaleKeyframeAnimation extends KeyframeAnimation<ScaleXY> {
ScaleKeyframeAnimation(List<Keyframe<ScaleXY>> keyframes) : super(keyframes);
@override
ScaleXY getValue(Keyframe<ScaleXY> keyframe, double keyframeProgress) {
if (keyframe.startValue == null || keyframe.endValue == null) {
throw StateError('Missing values for keyframe.');
}
var startTransform = keyframe.startValue;
var endTransform = keyframe.endValue;
if (valueCallback != null) {
var value = valueCallback.getValueInternal(
keyframe.startFrame,
keyframe.endFrame,
startTransform,
endTransform,
keyframeProgress,
getLinearCurrentKeyframeProgress(),
progress);
if (value != null) {
return value;
}
}
return ScaleXY.lerp(startTransform, endTransform, keyframeProgress);
}
}

View File

@ -7,7 +7,6 @@ import '../../model/layer/base_layer.dart';
import '../../utils.dart'; import '../../utils.dart';
import '../../value/keyframe.dart'; import '../../value/keyframe.dart';
import '../../value/lottie_value_callback.dart'; import '../../value/lottie_value_callback.dart';
import '../../value/scale_xy.dart';
import 'base_keyframe_animation.dart'; import 'base_keyframe_animation.dart';
import 'double_keyframe_animation.dart'; import 'double_keyframe_animation.dart';
import 'value_callback_keyframe_animation.dart'; import 'value_callback_keyframe_animation.dart';
@ -37,7 +36,7 @@ class TransformKeyframeAnimation {
BaseKeyframeAnimation<Offset, Offset> /*?*/ _anchorPoint; BaseKeyframeAnimation<Offset, Offset> /*?*/ _anchorPoint;
BaseKeyframeAnimation<dynamic, Offset> /*?*/ _position; BaseKeyframeAnimation<dynamic, Offset> /*?*/ _position;
BaseKeyframeAnimation<ScaleXY, ScaleXY> /*?*/ _scale; BaseKeyframeAnimation<Offset, Offset> /*?*/ _scale;
BaseKeyframeAnimation<double, double> /*?*/ _rotation; BaseKeyframeAnimation<double, double> /*?*/ _rotation;
DoubleKeyframeAnimation /*?*/ _skew; DoubleKeyframeAnimation /*?*/ _skew;
DoubleKeyframeAnimation /*?*/ _skewAngle; DoubleKeyframeAnimation /*?*/ _skewAngle;
@ -141,8 +140,8 @@ class TransformKeyframeAnimation {
if (_scale != null) { if (_scale != null) {
final scale = _scale.value; final scale = _scale.value;
if (scale.x != 1 || scale.y != 1) { if (scale.dx != 1 || scale.dy != 1) {
_matrix.scale(scale.x, scale.y); _matrix.scale(scale.dx, scale.dy);
} }
} }
@ -169,7 +168,7 @@ class TransformKeyframeAnimation {
} }
if (scale != null) { if (scale != null) {
_matrix.scale(scale.x, scale.y); _matrix.scale(scale.dx, scale.dy);
} }
if (rotation != null) { if (rotation != null) {
@ -200,9 +199,9 @@ class TransformKeyframeAnimation {
} else if (property == LottieProperty.transformScale) { } else if (property == LottieProperty.transformScale) {
if (_scale == null) { if (_scale == null) {
_scale = ValueCallbackKeyframeAnimation( _scale = ValueCallbackKeyframeAnimation(
callback as LottieValueCallback<ScaleXY>, ScaleXY.one()); callback as LottieValueCallback<Offset>, Offset(1, 1));
} else { } else {
_scale.setValueCallback(callback as LottieValueCallback<ScaleXY>); _scale.setValueCallback(callback as LottieValueCallback<Offset>);
} }
} else if (property == LottieProperty.transformRotation) { } else if (property == LottieProperty.transformRotation) {
if (_rotation == null) { if (_rotation == null) {

View File

@ -34,7 +34,8 @@ class ValueCallbackKeyframeAnimation<K, A> extends BaseKeyframeAnimation<K, A> {
@override @override
A get value { A get value {
return valueCallback.getValueInternal(0.0, 0.0, valueCallbackValue, return valueCallback.getValueInternal(0.0, 0.0, valueCallbackValue,
valueCallbackValue, progress, progress, progress); valueCallbackValue, progress, progress, progress) ??
valueCallbackValue;
} }
@override @override

View File

@ -23,16 +23,19 @@ class Lottie extends StatefulWidget {
bool animate, bool animate,
bool repeat, bool repeat,
bool reverse, bool reverse,
this.delegates,
}) : animate = animate ?? true, }) : animate = animate ?? true,
reverse = reverse ?? false, reverse = reverse ?? false,
repeat = repeat ?? true, repeat = repeat ?? true,
super(key: key); super(key: key);
/// Creates a widget that displays an [LottieComposition] obtained from an [AssetBundle].
static LottieBuilder asset(String name, static LottieBuilder asset(String name,
{Animation<double> controller, {Animation<double> controller,
bool animate, bool animate,
bool repeat, bool repeat,
bool reverse, bool reverse,
LottieDelegates delegates,
void Function(LottieComposition) onLoaded, void Function(LottieComposition) onLoaded,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
Key key, Key key,
@ -49,6 +52,7 @@ class Lottie extends StatefulWidget {
animate: animate, animate: animate,
repeat: repeat, repeat: repeat,
reverse: reverse, reverse: reverse,
delegates: delegates,
imageProviderFactory: imageProviderFactory, imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded, onLoaded: onLoaded,
key: key, key: key,
@ -61,12 +65,14 @@ class Lottie extends StatefulWidget {
package: package, package: package,
); );
/// Creates a widget that displays an [LottieComposition] obtained from a [File].
static LottieBuilder file( static LottieBuilder file(
File file, { File file, {
Animation<double> controller, Animation<double> controller,
bool animate, bool animate,
bool repeat, bool repeat,
bool reverse, bool reverse,
LottieDelegates delegates,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
void Function(LottieComposition) onLoaded, void Function(LottieComposition) onLoaded,
Key key, Key key,
@ -82,6 +88,7 @@ class Lottie extends StatefulWidget {
animate: animate, animate: animate,
repeat: repeat, repeat: repeat,
reverse: reverse, reverse: reverse,
delegates: delegates,
imageProviderFactory: imageProviderFactory, imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded, onLoaded: onLoaded,
key: key, key: key,
@ -92,12 +99,14 @@ class Lottie extends StatefulWidget {
alignment: alignment, alignment: alignment,
); );
/// Creates a widget that displays an [LottieComposition] obtained from a [Uint8List].
static LottieBuilder memory( static LottieBuilder memory(
Uint8List bytes, { Uint8List bytes, {
Animation<double> controller, Animation<double> controller,
bool animate, bool animate,
bool repeat, bool repeat,
bool reverse, bool reverse,
LottieDelegates delegates,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
void Function(LottieComposition) onLoaded, void Function(LottieComposition) onLoaded,
Key key, Key key,
@ -113,6 +122,7 @@ class Lottie extends StatefulWidget {
animate: animate, animate: animate,
repeat: repeat, repeat: repeat,
reverse: reverse, reverse: reverse,
delegates: delegates,
imageProviderFactory: imageProviderFactory, imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded, onLoaded: onLoaded,
key: key, key: key,
@ -123,12 +133,14 @@ class Lottie extends StatefulWidget {
alignment: alignment, alignment: alignment,
); );
/// Creates a widget that displays an [LottieComposition] obtained from the network.
static LottieBuilder network( static LottieBuilder network(
String url, { String url, {
Animation<double> controller, Animation<double> controller,
bool animate, bool animate,
bool repeat, bool repeat,
bool reverse, bool reverse,
LottieDelegates delegates,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
void Function(LottieComposition) onLoaded, void Function(LottieComposition) onLoaded,
Key key, Key key,
@ -144,6 +156,7 @@ class Lottie extends StatefulWidget {
animate: animate, animate: animate,
repeat: repeat, repeat: repeat,
reverse: reverse, reverse: reverse,
delegates: delegates,
imageProviderFactory: imageProviderFactory, imageProviderFactory: imageProviderFactory,
onLoaded: onLoaded, onLoaded: onLoaded,
key: key, key: key,
@ -180,13 +193,13 @@ class Lottie extends StatefulWidget {
/// The property has no effect if [animate] is false, [repeat] is false or [controller] is not null. /// The property has no effect if [animate] is false, [repeat] is false or [controller] is not null.
final bool reverse; final bool reverse;
/// If non-null, require the Lottie composition to have this width. /// If non-null, requires the composition to have this width.
/// ///
/// If null, the composition will pick a size that best preserves its intrinsic /// If null, the composition will pick a size that best preserves its intrinsic
/// aspect ratio. /// aspect ratio.
final double width; final double width;
/// If non-null, require the Lottie composition to have this height. /// If non-null, require the composition to have this height.
/// ///
/// If null, the composition will pick a size that best preserves its intrinsic /// If null, the composition will pick a size that best preserves its intrinsic
/// aspect ratio. /// aspect ratio.
@ -215,6 +228,13 @@ class Lottie extends StatefulWidget {
/// relative to text direction. /// relative to text direction.
final AlignmentGeometry alignment; final AlignmentGeometry alignment;
/// A group of options to further customize the lottie animation.
/// - A [text] delegate to dynamically change some text displayed in the animation
/// - A value callback to change the properties of the animation at runtime.
/// - A text style factory to map between a font family specified in the animation
/// and the font family in your assets.
final LottieDelegates delegates;
@override @override
_LottieState createState() => _LottieState(); _LottieState createState() => _LottieState();
} }
@ -268,6 +288,7 @@ class _LottieState extends State<Lottie> with TickerProviderStateMixin {
animation: _progressAnimation, animation: _progressAnimation,
builder: (context, _) => RawLottie( builder: (context, _) => RawLottie(
composition: widget.composition, composition: widget.composition,
delegates: widget.delegates,
progress: _progressAnimation.value, progress: _progressAnimation.value,
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,

View File

@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../lottie.dart'; import '../lottie.dart';
import 'lottie.dart'; import 'lottie.dart';
import 'lottie_drawable.dart';
import 'providers/asset_provider.dart'; import 'providers/asset_provider.dart';
import 'providers/file_provider.dart'; import 'providers/file_provider.dart';
import 'providers/load_image.dart'; import 'providers/load_image.dart';
@ -40,6 +39,7 @@ class LottieBuilder extends StatefulWidget {
this.animate, this.animate,
this.reverse, this.reverse,
this.repeat, this.repeat,
this.delegates,
this.onLoaded, this.onLoaded,
this.frameBuilder, this.frameBuilder,
this.width, this.width,
@ -49,7 +49,7 @@ class LottieBuilder extends StatefulWidget {
}) : assert(lottie != null), }) : assert(lottie != null),
super(key: key); super(key: key);
/// Creates a widget that displays an [LottieStream] obtained from the network. /// Creates a widget that displays an [LottieComposition] obtained from the network.
LottieBuilder.network( LottieBuilder.network(
String src, { String src, {
Map<String, String> headers, Map<String, String> headers,
@ -57,6 +57,7 @@ class LottieBuilder extends StatefulWidget {
this.animate, this.animate,
this.reverse, this.reverse,
this.repeat, this.repeat,
this.delegates,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
this.onLoaded, this.onLoaded,
Key key, Key key,
@ -69,13 +70,11 @@ class LottieBuilder extends StatefulWidget {
headers: headers, imageProviderFactory: imageProviderFactory), headers: headers, imageProviderFactory: imageProviderFactory),
super(key: key); super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from a [File]. /// Creates a widget that displays an [LottieComposition] obtained from a [File].
///
/// The [file], [scale], and [repeat] arguments must not be null.
/// ///
/// Either the [width] and [height] arguments should be specified, or the /// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints. /// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which /// Otherwise, the image dimensions will change as the animation is loaded, which
/// will result in ugly layout changes. /// will result in ugly layout changes.
/// ///
/// On Android, this may require the /// On Android, this may require the
@ -87,6 +86,7 @@ class LottieBuilder extends StatefulWidget {
this.animate, this.animate,
this.reverse, this.reverse,
this.repeat, this.repeat,
this.delegates,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
this.onLoaded, this.onLoaded,
Key key, Key key,
@ -98,12 +98,14 @@ class LottieBuilder extends StatefulWidget {
}) : lottie = FileLottie(file, imageProviderFactory: imageProviderFactory), }) : lottie = FileLottie(file, imageProviderFactory: imageProviderFactory),
super(key: key); super(key: key);
/// Creates a widget that displays an [LottieComposition] obtained from an [AssetBundle].
LottieBuilder.asset( LottieBuilder.asset(
String name, { String name, {
this.controller, this.controller,
this.animate, this.animate,
this.reverse, this.reverse,
this.repeat, this.repeat,
this.delegates,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
this.onLoaded, this.onLoaded,
Key key, Key key,
@ -120,13 +122,14 @@ class LottieBuilder extends StatefulWidget {
imageProviderFactory: imageProviderFactory), imageProviderFactory: imageProviderFactory),
super(key: key); super(key: key);
/// Creates a widget that displays an [LottieDrawable] obtained from a [Uint8List]. /// Creates a widget that displays an [LottieComposition] obtained from a [Uint8List].
LottieBuilder.memory( LottieBuilder.memory(
Uint8List bytes, { Uint8List bytes, {
this.controller, this.controller,
this.animate, this.animate,
this.reverse, this.reverse,
this.repeat, this.repeat,
this.delegates,
LottieImageProviderFactory imageProviderFactory, LottieImageProviderFactory imageProviderFactory,
this.onLoaded, this.onLoaded,
Key key, Key key,
@ -139,7 +142,7 @@ class LottieBuilder extends StatefulWidget {
MemoryLottie(bytes, imageProviderFactory: imageProviderFactory), MemoryLottie(bytes, imageProviderFactory: imageProviderFactory),
super(key: key); super(key: key);
/// The lottie animation to display. /// The lottie animation to load.
/// Example of providers: [AssetLottie], [NetworkLottie], [FileLottie], [MemoryLottie] /// Example of providers: [AssetLottie], [NetworkLottie], [FileLottie], [MemoryLottie]
final LottieProvider lottie; final LottieProvider lottie;
@ -170,6 +173,13 @@ class LottieBuilder extends StatefulWidget {
/// The property has no effect if [animate] is false, [repeat] is false or [controller] is not null. /// The property has no effect if [animate] is false, [repeat] is false or [controller] is not null.
final bool reverse; final bool reverse;
/// A group of options to further customize the lottie animation.
/// - A [text] delegate to dynamically change some text displayed in the animation
/// - A value callback to change the properties of the animation at runtime.
/// - A text style factory to map between a font family specified in the animation
/// and the font family in your assets.
final LottieDelegates delegates;
/// 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.
/// ///
@ -368,6 +378,7 @@ class _LottieBuilderState extends State<LottieBuilder> {
animate: widget.animate, animate: widget.animate,
reverse: widget.reverse, reverse: widget.reverse,
repeat: widget.repeat, repeat: widget.repeat,
delegates: widget.delegates,
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
fit: widget.fit, fit: widget.fit,

View File

@ -0,0 +1,64 @@
import 'package:flutter/widgets.dart';
import 'lottie_drawable.dart';
import 'value_delegate.dart';
// TODO(xha): recognize style Bold, Medium, Regular, SemiBold, etc...
TextStyle _defaultTextStyleDelegate(LottieFontStyle font) =>
TextStyle(fontFamily: font.fontFamily);
@immutable
class LottieDelegates {
/// Specify a callback to dynamically changes the text displayed in the lottie
/// animation.
/// For instance, this is useful when you want to translate the text in the animation.
final String Function(String) /*?*/ text;
/// A callback to map between a font family specified in the json animation
/// with the font family in your assets.
/// This is useful either if:
/// - the name of the font in your asset doesn't match the one in the json file.
/// - you want to use an other font than the one declared in the json
///
/// If the callback is null, the font family from the json is used as it.
///
/// Given an object containing the font family and style specified in the json
/// return a configured `TextStyle` that will be used as the base style when
/// painting the text.
final TextStyle Function(LottieFontStyle) textStyle;
/// A list of value delegates to dynamically modify the animation
/// properties at runtime.
///
/// Example:
/// ```dart
/// Lottie.asset(
/// 'lottiefile.json',
/// delegates: LottieDelegates(
/// value: [
/// ValueDelegate.color(['lake', 'fill'], value: Colors.blue),
/// ValueDelegate.opacity(['**', 'fill'], callback: (frameInfo) => 0.5 * frameInfo.overallProgress),
/// ],
/// ),
/// );
/// ```
final List<ValueDelegate> values;
//TODO(xha): imageDelegate to change the image to display?
LottieDelegates({
this.text,
TextStyle Function(LottieFontStyle) textStyle,
this.values,
}) : textStyle = textStyle ?? _defaultTextStyleDelegate;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LottieDelegates &&
text == other.text &&
textStyle == other.textStyle &&
values == other.values;
@override
int get hashCode => hashValues(text, textStyle, values);
}

View File

@ -1,42 +1,65 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import 'composition.dart'; import 'composition.dart';
import 'lottie_delegates.dart';
import 'model/key_path.dart';
import 'model/layer/composition_layer.dart'; import 'model/layer/composition_layer.dart';
import 'parser/layer_parser.dart'; import 'parser/layer_parser.dart';
import 'text_delegate.dart'; import 'value_delegate.dart';
class LottieDrawable { class LottieDrawable {
final LottieComposition composition; final LottieComposition composition;
final _matrix = Matrix4.identity(); final _matrix = Matrix4.identity();
CompositionLayer _compositionLayer; CompositionLayer _compositionLayer;
final Size size; final Size size;
TextDelegate /*?*/ textDelegate; LottieDelegates _delegates;
bool _isDirty = true;
LottieDrawable(this.composition, {this.textDelegate}) LottieDrawable(this.composition, {LottieDelegates delegates})
: size = Size(composition.bounds.width.toDouble(), : size = Size(composition.bounds.width.toDouble(),
composition.bounds.height.toDouble()) { composition.bounds.height.toDouble()) {
this.delegates = delegates;
_compositionLayer = CompositionLayer( _compositionLayer = CompositionLayer(
this, LayerParser.parse(composition), composition.layers, composition); this, LayerParser.parse(composition), composition.layers, composition);
} }
CompositionLayer get compositionLayer => _compositionLayer;
/// Sets whether to apply opacity to the each layer instead of shape. /// Sets whether to apply opacity to the each layer instead of shape.
/// ///
/// Opacity is normally applied directly to a shape. In cases where translucent shapes overlap, applying opacity to a layer will be more accurate /// Opacity is normally applied directly to a shape. In cases where translucent
/// at the expense of performance. /// shapes overlap, applying opacity to a layer will be more accurate at the
/// expense of performance.
/// ///
/// The default value is false. /// The default value is false.
/// ///
/// Note: This process is very expensive. The performance impact will be reduced when hardware acceleration is enabled. /// Note: This process is very expensive. The performance impact will be reduced
/// when hardware acceleration is enabled.
bool isApplyingOpacityToLayersEnabled = false; bool isApplyingOpacityToLayersEnabled = false;
void invalidateSelf() {} void invalidateSelf() {
_isDirty = true;
}
double get progress => _progress;
double _progress = 0.0;
bool setProgress(double value) {
_isDirty = false;
_progress = value;
_compositionLayer.setProgress(value);
return _isDirty;
}
LottieDelegates get delegates => _delegates;
set delegates(LottieDelegates delegates) {
delegates ??= LottieDelegates();
if (_delegates != delegates) {
_delegates = delegates;
_updateValueDelegates(delegates.values);
}
}
bool get useTextGlyphs { bool get useTextGlyphs {
return textDelegate == null && composition.characters.isNotEmpty; return delegates.text == null && composition.characters.isNotEmpty;
} }
ui.Image getImageAsset(String ref) { ui.Image getImageAsset(String ref) {
@ -49,13 +72,54 @@ class LottieDrawable {
} }
TextStyle getTextStyle(String font, String style) { TextStyle getTextStyle(String font, String style) {
//TODO(xha): allow the user to map Font in the animation with FontFamily loaded for flutter return _delegates
// Support to inherit TextStyle from DefaultTextStyle applied for the Lottie wiget .textStyle(LottieFontStyle(fontFamily: font, style: style));
return TextStyle(fontFamily: font);
} }
void draw(ui.Canvas canvas, ui.Rect rect, List<ValueDelegate> _valueDelegates = <ValueDelegate>[];
{@required double progress, BoxFit fit, Alignment alignment}) { void _updateValueDelegates(List<ValueDelegate> newDelegates) {
if (identical(_valueDelegates, newDelegates)) return;
newDelegates ??= const [];
var delegates = <ValueDelegate>[];
for (var newDelegate in newDelegates) {
var existingDelegate = _valueDelegates
.firstWhere((f) => f.isSameProperty(newDelegate), orElse: () => null);
if (existingDelegate != null) {
var resolved = internalResolved(existingDelegate);
resolved.updateDelegate(newDelegate);
delegates.add(existingDelegate);
} else {
var keyPaths = _resolveKeyPath(KeyPath(newDelegate.keyPath));
var resolvedValueDelegate = internalResolve(newDelegate, keyPaths);
resolvedValueDelegate.addValueCallback(this);
delegates.add(newDelegate);
}
}
for (var oldDelegate in _valueDelegates) {
if (delegates.every((c) => !c.isSameProperty(oldDelegate))) {
var resolved = internalResolved(oldDelegate);
resolved.clear();
}
}
_valueDelegates = delegates;
}
/// Takes a {@link KeyPath}, potentially with wildcards or globstars and resolve it to a list of
/// zero or more actual {@link KeyPath Keypaths} that exist in the current animation.
/// <p>
/// If you want to set value callbacks for any of these values, it is recommend to use the
/// returned {@link KeyPath} objects because they will be internally resolved to their content
/// and won't trigger a tree walk of the animation contents when applied.
List<KeyPath> _resolveKeyPath(KeyPath keyPath) {
var keyPaths = <KeyPath>[];
_compositionLayer.resolveKeyPath(keyPath, 0, keyPaths, KeyPath([]));
return keyPaths;
}
void draw(ui.Canvas canvas, ui.Rect rect, {BoxFit fit, Alignment alignment}) {
if (rect.isEmpty) { if (rect.isEmpty) {
return; return;
} }
@ -79,9 +143,12 @@ class LottieDrawable {
_matrix.translate(destinationRect.left, destinationRect.top); _matrix.translate(destinationRect.left, destinationRect.top);
_matrix.scale(destinationRect.size.width / sourceRect.width, _matrix.scale(destinationRect.size.width / sourceRect.width,
destinationRect.size.height / sourceRect.height); destinationRect.size.height / sourceRect.height);
progress ??= 0; _compositionLayer.draw(canvas, rect.size, _matrix, parentAlpha: 255);
_compositionLayer
..setProgress(progress)
..draw(canvas, rect.size, _matrix, parentAlpha: 255);
} }
} }
class LottieFontStyle {
final String fontFamily, style;
LottieFontStyle({this.fontFamily, this.style});
}

View File

@ -1,5 +1,4 @@
import 'dart:ui'; import 'dart:ui';
import 'value/scale_xy.dart';
/// Property values are the same type as the generic type of their corresponding /// Property values are the same type as the generic type of their corresponding
/// {@link LottieValueCallback}. With this, we can use generics to maintain type safety /// {@link LottieValueCallback}. With this, we can use generics to maintain type safety
@ -62,69 +61,69 @@ abstract class LottieProperty {
static final int opacity = 4; static final int opacity = 4;
/// In Px */ /// In Px */
static final Offset transformAnchorPoint = Offset.zero; static final Offset transformAnchorPoint = Offset(5, 5);
/// In Px */ /// In Px */
static final Offset transformPosition = Offset.zero; static final Offset transformPosition = Offset(6, 6);
/// In Px */ /// In Px */
static final Offset ellipseSize = Offset.zero; static final Offset ellipseSize = Offset(7, 7);
/// In Px */ /// In Px */
static final Offset rectangleSize = Offset.zero; static final Offset rectangleSize = Offset(8, 8);
/// In degrees */ /// In degrees */
static final double cornerRadius = 0.0; static final double cornerRadius = 9.0;
/// In Px */ /// In Px */
static final Offset position = Offset.zero; static final Offset position = Offset(10, 10);
static final ScaleXY transformScale = ScaleXY.one(); static final Offset transformScale = Offset(11, 11);
/// In degrees */ /// In degrees */
static final double transformRotation = 1.0; static final double transformRotation = 12.0;
/// 0-85 */ /// 0-85 */
static final double transformSkew = 0.0; static final double transformSkew = 13.0;
/// In degrees */ /// In degrees */
static final double transformSkewAngle = 0.0; static final double transformSkewAngle = 14.0;
/// In Px */ /// In Px */
static final double strokeWidth = 2.0; static final double strokeWidth = 15.0;
static final double textTracking = 3.0; static final double textTracking = 16.0;
static final double repeaterCopies = 4.0; static final double repeaterCopies = 17.0;
static final double repeaterOffset = 5.0; static final double repeaterOffset = 18.0;
static final double polystarPoints = 6.0; static final double polystarPoints = 19.0;
/// In degrees */ /// In degrees */
static final double polystarRotation = 7.0; static final double polystarRotation = 20.0;
/// In Px */ /// In Px */
static final double polystarInnerRadius = 8.0; static final double polystarInnerRadius = 21.0;
/// In Px */ /// In Px */
static final double polystarOuterRadius = 9.0; static final double polystarOuterRadius = 22.0;
/// [0,100] */ /// [0,100] */
static final double polystarInnerRoundedness = 10.0; static final double polystarInnerRoundedness = 23.0;
/// [0,100] */ /// [0,100] */
static final double polystarOuterRoundedness = 11.0; static final double polystarOuterRoundedness = 24.0;
/// [0,100] */ /// [0,100] */
static final double transformStartOpacity = 12.0; static final double transformStartOpacity = 25.0;
/// [0,100] */ /// [0,100] */
static final double transformEndOpacity = 12.1; static final double transformEndOpacity = 26.0;
/// The time value in seconds */ /// The time value in seconds */
static final double timeRemap = 13.0; static final double timeRemap = 27.0;
/// In Dp */ /// In Dp
static final double textSize = 14.0; static final double textSize = 28.0;
static final ColorFilter colorFilter = static final ColorFilter colorFilter =
ColorFilter.mode(Color(0xFF000000), BlendMode.dst); ColorFilter.mode(Color(0xFF000000), BlendMode.dst);
static final List<Color> gradientColor = const []; static final List<Color> gradientColor = [];
} }

View File

@ -1,19 +1,19 @@
import 'dart:ui';
import '../../animation/keyframe/base_keyframe_animation.dart'; import '../../animation/keyframe/base_keyframe_animation.dart';
import '../../animation/keyframe/scale_keyframe_animation.dart'; import '../../animation/keyframe/point_keyframe_animation.dart';
import '../../value/keyframe.dart'; import '../../value/keyframe.dart';
import '../../value/scale_xy.dart';
import 'base_animatable_value.dart'; import 'base_animatable_value.dart';
class AnimatableScaleValue extends BaseAnimatableValue<ScaleXY, ScaleXY> { class AnimatableScaleValue extends BaseAnimatableValue<Offset, Offset> {
AnimatableScaleValue.one() : this(ScaleXY.one()); AnimatableScaleValue.one() : this(Offset(1, 1));
AnimatableScaleValue(ScaleXY value) : super.fromValue(value); AnimatableScaleValue(Offset value) : super.fromValue(value);
AnimatableScaleValue.fromKeyframes(List<Keyframe<ScaleXY>> keyframes) AnimatableScaleValue.fromKeyframes(List<Keyframe<Offset>> keyframes)
: super.fromKeyframes(keyframes); : super.fromKeyframes(keyframes);
@override @override
BaseKeyframeAnimation<ScaleXY, ScaleXY> createAnimation() { BaseKeyframeAnimation<Offset, Offset> createAnimation() {
return ScaleKeyframeAnimation(keyframes); return PointKeyframeAnimation(keyframes);
} }
} }

View File

@ -1,4 +1,5 @@
import 'dart:ui'; import 'dart:ui';
import 'package:characters/characters.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import '../../animation/content/content_group.dart'; import '../../animation/content/content_group.dart';
@ -22,7 +23,6 @@ class TextLayer extends BaseLayer {
final _matrix = Matrix4.identity(); final _matrix = Matrix4.identity();
final _fillPaint = Paint()..style = PaintingStyle.fill; final _fillPaint = Paint()..style = PaintingStyle.fill;
final _strokePaint = Paint()..style = PaintingStyle.stroke; final _strokePaint = Paint()..style = PaintingStyle.stroke;
TextStyle _textStyle;
final _contentsForCharacter = <FontCharacter, List<ContentGroup>>{}; final _contentsForCharacter = <FontCharacter, List<ContentGroup>>{};
final TextKeyframeAnimation _textAnimation; final TextKeyframeAnimation _textAnimation;
final LottieComposition _composition; final LottieComposition _composition;
@ -227,14 +227,14 @@ class TextLayer extends BaseLayer {
void _drawTextWithFont(DocumentData documentData, Font font, void _drawTextWithFont(DocumentData documentData, Font font,
Matrix4 parentMatrix, Canvas canvas) { Matrix4 parentMatrix, Canvas canvas) {
var parentScale = parentMatrix.getScale(); var parentScale = parentMatrix.getScale();
_textStyle = lottieDrawable.getTextStyle(font.family, font.style); var textStyle = lottieDrawable.getTextStyle(font.family, font.style);
if (_textStyle == null) { if (textStyle == null) {
return; return;
} }
var text = documentData.text; var text = documentData.text;
var textDelegate = lottieDrawable.textDelegate; var textDelegate = lottieDrawable.delegates.text;
if (textDelegate != null) { if (textDelegate != null) {
text = textDelegate.getTextInternal(text); text = textDelegate(text);
} }
double textSize; double textSize;
if (_textSizeCallbackAnimation != null) { if (_textSizeCallbackAnimation != null) {
@ -244,8 +244,8 @@ class TextLayer extends BaseLayer {
} else { } else {
textSize = documentData.size; textSize = documentData.size;
} }
_textStyle = textStyle =
_textStyle.copyWith(fontSize: textSize * window.devicePixelRatio); textStyle.copyWith(fontSize: textSize * window.devicePixelRatio);
// Line height // Line height
var lineHeight = documentData.lineHeight * window.devicePixelRatio; var lineHeight = documentData.lineHeight * window.devicePixelRatio;
@ -256,7 +256,7 @@ class TextLayer extends BaseLayer {
for (var l = 0; l < textLineCount; l++) { for (var l = 0; l < textLineCount; l++) {
var textLine = textLines[l]; var textLine = textLines[l];
var textPainter = TextPainter( var textPainter = TextPainter(
text: TextSpan(text: textLine, style: _textStyle), text: TextSpan(text: textLine, style: textStyle),
textDirection: _textDirection); textDirection: _textDirection);
textPainter.layout(); textPainter.layout();
var textLineWidth = textPainter.width; var textLineWidth = textPainter.width;
@ -270,7 +270,7 @@ class TextLayer extends BaseLayer {
canvas.translate(0, translateY); canvas.translate(0, translateY);
// Draw each line // Draw each line
_drawFontTextLine(textLine, documentData, canvas, parentScale); _drawFontTextLine(textLine, textStyle, documentData, canvas, parentScale);
// Reset canvas // Reset canvas
canvas.transform(parentMatrix.storage); canvas.transform(parentMatrix.storage);
@ -284,14 +284,13 @@ class TextLayer extends BaseLayer {
return textLinesArray; return textLinesArray;
} }
void _drawFontTextLine(String text, DocumentData documentData, Canvas canvas, void _drawFontTextLine(String text, TextStyle textStyle,
double parentScale) { DocumentData documentData, Canvas canvas, double parentScale) {
for (var i = 0; i < text.length;) { for (var char in text.characters) {
var charString = _codePointToString(text, i); var charString = char;
i += charString.length; _drawCharacterFromFont(charString, textStyle, documentData, canvas);
_drawCharacterFromFont(charString, documentData, canvas);
var textPainter = TextPainter( var textPainter = TextPainter(
text: TextSpan(text: charString, style: _textStyle), text: TextSpan(text: charString, style: textStyle),
textDirection: _textDirection); textDirection: _textDirection);
textPainter.layout(); textPainter.layout();
var charWidth = textPainter.width; var charWidth = textPainter.width;
@ -369,18 +368,19 @@ class TextLayer extends BaseLayer {
canvas.drawPath(path, paint); canvas.drawPath(path, paint);
} }
void _drawCharacterFromFont( void _drawCharacterFromFont(String character, TextStyle textStyle,
String character, DocumentData documentData, Canvas canvas) { DocumentData documentData, Canvas canvas) {
if (documentData.strokeOverFill) { if (documentData.strokeOverFill) {
_drawCharacter(character, _fillPaint, canvas); _drawCharacter(character, textStyle, _fillPaint, canvas);
_drawCharacter(character, _strokePaint, canvas); _drawCharacter(character, textStyle, _strokePaint, canvas);
} else { } else {
_drawCharacter(character, _strokePaint, canvas); _drawCharacter(character, textStyle, _strokePaint, canvas);
_drawCharacter(character, _fillPaint, canvas); _drawCharacter(character, textStyle, _fillPaint, canvas);
} }
} }
void _drawCharacter(String character, Paint paint, Canvas canvas) { void _drawCharacter(
String character, TextStyle textStyle, Paint paint, Canvas canvas) {
if (paint.color.alpha == 0) { if (paint.color.alpha == 0) {
return; return;
} }
@ -388,18 +388,17 @@ class TextLayer extends BaseLayer {
return; return;
} }
TextStyle textStyle;
if (paint.style == PaintingStyle.fill) { if (paint.style == PaintingStyle.fill) {
textStyle = _textStyle.copyWith(foreground: paint); textStyle = textStyle.copyWith(foreground: paint);
} else if (paint.style == PaintingStyle.stroke) { } else if (paint.style == PaintingStyle.stroke) {
textStyle = _textStyle.copyWith(background: paint); textStyle = textStyle.copyWith(background: paint);
} }
var painter = TextPainter( var painter = TextPainter(
text: TextSpan(text: character, style: textStyle), text: TextSpan(text: character, style: textStyle),
textDirection: _textDirection, textDirection: _textDirection,
); );
painter.layout(); painter.layout();
painter.paint(canvas, Offset(0, -_textStyle.fontSize)); painter.paint(canvas, Offset(0, -textStyle.fontSize));
} }
List<ContentGroup> _getContentsForCharacter(FontCharacter character) { List<ContentGroup> _getContentsForCharacter(FontCharacter character) {
@ -417,11 +416,6 @@ class TextLayer extends BaseLayer {
return contents; return contents;
} }
//TODO(xha): use package:character to correctly iterate over visual glyphs
String _codePointToString(String text, int startIndex) {
return text[startIndex];
}
@override @override
void addValueCallback<T>(T property, LottieValueCallback<T> /*?*/ callback) { void addValueCallback<T>(T property, LottieValueCallback<T> /*?*/ callback) {
super.addValueCallback(property, callback); super.addValueCallback(property, callback);

View File

@ -170,7 +170,8 @@ class AnimatableTransformParser {
static bool isScaleIdentity(AnimatableScaleValue scale) { static bool isScaleIdentity(AnimatableScaleValue scale) {
return scale == null || return scale == null ||
(scale.isStatic && scale.keyframes.first.startValue.equals(1.0, 1.0)); (scale.isStatic &&
scale.keyframes.first.startValue == Offset(1.0, 1.0));
} }
static bool isSkewIdentity(AnimatableDoubleValue skew) { static bool isSkewIdentity(AnimatableDoubleValue skew) {

View File

@ -14,7 +14,7 @@ Color colorParser(JsonReader reader, {double scale}) {
reader.endArray(); reader.endArray();
} }
if (r <= 1 && g <= 1 && b <= 11) { if (r <= 1 && g <= 1 && b <= 1) {
r *= 255; r *= 255;
g *= 255; g *= 255;
b *= 255; b *= 255;

View File

@ -1,7 +1,7 @@
import '../value/scale_xy.dart'; import 'dart:ui';
import 'moshi/json_reader.dart'; import 'moshi/json_reader.dart';
ScaleXY scaleXYParser(JsonReader reader, {double scale}) { Offset scaleXYParser(JsonReader reader, {double scale}) {
var isArray = reader.peek() == Token.beginArray; var isArray = reader.peek() == Token.beginArray;
if (isArray) { if (isArray) {
reader.beginArray(); reader.beginArray();
@ -14,5 +14,5 @@ ScaleXY scaleXYParser(JsonReader reader, {double scale}) {
if (isArray) { if (isArray) {
reader.endArray(); reader.endArray();
} }
return ScaleXY(sx / 100.0 * scale, sy / 100.0 * scale); return Offset(sx / 100.0 * scale, sy / 100.0 * scale);
} }

View File

@ -12,6 +12,7 @@ class RawLottie extends LeafRenderObjectWidget {
const RawLottie({ const RawLottie({
Key key, Key key,
this.composition, this.composition,
this.delegates,
double progress, double progress,
this.width, this.width,
this.height, this.height,
@ -24,6 +25,9 @@ class RawLottie extends LeafRenderObjectWidget {
/// The Lottie composition to display. /// The Lottie composition to display.
final LottieComposition composition; final LottieComposition composition;
/// Allows to modify the Lottie animation at runtime
final LottieDelegates delegates;
/// The progress of the Lottie animation (between 0.0 and 1.0). /// The progress of the Lottie animation (between 0.0 and 1.0).
final double progress; final double progress;
@ -66,6 +70,7 @@ class RawLottie extends LeafRenderObjectWidget {
RenderLottie createRenderObject(BuildContext context) { RenderLottie createRenderObject(BuildContext context) {
return RenderLottie( return RenderLottie(
composition: composition, composition: composition,
delegates: delegates,
progress: progress, progress: progress,
width: width, width: width,
height: height, height: height,
@ -77,8 +82,7 @@ class RawLottie extends LeafRenderObjectWidget {
@override @override
void updateRenderObject(BuildContext context, RenderLottie renderObject) { void updateRenderObject(BuildContext context, RenderLottie renderObject) {
renderObject renderObject
..composition = composition ..setComposition(composition, progress: progress, delegates: delegates)
..progress = progress
..width = width ..width = width
..height = height ..height = height
..alignment = alignment ..alignment = alignment

View File

@ -1,4 +1,5 @@
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import '../lottie.dart'; import '../lottie.dart';
import 'lottie_drawable.dart'; import 'lottie_drawable.dart';
@ -9,6 +10,7 @@ import 'lottie_drawable.dart';
class RenderLottie extends RenderBox { class RenderLottie extends RenderBox {
RenderLottie({ RenderLottie({
LottieComposition composition, LottieComposition composition,
LottieDelegates delegates,
double progress = 0.0, double progress = 0.0,
double width, double width,
double height, double height,
@ -16,37 +18,49 @@ class RenderLottie extends RenderBox {
AlignmentGeometry alignment = Alignment.center, AlignmentGeometry alignment = Alignment.center,
}) : assert(alignment != null), }) : assert(alignment != null),
assert(progress != null && progress >= 0.0 && progress <= 1.0), assert(progress != null && progress >= 0.0 && progress <= 1.0),
_composition = composition, _drawable = composition != null
_progress = progress, ? (LottieDrawable(composition)
..setProgress(progress)
..delegates = delegates)
: null,
_width = width, _width = width,
_height = height, _height = height,
_fit = fit, _fit = fit,
_alignment = alignment; _alignment = alignment;
/// The lottie composition to display. /// The lottie composition to display.
LottieComposition get composition => _composition; LottieComposition get composition => _drawable?.composition;
LottieComposition _composition; LottieDrawable _drawable;
set composition(LottieComposition value) { void setComposition(LottieComposition composition,
if (value == _composition) { {@required double progress, @required LottieDelegates delegates}) {
return; var needsLayout = false;
} var needsPaint = false;
_composition = value; if (composition == null) {
_drawable = null; _drawable = null;
markNeedsPaint(); needsPaint = true;
if (_width == null || _height == null) { needsLayout = true;
markNeedsLayout(); } else {
if (_drawable?.composition != composition) {
_drawable = LottieDrawable(composition);
needsLayout = true;
needsPaint = true;
}
needsPaint |= _drawable.setProgress(progress);
if (_drawable.delegates != delegates) {
_drawable.delegates = delegates;
needsPaint = true;
} }
} }
double get progress => _progress; if (needsPaint) {
double _progress; markNeedsPaint();
set progress(double value) {
if (value == _progress) {
return;
} }
_progress = value; if (needsLayout && (_width == null || _height == null)) {
markNeedsLayout(); markNeedsLayout();
} }
}
/// If non-null, requires the composition to have this width. /// If non-null, requires the composition to have this width.
/// ///
@ -116,14 +130,12 @@ class RenderLottie extends RenderBox {
height: _height, height: _height,
).enforce(constraints); ).enforce(constraints);
if (_composition == null) { if (_drawable == null) {
return constraints.smallest; return constraints.smallest;
} }
return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size( return constraints
_composition.bounds.width.toDouble(), .constrainSizeAndAttemptToPreserveAspectRatio(_drawable.size);
_composition.bounds.height.toDouble(),
));
} }
@override @override
@ -168,17 +180,12 @@ class RenderLottie extends RenderBox {
size = _sizeForConstraints(constraints); size = _sizeForConstraints(constraints);
} }
LottieDrawable _drawable;
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_composition == null) return; if (_drawable == null) return;
_drawable ??= LottieDrawable(_composition);
_drawable.draw(context.canvas, offset & size, _drawable.draw(context.canvas, offset & size,
progress: _progress, fit: _fit, alignment: _alignment.resolve(TextDirection.ltr));
fit: _fit,
alignment: _alignment.resolve(TextDirection.ltr));
} }
@override @override

View File

@ -1,60 +0,0 @@
import 'lottie_drawable.dart';
/// Extend this class to replace animation text with custom text. This can be useful to handle
/// translations.
///
/// The only method you should have to override is {@link #getText(String)}.
class TextDelegate {
final _stringMap = <String, String>{};
final LottieDrawable /*?*/ drawable;
bool _cacheText = true;
TextDelegate(this.drawable);
/// Override this to replace the animation text with something dynamic. This can be used for
/// translations or custom data.
String getText(String input) {
return input;
}
/// Update the text that will be rendered for the given input text.
void setText(String input, String output) {
_stringMap[input] = output;
_invalidate();
}
/// Sets whether or not {@link TextDelegate} will cache (memoize) the results of getText.
/// If this isn't necessary then set it to false.
void setCacheText(bool cacheText) {
_cacheText = cacheText;
}
/// Invalidates a cached string with the given input.
void invalidateText(String input) {
_stringMap.remove(input);
_invalidate();
}
/// Invalidates all cached strings
void invalidateAllText() {
_stringMap.clear();
_invalidate();
}
String getTextInternal(String input) {
if (_cacheText && _stringMap.containsKey(input)) {
return _stringMap[input];
}
var text = getText(input);
if (_cacheText) {
_stringMap[input] = text;
}
return text;
}
void _invalidate() {
if (drawable != null) {
drawable.invalidateSelf();
}
}
}

View File

@ -0,0 +1,12 @@
import 'dart:ui';
import 'lottie_frame_info.dart';
double Function(LottieFrameInfo<double>) relativeDoubleValueCallback(
double offset) {
return (LottieFrameInfo<double> frameInfo) {
var originalValue = lerpDouble(frameInfo.startValue, frameInfo.endValue,
frameInfo.interpolatedKeyframeProgress);
return originalValue + offset;
};
}

View File

@ -0,0 +1,11 @@
import 'dart:ui';
import 'lottie_frame_info.dart';
int Function(LottieFrameInfo<int>) relativeIntegerValueCallback(int offset) {
return (LottieFrameInfo<int> frameInfo) {
var originalValue = lerpDouble(frameInfo.startValue, frameInfo.endValue,
frameInfo.interpolatedKeyframeProgress);
return (originalValue + offset).round();
};
}

View File

@ -0,0 +1,12 @@
import 'dart:ui';
import 'lottie_frame_info.dart';
Offset Function(LottieFrameInfo<Offset>) relativeOffsetValueCallback(
Offset offset) {
return (LottieFrameInfo<Offset> frameInfo) {
var point = Offset.lerp(frameInfo.startValue, frameInfo.endValue,
frameInfo.interpolatedKeyframeProgress);
return point.translate(offset.dx, offset.dy);
};
}

View File

@ -4,26 +4,34 @@ import 'lottie_frame_info.dart';
/// Allows you to set a callback on a resolved {@link com.airbnb.lottie.model.KeyPath} to modify /// Allows you to set a callback on a resolved {@link com.airbnb.lottie.model.KeyPath} to modify
/// its animation values at runtime. /// its animation values at runtime.
class LottieValueCallback<T> { class LottieValueCallback<T> {
BaseKeyframeAnimation /*?*/ animation; LottieValueCallback(this._value);
BaseKeyframeAnimation /*?*/ _animation;
BaseKeyframeAnimation get animation => _animation;
/// This can be set with {@link #setValue(Object)} to use a value instead of deferring /// This can be set with {@link #setValue(Object)} to use a value instead of deferring
/// to the callback. /// to the callback.
///*/ ///*/
T /*?*/ value; T /*?*/ _value;
T get value => _value;
LottieValueCallback(this.value); T Function(LottieFrameInfo<T>) callback;
/// Override this if you haven't set a static value in the constructor or with setValue. /// Override this if you haven't set a static value in the constructor or with setValue.
/// ///
/// Return null to resort to the default value. /// Return null to resort to the default value.
T getValue(LottieFrameInfo<T> frameInfo) { T getValue(LottieFrameInfo<T> frameInfo) {
if (callback != null) {
return callback(frameInfo);
}
return value; return value;
} }
void setValue(T /*?*/ value) { void setValue(T /*?*/ value) {
this.value = value; _value = value;
if (animation != null) { if (_animation != null) {
animation.notifyListeners(); _animation.notifyListeners();
} }
} }
@ -35,11 +43,17 @@ class LottieValueCallback<T> {
double linearKeyframeProgress, double linearKeyframeProgress,
double interpolatedKeyframeProgress, double interpolatedKeyframeProgress,
double overallProgress) { double overallProgress) {
return getValue(LottieFrameInfo(startFrame, endFrame, startValue, endValue, return getValue(LottieFrameInfo<T>(
linearKeyframeProgress, interpolatedKeyframeProgress, overallProgress)); startFrame,
endFrame,
startValue,
endValue,
linearKeyframeProgress,
interpolatedKeyframeProgress,
overallProgress));
} }
void setAnimation(BaseKeyframeAnimation /*?*/ animation) { void setAnimation(BaseKeyframeAnimation /*?*/ animation) {
this.animation = animation; _animation = animation;
} }
} }

View File

@ -1,23 +0,0 @@
import 'dart:ui';
//TODO(xha): delete and use Offset
class ScaleXY {
final double x;
final double y;
ScaleXY(this.x, this.y);
ScaleXY.one() : this(1, 1);
@override
bool operator ==(other) {
return other is ScaleXY && x == other.x && y == other.y;
}
@override
int get hashCode => hashValues(x, y);
bool equals(double x, double y) => this.x == x && this.y == y;
static ScaleXY lerp(ScaleXY a, ScaleXY b, double t) =>
ScaleXY(lerpDouble(a.x, b.x, t), lerpDouble(a.y, b.y, t));
}

331
lib/src/value_delegate.dart Normal file
View File

@ -0,0 +1,331 @@
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'lottie_drawable.dart';
import 'lottie_property.dart';
import 'model/key_path.dart';
import 'value/lottie_frame_info.dart';
import 'value/lottie_relative_double_value_callback.dart';
import 'value/lottie_relative_integer_value_callback.dart';
import 'value/lottie_relative_point_value_callback.dart';
import 'value/lottie_value_callback.dart';
class ValueDelegate<T> {
final List<String> keyPath;
final T property;
final T value;
final T Function(LottieFrameInfo<T>) callback;
ValueDelegate._(this.keyPath, this.property, this.value, this.callback)
: assert(value == null || callback == null,
"Value and callback can't be both specified.");
static ValueDelegate<Offset> _offset(
List<String> keyPath,
Offset property,
Offset value,
Offset Function(LottieFrameInfo<Offset>) callback,
Offset relative) {
if (relative != null) {
assert(callback == null);
callback = relativeOffsetValueCallback(relative);
}
return ValueDelegate<Offset>._(keyPath, property, value, callback);
}
static ValueDelegate<double> _double(
List<String> keyPath,
double property,
double value,
double Function(LottieFrameInfo<double>) callback,
double relative) {
if (relative != null) {
assert(callback == null);
callback = relativeDoubleValueCallback(relative);
}
return ValueDelegate<double>._(keyPath, property, value, callback);
}
static ValueDelegate<int> _int(List<String> keyPath, int property, int value,
int Function(LottieFrameInfo<int>) callback, int relative) {
if (relative != null) {
assert(callback == null);
callback = relativeIntegerValueCallback(relative);
}
return ValueDelegate<int>._(keyPath, property, value, callback);
}
static ValueDelegate<Color> color(List<String> keyPath,
{Color value, Color Function(LottieFrameInfo<Color>) callback}) =>
ValueDelegate._(keyPath, LottieProperty.color, value, callback);
static ValueDelegate<Color> strokeColor(List<String> keyPath,
{Color value, Color Function(LottieFrameInfo<Color>) callback}) =>
ValueDelegate._(keyPath, LottieProperty.strokeColor, value, callback);
/// Opacity value are 0-100 to match after effects
static ValueDelegate<int> transformOpacity(List<String> keyPath,
{int value,
int Function(LottieFrameInfo<int>) callback,
int relative}) =>
_int(keyPath, LottieProperty.transformOpacity, value, callback, relative);
/// Opacity value are 0-100 to match after effects
static ValueDelegate<int> opacity(List<String> keyPath,
{int value,
int Function(LottieFrameInfo<int>) callback,
int relative}) =>
_int(keyPath, LottieProperty.opacity, value, callback, relative);
static ValueDelegate<Offset> transformAnchorPoint(
List<String> keyPath, {
Offset value,
Offset Function(LottieFrameInfo<Offset>) callback,
Offset relative,
}) {
return _offset(keyPath, LottieProperty.transformAnchorPoint, value,
callback, relative);
}
static ValueDelegate<Offset> transformPosition(
List<String> keyPath, {
Offset value,
Offset Function(LottieFrameInfo<Offset>) callback,
Offset relative,
}) =>
_offset(
keyPath, LottieProperty.transformPosition, value, callback, relative);
static ValueDelegate<Offset> ellipseSize(
List<String> keyPath, {
Offset value,
Offset Function(LottieFrameInfo<Offset>) callback,
Offset relative,
}) =>
_offset(keyPath, LottieProperty.ellipseSize, value, callback, relative);
static ValueDelegate<Offset> rectangleSize(
List<String> keyPath, {
Offset value,
Offset Function(LottieFrameInfo<Offset>) callback,
Offset relative,
}) =>
_offset(keyPath, LottieProperty.rectangleSize, value, callback, relative);
static ValueDelegate<double> cornerRadius(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.cornerRadius, value, callback, relative);
static ValueDelegate<Offset> position(
List<String> keyPath, {
Offset value,
Offset Function(LottieFrameInfo<Offset>) callback,
Offset relative,
}) =>
_offset(keyPath, LottieProperty.position, value, callback, relative);
static ValueDelegate<Offset> transformScale(
List<String> keyPath, {
Offset value,
Offset Function(LottieFrameInfo<Offset>) callback,
Offset relative,
}) =>
_offset(
keyPath, LottieProperty.transformScale, value, callback, relative);
/// In degrees
static ValueDelegate<double> transformRotation(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(
keyPath, LottieProperty.transformRotation, value, callback, relative);
static ValueDelegate<double> transformSkew(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.transformSkew, value, callback, relative);
static ValueDelegate<double> transformSkewAngle(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.transformSkewAngle, value, callback,
relative);
static ValueDelegate<double> strokeWidth(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.strokeWidth, value, callback, relative);
static ValueDelegate<double> textTracking(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.textTracking, value, callback, relative);
static ValueDelegate<double> repeaterCopies(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(
keyPath, LottieProperty.repeaterCopies, value, callback, relative);
static ValueDelegate<double> repeaterOffset(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(
keyPath, LottieProperty.repeaterOffset, value, callback, relative);
static ValueDelegate<double> polystarPoints(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(
keyPath, LottieProperty.polystarPoints, value, callback, relative);
/// In degrees
static ValueDelegate<double> polystarRotation(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(
keyPath, LottieProperty.polystarRotation, value, callback, relative);
static ValueDelegate<double> polystarInnerRadius(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.polystarInnerRadius, value, callback,
relative);
static ValueDelegate<double> polystarOuterRadius(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.polystarOuterRadius, value, callback,
relative);
static ValueDelegate<double> polystarInnerRoundedness(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.polystarInnerRoundedness, value, callback,
relative);
static ValueDelegate<double> polystarOuterRoundedness(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.polystarOuterRoundedness, value, callback,
relative);
/// Opacity value are 0-100 to match after effects
static ValueDelegate<double> transformStartOpacity(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.transformStartOpacity, value, callback,
relative);
/// Opacity value are 0-100 to match after effects
static ValueDelegate<double> transformEndOpacity(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.transformEndOpacity, value, callback,
relative);
/// The time value in seconds
static ValueDelegate<double> timeRemap(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.timeRemap, value, callback, relative);
static ValueDelegate<double> textSize(List<String> keyPath,
{double value,
double Function(LottieFrameInfo<double>) callback,
double relative}) =>
_double(keyPath, LottieProperty.textSize, value, callback, relative);
static ValueDelegate<ColorFilter> colorFilter(List<String> keyPath,
{ColorFilter value,
ColorFilter Function(LottieFrameInfo<ColorFilter>) callback}) =>
ValueDelegate._(keyPath, LottieProperty.colorFilter, value, callback);
static ValueDelegate<List<Color>> gradientColor(List<String> keyPath,
{List<Color> value,
List<Color> Function(LottieFrameInfo<List<Color>>) callback}) =>
ValueDelegate._(keyPath, LottieProperty.gradientColor, value, callback);
ResolvedValueDelegate<T> _resolved;
ResolvedValueDelegate _resolve(List<KeyPath> resolvedPaths) {
_resolved = ResolvedValueDelegate<T>(this, resolvedPaths);
return _resolved;
}
bool isSameProperty(ValueDelegate other) {
if (identical(this, other)) return true;
return other is ValueDelegate<T> &&
const ListEquality().equals(other.keyPath, keyPath) &&
other.property == property;
}
}
ResolvedValueDelegate internalResolved(ValueDelegate valueDelegate) {
return valueDelegate._resolved;
}
ResolvedValueDelegate internalResolve(
ValueDelegate delegate, List<KeyPath> resolvedPaths) {
return delegate._resolve(resolvedPaths);
}
class ResolvedValueDelegate<T> {
final ValueDelegate<T> valueDelegate;
final List<KeyPath> keyPaths;
final LottieValueCallback<T> valueCallback;
ResolvedValueDelegate(this.valueDelegate, this.keyPaths)
: valueCallback = LottieValueCallback(valueDelegate.value)
..callback = valueDelegate.callback;
T get property => valueDelegate.property;
void updateDelegate(ValueDelegate<T> delegate) {
valueCallback
..setValue(delegate.value)
..callback = delegate.callback;
}
void clear() {
valueCallback
..setValue(null)
..callback = null;
}
/// Add a property callback for the specified {@link KeyPath}. This {@link KeyPath} can resolve
/// to multiple contents. In that case, the callbacks's value will apply to all of them.
/// <p>
/// Internally, this will check if the {@link KeyPath} has already been resolved with
/// {@link #resolveKeyPath(KeyPath)} and will resolve it if it hasn't.
void addValueCallback(LottieDrawable drawable) {
for (var keyPath in keyPaths) {
keyPath.resolvedElement.addValueCallback<T>(property, valueCallback);
}
if (keyPaths.isNotEmpty) {
drawable.invalidateSelf();
if (property == LottieProperty.timeRemap) {
// Time remapping values are read in setProgress. In order for the new value
// to apply, we have to re-set the progress with the current progress so that the
// time remapping can be reapplied.
drawable.setProgress(drawable.progress);
}
}
}
}

View File

@ -43,6 +43,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
characters:
dependency: "direct main"
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
charcode: charcode:
dependency: "direct main" dependency: "direct main"
description: description:
@ -171,7 +178,7 @@ packages:
name: package_config name: package_config
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.9.1"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@ -199,7 +206,7 @@ packages:
name: pub_semver name: pub_semver
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.2" version: "1.4.3"
quiver: quiver:
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.2 version: 0.3.0
homepage: https://github.com/xvrh/lottie-flutter homepage: https://github.com/xvrh/lottie-flutter
environment: environment:
@ -10,6 +10,7 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
archive: ^2.0.0 archive: ^2.0.0
characters:
charcode: ^1.0.0 charcode: ^1.0.0
logging: ^0.11.0 logging: ^0.11.0
meta: ^1.1.8 meta: ^1.1.8

View File

@ -21,8 +21,9 @@ void main() {
var recorder = PictureRecorder(); var recorder = PictureRecorder();
var canvas = Canvas(recorder); var canvas = Canvas(recorder);
for (var progress = 0; progress <= 100; progress += 20) { for (var progress = 0; progress <= 100; progress += 20) {
drawable.draw(canvas, Rect.fromLTWH(0, 0, 200, 200), drawable
progress: progress / 100); ..setProgress(progress / 100)
..draw(canvas, Rect.fromLTWH(0, 0, 200, 200));
} }
}); });
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB