diff --git a/examples/pubspec.yaml b/examples/pubspec.yaml index b055f2f824d..ae65a0c2813 100644 --- a/examples/pubspec.yaml +++ b/examples/pubspec.yaml @@ -11,8 +11,7 @@ environment: dependencies: flame: ^1.0.0 - flame_svg: - path: ../packages/flame_svg + flame_svg: ^1.0.0 dashbook: 0.1.5 flutter: sdk: flutter diff --git a/packages/flame/lib/src/cache/memory_cache.dart b/packages/flame/lib/src/cache/memory_cache.dart index 5308676f3a6..ce168a728db 100644 --- a/packages/flame/lib/src/cache/memory_cache.dart +++ b/packages/flame/lib/src/cache/memory_cache.dart @@ -24,4 +24,6 @@ class MemoryCache { bool containsKey(K key) => _cache.containsKey(key); int get size => _cache.length; + + Iterable get keys => _cache.keys; } diff --git a/packages/flame_svg/lib/svg.dart b/packages/flame_svg/lib/svg.dart index c2a23f8d44d..8eb02bf1e21 100644 --- a/packages/flame_svg/lib/svg.dart +++ b/packages/flame_svg/lib/svg.dart @@ -1,4 +1,7 @@ +import 'dart:ui'; + import 'package:flame/assets.dart'; +import 'package:flame/cache.dart'; import 'package:flame/extensions.dart'; import 'package:flame/flame.dart'; import 'package:flame/game.dart'; @@ -7,11 +10,17 @@ import 'package:flutter_svg/flutter_svg.dart'; /// A [Svg] to be rendered on a Flame [Game]. class Svg { /// The [DrawableRoot] that this [Svg] represents. - DrawableRoot svgRoot; + final DrawableRoot svgRoot; /// Creates an [Svg] with the received [svgRoot]. Svg(this.svgRoot); + final MemoryCache _imageCache = MemoryCache(); + + final _paint = Paint()..filterQuality = FilterQuality.high; + + final List _lock = []; + /// Loads an [Svg] with the received [cache]. When no [cache] is provided, /// the global [Flame.assets] is used. static Future load(String fileName, {AssetsCache? cache}) async { @@ -22,10 +31,14 @@ class Svg { /// Renders the svg on the [canvas] using the dimensions provided by [size]. void render(Canvas canvas, Vector2 size) { - canvas.save(); - svgRoot.scaleCanvasToViewBox(canvas, size.toSize()); - svgRoot.draw(canvas, svgRoot.viewport.viewBoxRect); - canvas.restore(); + final _size = size.toSize(); + final image = _getImage(_size); + + if (image != null) { + canvas.drawImage(image, Offset.zero, _paint); + } else { + _render(canvas, _size); + } } /// Renders the svg on the [canvas] on the given [position] using the @@ -37,6 +50,41 @@ class Svg { ) { canvas.renderAt(position, (c) => render(c, size)); } + + Image? _getImage(Size size) { + final image = _imageCache.getValue(size); + + if (image == null && !_lock.contains(size)) { + _lock.add(size); + final recorder = PictureRecorder(); + + final canvas = Canvas(recorder); + _render(canvas, size); + final _picture = recorder.endRecording(); + + _picture.toImage(size.width.toInt(), size.height.toInt()).then((image) { + _imageCache.setValue(size, image); + _lock.remove(size); + _picture.dispose(); + }); + } + + return image; + } + + void _render(Canvas canvas, Size size) { + svgRoot.scaleCanvasToViewBox(canvas, size); + svgRoot.draw(canvas, svgRoot.viewport.viewBoxRect); + } + + /// Clear all the stored cache from this SVG, be sure to call + /// this method once the instance is no longer needed to avoid + /// memory leaks + void dispose() { + _imageCache.keys.forEach((key) { + _imageCache.getValue(key)?.dispose(); + }); + } } /// Provides a loading method for [Svg] on the [Game] class. diff --git a/packages/flame_svg/lib/svg_component.dart b/packages/flame_svg/lib/svg_component.dart index 8413cee91e8..1684d8d18c0 100644 --- a/packages/flame_svg/lib/svg_component.dart +++ b/packages/flame_svg/lib/svg_component.dart @@ -7,18 +7,19 @@ import './svg.dart'; /// Wraps [Svg] in a Flame component. class SvgComponent extends PositionComponent { /// The wrapped instance of [Svg]. - Svg? svg; + Svg? _svg; /// Creates an [SvgComponent] SvgComponent({ - this.svg, + Svg? svg, Vector2? position, Vector2? size, Vector2? scale, double? angle, Anchor? anchor, int? priority, - }) : super( + }) : _svg = svg, + super( position: position, size: size, scale: scale, @@ -50,8 +51,24 @@ class SvgComponent extends PositionComponent { priority: priority, ); + /// Sets a new [svg] instance + set svg(Svg? svg) { + _svg?.dispose(); + _svg = svg; + } + + /// Returns the current [svg] instance + Svg? get svg => _svg; + @override void render(Canvas canvas) { - svg?.render(canvas, size); + _svg?.render(canvas, size); + } + + @override + void onRemove() { + super.onRemove(); + + _svg?.dispose(); } } diff --git a/packages/flame_svg/pubspec.yaml b/packages/flame_svg/pubspec.yaml index ff50e20020a..d0e48ee5efa 100644 --- a/packages/flame_svg/pubspec.yaml +++ b/packages/flame_svg/pubspec.yaml @@ -17,3 +17,5 @@ dependencies: dev_dependencies: dartdoc: ^4.1.0 flame_lint: ^0.0.1 + test: ^1.17.12 + mocktail: ^0.2.0 diff --git a/packages/flame_svg/test/svg_component_test.dart b/packages/flame_svg/test/svg_component_test.dart new file mode 100644 index 00000000000..4b731d5e2a1 --- /dev/null +++ b/packages/flame_svg/test/svg_component_test.dart @@ -0,0 +1,32 @@ +import 'package:flame_svg/flame_svg.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockSvg extends Mock implements Svg {} + +void main() { + group('SvgComponent', () { + late Svg svg; + + setUp(() { + svg = MockSvg(); + when(svg.dispose).thenAnswer((_) {}); + }); + + test('disposes the svg instance when it is removed', () { + final component = SvgComponent(svg: svg); + component.onRemove(); + + verify(svg.dispose).called(1); + }); + + test('disposes the old svg instance when a new one is received', () { + final component = SvgComponent(svg: svg); + + final newSvg = MockSvg(); + component.svg = newSvg; + + verify(svg.dispose).called(1); + }); + }); +}