Flutter Custom Paint Tutorial | Build a Radial Progress

RJS Tech
6 min readJun 4, 2018

--

Flutter is on the rolls nowadays! Like everything related to UI, Flutter makes custom drawing easy too. We will be building a radial progress, sort of a radial dial (and add some animations too!).

This will be what we are going to build in the end.

I am assuming you know the basics of Flutter.. though I will be trying to explain the basics as much as I can along the way. So, lets jump right in!

import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';void main() => runApp(new MyApp());class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Experiments',
theme: new ThemeData(
),
home: new Home(),
debugShowCheckedModeBanner: false,
);
}
}

This is what gets the app fired up, MyApp is our root widget which returns a MaterialApp, lets go ahead and define the Home widget, what we mentioned in the home property of the MaterialApp.

class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
elevation: 0.0,
title: new Text("Flutter Experiments",
style: new TextStyle(
color: Colors.white,
fontFamily: 'Nunito',
letterSpacing: 1.0
),
),
backgroundColor: new Color(0xFF2979FF),
centerTitle: true
),
body:new HomeContent()
);
}
}

Now I have a Scaffold,if you are new to flutter, Scaffold manages the layout of general components of a material design app. I have a AppBar with a blue background and “Flutter Experiments” written on it in white. I have also added a font called Nunito, you can easily get it on Google Fonts. Now see we need to define the Widget HomeContent,that’s the body property of our Scaffold.

Now HomeContent has to be a StatefulWidget, because we need to maintain how much percentage the progress has been completed.

class HomeContent extends StatefulWidget {
@override
_HomeContentState createState() => _HomeContentState();
}
class _HomeContentState extends State<HomeContent>{ double percentage; @override
void initState() {
super.initState();
setState(() {
percentage = 0.0;
});

}
@override
Widget build(BuildContext context) {
return new Center(
child: new Container(
height: 200.0,
width: 200.0,
child: new CustomPaint(
foregroundPainter: new MyPainter(
lineColor: Colors.amber,
completeColor: Colors.blueAccent,
completePercent: percentage,
width: 8.0
),
child: new Padding(
padding: const EdgeInsets.all(8.0),
child: new RaisedButton(
color: Colors.purple,
splashColor: Colors.blueAccent,
shape: new CircleBorder(),
child: new Text("Click"),
onPressed: (){
setState(() {
percentage += 10.0;
if(percentage>100.0){
percentage=0.0;
}
});
}),
),
),
),
);
}
}

So, here I initialize the percentage at 0.0 and every time the button is clicked I increased the percentage by 10.0. Regarding the UI, I have got a CustomPaint, I will go over its details just in moment, as its child, I have a RaisedButton surrounded with some Padding and some nice colours. Also, see the shape property is CircleBorder , which makes our button circular. See, here is something quite awesome, CustomPaint is something we use to draw custom graphics, like a canvas… yet we can put in traditional Widgets as its child, making it way more useful than what we have in other platforms. In the above code, lets look at the interesting part where we actually have our “Painter”.

