From b5a86b8b18409a4019aa66a6011df15ac05d1b03 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 22 Mar 2022 17:44:03 -0700 Subject: [PATCH] encode viewport into codec and correctly size widget (#16) * encode viewport into codec and correctly size widget * remove extra params * add more unit tests * format * just encode size --- .../vector_graphics/lib/src/listener.dart | 25 +++- .../vector_graphics/lib/vector_graphics.dart | 109 +++++++++++++----- .../test/vector_graphics_test.dart | 70 +++++++++++ .../lib/vector_graphics_codec.dart | 36 +++++- .../test/vector_graphics_codec_test.dart | 39 +++++++ .../lib/vector_graphics_compiler.dart | 2 + 6 files changed, 251 insertions(+), 30 deletions(-) diff --git a/packages/vector_graphics/lib/src/listener.dart b/packages/vector_graphics/lib/src/listener.dart index 4966c8a758d9..c77317fb8eae 100644 --- a/packages/vector_graphics/lib/src/listener.dart +++ b/packages/vector_graphics/lib/src/listener.dart @@ -3,6 +3,21 @@ import 'dart:typed_data'; import 'package:vector_graphics_codec/vector_graphics_codec.dart'; +/// The deocded result of a vector graphics asset. +class PictureInfo { + /// Construct a new [PictureInfo]. + const PictureInfo(this.picture, this.size); + + /// The picture to be drawn with [ui.canvas.drawPicture] + final ui.Picture picture; + + /// The target size of the picture. + /// + /// This information should be used to scale and position + /// the picture based on the available space and alignment. + final ui.Size size; +} + /// A listener implementation for the vector graphics codec that converts the /// format into a [ui.Picture]. class FlutterVectorGraphicsListener extends VectorGraphicsCodecListener { @@ -21,6 +36,7 @@ class FlutterVectorGraphicsListener extends VectorGraphicsCodecListener { final List _paths = []; final List _shaders = []; ui.Path? _currentPath; + ui.Size _size = ui.Size.zero; bool _done = false; static final _emptyPaint = ui.Paint(); @@ -28,10 +44,10 @@ class FlutterVectorGraphicsListener extends VectorGraphicsCodecListener { /// Convert the vector graphics asset this listener decoded into a [ui.Picture]. /// /// This method can only be called once for a given listener instance. - ui.Picture toPicture() { + PictureInfo toPicture() { assert(!_done); _done = true; - return _recorder.endRecording(); + return PictureInfo(_recorder.endRecording(), _size); } @override @@ -202,4 +218,9 @@ class FlutterVectorGraphicsListener extends VectorGraphicsCodecListener { ); _shaders.add(gradient); } + + @override + void onSize(double width, double height) { + _size = ui.Size(width, height); + } } diff --git a/packages/vector_graphics/lib/vector_graphics.dart b/packages/vector_graphics/lib/vector_graphics.dart index ae1cf90316f9..57c11a974900 100644 --- a/packages/vector_graphics/lib/vector_graphics.dart +++ b/packages/vector_graphics/lib/vector_graphics.dart @@ -15,7 +15,7 @@ const VectorGraphicsCodec _codec = VectorGraphicsCodec(); /// Decode a vector graphics binary asset into a [ui.Picture]. /// /// Throws a [StateError] if the data is invalid. -ui.Picture decodeVectorGraphics(ByteData data) { +PictureInfo decodeVectorGraphics(ByteData data) { final FlutterVectorGraphicsListener listener = FlutterVectorGraphicsListener(); _codec.decode(data, listener); @@ -50,16 +50,59 @@ ui.Picture decodeVectorGraphics(ByteData data) { /// } /// ``` class VectorGraphic extends StatefulWidget { - const VectorGraphic({Key? key, required this.bytesLoader}) : super(key: key); + const VectorGraphic({ + Key? key, + required this.bytesLoader, + this.width, + this.height, + this.fit = BoxFit.contain, + this.alignment = Alignment.center, + }) : super(key: key); final BytesLoader bytesLoader; + /// If specified, the width to use for the vector graphic. If unspecified, + /// the vector graphic will take the width of its parent. + final double? width; + + /// If specified, the height to use for the vector graphic. If unspecified, + /// the vector graphic will take the height of its parent. + final double? height; + + /// How to inscribe the picture into the space allocated during layout. + /// The default is [BoxFit.contain]. + final BoxFit fit; + + /// How to align the picture within its parent widget. + /// + /// The alignment aligns the given position in the picture to the given position + /// in the layout bounds. For example, an [Alignment] alignment of (-1.0, + /// -1.0) aligns the image to the top-left corner of its layout bounds, while a + /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the + /// picture with the bottom right corner of its layout bounds. Similarly, an + /// alignment of (0.0, 1.0) aligns the bottom middle of the image with the + /// middle of the bottom edge of its layout bounds. + /// + /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a + /// [AlignmentDirectional]), then a [TextDirection] must be available + /// when the picture is painted. + /// + /// 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; + @override State createState() => _VectorGraphicsWidgetState(); } class _VectorGraphicsWidgetState extends State { - ui.Picture? _picture; + PictureInfo? _pictureInfo; @override void initState() { @@ -77,28 +120,41 @@ class _VectorGraphicsWidgetState extends State { @override void dispose() { - _picture?.dispose(); - _picture = null; + _pictureInfo?.picture.dispose(); + _pictureInfo = null; super.dispose(); } void _loadAssetBytes() { widget.bytesLoader.loadBytes().then((ByteData data) { - final ui.Picture picture = decodeVectorGraphics(data); + final PictureInfo pictureInfo = decodeVectorGraphics(data); setState(() { - _picture?.dispose(); - _picture = picture; + _pictureInfo?.picture.dispose(); + _pictureInfo = pictureInfo; }); }); } @override Widget build(BuildContext context) { - final ui.Picture? picture = _picture; - if (picture == null) { - return const SizedBox(); + final PictureInfo? pictureInfo = _pictureInfo; + if (pictureInfo == null) { + return SizedBox(width: widget.width, height: widget.height); } - return _RawVectorGraphicsWidget(picture: picture); + return SizedBox( + width: widget.width, + height: widget.height, + child: FittedBox( + fit: widget.fit, + alignment: widget.alignment, + child: SizedBox.fromSize( + size: pictureInfo.size, + child: _RawVectorGraphicsWidget( + pictureInfo: pictureInfo, + ), + ), + ), + ); } } @@ -172,41 +228,40 @@ class NetworkBytesLoader extends BytesLoader { } class _RawVectorGraphicsWidget extends SingleChildRenderObjectWidget { - const _RawVectorGraphicsWidget({Key? key, required this.picture}) - : super(key: key); + const _RawVectorGraphicsWidget({ + Key? key, + required this.pictureInfo, + }) : super(key: key); - final ui.Picture picture; + final PictureInfo pictureInfo; @override RenderObject createRenderObject(BuildContext context) { - return _RenderVectorGraphics(picture); + return _RenderVectorGraphics(pictureInfo); } @override void updateRenderObject( BuildContext context, covariant _RenderVectorGraphics renderObject) { - renderObject.picture = picture; + renderObject.pictureInfo = pictureInfo; } } class _RenderVectorGraphics extends RenderProxyBox { - _RenderVectorGraphics(this._picture); + _RenderVectorGraphics(this._pictureInfo); - ui.Picture get picture => _picture; - ui.Picture _picture; - set picture(ui.Picture value) { - if (identical(value, _picture)) { + PictureInfo get pictureInfo => _pictureInfo; + PictureInfo _pictureInfo; + set pictureInfo(PictureInfo value) { + if (identical(value, _pictureInfo)) { return; } - _picture = value; + _pictureInfo = value; markNeedsPaint(); } @override void paint(PaintingContext context, ui.Offset offset) { - if (offset != Offset.zero) { - context.canvas.translate(offset.dx, offset.dy); - } - context.canvas.drawPicture(picture); + context.canvas.drawPicture(_pictureInfo.picture); } } diff --git a/packages/vector_graphics/test/vector_graphics_test.dart b/packages/vector_graphics/test/vector_graphics_test.dart index 7a14d00c20db..6e5bdb855823 100644 --- a/packages/vector_graphics/test/vector_graphics_test.dart +++ b/packages/vector_graphics/test/vector_graphics_test.dart @@ -1,7 +1,9 @@ import 'dart:typed_data'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:vector_graphics/src/listener.dart'; +import 'package:vector_graphics/vector_graphics.dart'; import 'package:vector_graphics_codec/vector_graphics_codec.dart'; @@ -55,4 +57,72 @@ void main() { expect(listener.toPicture, throwsAssertionError); }); + + testWidgets('Creates layout widgets when VectorGraphic is sized', + (WidgetTester tester) async { + final buffer = VectorGraphicsBuffer(); + await tester.pumpWidget(VectorGraphic( + bytesLoader: TestBytesLoader(buffer.done()), + width: 100, + height: 100, + )); + await tester.pumpAndSettle(); + + expect(find.byType(SizedBox), findsNWidgets(2)); + + final SizedBox sizedBox = + (find.byType(SizedBox).evaluate().first.widget as SizedBox); + + expect(sizedBox.width, 100); + expect(sizedBox.height, 100); + }); + + testWidgets('Creates alignment widgets when VectorGraphic is aligned', + (WidgetTester tester) async { + final buffer = VectorGraphicsBuffer(); + await tester.pumpWidget(VectorGraphic( + bytesLoader: TestBytesLoader(buffer.done()), + alignment: Alignment.centerLeft, + fit: BoxFit.fitHeight, + )); + await tester.pumpAndSettle(); + + expect(find.byType(FittedBox), findsOneWidget); + + final FittedBox fittedBox = + (find.byType(FittedBox).evaluate().first.widget as FittedBox); + + expect(fittedBox.fit, BoxFit.fitHeight); + expect(fittedBox.alignment, Alignment.centerLeft); + }); + + testWidgets('Sizes VectorGraphic based on encoded viewbox information', + (WidgetTester tester) async { + final buffer = VectorGraphicsBuffer(); + codec.writeSize(buffer, 100, 200); + + await tester.pumpWidget(VectorGraphic( + bytesLoader: TestBytesLoader(buffer.done()), + )); + await tester.pumpAndSettle(); + + expect(find.byType(SizedBox), findsNWidgets(2)); + + final SizedBox sizedBox = + (find.byType(SizedBox).evaluate().last.widget as SizedBox); + + expect(sizedBox.width, 100); + expect(sizedBox.height, 200); + }); +} + +class TestBytesLoader extends BytesLoader { + TestBytesLoader(this.data); + + final ByteData data; + + @override + Future loadBytes() async { + return data; + } } diff --git a/packages/vector_graphics_codec/lib/vector_graphics_codec.dart b/packages/vector_graphics_codec/lib/vector_graphics_codec.dart index 27a878aecc6e..39e26fefda7c 100644 --- a/packages/vector_graphics_codec/lib/vector_graphics_codec.dart +++ b/packages/vector_graphics_codec/lib/vector_graphics_codec.dart @@ -22,6 +22,7 @@ class VectorGraphicsCodec { static const int _restore = 38; static const int _linearGradientTag = 39; static const int _radialGradientTag = 40; + static const int _sizeTag = 41; static const int _version = 1; static const int _magicNumber = 0x00882d62; @@ -94,12 +95,32 @@ class VectorGraphicsCodec { case _saveLayer: _readSaveLayer(buffer, listener); continue; + case _sizeTag: + _readSize(buffer, listener); + continue; default: throw StateError('Unknown type tag $type'); } } } + /// Encode the dimensions of the vector graphic. + /// + /// This should be the first attribute encoded. + void writeSize( + VectorGraphicsBuffer buffer, + double width, + double height, + ) { + if (buffer._decodePhase.index != _CurrentSection.size.index) { + throw StateError('Size already written'); + } + buffer._decodePhase = _CurrentSection.shaders; + buffer._putUint8(_sizeTag); + buffer._putFloat64(width); + buffer._putFloat64(height); + } + /// Encode a draw path command in the current buffer. /// /// Requires that [pathId] and [paintId] to already be encoded. @@ -539,11 +560,23 @@ class VectorGraphicsCodec { final int paintId = buffer.getInt32(); listener?.onSaveLayer(paintId); } + + void _readSize(_ReadBuffer buffer, VectorGraphicsCodecListener? listener) { + final double width = buffer.getFloat64(); + final double height = buffer.getFloat64(); + listener?.onSize(width, height); + } } /// Implement this listener class to support decoding of vector_graphics binary /// assets. abstract class VectorGraphicsCodecListener { + /// The size of the vector graphic has been decoded. + void onSize( + double width, + double height, + ); + /// A paint object has been decoded. /// /// If the paint object is for a fill, then [strokeCap], [strokeJoin], @@ -636,6 +669,7 @@ abstract class VectorGraphicsCodecListener { } enum _CurrentSection { + size, shaders, paints, paths, @@ -684,7 +718,7 @@ class VectorGraphicsBuffer { /// /// Objects must be written in the correct order, the same as the /// enum order. - _CurrentSection _decodePhase = _CurrentSection.shaders; + _CurrentSection _decodePhase = _CurrentSection.size; /// Write a Uint8 into the buffer. void _putUint8(int byte) { diff --git a/packages/vector_graphics_codec/test/vector_graphics_codec_test.dart b/packages/vector_graphics_codec/test/vector_graphics_codec_test.dart index af6a4661f880..5382071f6dbf 100644 --- a/packages/vector_graphics_codec/test/vector_graphics_codec_test.dart +++ b/packages/vector_graphics_codec/test/vector_graphics_codec_test.dart @@ -277,6 +277,23 @@ void main() { ), ]); }); + + test('Encodes a size', () { + final buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + codec.writeSize(buffer, 20, 30); + codec.decode(buffer.done(), listener); + + expect(listener.commands, [const OnSize(20, 30)]); + }); + + test('Only supports a single size', () { + final buffer = VectorGraphicsBuffer(); + + codec.writeSize(buffer, 20, 30); + expect(() => codec.writeSize(buffer, 1, 1), throwsStateError); + }); } class TestListener extends VectorGraphicsCodecListener { @@ -409,6 +426,11 @@ class TestListener extends VectorGraphicsCodecListener { id: id, )); } + + @override + void onSize(double width, double height) { + commands.add(OnSize(width, height)); + } } class OnLinearGradient { @@ -713,6 +735,23 @@ class OnPathStart { String toString() => 'OnPathStart($id, $fillType)'; } +class OnSize { + const OnSize(this.width, this.height); + + final double width; + final double height; + + @override + int get hashCode => Object.hash(width, height); + + @override + bool operator ==(Object other) => + other is OnSize && other.width == width && other.height == height; + + @override + String toString() => 'OnSize($width, $height)'; +} + bool _listEquals(List? left, List? right) { if (left == null && right == null) { return true; diff --git a/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart b/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart index fa365abe210c..36711f830d91 100644 --- a/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart +++ b/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart @@ -36,6 +36,8 @@ Future encodeSVG(String input, String filename) async { final VectorInstructions instructions = await parse(input, key: filename); final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + codec.writeSize(buffer, instructions.width, instructions.height); + final Map fillIds = {}; final Map strokeIds = {}; final Map shaderIds = {};