From e22474175e53ae38465b8c355c2ac5fc94a88ea7 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 4 Aug 2024 19:06:21 +0100 Subject: [PATCH] Add support for animated transitions between the various loading states --- .../vector_graphics/example/lib/main.dart | 16 +- .../lib/src/state_transition_config.dart | 48 +++++ .../lib/src/vector_graphics.dart | 196 +++++++++++------- .../vector_graphics/lib/vector_graphics.dart | 1 + .../lib/vector_graphics_compat.dart | 1 + 5 files changed, 182 insertions(+), 80 deletions(-) create mode 100644 packages/vector_graphics/lib/src/state_transition_config.dart diff --git a/packages/vector_graphics/example/lib/main.dart b/packages/vector_graphics/example/lib/main.dart index c8a1b739..74a98bb0 100644 --- a/packages/vector_graphics/example/lib/main.dart +++ b/packages/vector_graphics/example/lib/main.dart @@ -24,10 +24,22 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: const Scaffold( + home: Scaffold( body: Center( child: VectorGraphic( - loader: NetworkSvgLoader( + placeholderBuilder: (BuildContext context) => Container( + color: Colors.green, + width: double.infinity, + height: double.infinity, + child: const Center( + child: SizedBox( + width: 25, + height: 25, + child: CircularProgressIndicator(), + ), + )), + transitionConfig: const VectorGraphicsStateTransitionConfig(), + loader: const NetworkSvgLoader( 'https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg', ), ), diff --git a/packages/vector_graphics/lib/src/state_transition_config.dart b/packages/vector_graphics/lib/src/state_transition_config.dart new file mode 100644 index 00000000..28e65f0d --- /dev/null +++ b/packages/vector_graphics/lib/src/state_transition_config.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +/// The configuration of transition animation between loading/loaded states +/// +/// Containing all parameters supported by the [AnimatedSwitcher] widget which is +/// used for the animation. +class VectorGraphicsStateTransitionConfig { + + /// Creates a [VectorGraphicsStateTransitionConfig] with the given parameters. + /// + /// All parameters are optional and have default values to simplify usage. + const VectorGraphicsStateTransitionConfig({ + this.duration = const Duration(milliseconds: 400), + this.reverseDuration, + this.switchInCurve = Curves.linear, + this.switchOutCurve = Curves.linear, + this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, + this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, + }); + + /// The duration of the transition from one state to the next. + /// + /// Defaults to 400 milliseconds. + final Duration duration; + + /// The duration of the reverse transition from the next state to the previous one. + final Duration? reverseDuration; + + /// The curve to apply when transitioning in. + /// + /// Defaults to [Curves.linear]. + final Curve switchInCurve; + + /// The curve to apply when transitioning out. + /// + /// Defaults to [Curves.linear]. + final Curve switchOutCurve; + + /// The builder for constructing the transition animations. + /// + /// Defaults to [AnimatedSwitcher.defaultTransitionBuilder]. + final AnimatedSwitcherTransitionBuilder transitionBuilder; + + /// The builder for laying out the child widgets during the transition. + /// + /// Defaults to [AnimatedSwitcher.defaultLayoutBuilder]. + final AnimatedSwitcherLayoutBuilder layoutBuilder; +} diff --git a/packages/vector_graphics/lib/src/vector_graphics.dart b/packages/vector_graphics/lib/src/vector_graphics.dart index 290e8a78..2e2a7f85 100644 --- a/packages/vector_graphics/lib/src/vector_graphics.dart +++ b/packages/vector_graphics/lib/src/vector_graphics.dart @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:vector_graphics/src/state_transition_config.dart'; import 'package:vector_graphics_codec/vector_graphics_codec.dart'; @@ -18,6 +19,7 @@ import 'render_vector_graphic.dart'; export 'listener.dart' show PictureInfo; export 'loader.dart'; +export 'state_transition_config.dart'; /// How the vector graphic will be rendered by the Flutter framework. /// @@ -120,6 +122,7 @@ class VectorGraphic extends StatefulWidget { this.clipBehavior = Clip.hardEdge, this.placeholderBuilder, this.errorBuilder, + this.transitionConfig, this.colorFilter, this.opacity, this.clipViewbox = true, @@ -138,6 +141,7 @@ class VectorGraphic extends StatefulWidget { this.excludeFromSemantics = false, this.clipBehavior = Clip.hardEdge, this.placeholderBuilder, + this.transitionConfig, this.errorBuilder, this.colorFilter, this.opacity, @@ -218,6 +222,11 @@ class VectorGraphic extends StatefulWidget { /// A callback that fires if some exception happens during data acquisition or decoding. final VectorGraphicsErrorWidget? errorBuilder; + /// If provided, will be used to animate the transition between the various states of svg loading + /// + /// For example used to transition from the placeholder to error/success states. + final VectorGraphicsStateTransitionConfig? transitionConfig; + /// If provided, a color filter to apply to the vector graphic when painting. /// /// For example, `ColorFilter.mode(Colors.red, BlendMode.srcIn)` to give the vector @@ -424,93 +433,40 @@ class _VectorGraphicWidgetState extends State { Widget build(BuildContext context) { final PictureInfo? pictureInfo = _pictureInfo?.pictureInfo; + final Object? localError = _error; + final VectorGraphicsErrorWidget? localErrorBuilder = widget.errorBuilder; + final VectorGraphicsStateTransitionConfig? localTransitionConfig = widget.transitionConfig; + Widget child; if (pictureInfo != null) { // If the caller did not specify a width or height, fall back to the // size of the graphic. // If the caller did specify a width or height, preserve the aspect ratio // of the graphic and center it within that width and height. - double? width = widget.width; - double? height = widget.height; - - if (width == null && height == null) { - width = pictureInfo.size.width; - height = pictureInfo.size.height; - } else if (height != null && !pictureInfo.size.isEmpty) { - width = height / pictureInfo.size.height * pictureInfo.size.width; - } else if (width != null && !pictureInfo.size.isEmpty) { - height = width / pictureInfo.size.width * pictureInfo.size.height; - } - - assert(width != null && height != null); - - double scale = 1.0; - scale = math.min( - pictureInfo.size.width / width!, - pictureInfo.size.height / height!, + child = _buildSvgChild(pictureInfo, context); + } else if (localError != null && localErrorBuilder != null) { + child = _buildErrorChild(context, localError, localErrorBuilder); + } else { + child = Container( + key: const ValueKey('svg-placeholder-state'), + child: widget.placeholderBuilder?.call(context) ?? + SizedBox( + width: widget.width, + height: widget.height, + ), ); + } - if (_webRenderObject) { - child = _RawWebVectorGraphicWidget( - pictureInfo: pictureInfo, - assetKey: _pictureInfo!.key, - colorFilter: widget.colorFilter, - opacity: widget.opacity, - ); - } else if (widget.strategy == RenderingStrategy.raster) { - child = _RawVectorGraphicWidget( - pictureInfo: pictureInfo, - assetKey: _pictureInfo!.key, - colorFilter: widget.colorFilter, - opacity: widget.opacity, - scale: scale, - ); - } else { - child = _RawPictureVectorGraphicWidget( - pictureInfo: pictureInfo, - assetKey: _pictureInfo!.key, - colorFilter: widget.colorFilter, - opacity: widget.opacity, - ); - } - - if (widget.matchTextDirection) { - final TextDirection direction = Directionality.of(context); - if (direction == TextDirection.rtl) { - child = Transform( - transform: Matrix4.identity() - ..translate(pictureInfo.size.width) - ..scale(-1.0, 1.0), - child: child, - ); - } - } - - child = SizedBox( - width: width, - height: height, - child: FittedBox( - fit: widget.fit, - alignment: widget.alignment, - clipBehavior: widget.clipBehavior, - child: SizedBox.fromSize( - size: pictureInfo.size, - child: child, - ), - ), - ); - } else if (_error != null && widget.errorBuilder != null) { - child = widget.errorBuilder!( - context, - _error!, - _stackTrace ?? StackTrace.empty, + if (localTransitionConfig != null) { + child = AnimatedSwitcher( + duration: localTransitionConfig.duration, + reverseDuration: localTransitionConfig.reverseDuration, + switchInCurve: localTransitionConfig.switchInCurve, + switchOutCurve: localTransitionConfig.switchOutCurve, + transitionBuilder: localTransitionConfig.transitionBuilder, + layoutBuilder: localTransitionConfig.layoutBuilder, + child: child, ); - } else { - child = widget.placeholderBuilder?.call(context) ?? - SizedBox( - width: widget.width, - height: widget.height, - ); } if (!widget.excludeFromSemantics) { @@ -523,6 +479,90 @@ class _VectorGraphicWidgetState extends State { } return child; } + + Widget _buildErrorChild(BuildContext context, Object error, + VectorGraphicsErrorWidget errorBuilder) => + Container( + key: const ValueKey('svg-error-state'), + child: errorBuilder( + context, + error, + _stackTrace ?? StackTrace.empty, + ), + ); + + Widget _buildSvgChild(PictureInfo pictureInfo, BuildContext context) { + double? width = widget.width; + double? height = widget.height; + if (width == null && height == null) { + width = pictureInfo.size.width; + height = pictureInfo.size.height; + } else if (height != null && !pictureInfo.size.isEmpty) { + width = height / pictureInfo.size.height * pictureInfo.size.width; + } else if (width != null && !pictureInfo.size.isEmpty) { + height = width / pictureInfo.size.width * pictureInfo.size.height; + } + + assert(width != null && height != null); + + double scale = 1.0; + scale = math.min( + pictureInfo.size.width / width!, + pictureInfo.size.height / height!, + ); + + Widget child; + if (_webRenderObject) { + child = _RawWebVectorGraphicWidget( + pictureInfo: pictureInfo, + assetKey: _pictureInfo!.key, + colorFilter: widget.colorFilter, + opacity: widget.opacity, + ); + } else if (widget.strategy == RenderingStrategy.raster) { + child = _RawVectorGraphicWidget( + pictureInfo: pictureInfo, + assetKey: _pictureInfo!.key, + colorFilter: widget.colorFilter, + opacity: widget.opacity, + scale: scale, + ); + } else { + child = _RawPictureVectorGraphicWidget( + pictureInfo: pictureInfo, + assetKey: _pictureInfo!.key, + colorFilter: widget.colorFilter, + opacity: widget.opacity, + ); + } + + if (widget.matchTextDirection) { + final TextDirection direction = Directionality.of(context); + if (direction == TextDirection.rtl) { + child = Transform( + transform: Matrix4.identity() + ..translate(pictureInfo.size.width) + ..scale(-1.0, 1.0), + child: child, + ); + } + } + + return SizedBox( + key: const ValueKey('svg-loaded-state'), + width: width, + height: height, + child: FittedBox( + fit: widget.fit, + alignment: widget.alignment, + clipBehavior: widget.clipBehavior, + child: SizedBox.fromSize( + size: pictureInfo.size, + child: child, + ), + ), + ); + } } class _RawVectorGraphicWidget extends SingleChildRenderObjectWidget { diff --git a/packages/vector_graphics/lib/vector_graphics.dart b/packages/vector_graphics/lib/vector_graphics.dart index 9622a38c..743ef42e 100644 --- a/packages/vector_graphics/lib/vector_graphics.dart +++ b/packages/vector_graphics/lib/vector_graphics.dart @@ -10,4 +10,5 @@ export 'src/vector_graphics.dart' BytesLoader, VectorGraphic, VectorGraphicUtilities, + VectorGraphicsStateTransitionConfig, vg; diff --git a/packages/vector_graphics/lib/vector_graphics_compat.dart b/packages/vector_graphics/lib/vector_graphics_compat.dart index 06cc6001..484c5db1 100644 --- a/packages/vector_graphics/lib/vector_graphics_compat.dart +++ b/packages/vector_graphics/lib/vector_graphics_compat.dart @@ -10,6 +10,7 @@ export 'src/vector_graphics.dart' BytesLoader, VectorGraphic, VectorGraphicUtilities, + VectorGraphicsStateTransitionConfig, vg, RenderingStrategy, createCompatVectorGraphic;