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,
),
);
}
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.
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,
),
);
}