60fps High-Performance GPU-Grafikanimationen mit Flutter CustomPainter & Canvas API

Flutter CustomPainter & Canvas API: 60fps High-Performance GPU-Grafikanimationen
Beim Erstellen komplexer dynamischer Charts, Audio-Visualizer oder Partikelsysteme in Flutter führt das Instanziieren von hunderten Standard-Widgets oft zu schwerem UI-Ruckeln (Jank) durch CPU-Overhead.
Die finale Lösung dafür lautet CustomPainter in Kombination mit der Canvas API. Zusammen mit Flutters neuem Impeller GPU Rendering-Engine wird das Rebuilding des Widget-Baums umgangen und die Grafik direkt auf der GPU mit 60fps oder 120fps gerendert.
Dieser Leitfaden behandelt die Rendering-Pipeline von CustomPainter, die Ebenen-Isolierung mit RepaintBoundary sowie ein vollständiges 60fps Partikel-Animationsbeispiel.
1. Widget-Baum vs. CustomPainter Architektur
| Merkmal | Standard Widget-Struktur | CustomPainter & Canvas API |
|---|---|---|
| Pipeline | Widget Tree ➡️ RenderObject ➡️ Ebenen | Einzelnes RenderCustomPaint ➡️ Direktes Canvas Zeichnen |
| 1.000 Elemente | 1.000 Element/RenderObject Instanzen | 1 Painter führt GPU-Canvas-Befehle in einer Schleife aus |
| CPU vs. GPU | Hoher CPU-Layout/Build-Overhead | Beschleunigte GPU-Rasterisierung |
| Anwendungsfälle | Formulare, Listen, statisches UI | Partikelsysteme, Echtzeit-Charts, Spiele |
2. 3 Performance-Regeln für 60fps Animationen
Regel 1: Strikte shouldRepaint Steuerung
Die Methode shouldRepaint(covariant CustomPainter oldDelegate) entscheidet, ob die Canvas neu gezeichnet wird. Vermeiden Sie es, bedingungslos true zurückzugeben.
Regel 2: Ebenen-Isolierung mit RepaintBoundary
Das Umschließen des CustomPaint-Widgets mit einem RepaintBoundary isoliert die Zeichenebene von anderen Widgets.
RepaintBoundary(
child: CustomPaint(
painter: ParticlePainter(particles: _particles),
size: Size.infinite,
),
)
Regel 3: AnimationController an den repaint-Parameter von CustomPainter übergeben
Statt setState() aufzurufen und den gesamten Widget-Baum neu zu erstellen, verbinden Sie den AnimationController direkt mit dem repaint: Listenable-Parameter des CustomPainter-Konstruktors. Das an super(repaint: repaint) übergebene Listenable löst bei jeder Änderung automatisch paint() aus, sodass ohne Rebuild pro Frame nur die Canvas-Ebene neu gezeichnet wird. (Zur Klarstellung: willChange von CustomPaint ist lediglich ein einfacher bool-Hinweis für den Raster-Cache und akzeptiert kein Listenable.)
3. 60fps GPU-Partikelanimation Beispielcode
Ein vollständiges Flutter-Beispiel, das 150 Partikel auf einer GPU-Canvas mit 60fps animiert:
import 'dart:math';
import 'package:flutter/material.dart';
class Particle {
double x, y, dx, dy, radius;
Color color;
Particle({
required this.x,
required this.y,
required this.dx,
required this.dy,
required this.radius,
required this.color,
});
void update(Size size) {
x += dx;
y += dy;
if (x < 0 || x > size.width) dx = -dx;
if (y < 0 || y > size.height) dy = -dy;
}
}
class ParticleCanvasScreen extends StatefulWidget {
const ParticleCanvasScreen({super.key});
@override
State<ParticleCanvasScreen> createState() => _ParticleCanvasScreenState();
}
class _ParticleCanvasScreenState extends State<ParticleCanvasScreen>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final List<Particle> _particles = [];
final Random _random = Random();
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat();
for (int i = 0; i < 150; i++) {
_particles.add(Particle(
x: _random.nextDouble() * 400,
y: _random.nextDouble() * 800,
dx: (_random.nextDouble() - 0.5) * 3,
dy: (_random.nextDouble() - 0.5) * 3,
radius: _random.nextDouble() * 4 + 2,
color: Colors.indigoAccent.withValues(alpha: _random.nextDouble() * 0.7 + 0.3),
));
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0F172A),
body: RepaintBoundary(
child: CustomPaint(
painter: ParticlePainter(particles: _particles, repaint: _controller),
size: Size.infinite,
),
),
);
}
}
class ParticlePainter extends CustomPainter {
final List<Particle> particles;
ParticlePainter({required this.particles, required Listenable repaint})
: super(repaint: repaint);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..style = PaintingStyle.fill;
for (var particle in particles) {
particle.update(size);
paint.color = particle.color;
canvas.drawCircle(Offset(particle.x, particle.y), particle.radius, paint);
}
}
@override
bool shouldRepaint(covariant ParticlePainter oldDelegate) => true;
}
4. Fazit
- Nutzen Sie
CustomPainterundCanvas APIfür große Mengen dynamischer Grafikelemente. - Isolieren Sie Zeichenebenen mit
RepaintBoundary. - Überwachen Sie die Frame-Zeiten mit dem Flutter DevTools Performance Overlay auf unter 16,6 ms.