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
.
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:
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
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.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
The ColorBlendMask widget is just a wrapper around ShaderMask:
The blend mode for TopPaint layer should be:
final result should look like this: