diff --git a/packages/vector_graphics/CHANGELOG.md b/packages/vector_graphics/CHANGELOG.md index b01cb1ae3c24..a24016e6fa20 100644 --- a/packages/vector_graphics/CHANGELOG.md +++ b/packages/vector_graphics/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 1.1.20 * Updates minimum supported SDK version to Flutter 3.32/Dart 3.8. +* Respect BoxFit parameter when viewbox is specified. ## 1.1.19 diff --git a/packages/vector_graphics/lib/src/listener.dart b/packages/vector_graphics/lib/src/listener.dart index 7f5f2b8fd7ab..cc9e96af8376 100644 --- a/packages/vector_graphics/lib/src/listener.dart +++ b/packages/vector_graphics/lib/src/listener.dart @@ -20,7 +20,7 @@ import 'loader.dart'; const VectorGraphicsCodec _codec = VectorGraphicsCodec(); -/// The deocded result of a vector graphics asset. +/// The decoded result of a vector graphics asset. class PictureInfo { /// Construct a new [PictureInfo]. PictureInfo._(this.picture, this.size); diff --git a/packages/vector_graphics/lib/src/vector_graphics.dart b/packages/vector_graphics/lib/src/vector_graphics.dart index c5a550a6959d..c35cf0812dea 100644 --- a/packages/vector_graphics/lib/src/vector_graphics.dart +++ b/packages/vector_graphics/lib/src/vector_graphics.dart @@ -309,7 +309,7 @@ class _PictureKey { } class _VectorGraphicWidgetState extends State { - _PictureData? _pictureInfo; + _PictureData? _pictureData; Object? _error; StackTrace? _stackTrace; Locale? locale; @@ -338,8 +338,8 @@ class _VectorGraphicWidgetState extends State { @override void dispose() { - _maybeReleasePicture(_pictureInfo); - _pictureInfo = null; + _maybeReleasePicture(_pictureData); + _pictureData = null; super.dispose(); } @@ -407,8 +407,8 @@ class _VectorGraphicWidgetState extends State { if (data != null) { data.count += 1; setState(() { - _maybeReleasePicture(_pictureInfo); - _pictureInfo = data; + _maybeReleasePicture(_pictureData); + _pictureData = data; }); return; } @@ -431,8 +431,8 @@ class _VectorGraphicWidgetState extends State { } setState(() { - _maybeReleasePicture(_pictureInfo); - _pictureInfo = data; + _maybeReleasePicture(_pictureData); + _pictureData = data; }); } catch (error, stackTrace) { _handleError(error, stackTrace); @@ -443,7 +443,7 @@ class _VectorGraphicWidgetState extends State { @override Widget build(BuildContext context) { - final PictureInfo? pictureInfo = _pictureInfo?.pictureInfo; + final PictureInfo? pictureInfo = _pictureData?.pictureInfo; Widget child; if (pictureInfo != null) { @@ -454,34 +454,31 @@ class _VectorGraphicWidgetState extends State { 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) { + 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); - var scale = 1.0; - scale = math.min( - pictureInfo.size.width / width!, - pictureInfo.size.height / height!, - ); + if (width != null && height != null) { + scale = math.min( + pictureInfo.size.width / width, + pictureInfo.size.height / height, + ); + } if (_webRenderObject) { child = _RawWebVectorGraphicWidget( pictureInfo: pictureInfo, - assetKey: _pictureInfo!.key, + assetKey: _pictureData!.key, colorFilter: widget.colorFilter, opacity: widget.opacity, ); } else if (widget.strategy == RenderingStrategy.raster) { child = _RawVectorGraphicWidget( pictureInfo: pictureInfo, - assetKey: _pictureInfo!.key, + assetKey: _pictureData!.key, colorFilter: widget.colorFilter, opacity: widget.opacity, scale: scale, @@ -489,7 +486,7 @@ class _VectorGraphicWidgetState extends State { } else { child = _RawPictureVectorGraphicWidget( pictureInfo: pictureInfo, - assetKey: _pictureInfo!.key, + assetKey: _pictureData!.key, colorFilter: widget.colorFilter, opacity: widget.opacity, ); @@ -507,16 +504,16 @@ class _VectorGraphicWidgetState extends State { } } - 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), - ), + child = FittedBox( + fit: widget.fit, + alignment: widget.alignment, + clipBehavior: widget.clipBehavior, + child: SizedBox.fromSize(size: pictureInfo.size, child: child), ); + + if (width != null && height != null) { + child = SizedBox(width: width, height: height, child: child); + } } else if (_error != null && widget.errorBuilder != null) { child = widget.errorBuilder!( context, diff --git a/packages/vector_graphics/pubspec.yaml b/packages/vector_graphics/pubspec.yaml index 98bb911fdfa2..3bf9692ed6fa 100644 --- a/packages/vector_graphics/pubspec.yaml +++ b/packages/vector_graphics/pubspec.yaml @@ -2,7 +2,7 @@ name: vector_graphics description: A vector graphics rendering package for Flutter using a binary encoding. repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22 -version: 1.1.19 +version: 1.1.20 environment: sdk: ^3.8.0 diff --git a/packages/vector_graphics/test/goldens/boxfit_contain_with_viewbox.png b/packages/vector_graphics/test/goldens/boxfit_contain_with_viewbox.png new file mode 100644 index 000000000000..3a780ac379f4 Binary files /dev/null and b/packages/vector_graphics/test/goldens/boxfit_contain_with_viewbox.png differ diff --git a/packages/vector_graphics/test/goldens/boxfit_cover_with_viewbox.png b/packages/vector_graphics/test/goldens/boxfit_cover_with_viewbox.png new file mode 100644 index 000000000000..337d59fe5880 Binary files /dev/null and b/packages/vector_graphics/test/goldens/boxfit_cover_with_viewbox.png differ diff --git a/packages/vector_graphics/test/goldens/boxfit_fill_with_viewbox.png b/packages/vector_graphics/test/goldens/boxfit_fill_with_viewbox.png new file mode 100644 index 000000000000..67a2186b1365 Binary files /dev/null and b/packages/vector_graphics/test/goldens/boxfit_fill_with_viewbox.png differ diff --git a/packages/vector_graphics/test/goldens/boxfit_none_with_viewbox.png b/packages/vector_graphics/test/goldens/boxfit_none_with_viewbox.png new file mode 100644 index 000000000000..367b8a1ffa9d Binary files /dev/null and b/packages/vector_graphics/test/goldens/boxfit_none_with_viewbox.png differ diff --git a/packages/vector_graphics/test/vg_with_image_blank.png b/packages/vector_graphics/test/goldens/vg_with_image_blank.png similarity index 100% rename from packages/vector_graphics/test/vg_with_image_blank.png rename to packages/vector_graphics/test/goldens/vg_with_image_blank.png diff --git a/packages/vector_graphics/test/vg_with_image_blue.png b/packages/vector_graphics/test/goldens/vg_with_image_blue.png similarity index 100% rename from packages/vector_graphics/test/vg_with_image_blue.png rename to packages/vector_graphics/test/goldens/vg_with_image_blue.png diff --git a/packages/vector_graphics/test/vector_graphics_test.dart b/packages/vector_graphics/test/vector_graphics_test.dart index b046ad3a4955..e7d0933acfe2 100644 --- a/packages/vector_graphics/test/vector_graphics_test.dart +++ b/packages/vector_graphics/test/vector_graphics_test.dart @@ -12,6 +12,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:vector_graphics/src/listener.dart'; import 'package:vector_graphics/src/vector_graphics.dart'; import 'package:vector_graphics_codec/vector_graphics_codec.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart' + show encodeSvg; const VectorGraphicsCodec codec = VectorGraphicsCodec(); @@ -159,6 +161,229 @@ void main() { expect(fittedBox.clipBehavior, Clip.hardEdge); }); + group('BoxFit', () { + Future<(RenderBox, RenderBox)> setupBoxFitVectorGraphic( + WidgetTester tester, { + required BoxFit fit, + }) async { + final buffer = VectorGraphicsBuffer(); + codec.writeSize(buffer, 100, 50); + + await tester.pumpWidget( + RepaintBoundary( + child: Center( + child: SizedBox( + width: 200, + height: 200, + child: VectorGraphic( + fit: fit, + loader: TestBytesLoader(buffer.done()), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final RenderProxyBox outsideBox = tester.renderObject( + find.byType(VectorGraphic), + ); + expect(outsideBox.size, const Size(200, 200)); + + final RenderBox insideBox = tester.renderObject( + find.byType(SizedBox).last, + ); + expect(insideBox.size, const Size(100, 50)); + + return (outsideBox, insideBox); + } + + testWidgets( + 'Scale on BoxFit.contain without constraints, but with viewbox', + (WidgetTester tester) async { + final (RenderBox outsideBox, RenderBox insideBox) = + await setupBoxFitVectorGraphic(tester, fit: BoxFit.contain); + + // Top left point as offset in child space + final Offset insidePoint = insideBox.localToGlobal(Offset.zero); + // Top left point as offset in parent space + final Offset outsidePoint = outsideBox.localToGlobal( + const Offset(0, 50), + ); + + expect(insidePoint, equals(outsidePoint)); + }, + ); + + testWidgets('Scale on BoxFit.cover without constraints, but with viewbox', ( + WidgetTester tester, + ) async { + final (RenderBox outsideBox, RenderBox insideBox) = + await setupBoxFitVectorGraphic(tester, fit: BoxFit.cover); + + // Top left point as offset in child space + final Offset insidePoint = insideBox.localToGlobal(Offset.zero); + // Top left point as offset in parent space + final Offset outsidePoint = outsideBox.localToGlobal( + const Offset(-100, 0), + ); + + expect(insidePoint, equals(outsidePoint)); + }); + + testWidgets('Scale on BoxFit.fill without constraints, but with viewbox', ( + WidgetTester tester, + ) async { + final (RenderBox outsideBox, RenderBox insideBox) = + await setupBoxFitVectorGraphic(tester, fit: BoxFit.fill); + + // Top left point as offset in child space + final Offset insidePoint = insideBox.localToGlobal(Offset.zero); + // Top left point as offset in parent space + final Offset outsidePoint = outsideBox.localToGlobal(Offset.zero); + + expect(insidePoint, equals(outsidePoint)); + }); + }); + + // TODO(gustl22): can be removed if redundant + group('BoxFit Goldens', () { + late ByteData vectorGraphicBuffer; + + setUpAll(() async { + final Uint8List bytes = encodeSvg( + xml: svgString, + debugName: 'test', + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + vectorGraphicBuffer = bytes.buffer.asByteData(); + }); + + testWidgets( + 'Scale on BoxFit.contain without constraints, but with viewbox', + (WidgetTester tester) async { + final goldenKey = UniqueKey(); + final vectorGraphic = UniqueKey(); + await tester.pumpWidget( + RepaintBoundary( + key: goldenKey, + child: Center( + child: Container( + width: 400, + height: 200, + color: Colors.white, + child: VectorGraphic( + key: vectorGraphic, + loader: TestBytesLoader(vectorGraphicBuffer), + // ignore: avoid_redundant_argument_values + fit: BoxFit.contain, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await expectLater( + find.byKey(goldenKey), + matchesGoldenFile('goldens/boxfit_contain_with_viewbox.png'), + ); + }, + ); + + testWidgets('Scale on BoxFit.cover without constraints, but with viewbox', ( + WidgetTester tester, + ) async { + final goldenKey = UniqueKey(); + final vectorGraphic = UniqueKey(); + await tester.pumpWidget( + RepaintBoundary( + key: goldenKey, + child: Center( + child: Container( + width: 400, + height: 200, + color: Colors.white, + child: VectorGraphic( + key: vectorGraphic, + loader: TestBytesLoader(vectorGraphicBuffer), + fit: BoxFit.cover, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await expectLater( + find.byKey(goldenKey), + matchesGoldenFile('goldens/boxfit_cover_with_viewbox.png'), + ); + }); + + testWidgets('Scale on BoxFit.fill without constraints, but with viewbox', ( + WidgetTester tester, + ) async { + final goldenKey = UniqueKey(); + final vectorGraphic = UniqueKey(); + await tester.pumpWidget( + RepaintBoundary( + key: goldenKey, + child: Center( + child: Container( + width: 400, + height: 200, + color: Colors.white, + child: VectorGraphic( + key: vectorGraphic, + loader: TestBytesLoader(vectorGraphicBuffer), + fit: BoxFit.fill, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await expectLater( + find.byKey(goldenKey), + matchesGoldenFile('goldens/boxfit_fill_with_viewbox.png'), + ); + }); + + testWidgets('Scale on BoxFit.none without constraints, but with viewbox', ( + WidgetTester tester, + ) async { + final goldenKey = UniqueKey(); + final vectorGraphic = UniqueKey(); + await tester.pumpWidget( + RepaintBoundary( + key: goldenKey, + child: Center( + child: Container( + width: 400, + height: 400, + color: Colors.white, + child: VectorGraphic( + key: vectorGraphic, + loader: TestBytesLoader(vectorGraphicBuffer), + fit: BoxFit.none, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await expectLater( + find.byKey(goldenKey), + matchesGoldenFile('goldens/boxfit_none_with_viewbox.png'), + ); + }); + }); + group('ClipBehavior', () { testWidgets('Sets clipBehavior to hardEdge if not provided', ( WidgetTester tester, @@ -209,9 +434,9 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.byType(SizedBox), findsNWidgets(2)); + expect(find.byType(SizedBox), findsNWidgets(1)); - final sizedBox = find.byType(SizedBox).evaluate().last.widget as SizedBox; + final sizedBox = find.byType(SizedBox).evaluate().single.widget as SizedBox; expect(sizedBox.width, 100); expect(sizedBox.height, 200); @@ -601,7 +826,7 @@ void main() { // A blank image, because the image hasn't loaded yet. await expectLater( find.byKey(key), - matchesGoldenFile('vg_with_image_blank.png'), + matchesGoldenFile('goldens/vg_with_image_blank.png'), ); expect(imageCache.currentSize, 1); @@ -615,10 +840,10 @@ void main() { expect(imageCache.statusForKey(imageKey).live, false); expect(imageCache.statusForKey(imageKey).keepAlive, true); - // A blue square, becuase the image is available now. + // A blue square, because the image is available now. await expectLater( find.byKey(key), - matchesGoldenFile('vg_with_image_blue.png'), + matchesGoldenFile('goldens/vg_with_image_blue.png'), ); }, skip: kIsWeb); @@ -787,3 +1012,26 @@ class ThrowingBytesLoader extends BytesLoader { throw UnimplementedError('Test exception'); } } + +const String svgString = ''' + + + + + + + + + + + + + + + + + + + + +''';