【Flutter】大量のAnimationを実装する時の負荷について
経緯
Flutter で複数の Animation を実装する際に、どのくらいの数が限度なのか気になった為、確認してみた。
実装する Animation
シンプルに数字のテキストを左右に動かします。
final controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,// with TickerProviderStateMixin
)..repeat(reverse: true);
final animation = Tween<double>(begin: 0.0, end: 100.0).animate(controller);
AnimatedBuilder(
animation: _animations,
builder: (context, child) {
return Transform.translate(
offset: Offset(_animations.value, 0),
child: Text('1')
);
},
);
大量の Animation を配置する
ここで 2 種類の手法を使います。
この 2 種類ではレンダリング手法が違うため、違う挙動が見られると思われるため。
- ListView による実装
- Column による実装
まずは共通の AnimationController,Animation が必要なので、
for 文で count 個の controller,animation をリストに入れます。
※今回は同一の Animation を使っていますが、想定は異なる Animation です。
final List<AnimationController> _controllers = [];
final List<Animation<double>> _animations = [];
void initState() {
super.initState();
for (int i = 0; i < count; i++) {
final controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
)..repeat(reverse: true);
final animation = Tween<double>(begin: 0.0, end: 100.0).animate(controller);
_controllers.add(controller);
_animations.add(animation);
}
}
1. ListView による実装
ListView では、現在画面上に表示されているアイテムのみがレンダリングされる。
ListView.builder(
itemCount: _animations.length,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _animations[index],
builder: (context, child) {
return Transform.translate(
offset: Offset(_animations[index].value, 0),
child: Text('${index + 1}')
);
},
);
},
),
2. Column による実装
Column,SingleChildScrollView で実装すると、すべてのアイテム(画面外も含む)が一度にレンダリングされる。
List.generate()でリストを作成し、Column に配置
SingleChildScrollView(
child: Column(
children: List.generate(_animations.length, (index) {
return Row(
children: [
Expanded(
child: AnimatedBuilder(
animation: _animations[index],
builder: (context, child) {
return Transform.translate(
offset: Offset(_animations[index].value, 0),
child: Text('${index + 1}'),
);
},
),
),
],
);
}),
),
),
動作結果
ListView を使った場合は、画面内しかレンダリングされないので、表示される Animation 数により負荷が変動する。
一方、Column の場合は、一度にすべてレンダリングされるので設定した Animation 数により負荷が増す。
私の環境、M2 MacBook air では ListView ではカクつきはなく、Column の 3000 で少々カクつきました。
また、スマートフォン Pixel 6a では Column の 1000 で少々、3000 で大きなカクつきが見られました。
結論的には、多様な実行環境を踏まえて一度にレンダリングする Animation 数 の限度は 500 ぐらいではないでしょうか。
検証サンプル
↓ 下記リンクより ListView と Column で Animation 数による負荷の差を確認できます。
追記 2023/12/9
テキストではなく画像を動かした場合の負荷も検証が必要と思い、追加で検証した。
結果的にはテキストも画像も視覚的には同様な結果となった。画像は約 500KB
また、上記の検証ページに column image を追加した。
コードサンプル
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
final int count; // 100 or 1000 or 3000
const ListViewPage({super.key, required this.count});
ListViewPageState createState() => ListViewPageState();
}
class ListViewPageState extends State<ListViewPage> with TickerProviderStateMixin {
final List<AnimationController> _controllers = [];
final List<Animation<double>> _animations = [];
void initState() {
super.initState();
for (int i = 0; i < widget.count; i++) {
final controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
)..repeat(reverse: true);
final animation = Tween<double>(begin: 0.0, end: 100.0).animate(controller);
_controllers.add(controller);
_animations.add(animation);
}
}
void dispose() {
for (var controller in _controllers) {
controller.dispose();
}
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(
'ListView:${widget.count}',
)),
body: ListView.builder(
itemCount: _animations.length,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _animations[index],
builder: (context, child) {
return Transform.translate(
offset: Offset(_animations[index].value, 0),
child: Text('${index + 1}'),
);
},
);
},
),
);
}
}
import 'package:flutter/material.dart';
class ColumnPage extends StatefulWidget {
final int count; // 100 or 1000 or 3000
const ColumnPage({super.key, required this.count});
ColumnPageState createState() => ColumnPageState();
}
class ColumnPageState extends State<ColumnPage> with TickerProviderStateMixin {
final List<AnimationController> _controllers = [];
final List<Animation<double>> _animations = [];
void initState() {
super.initState();
for (int i = 0; i < widget.count; i++) {
final controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
)..repeat(reverse: true);
final animation = Tween<double>(begin: 0.0, end: 100.0).animate(controller);
_controllers.add(controller);
_animations.add(animation);
}
}
void dispose() {
for (var controller in _controllers) {
controller.dispose();
}
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(
'column:${widget.count}',
)),
body: SingleChildScrollView(
child: Column(
children: List.generate(_animations.length, (index) {
return Row(
children: [
Expanded(
child: AnimatedBuilder(
animation: _animations[index],
builder: (context, child) {
return Transform.translate(
offset: Offset(_animations[index].value, 0),
child: Text('${index + 1}'),
);
},
),
),
],
);
}),
),
),
);
}
}
Discussion