How to Switch Specific Colors of an Image in Flutter
After quite trial and error, I found the solution. The source code and asset file are available on the Github Repository.
Required Pubspec Packages
# Provides server & web apps with the ability to load, manipulate and save images with various image file formats PNG, JPEG, GIF, BMP, WebP, TIFF, TGA, PSD, PVR, and OpenEXR.
image: ^2.1.19
# Allows painting & displaying Scalable Vector Graphics 1.1 files
flutter_svg: ^0.19.3
Below are the two approaches that I discovered during my research.
THE RASTER APPROACH
Image Color Switcher Widget
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image/image.dart' as External;
class ImageColorSwitcher extends StatefulWidget {
/// Holds the Image Path
final String imagePath;
/// Holds the MaterialColor
final MaterialColor color;
ImageColorSwitcher({this.imagePath, this.color});
@override
_ImageColorSwitcherState createState() => _ImageColorSwitcherState();
}
class _ImageColorSwitcherState extends State<ImageColorSwitcher> {
/// Holds the Image in Byte Format
Uint8List imageBytes;
@override
void initState() {
rootBundle.load(widget.imagePath).then(
(data) => setState(() => this.imageBytes = data.buffer.asUint8List()));
super.initState();
}
/// A function that switches the image color.
Future<Uint8List> switchColor(Uint8List bytes) async {
// Decode the bytes to [Image] type
final image = External.decodeImage(bytes);
// Convert the [Image] to RGBA formatted pixels
final pixels = image.getBytes(format: External.Format.rgba);
// Get the Pixel Length
final int length = pixels.lengthInBytes;
for (var i = 0; i < length; i += 4) {
/// PIXELS
/// =============================
/// | i | i + 1 | i + 2 | i + 3 |
/// =============================
// pixels[i] represents Red
// pixels[i + 1] represents Green
// pixels[i + 2] represents Blue
// pixels[i + 3] represents Alpha
// Detect the light blue color & switch it with the desired color's RGB value.
if (pixels[i] == 189 && pixels[i + 1] == 212 && pixels[i + 2] == 222) {
pixels[i] = widget.color.shade300.red;
pixels[i + 1] = widget.color.shade300.green;
pixels[i + 2] = widget.color.shade300.blue;
}
// Detect the darkish blue shade & switch it with the desired color's RGB value.
else if (pixels[i] == 63 && pixels[i + 1] == 87 && pixels[i + 2] == 101) {
pixels[i] = widget.color.shade900.red;
pixels[i + 1] = widget.color.shade900.green;
pixels[i + 2] = widget.color.shade900.blue;
}
}
return External.encodePng(image);
}
@override
Widget build(BuildContext context) {
return imageBytes == null
? Center(child: CircularProgressIndicator())
: FutureBuilder(
future: switchColor(imageBytes),
builder: (_, AsyncSnapshot<Uint8List> snapshot) {
return snapshot.hasData
? Container(
width: MediaQuery.of(context).size.width * 0.9,
decoration: BoxDecoration(
image: DecorationImage(
image: Image.memory(
snapshot.data,
).image)),
)
: CircularProgressIndicator();
},
);
}
}
-
I created a Stateful widget that would take the image path and the desired colour using the constructor.
-
In the
initState
method, I loaded up the image & assigned the raw bytes to theimageBytes
variable using thesetState
function. -
Next, I created a custom asynchronous function
switchColor
that would take theUint8List
bytes as a parameter, detect the RGB values, switch it with the desired colour and return an encoded png image. -
Inside the
build
method, incase theimageBytes
is not ready, I displayed aCircularProgressIndicator
else, aFutureBuilder
would callswitchColor
and return a containerized image.
Color Slider Widget
import 'package:flutter/material.dart';
/// A Custom Slider that returns a selected color.
class ColorSlider extends StatelessWidget {
/// Map holding the color name with its value
final Map<String, Color> _colorMap = {
'Red': Colors.red,
'Green': Colors.green,
'Blue': Colors.blue,
'Light Blue': Colors.lightBlue,
'Blue Grey': Colors.blueGrey,
'Brown': Colors.brown,
'Cyan': Colors.cyan,
'Purple': Colors.purple,
'Deep Purple': Colors.deepPurple,
'Light Green': Colors.lightGreen,
'Indigo': Colors.indigo,
'Amber': Colors.amber,
'Yellow': Colors.yellow,
'Lime': Colors.lime,
'Orange': Colors.orange,
'Dark Orange': Colors.deepOrange,
'Teal': Colors.teal,
'Pink': Colors.pink,
'Black': MaterialColor(
Colors.black.value,
{
50: Colors.black38,
100: Colors.black38,
200: Colors.black38,
300: Colors.grey.shade800,
400: Colors.black38,
500: Colors.black38,
600: Colors.black38,
700: Colors.black38,
800: Colors.black38,
900: Colors.black,
},
),
'White': MaterialColor(
Colors.white.value,
{
50: Colors.white,
100: Colors.white,
200: Colors.white,
300: Colors.white,
400: Colors.white,
500: Colors.white,
600: Colors.white,
700: Colors.white,
800: Colors.white,
900: Colors.grey.shade700,
},
),
'Grey': Colors.grey,
};
/// Triggers when tapped on a color
final Function(Color) onColorSelected;
ColorSlider({@required this.onColorSelected});
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
children: [
..._colorMap.entries.map((MapEntry<String, Color> colorEntry) {
return InkWell(
borderRadius: BorderRadius.circular(50.0),
onTap: () => onColorSelected(colorEntry.value),
child: Container(
height: 80,
width: 80,
margin: EdgeInsets.all(5.0),
decoration: BoxDecoration(
color: colorEntry.value,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: colorEntry.value.withOpacity(0.8),
offset: Offset(1.0, 2.0),
blurRadius: 3.0,
),
],
),
child: Center(
child:
// If the color is Black, change font color to white
colorEntry.key == 'Black'
? Text(colorEntry.key.toUpperCase(),
style: TextStyle(
fontSize: 8.75,
fontWeight: FontWeight.bold,
color: Colors.white))
: Text(colorEntry.key.toUpperCase(),
style: TextStyle(
fontSize: 8.75,
fontWeight: FontWeight.bold)))),
);
})
],
);
}
}
-
I declared a
Map<String, Color> _colorMap
that would hold the colour name and the Color value. -
Inside the
build
method, I created aListView
based upon the entries of the_colorMap
. -
I wrapped each
colorEntry
in a circular container usingBoxShape.circle
. -
To tap upon each colour, I wrapped each container in the
InkWell
widget. -
Inside the
onTap
function, I returned the selected map entry, i.e. theColor
value.
Raster Code Execution
import 'package:flutter/material.dart';
import 'package:image_color_switcher/widgets/color_slider.dart';
import 'package:image_color_switcher/widgets/image_color_switcher.dart';
void main() {
runApp(MyApp());
/// Hide the debug banner on the top right corner
WidgetsApp.debugAllowBannerOverride = false;
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// Holds the Color value returned from [ColorSlider]
Color colorCode;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Image Color Switcher',
home: Scaffold(
body: SafeArea(
child: Column(children: [
Expanded(
child: ImageColorSwitcher(
imagePath: 'assets/bike.png',
color: colorCode ?? Colors.red,
)),
Expanded(
child: ColorSlider(
onColorSelected: (color) => setState(() => colorCode = color),
)),
]))));
}
}
-
To integrate the
ColorSlider
with theImageColorSwitcher
, I declared aColor
variableColorCode
and assigned it the value coming from theColorSlider’s
onColorSelected
callback function. -
To avoid
null
values, I set red as the default selected colour. -
Finally, I wrapped both of these custom widgets inside a
Column
widget.
THE VECTOR APPROACH
SVG Color Slider Widget
import 'package:flutter/material.dart';
/// A Custom Slider that returns SVG colors and shades.
class SVGColorSlider extends StatelessWidget {
/// Map holding the Theme.color:shade with its value
final _colorMap = {
'Red.indianred:darkred': Color.fromARGB(255, 255, 0, 0),
'Green.#22b14c:#004000': Colors.green,
'Blue.lightskyblue:darkblue': Color.fromARGB(255, 0, 0, 255),
'Navy.#0000CD:#000080': Color.fromARGB(255, 0, 0, 128),
'Magenta.#FF00FF:#8B008B': Color.fromARGB(255, 255, 0, 255),
'Indigo.#9370DB:#4B0082': Color.fromARGB(255, 75, 0, 130),
'Orange.#FFA500:#FF8C00': Color.fromARGB(255, 255, 165, 0),
'Turquoise.#40E0D0:#00CED1': Color.fromARGB(255, 64, 224, 208),
'Purple.#9370DB:#6A0DAD': Colors.purple,
'Bronze.#CD7F32:#524741': Color.fromARGB(255, 82, 71, 65),
'Yellow.#FFFF19:#E0E200': Color.fromARGB(255, 255, 255, 0),
'Burgundy.#9D2735:#800020': Color.fromARGB(255, 128, 0, 32),
'Brown.chocolate:brown': Color.fromARGB(255, 165, 42, 42),
'Beige.beige:#d9b382': Color.fromARGB(255, 245, 245, 220),
'Maroon.#800000:#450000': Color.fromARGB(255, 128, 0, 0),
'Gold.goldenrod:darkgoldenrod': Color.fromARGB(255, 255, 215, 0),
'Grey.grey:darkgrey': Color.fromARGB(255, 128, 128, 128),
'Black.black:#1B1B1B:': Color.fromARGB(255, 0, 0, 0),
'Silver.#8B8B8B:silver': Color.fromARGB(255, 192, 192, 192),
// Multiple Options: antiquewhite,floralwhite,ghostwite
'White.ghostwhite:black': Color.fromARGB(255, 255, 255, 255),
'Slate.#708090:#284646': Color.fromARGB(255, 47, 79, 79),
};
/// Triggers when tapped on a color
final Function(String) onColorSelected;
SVGColorSlider({@required this.onColorSelected});
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
children: [
..._colorMap.entries.map((MapEntry<String, Color> mapEntry) {
return InkWell(
borderRadius: BorderRadius.circular(50.0),
onTap: () => onColorSelected(mapEntry.key),
child: Container(
height: 80,
width: 80,
margin: EdgeInsets.all(5.0),
decoration: BoxDecoration(
color: mapEntry.value,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: mapEntry.value,
offset: Offset(1.0, 2.0),
),
],
),
child: Center(
child:
/// Change The Font To Black For These Colors
mapEntry.key.contains('White') ||
mapEntry.key.contains('Beige') ||
mapEntry.key.contains('Yellow')
? Text(
mapEntry.key
.split(':')[0]
.split('.')[0]
.toUpperCase(),
style: TextStyle(
fontSize: 8.75,
fontWeight: FontWeight.bold,
))
:
/// Else Let The Font Be white
Text(
mapEntry.key
.split(':')[0]
.split('.')[0]
.toUpperCase(),
style: TextStyle(
fontSize: 8.75,
fontWeight: FontWeight.bold,
color: Colors.white)))),
);
})
],
);
}
}
-
I declared a
Map<String, Color> _colorMap
that would hold aString
& aColor
value. -
Inside the map key, I defined an encoded string
Theme.color:shade
likewise: ★ Theme: Name of the theme. ★ Color: Name or Hex value of the colour. ★ Shade: Name or Hex value of the shade. -
Inside the map value, I used the
Color.fromARGB constructor
. -
Inside the
build
method, I transformed the_colorMap
entries into circle shaped containers wrapped in aListView
. -
To display the container’s background colour, I used
mapEntry
values. -
Upon tapping the
onTap
function, I returned the selectedmapEntry
key (the encoded string) instead of theColor
value.
Bike Painter Widget
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class BikePainter extends StatelessWidget {
final String color, shade;
BikePainter({@required this.color, @required this.shade});
@override
Widget build(BuildContext context) {
final _bytes =
'''The code is too long, please visit https://gist.githubusercontent.com/Zujaj/2bad1cb88a5b44e95a6a87a89dd23922/raw/68e9597b0b3ab7dfe68a54154c920c335ed1ae18/bike_painter.dart''';
return SvgPicture.string(_bytes);
}
}
-
I declared two
String
variables,color
&shade
and passed them to theBike_Painter’s
constructor. -
Inside the
build
method, I declared a private variable_bytes
that would hold the SVG code. -
Hit
ctrl+H
to search for the hex values and replaced them with the variablescolor
&shade
. -
Finally, I passed the
_bytes
variable to theSvgPicture.string
constructor.
SVG Code Execution
import 'package:flutter/material.dart';
import 'package:image_color_switcher/widgets/bike_painter.dart';
import 'package:image_color_switcher/widgets/svg_color_slider.dart';
void main() {
runApp(MyApp());
/// Hide the debug banner on the top right corner
WidgetsApp.debugAllowBannerOverride = false;
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// Holds the encoded color string value returned from [SVGColorSlider]
String colorCode = '';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Image Color Switcher',
home: Scaffold(
body: SafeArea(
child: Column(children: [
Expanded(
child: BikePainter(
color: colorCode.isNotEmpty
? colorCode.split('.')[1].split(':')[0]
: '#bdd4de',
shade: colorCode.isNotEmpty
? colorCode.split('.')[1].split(':')[1]
: '#3f5765')),
Expanded(
child: SVGColorSlider(
onColorSelected: (color) => setState(() => colorCode = color),
)),
]))));
}
}
I integrated the BikePainter
& SVGColorSlider
widget inside the main.dart
file.
RESULT COMPARISON
The below figure illustrates the difference obtained from both approaches.
REFERENCE
1 : ImageColorSwitcher in Flutter: Part 1 Raster Image Coloring
2 : ImageColorSwitcher in Flutter: Part 2 Vector Image Coloring
Zujaj Misbah Khan
Name : Zujaj Misbah Khan Main Skills: Dart, Flutter, C# Extra Skills: Video Editing,Photoshop Country: Pakistan
Updated on December 26, 2022Comments
-
Zujaj Misbah Khan 11 months
The task is to simply take the default hex color of the vehicle's image (known prior - #bdd4de in this case) and dynamically switch it to the color selected by the user. For the shade, I can simply repeat this process and simply change it to a darker version of the chosen color.
I have tried using the ColorFiltered widget but it does not seem to fit the specific mentioned functionality. I am looking into trying the Canvas, however drawing the shape which needs to be colored is infeasible as I have a lot more vehicles and I feel that the approach of changing the specific hex should be the most optimal approach.