new CustomPaint(
foregroundPainter: new MyPainter(
lineColor: Colors.amber,
completeColor: Colors.blueAccent,
completePercent: percentage,
width: 8.0
),

In this snippet, notice we have opened a CustomPaint widget, and set the foregroundPainter property to an instance of MyPainter which we will define, let’s not worry about the properties now, we will take a look at it next. Lets understand what foregroundPainter means. The property could be painter too, foregroundPainter means that the graphics would be drawn over its child widget, setting the paint property would do the opposite.

Now lets declare the heart of the App, the MyPainter class.

class MyPainter extends CustomPainter{  Color lineColor;
Color completeColor;
double completePercent;
double width;
MyPainter({this.lineColor,this.completeColor,this.completePercent,this.width});
@override
void paint(Canvas canvas, Size size) {
Paint line = new Paint()
..color = lineColor
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = width;
Paint complete = new Paint()
..color = completeColor
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = width;
Offset center = new Offset(size.width/2, size.height/2);
double radius = min(size.width/2,size.height/2);
canvas.drawCircle(
center,
radius,
line
);
double arcAngle = 2*pi* (completePercent/100); canvas.drawArc(
new Rect.fromCircle(center: center,radius: radius),
-pi/2,
arcAngle,
false,
complete
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

Here we receive the various properties lineColor means the colour for the track, on which the progress would be drawn, completeColor for the progress completed, completePercent for the percentage of the progress completed. From its parent widget we send down this properties, here we just draw.

MyPainter extends the class CustomPainter and we have to override its paint method to draw the graphics. To draw, at first we have to declare something called Paint, it encapsulates all the properties that is needed to draw something on the screen. So we have two Paint objects, the first one is line which represents the track line on which the progress would be drawn. We give it a colour, that is supplied as a property, the strokeCap is round and we tell the style to be PaintingStyle.stoke as, we don’t need a filled circle we just need a stroke, then set the width to the supplied one. Similarly we declare the complete Paint object for the progress completed.

So, at first we need to draw a full circle and then an arc on it, that’s completed.

Offset center  = new Offset(size.width/2, size.height/2);
double radius = min(size.width/2,size.height/2);

In these two lines, we calculate the coordinates for the centre of the circle and the radius, its simple, the centre is width and height of the container divided by 2, we want the centre of the circles exactly at the middle. size is what we get as a parameter in the paint function, which is the size of the container. radius is the minimum of the half of the width and the height, now we are taking a minimum of both as the container might not be a square always.

canvas.drawCircle(
center,
radius,
line
);

Now we simply draw with the drawCirlce function called on the suppkied canvas object, where we pass in the center,the radius, and the Paintobject.

double arcAngle = 2*pi* (completePercent/100);    canvas.drawArc(
new Rect.fromCircle(center: center,radius: radius),
-pi/2,
arcAngle,
false,
complete
);

Now, we calculate the angle for the completed arc, which is shown. I am not going into the geometric calculation. To call drawArc we have to specify a Rect, which is the bounding box of the arc, we get the Rect surrounding the circle parameters we drew earlier. Then we give a start angle, which is -pi/2radians, keep in mind its not 0. The top is -pi/2, 0 is the right-most point of the circle. We supply in the arcAngle then, which is how much the arc should extend too. We pass in false after that to tell that we don’t want the end of the arc to be connected back to the centre and at last we send in the Paintobject, complete. That’s all we needed to draw using CustomPaint.

Now, let add the animations.

We need to edit the HomeContent widget.

class HomeContent extends StatefulWidget {
@override
_HomeContentState createState() => _HomeContentState();
}
class _HomeContentState extends State<HomeContent> with TickerProviderStateMixin { double percentage = 0.0;
double newPercentage = 0.0;
AnimationController percentageAnimationController;
@override
void initState() {
super.initState();
setState(() {
percentage = 0.0;
});
percentageAnimationController = new AnimationController(
vsync: this,
duration: new Duration(milliseconds: 1000)
)
..addListener((){
setState(() {
percentage = lerpDouble(percentage,newPercentage,percentageAnimationController.value);
});
});
} @override
Widget build(BuildContext context) {
return new Center(
child: new Container(
height: 200.0,
width: 200.0,
child: new CustomPaint(
foregroundPainter: new MyPainter(
lineColor: Colors.amber,
completeColor: Colors.blueAccent,
completePercent: percentage,
width: 8.0
),
child: new Padding(
padding: const EdgeInsets.all(8.0),
child: new RaisedButton(
color: Colors.purple,
splashColor: Colors.blueAccent,
shape: new CircleBorder(),
child: new Text("Click"),
onPressed: (){
setState(() {
percentage = newPercentage;
newPercentage += 10;
if(newPercentage>100.0){
percentage=0.0;
newPercentage=0.0;
}
percentageAnimationController.forward(from: 0.0);
});
}),
),
),
),
);
}
}

Focus at the interesting part

percentageAnimationController = new AnimationController(
vsync: this,
duration: new Duration(milliseconds: 1000)
)
..addListener((){
setState(() {
percentage = lerpDouble(percentage,newPercentage,percentageAnimationController.value);
});
});

We have added a listener on the AnimationController which is called at every step of the animation, and we change the percentage. To change the percentage, we get an interpolated value with the help of lerpDoublefunction, the value is within percentage and newPercentage which is 10 more than the original value, we interpolate it on the basis of the AnimationController's value.

We add this line in the onPressed of the button, to kick of the animation.

percentageAnimationController.forward(from: 0.0);

Thats what we need to build this simple piece of UI, which can be used at many places!

If you liked what you read, please leave some claps!

--

--

RJS Tech

Game & App developer in Kolkata, India. 20+ million users across the world. We enjoy discussing technology.