diff --git a/packages/vector_graphics/CHANGELOG.md b/packages/vector_graphics/CHANGELOG.md index 3407e7dc5fc..d8d75b2ba93 100644 --- a/packages/vector_graphics/CHANGELOG.md +++ b/packages/vector_graphics/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.20 + +* Moved color and colorFilter effects into the raster cache to reduce subsequent rendering overhead. + ## 1.1.19 * Updates minimum supported SDK version to Flutter 3.24/Dart 3.5. diff --git a/packages/vector_graphics/lib/src/render_vector_graphic.dart b/packages/vector_graphics/lib/src/render_vector_graphic.dart index a6b530469e5..df1e2669842 100644 --- a/packages/vector_graphics/lib/src/render_vector_graphic.dart +++ b/packages/vector_graphics/lib/src/render_vector_graphic.dart @@ -15,7 +15,7 @@ import 'listener.dart'; @immutable class RasterKey { /// Create a new [RasterKey]. - const RasterKey(this.assetKey, this.width, this.height); + const RasterKey(this.assetKey, this.width, this.height, this.paint); /// An object that is used to identify the raster data this key will store. /// @@ -28,16 +28,22 @@ class RasterKey { /// The width of this vector graphic raster, in physical pixels. final int height; + /// The paint of this vector graphic raster. + final Paint paint; + @override bool operator ==(Object other) { return other is RasterKey && other.assetKey == assetKey && other.width == width && - other.height == height; + other.height == height && + other.paint.color == paint.color && + other.paint.colorFilter == paint.colorFilter; } @override - int get hashCode => Object.hash(assetKey, width, height); + int get hashCode => + Object.hash(assetKey, width, height, paint.color, paint.colorFilter); } /// The cache entry for a rasterized vector graphic. @@ -116,6 +122,7 @@ class RenderVectorGraphic extends RenderBox { /// An optional [ColorFilter] to apply to the rasterized vector graphic. ColorFilter? get colorFilter => _colorFilter; ColorFilter? _colorFilter; + final Paint _colorPaint = Paint(); set colorFilter(ColorFilter? value) { if (colorFilter == value) { return; @@ -196,7 +203,7 @@ class RenderVectorGraphic extends RenderBox { } static RasterData _createRaster( - RasterKey key, double scaleFactor, PictureInfo info) { + RasterKey key, double scaleFactor, PictureInfo info, Paint colorPaint) { final int scaledWidth = key.width; final int scaledHeight = key.height; // In order to scale a picture, it must be placed in a new picture @@ -206,9 +213,16 @@ class RenderVectorGraphic extends RenderBox { // capture in a raster. final ui.PictureRecorder recorder = ui.PictureRecorder(); final ui.Canvas canvas = ui.Canvas(recorder); - + final Rect drawSize = + ui.Rect.fromLTWH(0, 0, scaledWidth.toDouble(), scaledHeight.toDouble()); + canvas.clipRect(drawSize); + final int saveCount = canvas.getSaveCount(); + if (colorPaint.color.opacity != 1.0 || colorPaint.colorFilter != null) { + canvas.saveLayer(drawSize, colorPaint); + } canvas.scale(scaleFactor); canvas.drawPicture(info.picture); + canvas.restoreToCount(saveCount); final ui.Picture rasterPicture = recorder.endRecording(); final ui.Image pending = @@ -235,7 +249,8 @@ class RenderVectorGraphic extends RenderBox { (pictureInfo.size.width * devicePixelRatio / scale).round(); final int scaledHeight = (pictureInfo.size.height * devicePixelRatio / scale).round(); - final RasterKey key = RasterKey(assetKey, scaledWidth, scaledHeight); + final RasterKey key = + RasterKey(assetKey, scaledWidth, scaledHeight, _colorPaint); // First check if the raster is available synchronously. This also handles // a no-op change that would resolve to an identical picture. @@ -249,7 +264,7 @@ class RenderVectorGraphic extends RenderBox { return; } final RasterData data = - _createRaster(key, devicePixelRatio / scale, pictureInfo); + _createRaster(key, devicePixelRatio / scale, pictureInfo, _colorPaint); data.count += 1; assert(!_liveRasterCache.containsKey(key)); @@ -296,18 +311,17 @@ class RenderVectorGraphic extends RenderBox { return; } + // _colorPaint is used to create raster cache. + if (colorFilter != null) { + _colorPaint.colorFilter = colorFilter; + } + _colorPaint.color = Color.fromRGBO(0, 0, 0, _opacityValue); + _maybeUpdateRaster(); final ui.Image image = _rasterData!.image; final int width = _rasterData!.key.width; final int height = _rasterData!.key.height; - // Use `FilterQuality.low` to scale the image, which corresponds to - // bilinear interpolation. - final Paint colorPaint = Paint()..filterQuality = ui.FilterQuality.low; - if (colorFilter != null) { - colorPaint.colorFilter = colorFilter; - } - colorPaint.color = Color.fromRGBO(0, 0, 0, _opacityValue); final Rect src = ui.Rect.fromLTWH( 0, 0, @@ -321,11 +335,13 @@ class RenderVectorGraphic extends RenderBox { pictureInfo.size.height, ); + // Use `FilterQuality.low` to scale the image, which corresponds to + // bilinear interpolation. context.canvas.drawImageRect( image, src, dst, - colorPaint, + Paint()..filterQuality = ui.FilterQuality.low, ); } } diff --git a/packages/vector_graphics/pubspec.yaml b/packages/vector_graphics/pubspec.yaml index df4ca92b8b3..eb1188575d9 100644 --- a/packages/vector_graphics/pubspec.yaml +++ b/packages/vector_graphics/pubspec.yaml @@ -2,7 +2,7 @@ name: vector_graphics description: A vector graphics rendering package for Flutter using a binary encoding. repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22 -version: 1.1.19 +version: 1.1.20 environment: sdk: ^3.6.0 diff --git a/packages/vector_graphics/test/render_vector_graphics_test.dart b/packages/vector_graphics/test/render_vector_graphics_test.dart index bf5f12889ca..8b85f23f0bb 100644 --- a/packages/vector_graphics/test/render_vector_graphics_test.dart +++ b/packages/vector_graphics/test/render_vector_graphics_test.dart @@ -156,7 +156,7 @@ void main() { expect(identical(context.canvas.images[0], context.canvas.images[1]), true); }); - test('Changing color filter does not re-rasterize', () async { + test('Changing color filter does re-rasterize', () async { final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( pictureInfo, 'test', @@ -175,11 +175,11 @@ void main() { const ui.ColorFilter.mode(Colors.red, ui.BlendMode.colorBurn); renderVectorGraphic.paint(context, Offset.zero); - expect(firstImage.debugDisposed, false); + expect(firstImage.debugDisposed, true); renderVectorGraphic.paint(context, Offset.zero); - expect(context.canvas.lastImage, equals(firstImage)); + expect(context.canvas.lastImage, isNot(firstImage)); }); test('Changing device pixel ratio does re-rasterize and dispose old raster', @@ -350,7 +350,8 @@ void main() { final FakePaintingContext context = FakePaintingContext(); renderVectorGraphic.paint(context, Offset.zero); - expect(context.canvas.lastPaint?.color, const Color.fromRGBO(0, 0, 0, 0.5)); + // opaque is used to generate raster cache. + expect(context.canvas.lastPaint?.color, const Color.fromRGBO(0, 0, 0, 1.0)); }); test('Disposing render object disposes picture', () async { @@ -403,7 +404,9 @@ void main() { final ui.PictureRecorder recorder = ui.PictureRecorder(); ui.Canvas(recorder); final ui.Image image = await recorder.endRecording().toImage(1, 1); - final RasterData data = RasterData(image, 1, const RasterKey('test', 1, 1)); + final Paint paint = Paint(); + final RasterData data = + RasterData(image, 1, RasterKey('test', 1, 1, paint)); data.dispose(); @@ -426,6 +429,99 @@ void main() { expect(context.canvas.totalSaves, 1); expect(context.canvas.totalSaveLayers, 1); }); + + testWidgets('Changing offset does not re-rasterize in raster strategy', + (WidgetTester tester) async { + final RenderVectorGraphic renderVectorGraphic = + RenderVectorGraphic(pictureInfo, 'testOffset', null, 1.0, null, 1.0); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + + renderVectorGraphic.paint(context, Offset.zero); + expect(context.canvas.lastImage, isNotNull); + + final ui.Image? oldImage = context.canvas.lastImage; + + renderVectorGraphic.paint(context, const Offset(20, 30)); + expect(context.canvas.lastImage, isNotNull); + expect(context.canvas.lastImage, equals(oldImage)); + + renderVectorGraphic.dispose(); + }); + + testWidgets('RenderVectorGraphic re-rasterizes when opacity changes', + (WidgetTester tester) async { + final FixedOpacityAnimation opacity = FixedOpacityAnimation(0.2); + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'testOpacity', + null, + 1.0, + opacity, + 1.0, + ); + + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + final ui.Image? oldImage = context.canvas.lastImage; + + opacity.value = 0.5; + opacity.notifyListeners(); + + // Changing opacity requires painting. + expect(renderVectorGraphic.debugNeedsPaint, true); + + // Changing opacity need create new raster cache. + renderVectorGraphic.paint(context, Offset.zero); + expect(context.canvas.lastImage, isNotNull); + + expect(context.canvas.lastImage, isNot(oldImage)); + + renderVectorGraphic.dispose(); + }); + + testWidgets( + 'Identical widgets reuse raster cache when available in raster startegy', + (WidgetTester tester) async { + final RenderVectorGraphic renderVectorGraphic1 = RenderVectorGraphic( + pictureInfo, + 'testOffset', + null, + 1.0, + null, + 1.0, + ); + final RenderVectorGraphic renderVectorGraphic2 = RenderVectorGraphic( + pictureInfo, + 'testOffset', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphic1.layout(BoxConstraints.tight(const Size(50, 50))); + renderVectorGraphic2.layout(BoxConstraints.tight(const Size(50, 50))); + + final FakePaintingContext context = FakePaintingContext(); + + renderVectorGraphic1.paint(context, Offset.zero); + + final ui.Image? image1 = context.canvas.lastImage; + + renderVectorGraphic2.paint(context, Offset.zero); + + final ui.Image? image2 = context.canvas.lastImage; + + expect(image1, isNotNull); + expect(image2, isNotNull); + + expect(image1, equals(image2)); + + renderVectorGraphic1.dispose(); + renderVectorGraphic2.dispose(); + }); } class FakeCanvas extends Fake implements Canvas {