Flutter - How to blend an image with a gradient colour?

102.9k Views Asked by At

I am attempting to replicate a login screen design my designer did for an app.

The background image uses a blendMode of softLight, the catch is that the colour it blends with is a gradient colour. Secondly there is actually two layers of different gradients (one purple gradient, one blue gradient)

Original Image:

Original Image

Final Gradient Image

Final Gradient Image

Now I have tried using colorBlendMode, e.g.

Image.asset(
      'assets/pioneer-party.jpg',
      fit: BoxFit.cover,
      color: Color(0xff0d69ff).withOpacity(1.0),
      colorBlendMode: BlendMode.softLight,
    ),

The problem is that the color property only takes a single colour.

I then tried BoxDecoration, e.g.

DecoratedBox(
      decoration: new BoxDecoration(
        color: const Color(0xff7c94b6),
        image: new DecorationImage(
          fit: BoxFit.cover,
          colorFilter: new ColorFilter.mode(Colors.purple.withOpacity(1.0), BlendMode.softLight),
          image: new NetworkImage(
            'http://www.allwhitebackground.com/images/2/2582-190x190.jpg',
          ),
        ),
      ),
    ),

Which still leaves me with the same problem. I then tried stacking each layer individually and then playing around with gradients to make it appear close to the design, e.g.

Image.asset(
      'assets/pioneer-party.jpg',
      fit: BoxFit.cover,
      color: Color(0xff0d69ff).withOpacity(1.0),
      colorBlendMode: BlendMode.softLight,
    ),
    DecoratedBox(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: FractionalOffset.topCenter,
          end: FractionalOffset.bottomCenter,
          colors: [
            Color(0xff0d69ff).withOpacity(0.0),
            Color(0xff0069ff).withOpacity(0.8),
          ],
        ),
      ),
    ),
    DecoratedBox(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: FractionalOffset.topLeft,
          end: FractionalOffset.bottomRight,
          colors: [
            Color(0xff692eff).withOpacity(0.8),
            Color(0xff642cf4).withOpacity(0.8),
            Color(0xff602ae9).withOpacity(0.8),
            Color(0xff5224c8).withOpacity(0.8),
            Color(0xff5e29e5).withOpacity(0.8),
          ],
        stops: [0.0,0.25,0.5,0.75,1.0]
        ),
      ),
    ),

Which gives me somewhat close to what I want, but not entirely what I need.

Does anyone know of a way to achieve this?

EDIT:

I was also thinking about blending the two images together, but haven't found a way of doing that except using opacity or something. Ideally would like it to be rendered natively rather than using "hacks" to achieve it.

8

There are 8 best solutions below

4
On

Use Stack to Get this Effect Its Very easy.

   Stack(children: <Widget>[
        Container(
          decoration: BoxDecoration(
            color: Colors.transparent,
            image: DecorationImage(
              fit: BoxFit.fill,
              image: AssetImage(
                'images/bg.jpg',
              ),
            ),
          ),
          height: 350.0,
        ),
        Container(
          height: 350.0,
          decoration: BoxDecoration(
              color: Colors.white,
              gradient: LinearGradient(
                  begin: FractionalOffset.topCenter,
                  end: FractionalOffset.bottomCenter,
                  colors: [
                    Colors.grey.withOpacity(0.0),
                    Colors.black,
                  ],
                  stops: [
                    0.0,
                    1.0
                  ])),
        )
      ]),  

Cheers

0
On

ShaderMask Widget Class will be helpful

ShaderMask(

      // gradient layer  ----------------------------
      shaderCallback: (bound) {
        return  LinearGradient(
      end: FractionalOffset.topCenter,
      begin: FractionalOffset.bottomCenter,
      colors: [
        Colors.black.withOpacity(0.99),
        Colors.black.withOpacity(0.7),
        Colors.transparent,
      ],
      stops: [
        0.0,
        0.3,
        0.45
      ]) 
      .createShader(bound);
      },

      blendMode: BlendMode.srcOver,


      // your widget  ------------------------
      child: Container(
        height: double.infinity,
        width: double.infinity,
        child:
        Image.asset("image.png")
      ),
    );
0
On
   return Scaffold(
      body: Container(
        width: double.infinity,
        decoration: BoxDecoration(
            image: DecorationImage(
                fit: BoxFit.cover,
                image: AssetImage('assets/images/example.png')
            )
        ),
        child: OverflowBox(
          child: Container(
            decoration: BoxDecoration(
              gradient: LinearGradient(
                  begin: begin,
                  end: end,
                  colors: [
                    Color(0xFF000000),
                    Color(0x2d2d2d00)
                  ]
              ),
            ),
          ),
        ),
      ),
    );

3
On

You can try this too:

ColorFiltered(
  colorFilter: ColorFilter.mode(Colors.red.withOpacity(0.4), BlendMode.srcOver),
  child: YourWidget(),
) 
0
On

Edit

The const double scale = 2.04015; ontly works when running Flutter in browser. For mobile, use const double scale = 0;

=================================================

Three and a half years late to the party, but I noticed that none of the answers grasped to even understood what the desired effect is. So let's first analyse the wanted outcome and then code it.

Gradient Maps

This effect is known as 'Gradient Map', as individual pixel values are being mapped to a gradient based on a predefined set of colours.

