Skip to content

Commit e0892ae

Browse files
[flutter_svg] feat: Expose the colorMapper property in SvgPicture (#9043)
## Description This pull request exposes the existing `colorMapper` functionality in the `flutter_svg` package, allowing developers to customize SVG colors during parsing. ## Related Issue - flutter/flutter#158634 ## Motivation The `colorMapper` functionality was already present within the `flutter_svg` package but was not directly accessible through the `SvgPicture` constructors. By exposing this property, developers gain a powerful and flexible way to dynamically modify the colors of SVG assets based on custom logic. This can be useful for various scenarios, such as theming or branding. ## Example Usage ```dart import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; const String svgString = ''' <svg viewBox="0 0 100 100"> <rect width="50" height="50" fill="#FF0000" /> <circle cx="75" cy="75" r="25" fill="#00FF00" /> </svg> '''; class MyColorMapper extends ColorMapper { const MyColorMapper(); @OverRide Color substitute( String? id, String elementName, String attributeName, Color color) { if (color == const Color(0xFFFF0000)) { return Colors.blue; } if (color == const Color(0xFF00FF00)) { return Colors.yellow; } return color; } } void main() { runApp(MaterialApp( home: Scaffold( body: Center( child: SvgPicture.string( svgString, width: 200, height: 200, colorMapper: const MyColorMapper(), ), ), ), )); } ```
1 parent fdc1ec7 commit e0892ae

11 files changed

+248
-4
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,4 @@ Taskulu LDA <[email protected]>
7878
Alexander Rabin <[email protected]>
7979
LinXunFeng <[email protected]>
8080
Hashir Shoaib <[email protected]>
81+
Ricardo Dalarme <[email protected]>

third_party/packages/flutter_svg/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.1.0
2+
3+
* Exposes `colorMapper` in `SvgPicture` constructors.
4+
15
## 2.0.17
26

37
* Implement errorBuilder callback

third_party/packages/flutter_svg/README.md

+48
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,53 @@ final Widget svgIcon = SvgPicture.asset(
3333
);
3434
```
3535

36+
For more advanced color manipulation, you can use the `colorMapper` property.
37+
This allows you to define a custom mapping function that will be called for
38+
every color encountered during SVG parsing, enabling you to substitute colors
39+
based on various criteria like the color value itself, the element name, or the
40+
attribute name.
41+
42+
To use this feature, you need to create a class that extends `ColorMapper` and
43+
override the `substitute` method.
44+
45+
Here's an example of how to implement a `ColorMapper` to replace specific colors in an SVG:
46+
47+
<?code-excerpt "example/lib/readme_excerpts.dart (ColorMapper)"?>
48+
```dart
49+
class _MyColorMapper extends ColorMapper {
50+
const _MyColorMapper();
51+
52+
@override
53+
Color substitute(
54+
String? id,
55+
String elementName,
56+
String attributeName,
57+
Color color,
58+
) {
59+
if (color == const Color(0xFFFF0000)) {
60+
return Colors.blue;
61+
}
62+
if (color == const Color(0xFF00FF00)) {
63+
return Colors.yellow;
64+
}
65+
return color;
66+
}
67+
}
68+
// ···
69+
const String svgString = '''
70+
<svg viewBox="0 0 100 100">
71+
<rect width="50" height="50" fill="#FF0000" />
72+
<circle cx="75" cy="75" r="25" fill="#00FF00" />
73+
</svg>
74+
''';
75+
final Widget svgIcon = SvgPicture.string(
76+
svgString,
77+
colorMapper: const _MyColorMapper(),
78+
);
79+
```
80+
81+
In this example, all red colors in the SVG will be rendered as blue, and all green colors will be rendered as yellow. You can customize the `substitute` method to implement more complex color mapping logic based on your requirements.
82+
3683
The default placeholder is an empty box (`LimitedBox`) - although if a `height`
3784
or `width` is specified on the `SvgPicture`, a `SizedBox` will be used instead
3885
(which ensures better layout experience). There is currently no way to show an
@@ -67,6 +114,7 @@ If you'd like to render the SVG to some other canvas, you can do something like:
67114
<?code-excerpt "example/lib/readme_excerpts.dart (OutputConversion)"?>
68115
```dart
69116
import 'dart:ui' as ui;
117+
70118
// ···
71119
const String rawSvg = '''<svg ...>...</svg>''';
72120
final PictureInfo pictureInfo =

third_party/packages/flutter_svg/example/lib/readme_excerpts.dart

+40
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
// #docregion OutputConversion
99
import 'dart:ui' as ui;
10+
1011
// #enddocregion OutputConversion
1112

1213
import 'package:flutter/material.dart';
@@ -101,3 +102,42 @@ Future<ui.Image> convertSvgOutput() async {
101102
// #enddocregion OutputConversion
102103
return image;
103104
}
105+
106+
// #docregion ColorMapper
107+
class _MyColorMapper extends ColorMapper {
108+
const _MyColorMapper();
109+
110+
@override
111+
Color substitute(
112+
String? id,
113+
String elementName,
114+
String attributeName,
115+
Color color,
116+
) {
117+
if (color == const Color(0xFFFF0000)) {
118+
return Colors.blue;
119+
}
120+
if (color == const Color(0xFF00FF00)) {
121+
return Colors.yellow;
122+
}
123+
return color;
124+
}
125+
}
126+
// #enddocregion ColorMapper
127+
128+
/// Demonstrates loading an SVG asset with a color mapping.
129+
Widget loadWithColorMapper() {
130+
// #docregion ColorMapper
131+
const String svgString = '''
132+
<svg viewBox="0 0 100 100">
133+
<rect width="50" height="50" fill="#FF0000" />
134+
<circle cx="75" cy="75" r="25" fill="#00FF00" />
135+
</svg>
136+
''';
137+
final Widget svgIcon = SvgPicture.string(
138+
svgString,
139+
colorMapper: const _MyColorMapper(),
140+
);
141+
// #enddocregion ColorMapper
142+
return svgIcon;
143+
}

third_party/packages/flutter_svg/lib/svg.dart

+22-3
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ class SvgPicture extends StatelessWidget {
195195
this.clipBehavior = Clip.hardEdge,
196196
this.errorBuilder,
197197
SvgTheme? theme,
198+
ColorMapper? colorMapper,
198199
ui.ColorFilter? colorFilter,
199200
@Deprecated('Use colorFilter instead.') ui.Color? color,
200201
@Deprecated('Use colorFilter instead.')
@@ -205,6 +206,7 @@ class SvgPicture extends StatelessWidget {
205206
packageName: package,
206207
assetBundle: bundle,
207208
theme: theme,
209+
colorMapper: colorMapper,
208210
),
209211
colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode);
210212

@@ -261,11 +263,13 @@ class SvgPicture extends StatelessWidget {
261263
this.errorBuilder,
262264
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
263265
SvgTheme? theme,
266+
ColorMapper? colorMapper,
264267
http.Client? httpClient,
265268
}) : bytesLoader = SvgNetworkLoader(
266269
url,
267270
headers: headers,
268271
theme: theme,
272+
colorMapper: colorMapper,
269273
httpClient: httpClient,
270274
),
271275
colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode);
@@ -319,8 +323,13 @@ class SvgPicture extends StatelessWidget {
319323
this.clipBehavior = Clip.hardEdge,
320324
this.errorBuilder,
321325
SvgTheme? theme,
326+
ColorMapper? colorMapper,
322327
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
323-
}) : bytesLoader = SvgFileLoader(file, theme: theme),
328+
}) : bytesLoader = SvgFileLoader(
329+
file,
330+
theme: theme,
331+
colorMapper: colorMapper,
332+
),
324333
colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode);
325334

