Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Switch Specific Colors of an Image in Flutter

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.

like image 712
Zujaj Misbah Khan Avatar asked Jan 08 '21 17:01

Zujaj Misbah Khan


People also ask

How do you change the color of an image on Flutter?

If you want to change the background color dynamically you will first have to make the background transparent by adding an alpha channel mask to the image (again using an image editor) You will then be able to define a background color by putting the image inside a widget that has a background color.

How do you specify colors in Flutter?

Create a variable of type Color and assign the following values to it. Color myHexColor = Color(0xff123456) // Here you notice I use the 0xff and that is the opacity or transparency // of the color and you can also change these values. Use myHexColor and you are ready to go.

How do you change the color of a PNG icon on Flutter?

Here's how you do it:Step 1: Locate the MaterialApp widget. Step 2: Inside the MaterialApp, add the theme parameter with ThemeData class assigned. Step 3: Inside the ThemeData add the iconTheme parameter and then assign the IconThemeData . Step 4:Inside the IconThemeData add the color parameter and set its color.

How do you use color filter in Flutter?

Use the ColorFilter. mode constructor to apply a Color using a BlendMode. Use the BackdropFilter widget instead, if the ColorFilter needs to be applied onto the content beneath child. These two images have two ColorFilters applied with different BlendModes, one with red color and BlendMode.


1 Answers

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

like image 113
Zujaj Misbah Khan Avatar answered Sep 27 '22 17:09

Zujaj Misbah Khan