Skip to content

Commit

Permalink
fix: flame svg perfomance (#1373)
Browse files Browse the repository at this point in the history
* fix: flame svg perfomance

* PR suggestions and some tests

* linting

* addressing comments
  • Loading branch information
erickzanardo committed Feb 21, 2022
1 parent 491889c commit bce2417
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 11 deletions.
3 changes: 1 addition & 2 deletions examples/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/flame/lib/src/cache/memory_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ class MemoryCache<K, V> {
bool containsKey(K key) => _cache.containsKey(key);

int get size => _cache.length;

Iterable<K> get keys => _cache.keys;
}
58 changes: 53 additions & 5 deletions packages/flame_svg/lib/svg.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<Size, Image> _imageCache = MemoryCache();

final _paint = Paint()..filterQuality = FilterQuality.high;

final List<Size> _lock = [];

/// Loads an [Svg] with the received [cache]. When no [cache] is provided,
/// the global [Flame.assets] is used.
static Future<Svg> load(String fileName, {AssetsCache? cache}) async {
Expand All @@ -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
Expand All @@ -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.
Expand Down
25 changes: 21 additions & 4 deletions packages/flame_svg/lib/svg_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
}
2 changes: 2 additions & 0 deletions packages/flame_svg/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ dependencies:
dev_dependencies:
dartdoc: ^4.1.0
flame_lint: ^0.0.1
test: ^1.17.12
mocktail: ^0.2.0
32 changes: 32 additions & 0 deletions packages/flame_svg/test/svg_component_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}

0 comments on commit bce2417

Please sign in to comment.