Custom Shapes and Drawing in Flutter Apps Using Painters

With Canvas and Paint we can change the look of a certain widget. That is done using a CustomPainter, which is a class that allows us to draw lines and shapes on a virtual Canvas.

CustomPaint

CustomPaint is the widget that allows us to insert the custom shapes we’ll create using a CustomPainter into the widget tree. It takes two arguments: a CustomPainter as the named painter and a child widget, which will be the widget on which the effect will be applied. For example, if we wanted to paint a Flutter logo widget using a custom painter we’ll create called LogoPainter, we’d insert the following into the widget tree:

 CustomPaint(
  painter: LogoPainter(),
  child: FlutterLogo(),
 ),

Creating a CustomPainter

The painter needs to be a subclass of the CustomPainter widget we have to define ourselves by overriding the paint and shouldRepaint methods.

The paint Method

The paint method takes two arguments: a Canvas and a Size.

The Canvas is the object on which we will perform drawing operations, such as:

  • Drawing lines, using canvas.drawLine(point1, point2, paint), where the points are represented by Offsets (that we’ll discuss later) and paint is of type Paint and decides the color and appearance of the line and will be discussed later.

  • Drawing circles using canvas.drawCircle(center, radius, paint), where center is an Offset, radius is a double and paint is a Paint object.

  • Inserting an Image using canvas.drawImage(image, point, paint), where image is the dart:ui library’s Image class, about which there will be a sidebar at the end of this section.

  • Drawing rectangles, using canvas.drawRect(rect, paint), where rect is of class Rect, about which we’re going to talk later in this section;

  • Drawing ellipses using canvas.drawOval(rect, paint), where the arguments are the same as the rectangle’s, but will result in an oval being drawn instead of a rectangle.

