Skip to content

アニメーション

Flutterアニメーション完全ガイド

Section titled “Flutterアニメーション完全ガイド”

Flutterでは、豊富なアニメーションAPIを提供しており、美しく滑らかなアニメーションを簡単に実装できます。ここでは、基本的なアニメーションから高度な実装まで、実務で使えるパターンを詳しく解説します。

最もシンプルなアニメーションウィジェットです。プロパティの変更が自動的にアニメーションされます。

class AnimatedContainerExample extends StatefulWidget {
@override
_AnimatedContainerExampleState createState() => _AnimatedContainerExampleState();
}
class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
bool _selected = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_selected = !_selected;
});
},
child: AnimatedContainer(
width: _selected ? 200.0 : 100.0,
height: _selected ? 100.0 : 200.0,
color: _selected ? Colors.red : Colors.blue,
duration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_selected ? 20.0 : 0.0),
),
),
);
}
}

透明度のアニメーションを実装します。

class AnimatedOpacityExample extends StatefulWidget {
@override
_AnimatedOpacityExampleState createState() => _AnimatedOpacityExampleState();
}
class _AnimatedOpacityExampleState extends State<AnimatedOpacityExample> {
double _opacity = 1.0;
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedOpacity(
opacity: _opacity,
duration: Duration(seconds: 1),
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
),
ElevatedButton(
onPressed: () {
setState(() {
_opacity = _opacity == 1.0 ? 0.0 : 1.0;
});
},
child: Text('Toggle Opacity'),
),
],
);
}
}

2. AnimationControllerを使用したアニメーション

Section titled “2. AnimationControllerを使用したアニメーション”

より細かい制御が必要な場合は、AnimationControllerを使用します。

class AnimationControllerExample extends StatefulWidget {
@override
_AnimationControllerExampleState createState() => _AnimationControllerExampleState();
}
class _AnimationControllerExampleState extends State<AnimationControllerExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animation,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
);
}
}

複数のアニメーションを組み合わせる

Section titled “複数のアニメーションを組み合わせる”
class CombinedAnimationExample extends StatefulWidget {
@override
_CombinedAnimationExampleState createState() => _CombinedAnimationExampleState();
}
class _CombinedAnimationExampleState extends State<CombinedAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.5,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * 3.14159,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
);
},
);
}
}

3. 各種アニメーションの実装例

Section titled “3. 各種アニメーションの実装例”
class FadeAnimationExample extends StatefulWidget {
@override
_FadeAnimationExampleState createState() => _FadeAnimationExampleState();
}
class _FadeAnimationExampleState extends State<FadeAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
);
}
}
class SlideAnimationExample extends StatefulWidget {
@override
_SlideAnimationExampleState createState() => _SlideAnimationExampleState();
}
class _SlideAnimationExampleState extends State<SlideAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _slideAnimation,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
);
}
}
class RotationAnimationExample extends StatefulWidget {
@override
_RotationAnimationExampleState createState() => _RotationAnimationExampleState();
}
class _RotationAnimationExampleState extends State<RotationAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _controller,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
}
}
class ScaleAnimationExample extends StatefulWidget {
@override
_ScaleAnimationExampleState createState() => _ScaleAnimationExampleState();
}
class _ScaleAnimationExampleState extends State<ScaleAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
);
}
}

画面間でウィジェットを共有する際に使用するアニメーションです。

// 画面1
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: Center(
child: Hero(
tag: 'hero-image',
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
child: Icon(Icons.arrow_forward),
),
);
}
}
// 画面2
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: Hero(
tag: 'hero-image',
child: Container(
width: 300,
height: 300,
color: Colors.blue,
),
),
),
);
}
}

5. PageRouteBuilderを使用したカスタムページ遷移

Section titled “5. PageRouteBuilderを使用したカスタムページ遷移”
class CustomPageRoute extends PageRouteBuilder {
final Widget child;
CustomPageRoute({required this.child})
: super(
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (context, animation, secondaryAnimation) => child,
);
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return SlideTransition(
position: Tween<Offset>(
begin: Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
)),
child: child,
);
}
}
// 使用例
Navigator.push(
context,
CustomPageRoute(child: SecondScreen()),
);

6. 実務でのアニメーション実装パターン

Section titled “6. 実務でのアニメーション実装パターン”

パターン1: ローディングアニメーション

Section titled “パターン1: ローディングアニメーション”
class LoadingAnimation extends StatefulWidget {
@override
_LoadingAnimationState createState() => _LoadingAnimationState();
}
class _LoadingAnimationState extends State<LoadingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _controller,
child: CircularProgressIndicator(),
);
}
}

パターン2: リストアイテムのアニメーション

Section titled “パターン2: リストアイテムのアニメーション”
class AnimatedListExample extends StatelessWidget {
final List<String> items = List.generate(10, (index) => 'Item $index');
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300 + (index * 50)),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 50 * (1 - value)),
child: child,
),
);
},
child: ListTile(
title: Text(items[index]),
),
);
},
);
}
}

パターン3: ボタンのタップアニメーション

Section titled “パターン3: ボタンのタップアニメーション”
class AnimatedButton extends StatefulWidget {
final VoidCallback onPressed;
final Widget child;
AnimatedButton({required this.onPressed, required this.child});
@override
_AnimatedButtonState createState() => _AnimatedButtonState();
}
class _AnimatedButtonState extends State<AnimatedButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 100),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => _controller.forward(),
onTapUp: (_) {
_controller.reverse();
widget.onPressed();
},
onTapCancel: () => _controller.reverse(),
child: ScaleTransition(
scale: _scaleAnimation,
child: widget.child,
),
);
}
}

7. パフォーマンス最適化のテクニック

Section titled “7. パフォーマンス最適化のテクニック”

再構築を最小限に抑えるために、AnimatedBuilderを使用します。

class OptimizedAnimation extends StatefulWidget {
@override
_OptimizedAnimationState createState() => _OptimizedAnimationState();
}
class _OptimizedAnimationState extends State<OptimizedAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.rotate(
angle: _animation.value * 2 * 3.14159,
child: child,
);
},
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
}
}

アニメーションしない部分はconstを使用して再構築を防ぎます。

AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: child, // constウィジェットを再利用
);
},
child: const Text('Static Text'), // constで再構築を防ぐ
)

問題1: アニメーションが滑らかでない

Section titled “問題1: アニメーションが滑らかでない”

解決策: vsyncを正しく設定し、不要な再構築を避けます。

// 正しい実装
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 1),
vsync: this, // 重要: vsyncを設定
);
}
}

解決策: disposeメソッドでコントローラーを確実に破棄します。

@override
void dispose() {
_controller.dispose(); // 必ず破棄
super.dispose();
}

問題3: 複数のアニメーションを同時に実行する

Section titled “問題3: 複数のアニメーションを同時に実行する”

解決策: TickerProviderStateMixinの代わりにTickerProviderStateMixinを使用します。

class MultipleAnimations extends StatefulWidget {
@override
_MultipleAnimationsState createState() => _MultipleAnimationsState();
}
class _MultipleAnimationsState extends State<MultipleAnimations>
with TickerProviderStateMixin { // SingleTickerProviderStateMixinではなく
late AnimationController _controller1;
late AnimationController _controller2;
@override
void initState() {
super.initState();
_controller1 = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
);
_controller2 = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
}
}

これで、Flutterでのアニメーション実装の基礎から実践的なパターンまでを理解できるようになりました。