Skip to content

Commit 32046d2

Browse files
committed
[vector_graphics] Move color and colorFilter effects into the raster cache for VectorGraphic when using the raster RenderingStrategy, to avoid additional saveLayer overhead during rendering.
1 parent e8e8da3 commit 32046d2

File tree

4 files changed

+135
-22
lines changed

4 files changed

+135
-22
lines changed

packages/vector_graphics/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 1.1.19
22

3+
* Moved color and colorFilter effects into the raster cache to reduce subsequent rendering overhead.
34
* Updates minimum supported SDK version to Flutter 3.27/Dart 3.6.
45

56
## 1.1.18

packages/vector_graphics/lib/src/render_vector_graphic.dart

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import 'listener.dart';
1515
@immutable
1616
class RasterKey {
1717
/// Create a new [RasterKey].
18-
const RasterKey(this.assetKey, this.width, this.height);
18+
const RasterKey(this.assetKey, this.width, this.height, this.paint);
1919

2020
/// An object that is used to identify the raster data this key will store.
2121
///
@@ -28,16 +28,22 @@ class RasterKey {
2828
/// The width of this vector graphic raster, in physical pixels.
2929
final int height;
3030

31+
/// The paint of this vector graphic raster.
32+
final Paint paint;
33+
3134
@override
3235
bool operator ==(Object other) {
3336
return other is RasterKey &&
3437
other.assetKey == assetKey &&
3538
other.width == width &&
36-
other.height == height;
39+
other.height == height &&
40+
other.paint.color == paint.color &&
41+
other.paint.colorFilter == paint.colorFilter;
3742
}
3843

3944
@override
40-
int get hashCode => Object.hash(assetKey, width, height);
45+
int get hashCode =>
46+
Object.hash(assetKey, width, height, paint.color, paint.colorFilter);
4147
}
4248

4349
/// The cache entry for a rasterized vector graphic.
@@ -116,6 +122,7 @@ class RenderVectorGraphic extends RenderBox {
116122
/// An optional [ColorFilter] to apply to the rasterized vector graphic.
117123
ColorFilter? get colorFilter => _colorFilter;
118124
ColorFilter? _colorFilter;
125+
final Paint _colorPaint = Paint();
119126
set colorFilter(ColorFilter? value) {
120127
if (colorFilter == value) {
121128
return;
@@ -196,7 +203,7 @@ class RenderVectorGraphic extends RenderBox {
196203
}
197204

198205
static RasterData _createRaster(
199-
RasterKey key, double scaleFactor, PictureInfo info) {
206+
RasterKey key, double scaleFactor, PictureInfo info, Paint colorPaint) {
200207
final int scaledWidth = key.width;
201208
final int scaledHeight = key.height;
202209
// In order to scale a picture, it must be placed in a new picture
@@ -206,9 +213,16 @@ class RenderVectorGraphic extends RenderBox {
206213
// capture in a raster.
207214
final ui.PictureRecorder recorder = ui.PictureRecorder();
208215
final ui.Canvas canvas = ui.Canvas(recorder);
209-
216+
final Rect drawSize =
217+
ui.Rect.fromLTWH(0, 0, scaledWidth.toDouble(), scaledHeight.toDouble());
218+
canvas.clipRect(drawSize);
219+
final int saveCount = canvas.getSaveCount();
220+
if (colorPaint.color.opacity != 1.0 || colorPaint.colorFilter != null) {
221+
canvas.saveLayer(drawSize, colorPaint);
222+
}
210223
canvas.scale(scaleFactor);
211224
canvas.drawPicture(info.picture);
225+
canvas.restoreToCount(saveCount);
212226
final ui.Picture rasterPicture = recorder.endRecording();
213227

214228
final ui.Image pending =
@@ -235,7 +249,8 @@ class RenderVectorGraphic extends RenderBox {
235249
(pictureInfo.size.width * devicePixelRatio / scale).round();
236250
final int scaledHeight =
237251
(pictureInfo.size.height * devicePixelRatio / scale).round();
238-
final RasterKey key = RasterKey(assetKey, scaledWidth, scaledHeight);
252+
final RasterKey key =
253+
RasterKey(assetKey, scaledWidth, scaledHeight, _colorPaint);
239254

240255
// First check if the raster is available synchronously. This also handles
241256
// a no-op change that would resolve to an identical picture.
@@ -249,7 +264,7 @@ class RenderVectorGraphic extends RenderBox {
249264
return;
250265
}
251266
final RasterData data =
252-
_createRaster(key, devicePixelRatio / scale, pictureInfo);
267+
_createRaster(key, devicePixelRatio / scale, pictureInfo, _colorPaint);
253268
data.count += 1;
254269

255270
assert(!_liveRasterCache.containsKey(key));
@@ -296,18 +311,17 @@ class RenderVectorGraphic extends RenderBox {
296311
return;
297312
}
298313

314+
// _colorPaint is used to create raster cache.
315+
if (colorFilter != null) {
316+
_colorPaint.colorFilter = colorFilter;
317+
}
318+
_colorPaint.color = Color.fromRGBO(0, 0, 0, _opacityValue);
319+
299320
_maybeUpdateRaster();
300321
final ui.Image image = _rasterData!.image;
301322
final int width = _rasterData!.key.width;
302323
final int height = _rasterData!.key.height;
303324

304-
// Use `FilterQuality.low` to scale the image, which corresponds to
305-
// bilinear interpolation.
306-
final Paint colorPaint = Paint()..filterQuality = ui.FilterQuality.low;
307-
if (colorFilter != null) {
308-
colorPaint.colorFilter = colorFilter;
309-
}
310-
colorPaint.color = Color.fromRGBO(0, 0, 0, _opacityValue);
311325
final Rect src = ui.Rect.fromLTWH(
312326
0,
313327
0,
@@ -321,11 +335,13 @@ class RenderVectorGraphic extends RenderBox {
321335
pictureInfo.size.height,
322336
);
323337

338+
// Use `FilterQuality.low` to scale the image, which corresponds to
339+
// bilinear interpolation.
324340
context.canvas.drawImageRect(
325341
image,
326342
src,
327343
dst,
328-
colorPaint,
344+
Paint()..filterQuality = ui.FilterQuality.low,
329345
);
330346
}
331347
}

packages/vector_graphics/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: vector_graphics
22
description: A vector graphics rendering package for Flutter using a binary encoding.
33
repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22
5-
version: 1.1.18
5+
version: 1.1.19
66

77
environment:
88
sdk: ^3.6.0

packages/vector_graphics/test/render_vector_graphics_test.dart

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ void main() {
156156
expect(identical(context.canvas.images[0], context.canvas.images[1]), true);
157157
});
158158

159-
test('Changing color filter does not re-rasterize', () async {
159+
test('Changing color filter does re-rasterize', () async {
160160
final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic(
161161
pictureInfo,
162162
'test',
@@ -175,11 +175,11 @@ void main() {
175175
const ui.ColorFilter.mode(Colors.red, ui.BlendMode.colorBurn);
176176
renderVectorGraphic.paint(context, Offset.zero);
177177

178-
expect(firstImage.debugDisposed, false);
178+
expect(firstImage.debugDisposed, true);
179179

180180
renderVectorGraphic.paint(context, Offset.zero);
181181

182-
expect(context.canvas.lastImage, equals(firstImage));
182+
expect(context.canvas.lastImage, isNot(firstImage));
183183
});
184184

185185
test('Changing device pixel ratio does re-rasterize and dispose old raster',
@@ -350,7 +350,8 @@ void main() {
350350
final FakePaintingContext context = FakePaintingContext();
351351
renderVectorGraphic.paint(context, Offset.zero);
352352

353-
expect(context.canvas.lastPaint?.color, const Color.fromRGBO(0, 0, 0, 0.5));
353+
// opaque is used to generate raster cache.
354+
expect(context.canvas.lastPaint?.color, const Color.fromRGBO(0, 0, 0, 1.0));
354355
});
355356

356357
test('Disposing render object disposes picture', () async {
@@ -403,7 +404,9 @@ void main() {
403404
final ui.PictureRecorder recorder = ui.PictureRecorder();
404405
ui.Canvas(recorder);
405406
final ui.Image image = await recorder.endRecording().toImage(1, 1);
406-
final RasterData data = RasterData(image, 1, const RasterKey('test', 1, 1));
407+
final Paint paint = Paint();
408+
final RasterData data =
409+
RasterData(image, 1, RasterKey('test', 1, 1, paint));
407410

408411
data.dispose();
409412

@@ -426,6 +429,99 @@ void main() {
426429
expect(context.canvas.totalSaves, 1);
427430
expect(context.canvas.totalSaveLayers, 1);
428431
});
432+
433+
testWidgets('Changing offset does not re-rasterize in raster strategy',
434+
(WidgetTester tester) async {
435+
final RenderVectorGraphic renderVectorGraphic =
436+
RenderVectorGraphic(pictureInfo, 'testOffset', null, 1.0, null, 1.0);
437+
renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50)));
438+
final FakePaintingContext context = FakePaintingContext();
439+
440+
renderVectorGraphic.paint(context, Offset.zero);
441+
expect(context.canvas.lastImage, isNotNull);
442+
443+
final ui.Image? oldImage = context.canvas.lastImage;
444+
445+
renderVectorGraphic.paint(context, const Offset(20, 30));
446+
expect(context.canvas.lastImage, isNotNull);
447+
expect(context.canvas.lastImage, equals(oldImage));
448+
449+
renderVectorGraphic.dispose();
450+
});
451+
452+
testWidgets('RenderVectorGraphic re-rasterizes when opacity changes',
453+
(WidgetTester tester) async {
454+
final FixedOpacityAnimation opacity = FixedOpacityAnimation(0.2);
455+
final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic(
456+
pictureInfo,
457+
'testOpacity',
458+
null,
459+
1.0,
460+
opacity,
461+
1.0,
462+
);
463+
464+
renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50)));
465+
final FakePaintingContext context = FakePaintingContext();
466+
renderVectorGraphic.paint(context, Offset.zero);
467+
468+
final ui.Image? oldImage = context.canvas.lastImage;
469+
470+
opacity.value = 0.5;
471+
opacity.notifyListeners();
472+
473+
// Changing opacity requires painting.
474+
expect(renderVectorGraphic.debugNeedsPaint, true);
475+
476+
// Changing opacity need create new raster cache.
477+
renderVectorGraphic.paint(context, Offset.zero);
478+
expect(context.canvas.lastImage, isNotNull);
479+
480+
expect(context.canvas.lastImage, isNot(oldImage));
481+
482+
renderVectorGraphic.dispose();
483+
});
484+
485+
testWidgets(
486+
'Identical widgets reuse raster cache when available in raster startegy',
487+
(WidgetTester tester) async {
488+
final RenderVectorGraphic renderVectorGraphic1 = RenderVectorGraphic(
489+
pictureInfo,
490+
'testOffset',
491+
null,
492+
1.0,
493+
null,
494+
1.0,
495+
);
496+
final RenderVectorGraphic renderVectorGraphic2 = RenderVectorGraphic(
497+
pictureInfo,
498+
'testOffset',
499+
null,
500+
1.0,
501+
null,
502+
1.0,
503+
);
504+
renderVectorGraphic1.layout(BoxConstraints.tight(const Size(50, 50)));
505+
renderVectorGraphic2.layout(BoxConstraints.tight(const Size(50, 50)));
506+
507+
final FakePaintingContext context = FakePaintingContext();
508+
509+
renderVectorGraphic1.paint(context, Offset.zero);
510+
511+
final ui.Image? image1 = context.canvas.lastImage;
512+
513+
renderVectorGraphic2.paint(context, Offset.zero);
514+
515+
final ui.Image? image2 = context.canvas.lastImage;
516+
517+
expect(image1, isNotNull);
518+
expect(image2, isNotNull);
519+
520+
expect(image1, equals(image2));
521+
522+
renderVectorGraphic1.dispose();
523+
renderVectorGraphic2.dispose();
524+
});
429525
}
430526

431527
class FakeCanvas extends Fake implements Canvas {

0 commit comments

Comments
 (0)