CustomPainter is an interface used by CustomPaint and RenderCustomPaint. This interface is the solution when we need to create a highly customized user interface.
Table of Contents
Draw a shape
We use CustomPaint to draw on.

CustomPaint(
painter: CenterCircle(),
child: Center(
child: Text('Loading...'),
),
)
class CenterCircle extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.red
..strokeWidth = 3
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
Offset center = Offset(size.width / 2, size.height / 2);
canvas.drawCircle(center, 100, paint);
}
@override
bool shouldRepaint(CenterCircle oldDelegate) => false;
}
Profile Card
Here is a Profile card with 3 layers: blue background, darker blue wave, hole for profile image.

Container(
width: MediaQuery.of(context).size.width,
height: 200,
child: CustomPaint(
painter: ProfileCard(
circleWidth: 64.0,
),
),
)
class ProfileCard extends CustomPainter {
final circleWidth;
ProfileCard({this.circleWidth});
@override
void paint(Canvas canvas, Size size) {
var fillPaint = Paint()
..color = Colors.blue
..strokeWidth = 1
..style = PaintingStyle.fill
..strokeCap = StrokeCap.round;
var wavePaint = Paint()
..color = Colors.blue[900].withOpacity(0.1)
..strokeWidth = 1
..style = PaintingStyle.fill
..strokeCap = StrokeCap.round;
Path path = Path();
path.moveTo(0, size.height);
path.cubicTo(size.width * 1/4, size.height * 1/4, size.width / 2, size.height / 2, size.width, 0);
path.lineTo(size.width, size.height);
var holePaint = Paint()
..color = Colors.lightBlue
..strokeWidth = 1
..style = PaintingStyle.fill
..strokeCap = StrokeCap.round;
Offset holeOffset = Offset(size.width / 2, size.height - circleWidth / 6);
canvas.drawRect(Rect.fromLTRB(0, 0, size.width, size.height), fillPaint);
canvas.drawPath(path, wavePaint );
canvas.drawCircle(holeOffset, circleWidth, holePaint);
}
@override
bool shouldRepaint(ProfileCard oldDelegate) => false;
@override
bool shouldRebuildSemantics(ProfileCard oldDelegate) => false;
}
Wave Animation
Combining with the AnimatedBuilder widget, we can animate canvas drawings.

WaveContainer(
width: double.infinity,
height: 100,
waveColor: Colors.green)
class WaveContainer extends StatefulWidget {
final Duration duration;
final double height;
final double width;
final Color waveColor;
const WaveContainer({
Key key,
this.duration,
@required this.height,
@required this.width,
this.waveColor,
}) : super(key: key);
@override
_WaveContainerState createState() => _WaveContainerState();
}
class _WaveContainerState extends State<WaveContainer>
with TickerProviderStateMixin {
AnimationController _animationController;
Duration _duration;
Color _waveColor;
@override
void initState() {
super.initState();
_duration = widget.duration ?? const Duration(milliseconds: 1000);
_animationController = AnimationController(vsync: this, duration: _duration);
_waveColor = widget.waveColor ?? Colors.lightBlueAccent;
_animationController.repeat();
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
child: AnimatedBuilder(
animation: _animationController,
builder: (BuildContext context, Widget child) {
return CustomPaint(
painter: WavePainter(
waveAnimation: _animationController, waveColor: _waveColor),
);
},
),
);
}
@override
void dispose() {
_animationController.stop();
_animationController.dispose();
super.dispose();
}
}
class WavePainter extends CustomPainter {
Animation<double> waveAnimation;
Color waveColor;
WavePainter({this.waveAnimation, this.waveColor});
@override
void paint(Canvas canvas, Size size) {
Path path = Path();
for (double i = 0.0; i < size.width; i++) {
path.lineTo(i,
sin((i / size.width * 2 * pi) + (waveAnimation.value * 2 * pi)) * 4);
}
path.lineTo(size.width, size.height);
path.lineTo(0.0, size.height);
path.close();
Paint wavePaint = Paint()..color = waveColor;
canvas.drawPath(path, wavePaint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
