Flutter Custom Shapes and Paths: Drawing Geometry-Based Widgets
Have you ever looked at a design mockup and thought, "How do I create that custom shape in Flutter?" While Flutter provides many built-in widgets, sometimes you need to draw custom geometric shapes that don't fit into standard rectangles and circles. That's where Flutter's powerful custom painting system comes in.
In this article, we'll explore how to create custom geometric shapes using Flutter's CustomPainter and Path classes. Whether you want to draw triangles, hexagons, stars, or completely custom shapes, you'll learn the fundamentals to bring your unique designs to life.
Understanding the Basics: CustomPainter and Canvas
At the heart of custom shape drawing in Flutter is the CustomPainter class. Think of it as your artist's canvas where you can draw anything you imagine. The CustomPainter works with a Canvas object that provides methods for drawing lines, shapes, and paths.
Here's a simple example to get started:
import 'package:flutter/material.dart';
class TrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
final path = Path();
path.moveTo(size.width / 2, 0); // Top point
path.lineTo(0, size.height); // Bottom left
path.lineTo(size.width, size.height); // Bottom right
path.close(); // Connect back to start
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
To use this painter, wrap it in a CustomPaint widget:
CustomPaint(
painter: TrianglePainter(),
size: Size(100, 100),
)
Working with Paths: Your Drawing Tool
The Path class is your primary tool for defining custom shapes. It works like connecting dots on paper - you move to a point, draw lines to other points, and create curves. Let's explore the essential Path methods:
Basic Path Operations
moveTo() - Moves the "pen" to a specific point without drawing
lineTo() - Draws a straight line from the current point to a new point
close() - Connects the last point back to the first point, creating a closed shape
Here's a practical example creating a hexagon:
class HexagonPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.purple
..style = PaintingStyle.fill
..strokeWidth = 2;
final path = Path();
final centerX = size.width / 2;
final centerY = size.height / 2;
final radius = size.width / 2;
// Calculate hexagon points (6 points in a circle)
for (int i = 0; i < 6; i++) {
final angle = (i * 60 - 90) * (3.14159 / 180); // Convert to radians
final x = centerX + radius * cos(angle);
final y = centerY + radius * sin(angle);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Don't forget to import dart:math for the trigonometric functions:
import 'dart:math' as math;
// Then use math.cos() and math.sin()
Creating Curved Shapes
Paths aren't limited to straight lines. Flutter provides several methods for creating smooth curves:
quadraticBezierTo() - Quadratic Bezier Curves
This method creates a curved line using a control point. Think of it as bending a straight line toward a point:
class CurvedShapePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.green
..style = PaintingStyle.fill;
final path = Path();
path.moveTo(0, size.height / 2);
path.quadraticBezierTo(
size.width / 2, 0, // Control point (creates the curve)
size.width, size.height / 2, // End point
);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
cubicBezierTo() - Cubic Bezier Curves
For more complex curves, use cubic Bezier curves with two control points:
path.cubicBezierTo(
controlPoint1X, controlPoint1Y, // First control point
controlPoint2X, controlPoint2Y, // Second control point
endPointX, endPointY, // End point
);
Practical Example: Creating a Star Shape
Let's combine what we've learned to create a star shape. Stars are great examples because they require both straight lines and precise calculations:
class StarPainter extends CustomPainter {
final Color color;
StarPainter({this.color = Colors.amber});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final path = Path();
final centerX = size.width / 2;
final centerY = size.height / 2;
final outerRadius = size.width / 2;
final innerRadius = outerRadius * 0.4; // Inner points are closer to center
// A 5-pointed star has 10 points total (5 outer, 5 inner)
for (int i = 0; i < 10; i++) {
final angle = (i * 36 - 90) * (math.pi / 180);
final radius = i.isEven ? outerRadius : innerRadius;
final x = centerX + radius * math.cos(angle);
final y = centerY + radius * math.sin(angle);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(StarPainter oldDelegate) => oldDelegate.color != color;
}
Notice how we alternate between outer and inner radii to create the star's points. The shouldRepaint method now checks if the color changed, which is more efficient than always repainting.
Advanced Techniques: Clipping and Masks
Sometimes you want to use a custom shape as a mask or clip for other widgets. Flutter provides ClipPath for this purpose:
class HexagonClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
final centerX = size.width / 2;
final centerY = size.height / 2;
final radius = size.width / 2;
for (int i = 0; i < 6; i++) {
final angle = (i * 60 - 90) * (math.pi / 180);
final x = centerX + radius * math.cos(angle);
final y = centerY + radius * math.sin(angle);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
// Usage:
ClipPath(
clipper: HexagonClipper(),
child: Image.network('https://example.com/image.jpg'),
)
Optimization Tips
When working with custom shapes, performance matters. Here are some best practices:
- Cache your paths: If your shape doesn't change, create the Path once and reuse it:
class OptimizedPainter extends CustomPainter {
static Path? _cachedPath;
@override
void paint(Canvas canvas, Size size) {
_cachedPath ??= _createPath(size);
// Use _cachedPath
}
}
- Implement shouldRepaint correctly: Only return true when your shape actually needs to be redrawn:
@override
bool shouldRepaint(MyPainter oldDelegate) {
return oldDelegate.color != color ||
oldDelegate.size != size;
}
- Use const constructors when possible: If your CustomPaint widget doesn't need to change, make it const:
const CustomPaint(
painter: TrianglePainter(),
size: Size(100, 100),
)
Real-World Example: Custom Badge Shape
Let's create a practical example - a custom badge shape that could be used for notifications or labels:
class BadgeShapePainter extends CustomPainter {
final Color backgroundColor;
final double cornerRadius;
BadgeShapePainter({
this.backgroundColor = Colors.red,
this.cornerRadius = 8.0,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = backgroundColor
..style = PaintingStyle.fill;
final path = Path();
// Rounded rectangle with a small triangle at the bottom
path.moveTo(cornerRadius, 0);
path.lineTo(size.width - cornerRadius, 0);
path.quadraticBezierTo(size.width, 0, size.width, cornerRadius);
path.lineTo(size.width, size.height - cornerRadius - 10);
path.lineTo(size.width / 2 + 5, size.height - 10);
path.lineTo(size.width / 2, size.height);
path.lineTo(size.width / 2 - 5, size.height - 10);
path.lineTo(0, size.height - cornerRadius - 10);
path.quadraticBezierTo(0, 0, cornerRadius, 0);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(BadgeShapePainter oldDelegate) {
return oldDelegate.backgroundColor != backgroundColor ||
oldDelegate.cornerRadius != cornerRadius;
}
}
// Usage with text:
Stack(
children: [
CustomPaint(
painter: BadgeShapePainter(backgroundColor: Colors.red),
size: Size(80, 30),
),
Center(
child: Text(
'NEW',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
)
Common Patterns and Shapes
Here are some quick reference patterns for common geometric shapes:
Diamond/Rhombus
path.moveTo(size.width / 2, 0);
path.lineTo(size.width, size.height / 2);
path.lineTo(size.width / 2, size.height);
path.lineTo(0, size.height / 2);
path.close();
Arrow
path.moveTo(0, size.height / 2);
path.lineTo(size.width * 0.7, size.height / 2);
path.lineTo(size.width * 0.7, 0);
path.lineTo(size.width, size.height / 2);
path.lineTo(size.width * 0.7, size.height);
path.lineTo(size.width * 0.7, size.height / 2);
path.close();
Heart Shape
final path = Path();
path.moveTo(size.width / 2, size.height * 0.7);
path.cubicBezierTo(
size.width / 2, size.height * 0.5,
size.width * 0.1, size.height * 0.3,
size.width * 0.1, size.height * 0.5,
);
path.cubicBezierTo(
size.width * 0.1, size.height * 0.7,
size.width / 2, size.height * 0.9,
size.width / 2, size.height * 0.9,
);
path.cubicBezierTo(
size.width / 2, size.height * 0.9,
size.width * 0.9, size.height * 0.7,
size.width * 0.9, size.height * 0.5,
);
path.cubicBezierTo(
size.width * 0.9, size.height * 0.3,
size.width / 2, size.height * 0.5,
size.width / 2, size.height * 0.7,
);
Conclusion
Creating custom geometric shapes in Flutter opens up endless possibilities for unique UI designs. By mastering CustomPainter, Path, and the various drawing methods, you can bring any geometric concept to life in your Flutter applications.
Remember to start simple with basic shapes like triangles and rectangles, then gradually work your way up to more complex curves and multi-point shapes. Don't forget to optimize your painters by caching paths and implementing efficient shouldRepaint methods.
The key is practice - experiment with different shapes, play with curves, and don't be afraid to combine multiple techniques. With these tools at your disposal, you're well-equipped to create stunning custom shapes that make your Flutter apps stand out.
Happy drawing!