How to Switch Specific Colors of an Image in Flutter

3,307

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 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

Share:
3,307
Zujaj Misbah Khan
Author by

Zujaj Misbah Khan

Name : Zujaj Misbah Khan Main Skills: Dart, Flutter, C# Extra Skills: Video Editing,Photoshop Country: Pakistan

Updated on December 26, 2022

Comments

  • Zujaj Misbah Khan
    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.

    Vehicle Color Change

    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.