This tutorial gives a introduction into developing a custom paint widget in Flutter.
1. Implementing a custom paint widget
A custom paint widget is an widget which takes a painter and takes a customer painter to execute paint commands.
The painter is an instance of the CustomPainter
class.
You can either use the painter
attribute which is executed before the child is drawn or the foregroundPainter
which is executed after the child is drawn (hence you draw on top of the child).
CustomPaint(
foregroundPainter: MyCustomPainer(),
child: SomeWidget(),
)
The implementation of CustomPainter
must implement two functions.
-
paint - Gets a canvas and a size and does the drawing
-
shouldRepaint - tells Flutter if redrawing is required, for example if the input of the widget change, it might tell Flutter to redraw itself
class MyCustomPainer extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(Offset(75, 75), 50, Paint());
canvas.drawLine(Offset(200, 200), Offset(20, 40), Paint());
canvas.drawRect(Rect.fromPoints(Offset.zero, Offset(50, 50)), Paint());
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
For example, the following code will generate the image below
Widget buildCustomPainer(BuildContext context) {
return CustomPaint(
child: Center(
child: Container(
color: Colors.red,
width: 200,
height: 200,
),
),
foregroundPainter: MyCustomPainer(),
);
}
}
class MyCustomPainer extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(Offset(75, 75), 50, Paint());
Paint p = Paint()
..style = PaintingStyle.stroke
..color = Colors.blue
..strokeWidth = 10;
canvas.drawLine(Offset(200, 200), Offset(20, 40), p);
canvas.drawRect(Rect.fromPoints(Offset(300, 300), Offset(50, 50)), p);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
2. Exercise: Implementing a custom painted widget
2.1. Creating the app frame
In this exercise you develop a clock as custom drawn widget.
This exercise is a based on the blog series from https://medium.com/@NPKompleet/creating-an-analog-clock-in-flutter-iv-3995d914c86e It has been updated to recent Dart API. |
Run flutter create clock
in this directory.
We use a few new widgets here:
-
AspectRatio - its child will always follow the defined aspectRadio, e.g., if 1.0 is given the widget and height will be the same
-
Stack - allow to stack widget on top of each other
Create the following widget to get your work started:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Clock',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
actions: <Widget>[
IconButton(
icon: Icon(Icons.ac_unit),
onPressed: null,
),
],
),
body: ClockFrame(),
),
);
}
}
class ClockFrame extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 40.0, right: 40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AspectRatio(
aspectRatio: 1.0,
child: Stack(children: <Widget>[
Container(
width: double.infinity,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black,
boxShadow: [
BoxShadow(
offset: Offset(0.0, 5.0),
blurRadius: 5.0,
)
],
),
),
Placeholder(),
]))
],
),
);
}
}
2.2. Create the painter
Create hands_hour.dart
file with the following widget.
import 'dart:math';
import 'package:flutter/material.dart';
class HourHandPainter extends CustomPainter {
final Paint hourHandPaint;
int hours;
int minutes;
HourHandPainter({this.hours, this.minutes}) : hourHandPaint = new Paint() {
hourHandPaint.color = Colors.black87;
hourHandPaint.style = PaintingStyle.fill;
}
@override
void paint(Canvas canvas, Size size) {
final radius = size.width / 2;
// To draw hour hand
canvas.save();
canvas.translate(radius, radius);
//checks if hour is greater than 12 before calculating rotation
canvas.rotate(this.hours >= 12
? 2 * pi * ((this.hours - 12) / 12 + (this.minutes / 720))
: 2 * pi * ((this.hours / 12) + (this.minutes / 720)));
Path path = new Path();
//hour hand stem
path.moveTo(-1.0, -radius + radius / 4);
path.lineTo(-5.0, -radius + radius / 2);
path.lineTo(-2.0, 0.0);
path.lineTo(2.0, 0.0);
path.lineTo(5.0, -radius + radius / 2);
path.lineTo(1.0, -radius + radius / 4);
path.close();
canvas.drawPath(path, hourHandPaint);
canvas.drawShadow(path, Colors.black, 2.0, false);
canvas.restore();
}
@override
bool shouldRepaint(HourHandPainter oldDelegate) {
return true;
}
}
Create hands_minute.dart
file with the following widget.
import 'dart:math';
import 'package:flutter/material.dart';
class MinuteHandPainter extends CustomPainter {
final Paint minuteHandPaint;
int minutes;
int seconds;
MinuteHandPainter({this.minutes, this.seconds})
: minuteHandPaint = new Paint() {
minuteHandPaint.color = const Color(0xFF333333);
minuteHandPaint.style = PaintingStyle.fill;
}
@override
void paint(Canvas canvas, Size size) {
final radius = size.width / 2;
canvas.save();
canvas.translate(radius, radius);
canvas.rotate(2 * pi * ((this.minutes + (this.seconds / 60)) / 60));
Path path = new Path();
path.moveTo(-1.5, -radius - 10.0);
path.lineTo(-2.0, 10.0);
path.lineTo(2.0, 8.0);
path.close();
canvas.drawPath(path, minuteHandPaint);
canvas.drawShadow(path, Colors.black, 4.0, false);
canvas.restore();
}
@override
bool shouldRepaint(MinuteHandPainter oldDelegate) {
return true;
}
}
Create hand_second.dart
file with the following widget.
import 'dart:math';
import 'package:flutter/material.dart';
class SecondHandPainter extends CustomPainter {
final Paint secondHandPaint;
final Paint secondHandPointsPaint;
int seconds;
SecondHandPainter({this.seconds})
: secondHandPaint = new Paint(),
secondHandPointsPaint = new Paint() {
secondHandPaint.color = Colors.red;
secondHandPaint.style = PaintingStyle.stroke;
secondHandPaint.strokeWidth = 2.0;
secondHandPointsPaint.color = Colors.red;
secondHandPointsPaint.style = PaintingStyle.fill;
}
@override
void paint(Canvas canvas, Size size) {
final radius = size.width / 2;
canvas.save();
canvas.translate(radius, radius);
canvas.rotate(2 * pi * this.seconds / 60);
Path path1 = new Path();
Path path2 = new Path();
path1.moveTo(0.0, -radius);
path1.lineTo(0.0, radius / 4);
path2.addOval(
Rect.fromCircle(radius: 7.0, center: new Offset(0.0, -radius)));
path2.addOval(Rect.fromCircle(radius: 5.0, center: new Offset(0.0, 0.0)));
canvas.drawPath(path1, secondHandPaint);
canvas.drawPath(path2, secondHandPointsPaint);
canvas.restore();
}
@override
bool shouldRepaint(SecondHandPainter oldDelegate) {
return this.seconds != oldDelegate.seconds;
}
}
Create the clock_dial_painter.dart
file with the following content:
import 'dart:math';
import 'package:flutter/material.dart';
class ClockDialPainter extends CustomPainter {
final clockText;
final hourTickMarkLength = 10.0;
final minuteTickMarkLength = 5.0;
final hourTickMarkWidth = 3.0;
final minuteTickMarkWidth = 1.5;
final Paint tickPaint;
final TextPainter textPainter;
final TextStyle textStyle;
final romanNumeralList = [
'XII',
'I',
'II',
'III',
'IV',
'V',
'VI',
'VII',
'VIII',
'IX',
'X',
'XI'
];
ClockDialPainter({this.clockText = ClockText.roman})
: tickPaint = new Paint(),
textPainter = new TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.rtl,
),
textStyle = const TextStyle(
color: Colors.black,
fontFamily: 'Times New Roman',
fontSize: 15.0,
) {
tickPaint.color = Colors.blueGrey;
}
@override
void paint(Canvas canvas, Size size) {
var tickMarkLength;
final angle = 2 * pi / 60;
final radius = size.width / 2;
canvas.save();
// drawing
canvas.translate(radius, radius);
for (var i = 0; i < 60; i++) {
//make the length and stroke of the tick marker longer and thicker depending
tickMarkLength = i % 5 == 0 ? hourTickMarkLength : minuteTickMarkLength;
tickPaint.strokeWidth =
i % 5 == 0 ? hourTickMarkWidth : minuteTickMarkWidth;
canvas.drawLine(new Offset(0.0, -radius),
new Offset(0.0, -radius + tickMarkLength), tickPaint);
//draw the text
if (i % 5 == 0) {
canvas.save();
canvas.translate(0.0, -radius + 20.0);
textPainter.text = new TextSpan(
text: this.clockText == ClockText.roman
? '${romanNumeralList[i ~/ 5]}'
: '${i == 0 ? 12 : i ~/ 5}',
style: textStyle,
);
//helps make the text painted vertically
canvas.rotate(-angle * i);
textPainter.layout();
textPainter.paint(canvas,
new Offset(-(textPainter.width / 2), -(textPainter.height / 2)));
canvas.restore();
}
canvas.rotate(angle);
}
canvas.restore();
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
enum ClockText { roman, arabic }
Create clock_face.dart
with the following content.
import 'dart:async';
import 'dart:math';
import 'package:clock/clock_dial_painter.dart';
import 'package:clock/clock_hands.dart';
import 'package:flutter/material.dart';
class ClockFace extends StatefulWidget {
@override
_ClockFaceState createState() => _ClockFaceState();
}
class _ClockFaceState extends State<ClockFace> {
Timer _timer;
DateTime dateTime;
@override
void initState() {
super.initState();
dateTime = new DateTime.now();
_timer = new Timer.periodic(const Duration(seconds: 1), setTime);
}
void setTime(Timer timer) {
setState(() {
dateTime = new DateTime.now();
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
// DateTime dateTime = calculateRandomTime();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10.0),
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
width: double.infinity,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
child: GestureDetector(
onTap: () {
Scaffold.of(context).removeCurrentSnackBar();
var snackebar = createSnackBar(dateTime);
Scaffold.of(context).showSnackBar(snackebar);
},
// onDoubleTap: () {
// setState(() {
// dateTime = calculateRandomTime();
// });
// },
child: Stack(
children: <Widget>[
//dial and numbers go here
new Container(
width: double.infinity,
height: double.infinity,
padding: const EdgeInsets.all(10.0),
child: new CustomPaint(
painter: new ClockDialPainter(clockText: ClockText.arabic),
),
),
//clock hands go here
// the point in the middle
Centerpoint(),
ClockHands(dateTime: dateTime),
],
),
),
),
),
);
}
}
SnackBar createSnackBar(DateTime date) {
TimeOfDay time = TimeOfDay.fromDateTime(date);
int myhour = time.hour == 0 ? 12 : time.hour;
var snackbar = SnackBar(
content: Text("$myhour:${time.minute}"),
action: SnackBarAction(
label: 'Done',
onPressed: () {},
),
);
return snackbar;
}
class Centerpoint extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 15.0,
height: 15.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.black,
),
),
);
}
}
DateTime calculateRandomTime() {
int newHour = Random().nextInt(13);
var newMinute = Random().nextInt(61);
var datae = DateTime.now();
DateTime time = datae.toLocal();
time = new DateTime(
time.year, time.month, time.day, newHour, newMinute, 0, 0, 0);
return time;
}
Finally the Placeholder in ClockFrame with it your ClockFace
widget.
3. Links and Literature
3.3. vogella Flutter and Dart example code
If you need more assistance we offer Online Training and Onsite training as well as consulting