Flutter, Google’s UI toolkit, allows developers to create natively compiled applications for mobile, web, and desktop from a single codebase. One of Flutter’s most powerful features is its custom painting capabilities, enabling developers to create unique and dynamic UIs. This blog post will guide you through creating animated custom paintings that respond to user interaction and time, bringing your Flutter apps to life.
What is Custom Painting in Flutter?
Custom painting in Flutter involves using the CustomPaint widget combined with a CustomPainter class to draw directly onto the canvas. This method offers unparalleled control over UI elements, allowing for complex shapes, animations, and interactive components not achievable with standard widgets.
Why Use Animated Custom Paintings?
- Unique UI Design: Create distinctive and branded user interfaces.
- Performance: Optimize rendering by painting directly on the canvas.
- Interactive Elements: Implement animations and reactions to user input or time, enhancing the user experience.
Setting Up a Custom Painter
To start, create a new Flutter project or navigate to an existing one. Then, create a CustomPainter class:
import 'package:flutter/material.dart';
import 'dart:math' as math;
class AnimatedPainter extends CustomPainter {
final Animation<double> animation;
AnimatedPainter({required this.animation}) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final radius = math.min(size.width, size.height) / 3;
final paint = Paint()
..color = Colors.blue.withOpacity(0.5 + 0.5 * animation.value)
..style = PaintingStyle.fill;
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Key aspects of this class:
- Extends CustomPainter: Inherits the painting capabilities.
- Animation Parameter: Takes an
Animation<double>object to drive the animation. - Paint Method: Draws a circle with a radius that changes based on the animation value.
- shouldRepaint Method: Returns
trueto repaint the canvas on every animation frame.
Implementing the Animation Controller
Next, create a StatefulWidget to manage the animation controller:
import 'package:flutter/material.dart';
class AnimatedPaintingExample extends StatefulWidget {
@override
_AnimatedPaintingExampleState createState() => _AnimatedPaintingExampleState();
}
class _AnimatedPaintingExampleState extends State<AnimatedPaintingExample> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..repeat(reverse: true);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animated Custom Painting'),
),
body: Center(
child: CustomPaint(
size: Size(300, 300),
painter: AnimatedPainter(animation: _controller),
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Explanation:
- StatefulWidget: Manages the animation controller.
- AnimationController: Controls the animation’s lifecycle.
- initState: Initializes the animation controller and sets it to repeat in reverse.
- CustomPaint Widget: Uses the
AnimatedPainterto paint on the canvas. - dispose: Disposes of the animation controller to prevent memory leaks.
Making the Painting Interactive
To make the painting interactive, you can use a GestureDetector:
import 'package:flutter/material.dart';
import 'dart:math' as math;
class InteractivePainter extends CustomPainter {
final Offset touchPoint;
final bool isTouched;
InteractivePainter({required this.touchPoint, required this.isTouched});
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final radius = math.min(size.width, size.height) / 3;
final paint = Paint()
..color = isTouched ? Colors.red : Colors.green
..style = PaintingStyle.fill;
canvas.drawCircle(center, radius, paint);
if (isTouched) {
final textPainter = TextPainter(
text: TextSpan(
text: 'Touched!',
style: TextStyle(color: Colors.white),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, touchPoint - Offset(textPainter.width / 2, textPainter.height / 2));
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class InteractivePaintingExample extends StatefulWidget {
@override
_InteractivePaintingExampleState createState() => _InteractivePaintingExampleState();
}
class _InteractivePaintingExampleState extends State<InteractivePaintingExample> {
Offset _touchPoint = Offset.zero;
bool _isTouched = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Interactive Custom Painting'),
),
body: GestureDetector(
onTapDown: (details) {
setState(() {
_isTouched = true;
_touchPoint = details.localPosition;
});
},
onTapUp: (details) {
setState(() {
_isTouched = false;
});
},
onTapCancel: () {
setState(() {
_isTouched = false;
});
},
child: Center(
child: CustomPaint(
size: Size(300, 300),
painter: InteractivePainter(touchPoint: _touchPoint, isTouched: _isTouched),
),
),
),
);
}
}
Key aspects of making the painting interactive:
- GestureDetector: Detects tap events.
- onTapDown: Updates the state to indicate the painting is touched.
- InteractivePainter: Changes the circle’s color when touched and displays the text.
Advanced Animations
Using Curves for Easing Effects
Enhance your animations by incorporating curves. These provide more natural-looking transitions. Modify the AnimationController and Tween to include a Curve.
import 'package:flutter/material.dart';
import 'dart:math' as math;
class AdvancedAnimatedPainter extends CustomPainter {
final Animation<double> animation;
AdvancedAnimatedPainter({required this.animation}) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final radius = 50 + 30 * animation.value; // Varying radius with animation
final angle = 2 * math.pi * animation.value; // Rotating angle
final paint = Paint()
..color = Color.fromRGBO(
(100 + 155 * animation.value).toInt(),
(50 + 205 * animation.value).toInt(),
(0 + 255 * animation.value).toInt(),
1,
)
..style = PaintingStyle.fill;
// Save the current canvas state
canvas.save();
// Translate and rotate the canvas
canvas.translate(center.dx, center.dy);
canvas.rotate(angle);
// Draw the rectangle
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: radius, height: radius), paint);
// Restore the canvas to its previous state
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class AdvancedAnimatedPaintingExample extends StatefulWidget {
@override
_AdvancedAnimatedPaintingExampleState createState() => _AdvancedAnimatedPaintingExampleState();
}
class _AdvancedAnimatedPaintingExampleState extends State<AdvancedAnimatedPaintingExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
);
// Using Curves for more natural-looking animations
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
..addListener(() {
setState(() {}); // Rebuild the widget on animation updates
});
_controller.repeat(reverse: true);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Advanced Animated Painting'),
),
body: Center(
child: CustomPaint(
size: const Size(300, 300),
painter: AdvancedAnimatedPainter(animation: _animation),
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Responding to Time (Clock Example)
Custom painters can be used to create clock-like UIs that respond to the current time.
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'dart:async';
class ClockPainter extends CustomPainter {
final DateTime dateTime;
ClockPainter({required this.dateTime});
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final radius = math.min(size.width, size.height) / 2;
final hourHandPaint = Paint()
..color = Colors.black
..strokeWidth = 4
..strokeCap = StrokeCap.round;
final minuteHandPaint = Paint()
..color = Colors.black
..strokeWidth = 3
..strokeCap = StrokeCap.round;
final secondHandPaint = Paint()
..color = Colors.red
..strokeWidth = 2
..strokeCap = StrokeCap.round;
// Hour hand
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.rotate(-math.pi / 2 + 2 * math.pi / 12 * (dateTime.hour + dateTime.minute / 60));
canvas.drawLine(Offset.zero, Offset(0, -radius * 0.5), hourHandPaint);
canvas.restore();
// Minute hand
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.rotate(-math.pi / 2 + 2 * math.pi / 60 * dateTime.minute);
canvas.drawLine(Offset.zero, Offset(0, -radius * 0.7), minuteHandPaint);
canvas.restore();
// Second hand
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.rotate(-math.pi / 2 + 2 * math.pi / 60 * dateTime.second);
canvas.drawLine(Offset.zero, Offset(0, -radius * 0.9), secondHandPaint);
canvas.restore();
// Center circle
final centerCirclePaint = Paint()..color = Colors.black;
canvas.drawCircle(center, 5, centerCirclePaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class ClockExample extends StatefulWidget {
@override
_ClockExampleState createState() => _ClockExampleState();
}
class _ClockExampleState extends State<ClockExample> {
DateTime _dateTime = DateTime.now();
late Timer _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (Timer t) {
setState(() {
_dateTime = DateTime.now();
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Clock Example'),
),
body: Center(
child: SizedBox(
width: 300,
height: 300,
child: CustomPaint(
painter: ClockPainter(dateTime: _dateTime),
),
),
),
);
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
}
By using these components, you can create an analogue clock that updates in real-time.
Conclusion
Creating animated custom paintings in Flutter allows developers to craft highly engaging and visually appealing applications. By using custom painters, animation controllers, and interactive elements, you can design unique UI components that respond to user interactions or time, taking your Flutter apps to the next level. Whether it’s simple animations or complex interactive UIs, custom painting offers a powerful toolset to bring your creative visions to life.