The Offset class is used to represent a point in the absolute virtual plane of the app screen by using two double numbers (for example (Offset(0.0, 0.0) is the origin of the virtual plane). You can choose offsets relative to various properties of the Paint widget using the properties and methods that are passed along with the Canvas to the paint method as part of the Size widget, as described in the official API reference.[52] Most of the time, you’re going to use Offset like vectors to define shapes relative to some other points using Size as we’ll see later.

The Paint class is used to define styling features (such as the color or the width) of the lines and shapes drawn on the canvas. The main ones are color and painting style: paint.color can be set to any color (for example Colors.green) and paint.style can be set to either PaintingStyle.fill or PaintingStyle.stroke, where the first one fills the shape with paint, whereas the second one only draws the edge. If you choose the latter, you may set paint.strokeWidth to any double value.

The Rect class is used to describe a rectangle in the plane. It is created from either two points (using the Rect.fromPoints(point1, point2) constructor), which will generate the smallest possible rectangle that encloses the two points, from the left and right X coordinates and top and bottom and top Y coordinates (using Rect.fromLTRB(left, top, right, bottom), where the arguments are all double values), or from a couple of edges, width and height (using Rect.fromLTWH(left, top, width, height), the arguments are all double values just like with the previous constructor). Rect.fromCircle(center, radius), with the same arguments as Canvas.drawCircle, is used to get a square from a circle.

The Size is used to generate Offset based on the size and positioning of the CustomPaint’s child. This can be done using methods such as Size.center(offset), which will return an Offset that is obtained by considering the center of the widget we’re painting as the origin of the plane and considering. Another way to see that is as the point obtained from the translation of the center of the widget by the vector offset. Other methods, such as Size.bottomRight(offset) can be used if you need to draw a point relative to other points of the widget.

The shouldRepaint Method

The shouldRepaint method has to be implemented, and it’s an important optimization factor when talking about complex and resource-intensive CustomPainters.

It returns a boolean value that tells the framework whether or not the widget needs to be repainted. It is called whenever a CustomPaint object attempts to get painted with a new instance of a CustomPainter class. This method gets the old custom painter instance as an argument, so you can compare it with the new one to decide whether anything worthy of a re-render has changed.

In our case, we’re just going to return false because the same object never gets called with different CustomPainters. For performance reasons, you should start with that and, if there are problems, try to make it more and more common that shouldRepaint returns true until all issues are fixed and the widgets get painted according to how you want them to be painted.

An Example of a CustomPainter

CustomPainters allow a great degree of customization, so they are best described with an example to show you how to put together the basic concepts we talked about, so that you can get an idea of how it’s done, keeping in mind that only the official API reference[54] can comprehensively list all of the possible transformations and shapes available:

 class​ LogoPainter ​extends​ CustomPainter {
  @override
 void​ paint(Canvas canvas, Size size) {
 var​ greenPaint = Paint()
  ..style = PaintingStyle.fill
  ..color = Colors.green;
 var​ redPaint = Paint()
  ..style = PaintingStyle.fill
  ..color = Colors.red;
 var​ whitePaint = Paint()
  ..style = PaintingStyle.fill
  ..color = Colors.white;
  canvas.drawColor(Colors.blue, BlendMode.color);
  canvas.drawOval(
  Rect.fromPoints(
  size.topLeft(Offset(-40, -100)),
  size.bottomRight(Offset(15, 10))
  ),
  greenPaint
  );
  canvas.drawOval(
  Rect.fromPoints(
  size.topLeft(
  Offset(-15, -10)
  ),
  size.bottomRight(Offset(40, 100))
  ),
  redPaint
  );
  canvas.drawCircle(size.center(Offset(0, 0)), 40.0, whitePaint);
 
  }
 
  @override
 bool​ shouldRepaint(CustomPainter old) {
 return​ ​false​;
  }
 }

This example takes advantage of cascade notation (covered in Cascade Notation), which is a way to call multiple methods on an object or alter some of its properties without having to repeat the name of the object by returning the altered object and not the result of the method (whitePaint..color=Colors.white returns whitePaint, meaning we can add another two dots with another instruction, and it will be executed on the whitePaint object).

It results in the following being drawn when painting a FlutterLogo:

images/Firebase/custompaint.png

Hero Animations Combined with CustomPaint

The combination of these canvas painting abilities with hero animation could make for interesting user interaction animation in Flutter apps: let’s imagine having two widgets similar to the one we’ve seen in the previous section, but in different colors and that serve as links to different views or different versions of the same view.

Let’s start by defining the painters: a red, green and white one just like the previous one:

 class​ GreenWhiteRedPainter ​extends​ CustomPainter {
  @override
 void​ paint(Canvas canvas, Size size) {
 var​ greenPaint = Paint();
  greenPaint.style =PaintingStyle.fill;
  greenPaint.color = Colors.green;
 var​ redPaint = Paint();
  redPaint.style =PaintingStyle.fill;
  redPaint.color = Colors.red;
 var​ whitePaint = Paint();
  whitePaint.style =PaintingStyle.fill;
  whitePaint.color = Colors.white;
  canvas.drawColor(Colors.blue, BlendMode.color);
  canvas.drawOval(
  Rect.fromPoints(
  size.topLeft(Offset(-40, -100)),
  size.bottomRight(Offset(15, 10))
  ),
  greenPaint
  );
  canvas.drawOval(
  Rect.fromPoints(
  size.topLeft(Offset(-15, -10)),
  size.bottomRight(Offset(40, 100))
  ),
  redPaint
  );
  canvas.drawCircle(size.center(Offset(0, 0)), 40.0, whitePaint);
 
  }
 
  @override
 bool​ shouldRepaint(CustomPainter old) {
 return​ ​false​;
  }
 }

and a blue and white one:

 class​ BlueWhiteBluePainter ​extends​ CustomPainter {
  @override
 void​ paint(Canvas canvas, Size size) {
 var​ bluePaint = Paint();
  bluePaint.style =PaintingStyle.fill;
  bluePaint.color = Colors.blue;
 var​ whitePaint = Paint();
  whitePaint.style =PaintingStyle.fill;
  whitePaint.color = Colors.white;
  canvas.drawColor(Colors.blue, BlendMode.color);
  canvas.drawOval(
  Rect.fromPoints(
  size.topLeft(Offset(-40, -100)),
  size.bottomRight(Offset(15, 10))
  ),
  bluePaint
  );
  canvas.drawOval(
  Rect.fromPoints(
  size.topLeft(Offset(-15, -10)),
  size.bottomRight(Offset(40, 100))
  ),
  bluePaint
  );
  canvas.drawCircle(size.center(Offset(0, 0)), 40.0, whitePaint);
 
  }
 
  @override
 bool​ shouldRepaint(CustomPainter old) {
 return​ ​false​;
  }
 }

We’ll use them in an app for two different widgets in a Row that will each pass themselves to a second view:

 class​ FirstPage ​extends​ StatelessWidget {
 
 final​ widgets = [
  Hero(
  tag: ​"Blue White Red"​,
  child: CustomPaint(
  painter: BlueWhiteBluePainter(),
  child: Text(​"1"​),
  ),
  ),
  Hero(
  tag: ​"Green White Red"​,
  child: CustomPaint(
  painter: GreenWhiteRedPainter(),
  child: Text(​"2"​),
  ),
  ),
  ];
 
  @override
  Widget build(BuildContext context) =>
  Scaffold(
  body:Center(
  child:Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: <Widget>[
  Hero(
  tag: ​"Green White Red"​,
  child: CustomPaint(
  painter: BlueWhiteBluePainter(),
  child: GestureDetector(
  child: Text(​"1"​),
  onTap: () => Navigator.pushReplacement(
  context,
  MaterialPageRoute(
  builder: (context) => SecondPage(
  widgets[0]
  )
  )
  ),
  ),
  ),
  ),
  Hero(
  tag: ​"Blue White Red"​,
  child: CustomPaint(
  painter: GreenWhiteRedPainter(),
  child: GestureDetector(
  child: Text(​"2"​),
  onTap: () => Navigator.pushReplacement(
  context,
  MaterialPageRoute(
  builder: (context) => SecondPage(
  widgets[1]
  )
  )
  ),
  ),
  ),
  ),
  ],
  )
  ),
  );
 }

that other view shows the widget in the top left corner of the screen, as a way to show what choice has been made and provides a way to go back:

 class​ SecondPage ​extends​ StatelessWidget {
  SecondPage(​this​.topLeft);
 
 final​ Widget topLeft;
 
  @override
  Widget build(BuildContext context) =>
  Scaffold(
  body: Padding(
  padding: EdgeInsets.only(left: 50.0, top: 150.0),
  child: topLeft,
  ),
  floatingActionButton: FloatingActionButton(
  child: Icon(Icons.looks_one),
  onPressed: () => Navigator.pushReplacement(
  context,
  MaterialPageRoute(
  builder: (context) => FirstPage()
  )
  ),
  ),
  );
 }

The first view will look like this:

images/Firebase/heropainthome.png

When clicking the number 1 the view will animate to show the following:

images/Firebase/heropaint1.png

and when clciking the number 2 the view will animate to show the following:

images/Firebase/heropaint2.png

Clicking the floating action button in the second page will animate to the initial page with both buttons.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset