skip to Main Content

I would like to darken parts of the CustomPainter by applying another CustomPainter on-top of it while not affecting layers below it.

The basic structure would be as follows:

Stack(
  children: [
    CustomPaint(
      painter: BottomPaint(), // unaffected
      child: const SizedBox.expand(),
    ),
    CustomPaint(
      painter: MiddlePaint(), // should be partially darkened
      child: const SizedBox.expand(),
    ),
    CustomPaint(
      painter: TopPaint(), // shape here should be applied as darkening mask to MiddlePaint
      child: const SizedBox.expand(),
    ),
  ],
),

The end result should look more or less like this where the dark area is achieved by drawing a rectangle either in MiddlePaint or TopPaint.

enter image description here

I’ve been testing various blend modes to achieve it, along with canvas.saveLayer() but none of the approaches I’ve tried so far is satisfactory.

Currently the blend modes approach lets me darken both layers in the Stack like in the example below:

enter image description here

Is there a way to darken only one CustomPainter by drawing rectangles either in a separate CustomPainter or within the same one?

Full example:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Stack(
          children: [
            CustomPaint(
              painter: BottomPaint(), // should be unaffected
              child: const SizedBox.expand(),
            ),
            CustomPaint(
              painter:
                  MiddlePaint(), // should be darkened where TopPaint is applied
              child: const SizedBox.expand(),
            ),
            CustomPaint(
              painter:
                  TopPaint(), // shape here should be applied as darkening mask to MiddlePaint
              child: const SizedBox.expand(),
            ),
          ],
        ),
      ),
    );
  }
}

class BottomPaint extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final fullRect = Offset.zero & size;
    canvas.saveLayer(fullRect, Paint());
    canvas.drawRect(
      Rect.fromLTWH(0, size.height / 3, size.width, size.height / 3),
      Paint()..color = Colors.red,
    );
    canvas.restore();
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

class MiddlePaint extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final fullRect = Offset.zero & size;

    final path = Path()
      ..moveTo(0, size.height)
      ..lineTo(size.width / 2, size.height / 2)
      ..lineTo(size.width, size.height)
      ..lineTo(0, size.height);
    canvas.drawPath(
      path,
      Paint()..color = Colors.blue,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

class TopPaint extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final numberOfSections = BlendMode.values.length;
    final sectionWidth = size.width / numberOfSections;
    final sectionHeight = size.height / 2;

    for (var i = 0; i < BlendMode.values.length; i++) {
      final blendMode = BlendMode.values[i];
      final x = i * sectionWidth;
      final y = size.height - sectionHeight;
      final rect = Rect.fromLTWH(x, y, sectionWidth, sectionHeight);
      canvas.drawRect(
        rect,
        Paint()
          ..color = Colors.black26
          ..blendMode = blendMode,
      );

      printName(blendMode, canvas, x, sectionWidth, y, sectionHeight);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

/// name of the blend mode below
void printName(
  BlendMode blendMode,
  Canvas canvas,
  double x,
  double sectionWidth,
  double y,
  double sectionHeight,
) {
  final textPainter = TextPainter(
    text: TextSpan(
      text: blendMode.name,
      style: const TextStyle(color: Colors.white),
    ),
    textDirection: TextDirection.ltr,
  );
  textPainter.layout();
  textPainter.paint(
    canvas,
    Offset(
      x + sectionWidth / 2 - textPainter.width / 2,
      y + sectionHeight - 20,
    ),
  );
}

2

Answers


  1. Chosen as BEST ANSWER

    Here's a solution suggested by Renan that uses SingleChildRenderObjectWidget to paint both painters in a single layer, and the TopPaint paints itself into a layer with BlendMode.srcATop.

    enter image description here

    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key, required this.title});
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Center(
            child: Stack(
              children: [
                CustomPaint(
                  painter: BottomPaint(),
                  child: const SizedBox.expand(),
                ),
                Layer(
                  children: [
                    CustomPaint(
                      painter: MiddlePaint(),
                      child: const SizedBox.expand(),
                    ),
                    CustomPaint(
                      painter: TopPaint(),
                      child: const SizedBox.expand(),
                    ),
                  ],
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class BottomPaint extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
        final fullRect = Offset.zero & size;
    
        canvas.drawRect(
          Rect.fromLTWH(0, size.height / 3, size.width, size.height / 3),
          Paint()..color = Colors.red,
        );
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) {
        return true;
      }
    }
    
    class MiddlePaint extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
        final path = Path()
          ..moveTo(0, size.height)
          ..lineTo(size.width / 2, size.height / 2)
          ..lineTo(size.width, size.height)
          ..lineTo(0, size.height);
        canvas.drawPath(
          path,
          Paint()..color = Colors.blue,
        );
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) {
        return true;
      }
    }
    
    class TopPaint extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
        canvas.saveLayer(
          Offset.zero & size,
          Paint()..blendMode = BlendMode.srcATop,
        );
    
        // canvas.drawRect(
        //   (Offset.zero & Size(size.width / 2, size.height)),
        //   Paint()..color = Colors.black26,
        // );
    
        const numberOfSections = 10;
        final sectionHeight = size.height / 2;
        final sectionWidth = size.width / numberOfSections;
    
        for (var i = 0; i < 10; i = i + 2) {
          final x = i * sectionWidth;
          final y = size.height - sectionHeight;
          final rect = Rect.fromLTWH(x, y, sectionWidth, sectionHeight);
          canvas.drawRect(rect, Paint()..color = Colors.black26);
        }
    
        canvas.restore();
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) {
        return true;
      }
    }
    
    class Layer extends StatelessWidget {
      const Layer({super.key, required this.children});
    
      final List<Widget> children;
    
      @override
      Widget build(BuildContext context) {
        return SaveLayerWidget(child: Stack(children: children));
      }
    }
    
    class SaveLayerWidget extends SingleChildRenderObjectWidget {
      const SaveLayerWidget({Key? key, required Widget child})
          : super(key: key, child: child);
    
      @override
      RenderObject createRenderObject(BuildContext context) {
        return _SaveLayerRenderObject();
      }
    }
    
    class _SaveLayerRenderObject extends RenderProxyBox {
      @override
      void paint(PaintingContext context, Offset offset) {
        final Paint paint = Paint();
        final Rect rect = offset & size;
        context.canvas.saveLayer(rect, paint);
        super.paint(context, offset);
        context.canvas.restore();
      }
    }
    
    /// name of the blend mode below
    void printName(
      BlendMode blendMode,
      Canvas canvas,
      double x,
      double sectionWidth,
      double y,
      double sectionHeight,
    ) {
      final textPainter = TextPainter(
        text: TextSpan(
          text: blendMode.name,
          style: const TextStyle(color: Colors.white),
        ),
        textDirection: TextDirection.ltr,
      );
      textPainter.layout();
      textPainter.paint(
        canvas,
        Offset(
          x + sectionWidth / 2 - textPainter.width / 2,
          y + sectionHeight - 20,
        ),
      );
    }
    

  2. Applying a ShaderMask on both MiddlePaint and TopPaint should allow you to achieve the desired effect. The colors of the gradient in ShaderMask don’t matter. We just use them to create a solid layer. What matters is the blend mode, which should be any mode that will only paint the destination image for example:
    blendMode: BlendMode.dstIn or blendMode: BlendMode.dst

    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key, required this.title});
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Center(
            child: Stack(
              children: [
                CustomPaint(
                  painter: BottomPaint(), // should be unaffected
                  child: const SizedBox.expand(),
                ),
                ColorBlendMask(
                  color: Colors.black,
                  blendMode: BlendMode.dstIn,
                  child: Stack(
                    children: [
                      CustomPaint(
                        painter:
                            MiddlePaint(), // should be darkened where TopPaint is applied
                        child: const SizedBox.expand(),
                      ),
                      CustomPaint(
                        painter:
                            TopPaint(), // shape here should be applied as darkening mask to MiddlePaint
                        child: const SizedBox.expand(),
                      ),
                    ],
                  ),
                )
              ],
            ),
          ),
        );
      }
    }
    

    The ColorBlendMask widget is just a wrapper around ShaderMask:

    class ColorBlendMask extends StatelessWidget {
      const ColorBlendMask({
        super.key,
        required this.child,
        required this.color,
        required this.blendMode,
      });
    
      final Widget child;
      final Color color;
      final BlendMode blendMode;
    
      @override
      Widget build(BuildContext context) {
        return ShaderMask(
          blendMode: blendMode,
          shaderCallback: (bounds) => LinearGradient(
            colors: [color, color],
            stops: const [0.0, 1.0],
          ).createShader(bounds),
          child: child,
        );
      }
    }
    

    The blend mode for TopPaint layer should be:

        canvas.drawRect(
          rect,
     Paint()
            ..color = Colors.black26
     ..blendMode = BlendMode.srcATop,
        );
    

    final result should look like this:

    result

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search