How to Switch Specific Colors of an Image in Flutter How to Switch Specific Colors of an Image in Flutter dart dart

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 filesflutter_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 the imageBytes variable using the setState function.

  • Next, I created a custom asynchronous function switchColor that would take the Uint8List 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 the imageBytes is not ready, I displayed a CircularProgressIndicator else, a FutureBuilder would call switchColor 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 a ListView based upon the entries of the _colorMap.

  • I wrapped each colorEntry in a circular container using BoxShape.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. the Color 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 the ImageColorSwitcher, I declared a Color variable ColorCode and assigned it the value coming from the ColorSlider’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 Columnwidget.

Raster Image Coloring

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 a String & a Color 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 a ListView.

  • To display the container’s background colour, I used mapEntry values.

  • Upon tapping the onTap function, I returned the selected mapEntry key (the encoded string) instead of the Color 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 the Bike_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 variables color & shade.

  • Finally, I passed the _bytes variable to the SvgPicture.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.

Vector Image Colouring

RESULT COMPARISON

The below figure illustrates the difference obtained from both approaches.

Result Comparison

REFERENCE

1 : ImageColorSwitcher in Flutter: Part 1 Raster Image Coloring

2 : ImageColorSwitcher in Flutter: Part 2 Vector Image Coloring