effidevFlutter · Edge de Cloudflare · Optimización de costes en la nube
Español

Creación de Animaciones Gráficas GPU a 60fps con Flutter CustomPainter y Canvas API

Flutter CustomPainter Canvas API GPU High Performance Animation

Animaciones GPU a 60fps con Flutter CustomPainter y Canvas API

Al desarrollar visualizadores de audio, gráficos dinámicos o sistemas de partículas en Flutter, instanciar cientos de objetos Widget estándar suele causar problemas severos de rendimiento (jank) debido a la carga en la CPU.

La solución definitiva consiste en utilizar CustomPainter y la Canvas API. Combinados con el motor de renderizado Impeller GPU de Flutter, permiten evitar la reconstrucción del árbol de widgets y dibujar gráficos directamente en la GPU a 60fps o 120fps.

Esta guía examina la canalización de renderizado de CustomPainter, el aislamiento de capas con RepaintBoundary y una implementación completa de partículas a 60fps.


1. Árbol de Widgets vs Arquitectura CustomPainter

Métrica Composición Estándar de Widgets CustomPainter & Canvas API
Canalización Widget Tree ➡️ RenderObject ➡️ Capas RenderCustomPaint único ➡️ Dibujo Canvas Directo
Carga de 1.000 elementos 1.000 instancias Element/RenderObject 1 Painter ejecutando instrucciones en bucle GPU
Carga CPU vs GPU Alto coste de diseño en CPU Rasterización acelerada por GPU
Casos de uso Formularios, listas, UI estática Sistemas de partículas, gráficos en tiempo real

2. 3 Reglas de Rendimiento para Animaciones a 60fps

Regla 1: Control Estricto en shouldRepaint

El método shouldRepaint(covariant CustomPainter oldDelegate) determina si es necesario redibujar el lienzo. Evite devolver true sin comprobar si las propiedades han cambiado realmente.

Regla 2: Aislamiento de Capas con RepaintBoundary

Envolver el widget CustomPaint con un RepaintBoundary aísla la capa de pintado respecto al resto de widgets contenedores.

RepaintBoundary(
  child: CustomPaint(
    painter: ParticlePainter(particles: _particles),
    size: Size.infinite,
  ),
)

Regla 3: Pasar AnimationController al parámetro repaint de CustomPainter

En lugar de llamar a setState() y reconstruir todo el árbol de widgets, conecte el AnimationController directamente al parámetro repaint: Listenable del constructor de CustomPainter. El Listenable que pasa a super(repaint: repaint) invoca automáticamente paint() cada vez que cambia, de modo que solo se repinta la capa del lienzo en cada frame, sin necesidad de reconstruir nada. (Tenga en cuenta que willChange de CustomPaint es solo una simple pista bool para la caché de rasterizado; no acepta un Listenable.)


3. Código Práctico de Animación de Partículas en GPU

Ejemplo completo en Flutter ejecutando 150 partículas a 60fps:

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. Conclusión

  1. Utilice CustomPainter y Canvas API para dibujar grandes volúmenes de elementos dinámicos.
  2. Aísle las capas con RepaintBoundary.
  3. Supervise el tiempo de renderizado con Flutter DevTools para asegurar tiempos por frame inferiores a 16.6ms.