twonly-app-dependencies/lottie/lib/src/lottie_builder.dart
2025-12-07 16:10:41 +01:00

592 lines
20 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import 'composition.dart';
import 'frame_rate.dart';
import 'lottie.dart';
import 'lottie_delegates.dart';
import 'options.dart';
import 'providers/asset_provider.dart';
import 'providers/file_provider.dart';
import 'providers/load_image.dart';
import 'providers/lottie_provider.dart';
import 'providers/memory_provider.dart';
import 'providers/network_provider.dart';
import 'render_cache.dart';
typedef LottieFrameBuilder =
Widget Function(
BuildContext context,
Widget child,
LottieComposition? composition,
);
/// Signature used by [Lottie.errorBuilder] to create a replacement widget to
/// render instead of the image.
typedef LottieErrorWidgetBuilder =
Widget Function(BuildContext context, Object error, StackTrace? stackTrace);
/// A widget that displays a Lottie animation.
///
/// Several constructors are provided for the various ways that a Lottie file
/// can be provided:
///
/// * [Lottie], for obtaining a composition from a [LottieProvider].
/// * [Lottie.asset], for obtaining a Lottie file from an [AssetBundle]
/// using a key.
/// * [Lottie.network], for obtaining a lottie file from a URL.
/// * [Lottie.file], for obtaining a lottie file from a [File].
/// * [Lottie.memory], for obtaining a lottie file from a [Uint8List].
///
class LottieBuilder extends StatefulWidget {
const LottieBuilder({
super.key,
required this.lottie,
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
this.delegates,
this.options,
this.onLoaded,
this.frameBuilder,
this.errorBuilder,
this.width,
this.height,
this.fit,
this.alignment,
this.addRepaintBoundary,
this.filterQuality,
this.onWarning,
this.renderCache,
});
/// Creates a widget that displays an [LottieComposition] obtained from the network.
LottieBuilder.network(
String src, {
http.Client? client,
Map<String, String>? headers,
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
this.delegates,
this.options,
LottieImageProviderFactory? imageProviderFactory,
this.onLoaded,
super.key,
this.frameBuilder,
this.errorBuilder,
this.width,
this.height,
this.fit,
this.alignment,
this.addRepaintBoundary,
this.filterQuality,
this.onWarning,
LottieDecoder? decoder,
this.renderCache,
bool? backgroundLoading,
}) : lottie = NetworkLottie(
src,
client: client,
headers: headers,
imageProviderFactory: imageProviderFactory,
decoder: decoder,
backgroundLoading: backgroundLoading,
);
/// Creates a widget that displays an [LottieComposition] obtained from a [File].
///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the animation is loaded, which
/// will result in ugly layout changes.
///
/// On Android, this may require the
/// `android.permission.READ_EXTERNAL_STORAGE` permission.
///
LottieBuilder.file(
Object file, {
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
this.delegates,
this.options,
LottieImageProviderFactory? imageProviderFactory,
this.onLoaded,
super.key,
this.frameBuilder,
this.errorBuilder,
this.width,
this.height,
this.fit,
this.alignment,
this.addRepaintBoundary,
this.filterQuality,
this.onWarning,
LottieDecoder? decoder,
this.renderCache,
bool? backgroundLoading,
}) : lottie = FileLottie(
file,
imageProviderFactory: imageProviderFactory,
decoder: decoder,
backgroundLoading: backgroundLoading,
);
/// Creates a widget that displays an [LottieComposition] obtained from an [AssetBundle].
LottieBuilder.asset(
String name, {
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
this.delegates,
this.options,
LottieImageProviderFactory? imageProviderFactory,
this.onLoaded,
super.key,
AssetBundle? bundle,
this.frameBuilder,
this.errorBuilder,
this.width,
this.height,
this.fit,
this.alignment,
String? package,
this.addRepaintBoundary,
this.filterQuality,
this.onWarning,
LottieDecoder? decoder,
this.renderCache,
bool? backgroundLoading,
}) : lottie = AssetLottie(
name,
bundle: bundle,
package: package,
imageProviderFactory: imageProviderFactory,
decoder: decoder,
backgroundLoading: backgroundLoading,
);
/// Creates a widget that displays an [LottieComposition] obtained from a [Uint8List].
LottieBuilder.memory(
Uint8List bytes, {
this.controller,
this.frameRate,
this.animate,
this.reverse,
this.repeat,
this.delegates,
this.options,
LottieImageProviderFactory? imageProviderFactory,
this.onLoaded,
this.errorBuilder,
super.key,
this.frameBuilder,
this.width,
this.height,
this.fit,
this.alignment,
this.addRepaintBoundary,
this.filterQuality,
this.onWarning,
LottieDecoder? decoder,
this.renderCache,
bool? backgroundLoading,
}) : lottie = MemoryLottie(
bytes,
imageProviderFactory: imageProviderFactory,
decoder: decoder,
backgroundLoading: backgroundLoading,
);
/// The lottie animation to load.
/// Example of providers: [AssetLottie], [NetworkLottie], [FileLottie], [MemoryLottie]
final LottieProvider lottie;
/// A callback called when the LottieComposition has been loaded.
/// You can use this callback to set the correct duration on the AnimationController
/// with `composition.duration`
final void Function(LottieComposition)? onLoaded;
/// The animation controller of the Lottie animation.
/// The animated value will be mapped to the `progress` property of the
/// Lottie animation.
final Animation<double>? controller;
/// The number of frames per second to render.
/// Use `FrameRate.composition` to use the original frame rate of the Lottie composition (default)
/// Use `FrameRate.max` to advance the animation progression at every frame.
final FrameRate? frameRate;
/// If no controller is specified, this value indicate whether or not the
/// Lottie animation should be played automatically (default to true).
/// If there is an animation controller specified, this property has no effect.
///
/// See [repeat] to control whether the animation should repeat.
final bool? animate;
/// Specify that the automatic animation should repeat in a loop (default to true).
/// The property has no effect if [animate] is false or [controller] is not null.
final bool? repeat;
/// Specify that the automatic animation should repeat in a loop in a "reverse"
/// mode (go from start to end and then continuously from end to start).
/// It default to false.
/// The property has no effect if [animate] is false, [repeat] is false or [controller] is not null.
final bool? reverse;
/// A 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;
/// Some options to enable/disable some feature of Lottie
/// - enableMergePaths: Enable merge path support
/// - enableApplyingOpacityToLayers: Enable layer-level opacity
final LottieOptions? options;
/// A builder function responsible for creating the widget that represents
/// this lottie animation.
///
/// If this is null, this widget will display a lottie animation that is painted as
/// soon as it is available (and will appear to "pop" in
/// if it becomes available asynchronously). Callers might use this builder to
/// add effects to the animation (such as fading the animation in when it becomes
/// available) or to display a placeholder widget while the animation is loading.
///
/// To have finer-grained control over the way that an animation's loading
/// progress is communicated to the user, see [loadingBuilder].
///
/// {@template lottie.chainedBuildersExample}
/// ```dart
/// Lottie(
/// ...
/// frameBuilder: (BuildContext context, Widget child) {
/// return Padding(
/// padding: EdgeInsets.all(8.0),
/// child: child,
/// );
/// }
/// )
/// ```
///
/// In this example, the widget hierarchy will contain the following:
///
/// ```dart
/// Center(
/// Padding(
/// padding: EdgeInsets.all(8.0),
/// child: <lottie>,
/// ),
/// )
/// ```
/// {@endtemplate}
///
/// {@tool snippet --template=stateless_widget_material}
///
/// The following sample demonstrates how to use this builder to implement an
/// animation that fades in once it's been loaded.
///
/// This sample contains a limited subset of the functionality that the
/// [FadeInImage] widget provides out of the box.
///
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// return DecoratedBox(
/// decoration: BoxDecoration(
/// color: Colors.white,
/// border: Border.all(),
/// borderRadius: BorderRadius.circular(20),
/// ),
/// child: Lottie.network(
/// 'https://example.com/animation.json',
/// frameBuilder: (BuildContext context, Widget child) {
/// if (wasSynchronouslyLoaded) {
/// return child;
/// }
/// return AnimatedOpacity(
/// child: child,
/// opacity: frame == null ? 0 : 1,
/// duration: const Duration(seconds: 1),
/// curve: Curves.easeOut,
/// );
/// },
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
final LottieFrameBuilder? frameBuilder;
/// If non-null, require the lottie animation to have this width.
///
/// If null, the lottie animation will pick a size that best preserves its intrinsic
/// aspect ratio.
///
/// It is strongly recommended that either both the [width] and the [height]
/// be specified, or that the widget be placed in a context that sets tight
/// layout constraints, so that the animation does not change size as it loads.
/// Consider using [fit] to adapt the animation's rendering to fit the given width
/// and height if the exact animation dimensions are not known in advance.
final double? width;
/// If non-null, require the lottie animation to have this height.
///
/// If null, the lottie animation will pick a size that best preserves its intrinsic
/// aspect ratio.
///
/// It is strongly recommended that either both the [width] and the [height]
/// be specified, or that the widget be placed in a context that sets tight
/// layout constraints, so that the animation does not change size as it loads.
/// Consider using [fit] to adapt the animation's rendering to fit the given width
/// and height if the exact animation dimensions are not known in advance.
final double? height;
/// How to inscribe the animation into the space allocated during layout.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
final BoxFit? fit;
/// How to align the animation within its bounds.
///
/// The alignment aligns the given position in the animation to the given position
/// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
/// -1.0) aligns the animation to the top-left corner of its layout bounds, while an
/// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
/// animation with the bottom right corner of its layout bounds. Similarly, an
/// alignment of (0.0, 1.0) aligns the bottom middle of the animation with the
/// middle of the bottom edge of its layout bounds.
///
/// To display a subpart of an animation, consider using a [CustomPainter] and
/// [Canvas.drawImageRect].
///
/// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
/// [AlignmentDirectional]), then an ambient [Directionality] widget
/// must be in scope.
///
/// Defaults to [Alignment.center].
///
/// See also:
///
/// * [Alignment], a class with convenient constants typically used to
/// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction.
final AlignmentGeometry? alignment;
/// Indicate to automatically add a `RepaintBoundary` widget around the animation.
/// This allows to optimize the app performance by isolating the animation in its
/// own `Layer`.
///
/// This property is `true` by default.
final bool? addRepaintBoundary;
/// The quality of the image layer. See [FilterQuality]
/// [FilterQuality.high] is highest quality but slowest.
///
/// Defaults to [FilterQuality.low]
final FilterQuality? filterQuality;
/// A callback called when there is a warning during the loading or painting
/// of the animation.
final WarningCallback? onWarning;
/// A builder function that is called if an error occurs during loading.
///
/// If this builder is not provided, any exceptions will be reported to
/// [FlutterError.onError]. If it is provided, the caller should either handle
/// the exception by providing a replacement widget, or rethrow the exception.
///
/// The following sample uses [errorBuilder] to show a '😢' in place of the
/// image that fails to load, and prints the error to the console.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return DecoratedBox(
/// decoration: BoxDecoration(
/// color: Colors.white,
/// ),
/// child: Lottie.network(
/// 'https://example.does.not.exist/lottie.json',
/// errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
/// // Appropriate logging or analytics, e.g.
/// // myAnalytics.recordError(
/// // 'An error occurred loading "https://example.does.not.exist/animation.json"',
/// // exception,
/// // stackTrace,
/// // );
/// return const Text('😢');
/// },
/// ),
/// );
/// }
/// ```
final ImageErrorWidgetBuilder? errorBuilder;
/// Opt-in to a special render mode where the frames of the animation are
/// lazily rendered and kept in a cache.
/// Subsequent runs of the animation will be cheaper to render.
///
/// This is useful is the animation is complex and can consume lot of energy
/// from the battery.
/// This will trade an excessive CPU usage for an increase memory usage.
/// The main use-case is a short and small (size on the screen) animation that is
/// played repeatedly.
///
/// There are 2 kinds of caches:
/// - [RenderCache.raster]: keep the frame rasterized in the cache (as [dart:ui.Image]).
/// Subsequent runs of the animation are very cheap for both the CPU and GPU but it takes
/// a lot of memory (rendered_width * rendered_height * frame_rate * duration_of_the_animation).
/// This should only be used for very short and very small animations.
/// - [RenderCache.drawingCommands]: keep the frame as a list of graphical operations ([dart:ui.Picture]).
/// Subsequent runs of the animation are cheaper for the CPU but not for the GPU.
/// Memory usage is a lot lower than RenderCache.raster.
///
/// The render cache is managed internally and will release the memory once the
/// animation disappear. The cache is shared between all animations.
/// Any change in the configuration of the animation (delegates, frame rate etc...)
/// will clear the cache entry.
/// For RenderCache.raster, any change in the size will invalidate the cache entry. The cache
/// use the final size visible on the screen (with all transforms applied).
///
/// In order to not exceed the memory limit of a device, the raster cache is constrained
/// to maximum 50MB. After that, animations are not cached anymore.
final RenderCache? renderCache;
@override
State<LottieBuilder> createState() => _LottieBuilderState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LottieProvider>('lottie', lottie));
properties.add(DiagnosticsProperty<Function>('frameBuilder', frameBuilder));
properties.add(DoubleProperty('width', width, defaultValue: null));
properties.add(DoubleProperty('height', height, defaultValue: null));
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
properties.add(
DiagnosticsProperty<AlignmentGeometry>(
'alignment',
alignment,
defaultValue: null,
),
);
}
}
class _LottieBuilderState extends State<LottieBuilder> {
Future<LottieComposition>? _loadingFuture;
@override
void didUpdateWidget(LottieBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.lottie != widget.lottie) {
_load();
}
}
void _load() {
var provider = widget.lottie;
_loadingFuture = widget.lottie.load(context: context).then((composition) {
// LottieProvider.load() can return a Synchronous future and the onLoaded
// callback can call setState, so we wrap it in a microtask to avoid an
// "!_isDirty" error.
scheduleMicrotask(() {
if (mounted && widget.lottie == provider) {
var onWarning = widget.onWarning;
composition.onWarning = onWarning;
if (onWarning != null) {
for (var warning in composition.warnings) {
onWarning(warning);
}
}
widget.onLoaded?.call(composition);
}
});
return composition;
});
}
@override
Widget build(BuildContext context) {
// We need to wait the first build instead of initState because AssetLottie
// provider may call DefaultAssetBundle.of
if (_loadingFuture == null) {
_load();
}
return FutureBuilder<LottieComposition>(
future: _loadingFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
var errorBuilder = widget.errorBuilder;
if (errorBuilder != null) {
return errorBuilder(context, snapshot.error!, snapshot.stackTrace);
} else if (kDebugMode) {
return SizedBox(
width: widget.width,
height: widget.height,
child: ErrorWidget(snapshot.error!),
);
}
}
var composition = snapshot.data;
var animate = widget.animate;
animate ??= (composition?.durationFrames ?? 0) > 1.0;
Widget result = Lottie(
composition: composition,
controller: widget.controller,
frameRate: widget.frameRate,
animate: animate,
reverse: widget.reverse,
repeat: widget.repeat,
delegates: widget.delegates,
options: widget.options,
width: widget.width,
height: widget.height,
fit: widget.fit,
alignment: widget.alignment,
addRepaintBoundary: widget.addRepaintBoundary,
filterQuality: widget.filterQuality,
renderCache: widget.renderCache,
);
if (widget.frameBuilder != null) {
result = widget.frameBuilder!(context, result, composition);
}
return result;
},
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(
DiagnosticsProperty<Future<LottieComposition>>(
'loadingFuture',
_loadingFuture,
),
);
}
}