Mastering Flutter Canvas: Drawing Basics, Shapes, Text Effects, and Animated Waves
This comprehensive guide walks you through Flutter's drawing fundamentals, covering Canvas, Paint, Path, Color, and advanced techniques such as custom text outlines, image shaders, and animated wave effects, with clear code examples and practical tips for creating dynamic graphics in mobile apps.
Flutter Drawing (Basic Application)
In mobile development, drawing is essential for buttons, animations, and custom graphics. This article introduces the core concepts of Flutter's drawing API, including Canvas, Paint, Path, and Color, and shows how to use them to create various visual effects.
Drawing API
All examples are based on Flutter 2.2. The Dart UI library can be found at https://api.flutter-io.cn/flutter/dart-ui/dart-ui-library.html .
Drawing Elements
Canvas : The drawing surface that records operations.
Paint : Describes style information such as color, stroke width, and shader.
Path : A collection of lines and curves that define shapes.
Color : Immutable 32‑bit ARGB values.
Canvas
Canvas provides methods like drawLine , drawRect , drawPath , clipRect , save , and restore . It also maintains a current transformation matrix and clipping region.
Paint
Paint is used to describe how shapes are rendered. Important properties include color , strokeWidth , style , blendMode , shader , and others.
Path
A Path consists of sub‑paths made of line, quadratic, cubic, and conic segments. Methods such as moveTo , lineTo , quadraticBezierTo , and close build the geometry.
Color
Colors can be created with constructors like Color(0xFF42A5F5) or Color.fromARGB(255, 66, 165, 245) . Be careful to provide eight hex digits; otherwise the color becomes fully transparent.
Simple Applications
Drawing Points and Lines
class Paper extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
alignment: Alignment.center,
child: CustomPaint(painter: MyPainter()),
);
}
}
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Paint paintLine = Paint()
..color = Colors.black
..strokeWidth = 3;
canvas.drawLine(Offset(-100, 0), Offset(100, 0), paintLine);
canvas.drawLine(Offset(0, 100), Offset(0, -100), paintLine);
final Paint paintPoint = Paint()
..color = Colors.red;
canvas.drawCircle(Offset(0, 0), 10, paintPoint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}Drawing a Five‑Pointed Star
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = Colors.red
..strokeWidth = 3
..style = PaintingStyle.stroke;
final Path path = Path();
path.moveTo(0, -100);
path.lineTo(59, 81);
path.lineTo(-95, -31);
path.lineTo(95, -31);
path.lineTo(-59, 81);
path.lineTo(0, -100);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}Text Outline Using Paint
Text(
'BillionBottle',
style: TextStyle(
fontSize: 100,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6
..color = Colors.blue[700]!,
),
),
Text(
'BillionBottle',
style: TextStyle(
fontSize: 100,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),Image Shader for Text Stroke
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 14
..color = Colors.black
..shader = ImageShader(
_image,
TileMode.repeated,
TileMode.repeated,
Float64List.fromList([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),
);Wave Loading Effect
Wave animations are common loading indicators. By using quadratic Bézier curves and animating the canvas translation, a smooth moving wave can be created.
Basic Wave with Bézier Curve
final double waveWidth = 80;
final double waveHeight = 40;
void paint(Canvas canvas, Size size) {
canvas.translate(ScreenUtils.screenWidth / 2, ScreenUtils.screenHeight / 2);
final Paint paint = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
final Path path = Path();
path.relativeQuadraticBezierTo(waveWidth / 2, -waveHeight * 2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth / 2, waveHeight * 2, waveWidth, 0);
canvas.drawPath(path, paint);
}Animated Wave Using AnimationController
class _PaperState extends State<Paper> with SingleTickerProviderStateMixin {
late final AnimationController _controllerX;
@override
void initState() {
super.initState();
_controllerX = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controllerX.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint(painter: MyPainter(_controllerX)),
);
}
}
class MyPainter extends CustomPainter {
final Animation<double> repaintX;
const MyPainter(this.repaintX) : super(repaint: repaintX);
@override
void paint(Canvas canvas, Size size) {
canvas.translate(ScreenUtils.screenWidth / 2, ScreenUtils.screenHeight / 2);
canvas.translate(2 * waveWidth * repaintX.value, 0);
// draw wave path (same as above)
}
@override
bool shouldRepaint(covariant MyPainter oldDelegate) =>
oldDelegate.repaintX != repaintX;
}Clipping the Wave Inside a Rectangle
canvas.clipRect(Rect.fromCenter(
center: Offset(waveWidth * 3, -60),
width: waveWidth * 2,
height: 200.0,
));Two‑Axis Animation with TickerProviderStateMixin
class _PaperState extends State<Paper2> with TickerProviderStateMixin {
late final AnimationController _controllerX;
late final AnimationController _controllerY;
@override
void initState() {
super.initState();
_controllerX = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
)..repeat();
_controllerY = AnimationController(
duration: const Duration(milliseconds: 5000),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controllerX.dispose();
_controllerY.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint(
painter: MyPainter(
CurveTween(curve: Curves.linear).animate(_controllerX),
_controllerY,
),
),
);
}
}Applying the Wave to a Logo
The final example combines background and foreground waves, clipping, and custom text to create a logo with a moving water effect.
// Inside MyPainter.paint()
canvas.translate(ScreenUtils.screenWidth / 2, ScreenUtils.screenHeight / 2);
canvas.clipRect(Rect.fromCenter(
center: Offset(waveWidth, -60),
width: waveWidth * 2,
height: 200.0,
));
// draw background wave (faster)
canvas.save();
canvas.translate(-4 * waveWidth * repaintX.value, -220 * repaintY.value);
canvas.drawPath(path, paint..color = Colors.orange.withAlpha(88));
canvas.restore();
// draw foreground wave
canvas.save();
canvas.translate(-2 * waveWidth * repaintX.value, -220 * repaintY.value);
canvas.drawPath(path, paint..color = Colors.red);
canvas.restore();
// draw clipped text inside the wave area
_drawTextWithParagraph(canvas, TextAlign.center, Colors.white);Conclusion
This article introduced the basic and advanced usage of Flutter's drawing APIs, including Canvas, Paint, Path, Color, image shaders, and animation controllers. By mastering these tools you can create custom graphics, loading animations, and sophisticated visual effects for your mobile applications.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
BaiPing Technology
Official account of the BaiPing app technology team. Dedicated to enhancing human productivity through technology. | DRINK FOR FUN!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
