Skip to content

Commit

Permalink
encode viewport into codec and correctly size widget (#16)
Browse files Browse the repository at this point in the history
* encode viewport into codec and correctly size widget

* remove extra params

* add more unit tests

* format

* just encode size
  • Loading branch information
jonahwilliams authored Mar 23, 2022
1 parent 2b3368e commit b5a86b8
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 30 deletions.
25 changes: 23 additions & 2 deletions packages/vector_graphics/lib/src/listener.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -21,17 +36,18 @@ class FlutterVectorGraphicsListener extends VectorGraphicsCodecListener {
final List<ui.Path> _paths = <ui.Path>[];
final List<ui.Shader> _shaders = <ui.Shader>[];
ui.Path? _currentPath;
ui.Size _size = ui.Size.zero;
bool _done = false;

static final _emptyPaint = ui.Paint();

/// 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
Expand Down Expand Up @@ -202,4 +218,9 @@ class FlutterVectorGraphicsListener extends VectorGraphicsCodecListener {
);
_shaders.add(gradient);
}

@override
void onSize(double width, double height) {
_size = ui.Size(width, height);
}
}
109 changes: 82 additions & 27 deletions packages/vector_graphics/lib/vector_graphics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<VectorGraphic> createState() => _VectorGraphicsWidgetState();
}

class _VectorGraphicsWidgetState extends State<VectorGraphic> {
ui.Picture? _picture;
PictureInfo? _pictureInfo;

@override
void initState() {
Expand All @@ -77,28 +120,41 @@ class _VectorGraphicsWidgetState extends State<VectorGraphic> {

@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,
),
),
),
);
}
}

Expand Down Expand Up @@ -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);
}
}
70 changes: 70 additions & 0 deletions packages/vector_graphics/test/vector_graphics_test.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<ByteData> loadBytes() async {
return data;
}
}
36 changes: 35 additions & 1 deletion packages/vector_graphics_codec/lib/vector_graphics_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -636,6 +669,7 @@ abstract class VectorGraphicsCodecListener {
}

enum _CurrentSection {
size,
shaders,
paints,
paths,
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit b5a86b8

Please sign in to comment.