effidevFlutter · Cloudflare-Edge · Cloud-Kostenoptimierung
Deutsch

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

Flutter CustomPainter Canvas API GPU High Performance Animation

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

  1. Nutzen Sie CustomPainter und Canvas API für große Mengen dynamischer Grafikelemente.
  2. Isolieren Sie Zeichenebenen mit RepaintBoundary.
  3. Überwachen Sie die Frame-Zeiten mit dem Flutter DevTools Performance Overlay auf unter 16,6 ms.