326335
/// Creates a widget that displays an SVG obtained from a [Uint8List].
@@ -369,8 +378,13 @@ class SvgPicture extends StatelessWidget {
369378
this.clipBehavior = Clip.hardEdge,
370379
this.errorBuilder,
371380
SvgTheme? theme,
381+
ColorMapper? colorMapper,
372382
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
373-
}) : bytesLoader = SvgBytesLoader(bytes, theme: theme),
383+
}) : bytesLoader = SvgBytesLoader(
384+
bytes,
385+
theme: theme,
386+
colorMapper: colorMapper,
387+
),
374388
colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode);
375389

376390
/// Creates a widget that displays an SVG obtained from a [String].
@@ -419,8 +433,13 @@ class SvgPicture extends StatelessWidget {
419433
this.clipBehavior = Clip.hardEdge,
420434
this.errorBuilder,
421435
SvgTheme? theme,
436+
ColorMapper? colorMapper,
422437
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
423-
}) : bytesLoader = SvgStringLoader(string, theme: theme),
438+
}) : bytesLoader = SvgStringLoader(
439+
string,
440+
theme: theme,
441+
colorMapper: colorMapper,
442+
),
424443
colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode);
425444

426445
static ColorFilter? _getColorFilter(

third_party/packages/flutter_svg/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: flutter_svg
22
description: An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
33
repository: https://github.com/flutter/packages/tree/main/third_party/packages/flutter_svg
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_svg%22
5-
version: 2.0.17
5+
version: 2.1.0
66

77
environment:
88
sdk: ^3.4.0
Loading
Loading
Loading
Loading

third_party/packages/flutter_svg/test/widget_svg_test.dart

+132
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,29 @@ Future<void> _checkWidgetAndGolden(Key key, String filename) async {
3939
await expectLater(widgetFinder, matchesGoldenFile('golden_widget/$filename'));
4040
}
4141

42+
class _TestColorMapper extends ColorMapper {
43+
const _TestColorMapper();
44+
45+
/// Substitutes specific colors for testing the SVG rendering.
46+
@override
47+
Color substitute(
48+
String? id, String elementName, String attributeName, Color color) {
49+
if (color == const Color(0xFF42A5F5)) {
50+
return const Color(0xFF00FF00); // Green
51+
}
52+
if (color == const Color(0xFF0D47A1)) {
53+
return const Color(0xFFFF0000); // Red
54+
}
55+
if (color == const Color(0xFF616161)) {
56+
return const Color(0xFF0000FF); // Blue
57+
}
58+
if (color == const Color(0xFF000000)) {
59+
return const Color(0xFFFFFF00); // Yellow
60+
}
61+
return color;
62+
}
63+
}
64+
4265
void main() {
4366
final MediaQueryData mediaQueryData =
4467
MediaQueryData.fromView(PlatformDispatcher.instance.implicitView!);
@@ -116,6 +139,28 @@ void main() {
116139
await _checkWidgetAndGolden(key, 'flutter_logo.string.png');
117140
});
118141

142+
testWidgets('SvgPicture.string with colorMapper',
143+
(WidgetTester tester) async {
144+
final GlobalKey key = GlobalKey();
145+
await tester.pumpWidget(
146+
MediaQuery(
147+
data: mediaQueryData,
148+
child: RepaintBoundary(
149+
key: key,
150+
child: SvgPicture.string(
151+
svgStr,
152+
width: 100.0,
153+
height: 100.0,
154+
colorMapper: const _TestColorMapper(),
155+
),
156+
),
157+
),
158+
);
159+
160+
await tester.pumpAndSettle();
161+
await _checkWidgetAndGolden(key, 'flutter_logo.string.color_mapper.png');
162+
});
163+
119164
testWidgets('SvgPicture natural size', (WidgetTester tester) async {
120165
final GlobalKey key = GlobalKey();
121166
await tester.pumpWidget(
@@ -250,6 +295,26 @@ void main() {
250295
await _checkWidgetAndGolden(key, 'flutter_logo.memory.png');
251296
});
252297

298+
testWidgets('SvgPicture.memory with colorMapper',
299+
(WidgetTester tester) async {
300+
final GlobalKey key = GlobalKey();
301+
await tester.pumpWidget(
302+
MediaQuery(
303+
data: mediaQueryData,
304+
child: RepaintBoundary(
305+
key: key,
306+
child: SvgPicture.memory(
307+
svgBytes,
308+
colorMapper: const _TestColorMapper(),
309+
),
310+
),
311+
),
312+
);
313+
await tester.pumpAndSettle();
314+
315+
await _checkWidgetAndGolden(key, 'flutter_logo.memory.color_mapper.png');
316+
});
317+
253318
testWidgets('SvgPicture.asset', (WidgetTester tester) async {
254319
final FakeAssetBundle fakeAsset = FakeAssetBundle();
255320
final GlobalKey key = GlobalKey();
@@ -269,6 +334,26 @@ void main() {
269334
await _checkWidgetAndGolden(key, 'flutter_logo.asset.png');
270335
});
271336

337+
testWidgets('SvgPicture.asset with colorMapper', (WidgetTester tester) async {
338+
final FakeAssetBundle fakeAsset = FakeAssetBundle();
339+
final GlobalKey key = GlobalKey();
340+
await tester.pumpWidget(
341+
MediaQuery(
342+
data: mediaQueryData,
343+
child: RepaintBoundary(
344+
key: key,
345+
child: SvgPicture.asset(
346+
'test.svg',
347+
bundle: fakeAsset,
348+
colorMapper: const _TestColorMapper(),
349+
),
350+
),
351+
),
352+
);
353+
await tester.pumpAndSettle();
354+
await _checkWidgetAndGolden(key, 'flutter_logo.asset.color_mapper.png');
355+
});
356+
272357
testWidgets('SvgPicture.asset DefaultAssetBundle',
273358
(WidgetTester tester) async {
274359
final FakeAssetBundle fakeAsset = FakeAssetBundle();
@@ -295,6 +380,33 @@ void main() {
295380
await _checkWidgetAndGolden(key, 'flutter_logo.asset.png');
296381
});
297382

383+
testWidgets('SvgPicture.asset DefaultAssetBundle with colorMapper',
384+
(WidgetTester tester) async {
385+
final FakeAssetBundle fakeAsset = FakeAssetBundle();
386+
final GlobalKey key = GlobalKey();
387+
await tester.pumpWidget(
388+
Directionality(
389+
textDirection: TextDirection.ltr,
390+
child: MediaQuery(
391+
data: mediaQueryData,
392+
child: DefaultAssetBundle(
393+
bundle: fakeAsset,
394+
child: RepaintBoundary(
395+
key: key,
396+
child: SvgPicture.asset(
397+
'test.svg',
398+
semanticsLabel: 'Test SVG',
399+
colorMapper: const _TestColorMapper(),
400+
),
401+
),
402+
),
403+
),
404+
),
405+
);
406+
await tester.pumpAndSettle();
407+
await _checkWidgetAndGolden(key, 'flutter_logo.asset.color_mapper.png');
408+
});
409+
298410
testWidgets('SvgPicture.network', (WidgetTester tester) async {
299411
final GlobalKey key = GlobalKey();
300412
await tester.pumpWidget(
@@ -313,6 +425,26 @@ void main() {
313425
await _checkWidgetAndGolden(key, 'flutter_logo.network.png');
314426
});
315427

428+
testWidgets('SvgPicture.network with colorMapper',
429+
(WidgetTester tester) async {
430+
final GlobalKey key = GlobalKey();
431+
await tester.pumpWidget(
432+
MediaQuery(
433+
data: mediaQueryData,
434+
child: RepaintBoundary(
435+
key: key,
436+
child: SvgPicture.network(
437+
'test.svg',
438+
httpClient: FakeHttpClient(),
439+
colorMapper: const _TestColorMapper(),
440+
),
441+
),
442+
),
443+
);
444+
await tester.pumpAndSettle();
445+
await _checkWidgetAndGolden(key, 'flutter_logo.network.color_mapper.png');
446+
});
447+
316448
testWidgets('SvgPicture.network with headers', (WidgetTester tester) async {
317449
final GlobalKey key = GlobalKey();
318450
final FakeHttpClient client = FakeHttpClient();

0 commit comments

Comments
 (0)