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; } }