Let's take a look. If an input is a grayscale image, Color color1 = Colors.blue, and Color color2 = Colors.purple, the individual pixel colors should be transformed in the following way -

Visual representation of gradient maps

Where

  • Black pixel color - transformed into blue color
  • White pixel color - transformed into purple color
  • Color in between - transformed into an appropriate color between blue and purple

Code

My initial thoughts were to take an image, decode it into Uint8List using getBytes(), process the list and finally encode it back into a png. While this approach worked, it was nowhere near performant for a realtime usecase.

Therefore, the second viable approach was to create an effect using a color matrices -

class GradientMap extends StatelessWidget {

  final Color color1;
  final Color color2;
  final double contrast;
  final ImageProvider imageProvider;

  const GradientMap({
    this.color1 = Colors.white,
    this.color2 = Colors.black,
    this.contrast = 0,
    required this.imageProvider,
    Key? key
  }) : super(key: key);


  ColorFilter grayscaleMatrix() => ColorFilter.matrix([
    0.2126,       0.7152,       0.0722,       0, 0,
    0.2126,       0.7152,       0.0722,       0, 0,
    0.2126,       0.7152,       0.0722,       0, 0,
    contrast-0.4, contrast-0.4, contrast-0.4, 1, 0,
  ]);


  ColorFilter invertMatrix() => const ColorFilter.matrix([
    -1,  0,  0, 0, 255,
     0, -1,  0, 0, 255,
     0,  0, -1, 0, 255,
     0,  0,  0, 1, 0,
  ]);


  ColorFilter tintMatrix () {
    final int r = color2.red;
    final int g = color2.green;
    final int b = color2.blue;

    final double rTint = r / 255;
    final double gTint = g / 255;
    final double bTint = b / 255;

    const double scale = 2.04015; // Use 0 on mobile instead
    const double translate = 1 - scale * 0.5;

    return ColorFilter.matrix(<double>[
      (rTint * scale), (rTint * scale), (rTint * scale), (0), (r * translate),
      (gTint * scale), (gTint * scale), (gTint * scale), (0), (g * translate),
      (bTint * scale), (bTint * scale), (bTint * scale), (0), (b * translate),
      (0            ), (0            ), (0            ), (1), (0            ),
    ]);
  }


  @override
  Widget build(BuildContext context) {
    return Container(
      color: color1,
      child: ColorFiltered(
        colorFilter: tintMatrix(),
        child: ColorFiltered(
          colorFilter: invertMatrix(),
          child: ColorFiltered(
            colorFilter: grayscaleMatrix(),
            child: Image(image: imageProvider)),
        ),
      ),
    );
  }
}

Alternatively, you can combine grayscaleMatrix() and invertMatrix() into a single one to eliminate a need for a third ColorFiltered() widget.

The widget takes as an input

  • color1 - the lighter Color
  • color2 - the darker Color
  • contrast - double blending between color1 and color2
  • imageProvider - ImageProvider such as AssetImage() or NetworkImage()

and displays the transformed image. enter image description here

Usage

Just wrap your ImageProvider with this widget in the following way -

GradientMap(
  color1: Color.fromRGBO(158, 80, 254, 1),
  color2: Color.fromRGBO(15, 4, 192, 1),
  contrast: 0,
  imageProvider: NetworkImage("https://i.imgur.com/C5xknx8.png"),
)

Drawbacks

While this approach is performant, the user does not have a control over the blending algorithm between the two input colors. Furthermore, the widget only takes two colors as an input. This eliminates a possibility to use multicolor gradients.

0
On
Container(
                  height: 35.h,
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.all(Radius.circular(6)),
                      image: DecorationImage(
                        image: AssetImage('assets/images/event_detail.png'),
                        fit: BoxFit.fill,

                      )),
                  child: Column(...

enter image description here

Blockquote If you wrap Column with Container it will see like that

Container(
                  height: 35.h,
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.all(Radius.circular(6)),
                      image: DecorationImage(
                        image: AssetImage('assets/images/event_detail.png'),
                        fit: BoxFit.fill,

                      )),
                  child: Container( //This is our new Container
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        begin: Alignment.bottomLeft,
                        end: Alignment.topRight,
                        colors: [
                          Colors.red.withOpacity(0.8),
                          Colors.red.withOpacity(0.1),
                        ],
                      )
                    ),
                    child: Column(...

enter image description here

0
On

you can wrap container inside container. This code working for me:

Container(
      decoration: const BoxDecoration(
        image: DecorationImage(
            fit: BoxFit.cover,
            image: AssetImage('assets/images/background.jpg')),
      ),
      child: Container(
        padding: const EdgeInsets.fromLTRB(12, 40, 12, 0),
        width: double.infinity,
        decoration: BoxDecoration(
          gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [Colors.black.withOpacity(0), Colors.black]),
        ),
        child: <YOUR WIDGET>
0
On

my solution was wrapping my image with shaderMask and this is an example that worked for me

ShaderMask(
  shaderCallback: (bounds) {
    return LinearGradient(
      colors: node.badge!.colorsArray,
    ).createShader(bounds);
  },
  child: Image.asset(
    Assets.main_logo,
    height: 27.0,
    fit: BoxFit.cover,
  ),
  blendMode: BlendMode.srcATop,
),

If you refer to the BlendMode documentation https://docs.flutter.io/flutter/dart-ui/BlendMode-class.html, you can actually find what you need a busy cat