mirror of
https://github.com/xvrh/lottie-flutter.git
synced 2025-08-06 16:39:36 +08:00
Compare commits
1 Commits
xha/text_t
...
v2.1.0
Author | SHA1 | Date | |
---|---|---|---|
8dcb052fe1 |
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,3 +1,18 @@
|
||||
|
||||
## [2.1.0]
|
||||
- Improve the cache to ensure that there is not an empty frame each time we load an animation.
|
||||
The method `AssetLottie('anim.json').load()` returns a `SynchronousFuture` if it has been loaded previously.
|
||||
- Expose the `LottieCache` singleton.
|
||||
It allows to change the cache behaviour and clear the entries.
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
Lottie.cache.maximumSize = 10;
|
||||
Lottie.cache.clear();
|
||||
Lottie.cache.evict(NetworkLottie('https://lottie.com/anim.json'));
|
||||
}
|
||||
```
|
||||
|
||||
## [2.0.0]
|
||||
- **Breaking change**: the lottie widget will be smaller if it relies on the intrinsic size of the composition.
|
||||
|
||||
|
59
example/lib/examples/cache.dart
Normal file
59
example/lib/examples/cache.dart
Normal file
@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
void main() => runApp(const App());
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(body: _Page()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Page extends StatefulWidget {
|
||||
@override
|
||||
__PageState createState() => __PageState();
|
||||
}
|
||||
|
||||
class __PageState extends State<_Page> {
|
||||
var _animationKey = UniqueKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Lottie.network(
|
||||
'https://assets10.lottiefiles.com/datafiles/QeC7XD39x4C1CIj/data.json',
|
||||
key: _animationKey,
|
||||
fit: BoxFit.contain,
|
||||
width: 200,
|
||||
height: 200,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Lottie.cache.clear();
|
||||
Lottie.cache.maximumSize = 10;
|
||||
},
|
||||
child: const Text('Clear cache'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_animationKey = UniqueKey();
|
||||
});
|
||||
},
|
||||
child: const Text('Recreate animation'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ packages:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
version: "3.3.5"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -43,6 +43,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -101,7 +108,7 @@ packages:
|
||||
name: golden_toolkit
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
version: "0.14.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -116,6 +123,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.5"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -136,7 +150,7 @@ packages:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.0.0"
|
||||
version: "2.1.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -178,7 +192,7 @@ packages:
|
||||
name: path_provider_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.21"
|
||||
version: "2.0.22"
|
||||
path_provider_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -228,6 +242,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.6.2"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -302,7 +323,7 @@ packages:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -311,5 +332,5 @@ packages:
|
||||
source: hosted
|
||||
version: "0.2.0+2"
|
||||
sdks:
|
||||
dart: ">=2.18.0 <3.0.0"
|
||||
dart: ">=2.18.4 <3.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
@ -10,7 +10,7 @@ export 'src/options.dart' show LottieOptions;
|
||||
export 'src/providers/asset_provider.dart' show AssetLottie;
|
||||
export 'src/providers/file_provider.dart' show FileLottie;
|
||||
export 'src/providers/load_image.dart' show LottieImageProviderFactory;
|
||||
export 'src/providers/lottie_provider.dart' show LottieProvider;
|
||||
export 'src/providers/lottie_provider.dart' show LottieProvider, LottieCache;
|
||||
export 'src/providers/memory_provider.dart' show MemoryLottie;
|
||||
export 'src/providers/network_provider.dart' show NetworkLottie;
|
||||
export 'src/raw_lottie.dart' show RawLottie;
|
||||
|
@ -4,6 +4,7 @@ import '../lottie.dart';
|
||||
import 'composition.dart';
|
||||
import 'l.dart';
|
||||
import 'lottie_builder.dart';
|
||||
import 'providers/lottie_provider.dart';
|
||||
|
||||
/// A widget to display a loaded [LottieComposition].
|
||||
/// The [controller] property allows to specify a custom AnimationController that
|
||||
@ -11,6 +12,9 @@ import 'lottie_builder.dart';
|
||||
/// automatically and the behavior could be adjusted with the properties [animate],
|
||||
/// [repeat] and [reverse].
|
||||
class Lottie extends StatefulWidget {
|
||||
/// The cache instance for recently loaded Lottie compositions.
|
||||
static LottieCache get cache => sharedLottieCache;
|
||||
|
||||
const Lottie({
|
||||
super.key,
|
||||
required this.composition,
|
||||
|
@ -25,9 +25,8 @@ class AssetLottie extends LottieProvider {
|
||||
final String? package;
|
||||
|
||||
@override
|
||||
Future<LottieComposition> load() async {
|
||||
var cacheKey = 'asset-$keyName-$bundle';
|
||||
return sharedLottieCache.putIfAbsent(cacheKey, () async {
|
||||
Future<LottieComposition> load() {
|
||||
return sharedLottieCache.putIfAbsent(this, () async {
|
||||
final chosenBundle = bundle ?? rootBundle;
|
||||
|
||||
var data = await chosenBundle.load(keyName);
|
||||
|
@ -12,9 +12,8 @@ class FileLottie extends LottieProvider {
|
||||
final Object /*io.File|html.File*/ file;
|
||||
|
||||
@override
|
||||
Future<LottieComposition> load() async {
|
||||
var cacheKey = 'file-${io.filePath(file)}';
|
||||
return sharedLottieCache.putIfAbsent(cacheKey, () async {
|
||||
Future<LottieComposition> load() {
|
||||
return sharedLottieCache.putIfAbsent(this, () async {
|
||||
var bytes = await io.loadFile(file);
|
||||
var composition = await LottieComposition.fromBytes(bytes,
|
||||
name: p.basenameWithoutExtension(io.filePath(file)),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../lottie.dart';
|
||||
import 'load_image.dart';
|
||||
|
||||
@ -19,37 +20,103 @@ abstract class LottieProvider {
|
||||
}
|
||||
|
||||
class LottieCache {
|
||||
final int maximumSize;
|
||||
final _cache = <String, Future<LottieComposition>>{};
|
||||
final Map<Object, Future<LottieComposition>> _pending =
|
||||
<Object, Future<LottieComposition>>{};
|
||||
final Map<Object, LottieComposition> _cache = <Object, LottieComposition>{};
|
||||
|
||||
LottieCache({int? maximumSize}) : maximumSize = maximumSize ?? 1000;
|
||||
/// Maximum number of entries to store in the cache.
|
||||
///
|
||||
/// Once this many entries have been cached, the least-recently-used entry is
|
||||
/// evicted when adding a new entry.
|
||||
int get maximumSize => _maximumSize;
|
||||
int _maximumSize = 1000;
|
||||
|
||||
Future<LottieComposition> putIfAbsent(
|
||||
String key, Future<LottieComposition> Function() load) {
|
||||
var composition = _cache[key];
|
||||
if (composition != null) {
|
||||
// Remove it so that we add it in front of the cache to prevent evicted
|
||||
_cache.remove(key);
|
||||
/// Changes the maximum cache size.
|
||||
///
|
||||
/// If the new size is smaller than the current number of elements, the
|
||||
/// extraneous elements are evicted immediately. Setting this to zero and then
|
||||
/// returning it to its original value will therefore immediately clear the
|
||||
/// cache.
|
||||
set maximumSize(int value) {
|
||||
assert(value >= 0);
|
||||
if (value == maximumSize) {
|
||||
return;
|
||||
}
|
||||
_maximumSize = value;
|
||||
if (maximumSize == 0) {
|
||||
clear();
|
||||
} else {
|
||||
composition = load();
|
||||
}
|
||||
|
||||
_cache[key] = composition;
|
||||
|
||||
_checkCacheSize();
|
||||
|
||||
return composition;
|
||||
}
|
||||
|
||||
void _checkCacheSize() {
|
||||
while (_cache.length > maximumSize) {
|
||||
_cache.remove(_cache.keys.first);
|
||||
while (_cache.length > maximumSize) {
|
||||
_cache.remove(_cache.keys.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evicts all entries from the cache.
|
||||
///
|
||||
/// This is useful if, for instance, the root asset bundle has been updated
|
||||
/// and therefore new images must be obtained.
|
||||
void clear() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
/// Evicts a single entry from the cache, returning true if successful.
|
||||
bool evict(Object key) {
|
||||
return _cache.remove(key) != null;
|
||||
}
|
||||
|
||||
/// Returns the previously cached [LottieComposition] for the given key, if available;
|
||||
/// if not, calls the given callback to obtain it first. In either case, the
|
||||
/// key is moved to the "most recently used" position.
|
||||
///
|
||||
/// The arguments must not be null. The `loader` cannot return null.
|
||||
Future<LottieComposition> putIfAbsent(
|
||||
Object key,
|
||||
Future<LottieComposition> Function() loader,
|
||||
) {
|
||||
var pendingResult = _pending[key];
|
||||
if (pendingResult != null) {
|
||||
return pendingResult;
|
||||
}
|
||||
|
||||
var result = _cache[key];
|
||||
if (result != null) {
|
||||
// Remove the provider from the list so that we can put it back in below
|
||||
// and thus move it to the end of the list.
|
||||
_cache.remove(key);
|
||||
} else {
|
||||
if (_cache.length == maximumSize && maximumSize > 0) {
|
||||
_cache.remove(_cache.keys.first);
|
||||
}
|
||||
pendingResult = loader();
|
||||
_pending[key] = pendingResult;
|
||||
pendingResult.then<void>((LottieComposition data) {
|
||||
_pending.remove(key);
|
||||
_add(key, data);
|
||||
|
||||
result = data; // in case it was a synchronous future.
|
||||
}).catchError((Object? e) {
|
||||
_pending.remove(key);
|
||||
});
|
||||
}
|
||||
if (result != null) {
|
||||
_add(key, result!);
|
||||
return SynchronousFuture<LottieComposition>(result!);
|
||||
}
|
||||
assert(_cache.length <= maximumSize);
|
||||
return pendingResult!;
|
||||
}
|
||||
|
||||
void _add(Object key, LottieComposition result) {
|
||||
if (maximumSize > 0) {
|
||||
assert(_cache.length < maximumSize);
|
||||
_cache[key] = result;
|
||||
}
|
||||
assert(_cache.length <= maximumSize);
|
||||
}
|
||||
|
||||
/// The number of entries in the cache.
|
||||
int get count => _cache.length;
|
||||
}
|
||||
|
||||
final sharedLottieCache = LottieCache();
|
||||
|
@ -13,10 +13,8 @@ class MemoryLottie extends LottieProvider {
|
||||
final Uint8List bytes;
|
||||
|
||||
@override
|
||||
Future<LottieComposition> load() async {
|
||||
// TODO(xha): hash the list content
|
||||
var cacheKey = 'memory-${bytes.hashCode}-${bytes.lengthInBytes}';
|
||||
return sharedLottieCache.putIfAbsent(cacheKey, () async {
|
||||
Future<LottieComposition> load() {
|
||||
return sharedLottieCache.putIfAbsent(this, () async {
|
||||
var composition = await LottieComposition.fromBytes(bytes,
|
||||
imageProviderFactory: imageProviderFactory);
|
||||
for (var image in composition.images.values) {
|
||||
@ -40,6 +38,8 @@ class MemoryLottie extends LottieProvider {
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
|
||||
//TODO(xha): compare bytes content
|
||||
return other is MemoryLottie && other.bytes == bytes;
|
||||
}
|
||||
|
||||
|
@ -15,9 +15,8 @@ class NetworkLottie extends LottieProvider {
|
||||
final Map<String, String>? headers;
|
||||
|
||||
@override
|
||||
Future<LottieComposition> load() async {
|
||||
var cacheKey = 'network-$url';
|
||||
return sharedLottieCache.putIfAbsent(cacheKey, () async {
|
||||
Future<LottieComposition> load() {
|
||||
return sharedLottieCache.putIfAbsent(this, () async {
|
||||
var resolved = Uri.base.resolve(url);
|
||||
var bytes = await network.loadHttp(resolved, headers: headers);
|
||||
|
||||
|
15
pubspec.lock
15
pubspec.lock
@ -21,7 +21,7 @@ packages:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
version: "3.3.5"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -199,7 +199,7 @@ packages:
|
||||
name: frontend_server_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.2.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -290,7 +290,7 @@ packages:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.3"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -305,6 +305,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.6.2"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -318,7 +325,7 @@ packages:
|
||||
name: pub_semver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1,6 +1,6 @@
|
||||
name: lottie
|
||||
description: Render After Effects animations natively on Flutter. This package is a pure Dart implementation of a Lottie player.
|
||||
version: 2.0.0
|
||||
version: 2.1.0
|
||||
repository: https://github.com/xvrh/lottie-flutter
|
||||
|
||||
environment:
|
||||
|
@ -35,10 +35,10 @@ void main() {
|
||||
tester.binding.window.physicalSizeTestValue = size;
|
||||
tester.binding.window.devicePixelRatioTestValue = 1.0;
|
||||
|
||||
var image = await tester.runAsync(() =>
|
||||
var image = await tester.runAsync(() async =>
|
||||
loadImage(FileImage(File('example/assets/Images/WeAccept/img_0.png'))));
|
||||
|
||||
var composition = (await tester.runAsync(() =>
|
||||
var composition = (await tester.runAsync(() async =>
|
||||
FileLottie(File('example/assets/spinning_carrousel.zip')).load()))!;
|
||||
|
||||
var delegates = LottieDelegates(image: (composition, asset) {
|
||||
|
@ -4,11 +4,10 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:lottie/src/providers/lottie_provider.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
sharedLottieCache.clear();
|
||||
Lottie.cache.clear();
|
||||
});
|
||||
|
||||
testWidgets('Should settle if no animation', (tester) async {
|
||||
@ -324,6 +323,72 @@ void main() {
|
||||
expect(find.byKey(errorKey), findsNothing);
|
||||
expect(loadedCall, 1);
|
||||
});
|
||||
|
||||
testWidgets('Cache should be synchronous', (tester) async {
|
||||
var hamburgerData =
|
||||
Future.value(bytesForFile('example/assets/HamburgerArrow.json'));
|
||||
var mockAsset = FakeAssetBundle({
|
||||
'hamburger.json': hamburgerData,
|
||||
});
|
||||
|
||||
var loadedCall = 0;
|
||||
var lottieWidget = LottieBuilder.asset(
|
||||
'hamburger.json',
|
||||
bundle: mockAsset,
|
||||
onLoaded: (c) {
|
||||
++loadedCall;
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpWidget(lottieWidget);
|
||||
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNull);
|
||||
await tester.pump();
|
||||
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNotNull);
|
||||
|
||||
await tester.pumpWidget(Column(
|
||||
children: [
|
||||
lottieWidget,
|
||||
lottieWidget,
|
||||
],
|
||||
));
|
||||
expect(tester.widget<Lottie>(find.byType(Lottie).at(0)).composition,
|
||||
isNotNull);
|
||||
expect(tester.widget<Lottie>(find.byType(Lottie).at(1)).composition,
|
||||
isNotNull);
|
||||
expect(loadedCall, 3);
|
||||
});
|
||||
|
||||
testWidgets('Cache can be cleared', (tester) async {
|
||||
var hamburgerData =
|
||||
Future.value(bytesForFile('example/assets/HamburgerArrow.json'));
|
||||
var mockAsset = FakeAssetBundle({
|
||||
'hamburger.json': hamburgerData,
|
||||
});
|
||||
|
||||
var loadedCall = 0;
|
||||
var lottieWidget = LottieBuilder.asset(
|
||||
'hamburger.json',
|
||||
bundle: mockAsset,
|
||||
onLoaded: (c) {
|
||||
++loadedCall;
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpWidget(lottieWidget);
|
||||
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNull);
|
||||
await tester.pump();
|
||||
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNotNull);
|
||||
|
||||
Lottie.cache.clear();
|
||||
|
||||
await tester.pumpWidget(Center(
|
||||
child: lottieWidget,
|
||||
));
|
||||
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNull);
|
||||
await tester.pump();
|
||||
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNotNull);
|
||||
expect(loadedCall, 2);
|
||||
});
|
||||
}
|
||||
|
||||
class SynchronousFile extends Fake implements File {
|
||||
|
Reference in New Issue
Block a user