Creating an Analog Clock in Flutter: IV

Philip Okonkwo
5 min readApr 12, 2018

--

The full code for the app can be found here. Other parts of the series can be found below:

Part I

Part II

Part III

Some Helping Hand

It’s time to put a hand on that clock. There are probably several ways to go about this. We could make each hand into its own widget in order to be able to handle them separately. Or we could create one clock hand widget and update the hands accordingly. We will be going the later route.

Create a new file called hand_hour.dart. Add the following code:

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();

//heart shape head for the hour hand
path.moveTo(0.0, -radius+15.0);
path.quadraticBezierTo(-3.5, -radius + 25.0, -15.0, -radius+radius/4);
path.quadraticBezierTo(-20.0, -radius+radius/3, -7.5, -radius+radius/3);
path.lineTo(0.0, -radius+radius/4);
path.lineTo(7.5, -radius+radius/3);
path.quadraticBezierTo(20.0, -radius+radius/3, 15.0, -radius+radius/4);
path.quadraticBezierTo(3.5, -radius + 25.0, 0.0, -radius+15.0);


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

This should be fairly familiar by now. We have created a new HourHandPainter class that extends the CustomPainter class and we have added logic in the paint() method to help draw the hour hand of the clock. The constructor takes two named arguments; hours and minutes. The contribution of the minutes is converted to its equivalent in hours and added to the hour to calculate the angle the hour hand should be rotated by.

Stateful Widgets

Now it is time to talk about Stateful widgets. Like we discussed earlier, stateful widgets are widgets that their look or properties change during the lifetime of the application. We are going to be making the three clock hands into one stateful widget since the rotation of the hands will be changing depending on the time of the day.

Create a new file called clock_hands.dart and add the following code:

import 'dart:async';

import 'package:clock/hand_hour.dart';
import 'package:flutter/material.dart';
class ClockHands extends StatefulWidget {
@override
_ClockHandState createState() => new _ClockHandState();
}

class _ClockHandState extends State<ClockHands> {
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();
}

@override
Widget build(BuildContext context) {
return new AspectRatio(
aspectRatio: 1.0,
child: new Container(
width: double.INFINITY,
padding: const EdgeInsets.all(20.0),
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
new CustomPaint( painter: new HourHandPainter(
hours: dateTime.hour, minutes: dateTime.minute),
),
]
)
)

);
}
}

The main difference between the Stateful and Stateless widget is that the logic for building up the Stateful widget is in another class that inherits from the State class. The initState method helps us set the initial state of the widget when it is first built. The setState method is used to update the state of the widget. In this specific example, we are using a Timer to periodically call the setState method to update the state of the widget. When the state of the widget is updated, we pass the current hour and minute to the HourHandPainter we created.

Go back to the clock_face.dart file and include the ClockHands widget we just created in the Stack that holds the ClockDialPainter. Make sure it is stacked on top of the ClockDialPainter. Run Flutter and, depending on the time, we get something like this:

We will do the same for the minute hand. Create a file named hand_minute.dart and add the following code:

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(-5.0, -radius/1.8);
path.lineTo(-2.0, 10.0);
path.lineTo(2.0, 10.0);
path.lineTo(5.0, -radius/1.8);
path.lineTo(1.5, -radius-10.0);
path.close();

canvas.drawPath(path, minuteHandPaint);
canvas.drawShadow(path, Colors.black, 4.0, false);


canvas.restore();

}

@override
bool shouldRepaint(MinuteHandPainter oldDelegate) {
return true;
}
}

As with the HourHandPainter, this MinuteHandPainter takes the minutes and seconds named arguments in its constructor and the contribution of the seconds is converted into its minutes equivalent and added to the minutes to determine the rotation of the minute hand.

While we are at it, we might as well add the a new file called hand_second.dart and add code for the second hand:

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(new Rect.fromCircle(radius: 7.0, center: new Offset(0.0, -radius)));
path2.addOval(new 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;
}
}

Now add both the minute hand and second hand to the ClockHands widget. The code becomes like this:

import 'dart:async';

import 'package:clock/hand_hour.dart';
import 'package:clock/hand_minute.dart';
import 'package:clock/hand_second.dart';
import 'package:flutter/material.dart';



class ClockHands extends StatefulWidget {
@override
_ClockHandState createState() => new _ClockHandState();
}

class _ClockHandState extends State<ClockHands> {
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();
}

@override
Widget build(BuildContext context) {
return new AspectRatio(
aspectRatio: 1.0,
child: new Container(
width: double.INFINITY,
padding: const EdgeInsets.all(20.0),
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
new CustomPaint( painter: new HourHandPainter(
hours: dateTime.hour, minutes: dateTime.minute),
),
new CustomPaint(painter: new MinuteHandPainter(
minutes: dateTime.minute, seconds: dateTime.second),
),
new CustomPaint(painter: new SecondHandPainter(seconds: dateTime.second),
),
]
)
)

);
}
}

This what we get:

A fully functional analog clock

Conclusion

Over the course of this series we have seen how to use different widgets and how to create our own. We have also seen how to create our own stateless and stateful widgets using the CustomPainter class.

Flutter is fun.

Over the course of this series we have seen how to use

--

--