diff --git a/packages/vector_graphics/CHANGELOG.md b/packages/vector_graphics/CHANGELOG.md index 958ea8a7b4d3..eb0a03cb1a80 100644 --- a/packages/vector_graphics/CHANGELOG.md +++ b/packages/vector_graphics/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 0.0.1 + * Added `VectorGraphic` which consumes encoded vector graphics assets using + a `BytesLoader`. + * Added `AssetBytesLoader` and `NetworkBytesLoader` as example loader + implementations. + ## 0.0.0 * Create repository diff --git a/packages/vector_graphics/lib/src/listener.dart b/packages/vector_graphics/lib/src/listener.dart new file mode 100644 index 000000000000..427f77d43b54 --- /dev/null +++ b/packages/vector_graphics/lib/src/listener.dart @@ -0,0 +1,130 @@ +import 'dart:ui' as ui; +import 'dart:typed_data'; + +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +/// A listener implementation for the vector graphics codec that converts the +/// format into a [ui.Picture]. +class FlutterVectorGraphicsListener extends VectorGraphicsCodecListener { + /// Create a new [FlutterVectorGraphicsListener]. + factory FlutterVectorGraphicsListener() { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + return FlutterVectorGraphicsListener._(canvas, recorder); + } + + FlutterVectorGraphicsListener._(this._canvas, this._recorder); + + final ui.PictureRecorder _recorder; + final ui.Canvas _canvas; + final List _paints = []; + final List _paths = []; + ui.Path? _currentPath; + 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() { + assert(!_done); + _done = true; + return _recorder.endRecording(); + } + + @override + void onDrawPath(int pathId, int? paintId) { + final ui.Path path = _paths[pathId]; + ui.Paint? paint; + if (paintId != null) { + paint = _paints[paintId]; + } + _canvas.drawPath(path, paint ?? _emptyPaint); + } + + @override + void onDrawVertices(Float32List vertices, Uint16List? indices, int? paintId) { + final ui.Vertices vextexData = + ui.Vertices.raw(ui.VertexMode.triangles, vertices, indices: indices); + ui.Paint? paint; + if (paintId != null) { + paint = _paints[paintId]; + } + _canvas.drawVertices( + vextexData, ui.BlendMode.srcOver, paint ?? _emptyPaint); + } + + @override + void onPaintObject({ + required int color, + required int? strokeCap, + required int? strokeJoin, + required int blendMode, + required double? strokeMiterLimit, + required double? strokeWidth, + required int paintStyle, + required int id, + }) { + assert(_paints.length == id, 'Expect ID to be ${_paints.length}'); + final ui.Paint paint = ui.Paint()..color = ui.Color(color); + + if (blendMode != 0) { + paint.blendMode = ui.BlendMode.values[blendMode]; + } + + if (paintStyle == 1) { + paint.style = ui.PaintingStyle.stroke; + if (strokeCap != null && strokeCap != 0) { + paint.strokeCap = ui.StrokeCap.values[strokeCap]; + } + if (strokeJoin != null && strokeJoin != 0) { + paint.strokeJoin = ui.StrokeJoin.values[strokeJoin]; + } + if (strokeMiterLimit != null && strokeMiterLimit != 4.0) { + paint.strokeMiterLimit = strokeMiterLimit; + } + if (strokeWidth != null && strokeWidth != 1.0) { + paint.strokeWidth = strokeWidth; + } + } + _paints.add(paint); + } + + @override + void onPathClose() { + _currentPath!.close(); + } + + @override + void onPathCubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + _currentPath!.cubicTo(x1, y1, x2, y2, x3, y3); + } + + @override + void onPathFinished() { + _currentPath = null; + } + + @override + void onPathLineTo(double x, double y) { + _currentPath!.lineTo(x, y); + } + + @override + void onPathMoveTo(double x, double y) { + _currentPath!.moveTo(x, y); + } + + @override + void onPathStart(int id, int fillType) { + assert(_currentPath == null); + assert(_paths.length == id, 'Expected Id to be $id'); + + final ui.Path path = ui.Path(); + path.fillType = ui.PathFillType.values[fillType]; + _paths.add(path); + _currentPath = path; + } +} diff --git a/packages/vector_graphics/lib/vector_graphics.dart b/packages/vector_graphics/lib/vector_graphics.dart index affce5668426..38cd06e6d442 100644 --- a/packages/vector_graphics/lib/vector_graphics.dart +++ b/packages/vector_graphics/lib/vector_graphics.dart @@ -1,7 +1,209 @@ -library vector_graphics; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'dart:io'; -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +import 'src/listener.dart'; + +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) { + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener(); + _codec.decode(data, listener); + return listener.toPicture(); +} + +/// A widget that displays a vector_graphics formatted asset. +/// +/// A bytes loader class should not be constructed directly in a build method, +/// if this is done the corresponding [VectorGraphic] widget may repeatedly +/// reload the bytes. +/// +/// ```dart +/// class MyVectorGraphic extends StatefulWidget { +/// +/// State createState() => +/// } +/// +/// class _MyVectorGraphicState extends State { +/// BytesLoader? loader; +/// +/// @override +/// void initState() { +/// super.initState(); +/// loader = AssetBytesLoader(assetName: 'foobar', assetBundle: DefaultAssetBundle.of(context)); +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return VectorGraphic(bytesLoader: loader!); +/// } +/// } +/// ``` +class VectorGraphic extends StatefulWidget { + const VectorGraphic({Key? key, required this.bytesLoader}) : super(key: key); + + final BytesLoader bytesLoader; + + @override + State createState() => _VectorGraphicsWidgetState(); +} + +class _VectorGraphicsWidgetState extends State { + ui.Picture? _picture; + + @override + void initState() { + _loadAssetBytes(); + super.initState(); + } + + @override + void didUpdateWidget(covariant VectorGraphic oldWidget) { + if (oldWidget.bytesLoader != widget.bytesLoader) { + _loadAssetBytes(); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + _picture?.dispose(); + _picture = null; + super.dispose(); + } + + void _loadAssetBytes() { + widget.bytesLoader.loadBytes().then((ByteData data) { + final ui.Picture picture = decodeVectorGraphics(data); + setState(() { + _picture?.dispose(); + _picture = picture; + }); + }); + } + + @override + Widget build(BuildContext context) { + final ui.Picture? picture = _picture; + if (picture == null) { + return const SizedBox(); + } + return _RawVectorGraphicsWidget(picture: picture); + } +} + +/// An interface that can be implemented to support decoding vector graphic +/// binary assets from different byte sources. +/// +/// A bytes loader class should not be constructed directly in a build method, +/// if this is done the corresponding [VectorGraphic] widget may repeatedly +/// reload the bytes. +/// +/// See also: +/// * [AssetBytesLoader], for loading from the asset bundle. +/// * [NetworkBytesLoader], for loading network bytes. +abstract class BytesLoader { + /// const constructor to allow subtypes to be const. + const BytesLoader(); + + /// Load the byte data for a vector graphic binary asset. + Future loadBytes(); +} + +/// A controller for loading vector graphics data from an asset bundle. +class AssetBytesLoader extends BytesLoader { + /// Create a new [VectorGraphicsAssetController]. + /// + /// The default asset bundle can be acquired using [DefaultAssetBundle.of]. + const AssetBytesLoader({ + required this.assetName, + this.packageName, + required this.assetBundle, + }); + + final String assetName; + final String? packageName; + final AssetBundle assetBundle; + + @override + Future loadBytes() { + return assetBundle.load(assetName); + } +} + +/// A controller for loading vector graphics data from over the network. +class NetworkBytesLoader extends BytesLoader { + const NetworkBytesLoader({ + required this.url, + this.headers, + this.client, + }); + + final Map? headers; + final Uri url; + final HttpClient? client; + + @override + Future loadBytes() async { + final HttpClient currentClient = client ?? HttpClient(); + final HttpClientRequest request = await currentClient.getUrl(url); + headers?.forEach(request.headers.add); + + final HttpClientResponse response = await request.close(); + if (response.statusCode != HttpStatus.ok) { + await response.drain>([]); + throw Exception('Failed to load VectorGraphic: ${response.statusCode}'); + } + final Uint8List bytes = await consolidateHttpClientResponseBytes( + response, + ); + return bytes.buffer.asByteData(); + } +} + +class _RawVectorGraphicsWidget extends SingleChildRenderObjectWidget { + const _RawVectorGraphicsWidget({Key? key, required this.picture}) + : super(key: key); + + final ui.Picture picture; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderVectorGraphics(picture); + } + + @override + void updateRenderObject( + BuildContext context, covariant _RenderVectorGraphics renderObject) { + renderObject.picture = picture; + } +} + +class _RenderVectorGraphics extends RenderProxyBox { + _RenderVectorGraphics(this._picture); + + ui.Picture get picture => _picture; + ui.Picture _picture; + set picture(ui.Picture value) { + if (identical(value, _picture)) { + return; + } + _picture = value; + markNeedsPaint(); + } + + @override + void paint(PaintingContext context, ui.Offset offset) { + context.canvas.drawPicture(picture); + } } diff --git a/packages/vector_graphics/pubspec.yaml b/packages/vector_graphics/pubspec.yaml index b10cce442cdf..ce1d47351c11 100644 --- a/packages/vector_graphics/pubspec.yaml +++ b/packages/vector_graphics/pubspec.yaml @@ -1,6 +1,6 @@ name: vector_graphics description: A vector graphics rendering package for Flutter. -version: 0.0.0 +version: 0.0.1 homepage: https://github.com/dnfield/vector_graphics environment: @@ -10,8 +10,14 @@ environment: dependencies: flutter: sdk: flutter + vector_graphics_codec: ^0.0.1 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.0 + +# Comment out before publishing +dependency_overrides: + vector_graphics_codec: + path: ../vector_graphics_codec diff --git a/packages/vector_graphics/test/vector_graphics_test.dart b/packages/vector_graphics/test/vector_graphics_test.dart index 879fa64130c0..7a14d00c20db 100644 --- a/packages/vector_graphics/test/vector_graphics_test.dart +++ b/packages/vector_graphics/test/vector_graphics_test.dart @@ -1,12 +1,58 @@ +import 'dart:typed_data'; + import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics/src/listener.dart'; + +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; -import 'package:vector_graphics/vector_graphics.dart'; +const VectorGraphicsCodec codec = VectorGraphicsCodec(); void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + test('Can decode a message without a stroke and vertices', () { + final buffer = VectorGraphicsBuffer(); + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener(); + final int paintId = codec.writeStroke(buffer, 44, 1, 2, 3, 4.0, 6.0); + codec.writeDrawVertices( + buffer, + Float32List.fromList([ + 0.0, + 2.0, + 3.0, + 4.0, + 2.0, + 4.0, + ]), + null, + paintId); + + codec.decode(buffer.done(), listener); + + expect(listener.toPicture, returnsNormally); + }); + + test('Can decode a message with a fill and path', () { + final buffer = VectorGraphicsBuffer(); + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener(); + final int paintId = codec.writeFill(buffer, 23, 0); + final int pathId = codec.writeStartPath(buffer, 0); + codec.writeMoveTo(buffer, 1, 2); + codec.writeLineTo(buffer, 2, 3); + codec.writeClose(buffer); + codec.writeFinishPath(buffer); + codec.writeDrawPath(buffer, pathId, paintId); + + codec.decode(buffer.done(), listener); + + expect(listener.toPicture, returnsNormally); + }); + + test('Asserts if toPicture is called more than once', () { + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener(); + listener.toPicture(); + + expect(listener.toPicture, throwsAssertionError); }); }