Flutter CustomPainter と Canvas API で実現する 60fps 超高速 GPU グラフィック アニメーション構築

Flutter CustomPainter & Canvas API: 60fps 超高速 GPU グラフィックアニメーション完全攻略
Flutter でリアルタイムオーディオビジュアライザー、グラフィックチャート、パーティクルエフェクトなどを実装する際、大量の Widget を生成すると CPU ボトルネックによるフレームドロップ (Jank) が発生します。
この性能限界を打破する強力な手法が CustomPainter と Canvas API です。Flutter の最新 Impeller GPU レンダリングエンジンと組み合わせることで、CPU 負荷を最小限に抑え、GPU レベルで 60fps / 120fps の滑らかなグラフィックを描画できます。
本ガイドでは、CustomPainter のパイプライン、RepaintBoundary によるレイヤー隔離、および実戦的な 60fps パーティクルアニメーションコードを解説します。
1. Widget トリー vs CustomPainter レンダリング比較
| 比較項目 | 標準 Widget 構成 | CustomPainter & Canvas API |
|---|---|---|
| パイプライン | Widget Tree ➡️ RenderObject ➡️ Layer 分割 | 単一 RenderCustomPaint ➡️ Direct Canvas 描画 |
| 1,000要素描画 | 1,000個の Element/RenderObject 生成 | 1つの Painter 内のループで GPU 描画 |
| CPU / GPU 負荷 | CPU のレイアウト計算・構築コスト大 | 高速 GPU ラスタライズ |
| 適した用途 | フォーム、リスト、静的 UI | パーティクル、リアルタイムグラフ、ゲーム UI |
2. 60fps アニメーション実現のための3大最適化原則
原則 1: shouldRepaint の厳密な判定
shouldRepaint(covariant CustomPainter oldDelegate) メソッドでプロパティの変化を正確にチェックし、不要な再描画を回避します。
原則 2: RepaintBoundary による描画隔離
CustomPaint を RepaintBoundary で囲むことで、周囲の静的 UI レイヤーと描画層を分離し、全画面への再描画波及を防ぎます。
RepaintBoundary(
child: CustomPaint(
painter: ParticlePainter(particles: _particles),
size: Size.infinite,
),
)
原則 3: AnimationController を CustomPainter の repaint パラメータに渡す
setState() を呼び出してウィジェットツリー全体を再構築する代わりに、CustomPainter のコンストラクタが持つ repaint: Listenable パラメータに AnimationController を直接接続します。super(repaint: repaint) に渡した Listenable が値の変化を検知するたびに自動で paint() を呼び出してくれるため、リビルドなしでキャンバスレイヤーだけが毎フレーム再描画されます(なお CustomPaint の willChange はラスターキャッシュの有無を伝えるだけの単純な bool ヒントであり、Listenable を受け取るパラメータではありません)。
3. 実戦 60fps GPU パーティクルアニメーションコード
150個のパーティクルを GPU 描画する完全な Flutter サンプルコードです。
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. まとめ
- 大量の動的グラフィック描画には
CustomPainterとCanvas APIを採用する。 - 描画レイヤーを
RepaintBoundaryで分離し、不要な再描画を防ぐ。 - Flutter DevTools でフレーム描画時間が 16.6ms 以内に収まっているか確認する。