GoRouter + Animations
画面遷移にアニメーションを追加
画面遷移時にインパクトのあるアニメーションが欲しい。リッチな動きをUIにつけてみたいと思った。
やってみたこと
pageBuilder
を使って画面遷移にアニメーションを追加してみた。transition
とは遷移という意味です。
今回はCustomTransitionPageを使ってリッチなアニメーションに仕立てています。
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
pageBuilder: (context, state) => CustomTransitionPage<void>(
key: state.pageKey,
child: const HomeScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
),
),
GoRoute(
path: '/details',
pageBuilder: (context, state) => CustomTransitionPage<void>(
key: state.pageKey,
child: const DetailsScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
),
),
],
);
Transition animations topic
GoRouter allows you to customize the transition animation for each GoRoute. To configure a custom transition animation, provide a pageBuilder parameter to the GoRoute constructor:
トランジションアニメーションのトピック
GoRouterでは、各GoRouteの遷移アニメーションをカスタマイズすることができます。カスタム トランジション アニメーションを設定するには、GoRoute コンストラクタに pageBuilder パラメータを指定します:
AIの解説によると
state.pageKeyはアニメーションの文脈で重要な役割を果たします。このkeyを指定する主な理由は以下の通りです:
ページの一意性の保証:
state.pageKeyは各ページに一意のキーを提供します。これにより、Flutter のウィジェットツリーが各ページを一意に識別できるようになります。
アニメーションの適切な処理:
画面遷移時のアニメーションを正しく処理するために重要です。キーがないと、Flutter は新しいページと古いページを区別できず、アニメーションが期待通りに動作しない可能性があります。
ページの状態管理:
キーは、ページの状態を適切に管理するのに役立ちます。特に、同じ型の複数のページがナビゲーションスタック上に存在する場合に重要です。
再構築の最適化:
Flutter のフレームワークがウィジェットツリーを効率的に再構築するのに役立ちます。
CustomTransitionPageを使用する場合、特にアニメーションを伴う遷移では、このkeyの指定が重要になります。state.pageKeyを使用することで、go_router が提供する状態管理の恩恵を受けつつ、カスタムアニメーションを実装できます。
ただし、アニメーションを使用しない単純なページ遷移の場合、keyを省略しても大きな問題は生じない可能性があります。しかし、一般的にはベストプラクティスとして、CustomTransitionPageを使用する際にはkeyを指定することをお勧めします。
アプリの規模や複雑さによっては、この細かい最適化が全体のパフォーマンスとユーザー体験に影響を与える可能性があります。特に、複雑なナビゲーション構造や頻繁なページ遷移を行うアプリでは、keyの適切な使用が重要になってきます。
Example
全体のソースコードはこちら。青い箱をタップすると立体的な画面遷移ができます。
使用したパッケージ
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:animations/animations.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({super.key});
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
pageBuilder: (context, state) => CustomTransitionPage<void>(
key: state.pageKey,
child: const HomeScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
),
),
GoRoute(
path: '/details',
pageBuilder: (context, state) => CustomTransitionPage<void>(
key: state.pageKey,
child: const DetailsScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
),
),
],
);
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
routerConfig: _router,
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: OpenContainer(
transitionType: ContainerTransitionType.fade,
openBuilder: (BuildContext context, VoidCallback _) {
return const DetailsScreen();
},
closedBuilder: (BuildContext context, VoidCallback openContainer) {
return Container(
width: 200,
height: 200,
color: Colors.blue,
child: const Center(
child: Text(
'Tap to open details',
style: TextStyle(color: Colors.white),
),
),
);
},
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
const DetailsScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Details Screen'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
context.pop();
},
child: const Text('Go back'),
),
],
),
),
);
}
}
最後に
今回は、go_routerを使用したアニメーションありの画面遷移を実装してみました。しかしこちらのパッケージ8ヶ月前から更新が止まっている💦
自作するしかないか💦
なんか違うものでもよくないか。
フェードインを使ってグラデーションが変化するのはどうだろうか...
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({super.key});
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
pageBuilder: (context, state) => CustomFadeTransitionPage(
key: state.pageKey,
child: const MainScreen(initialIndex: 0),
),
),
GoRoute(
path: '/details',
pageBuilder: (context, state) => CustomFadeTransitionPage(
key: state.pageKey,
child: const MainScreen(initialIndex: 1),
),
),
],
);
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Custom Fade Transition Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
routerConfig: _router,
);
}
}
class CustomFadeTransitionPage extends CustomTransitionPage<void> {
CustomFadeTransitionPage({
required super.child,
super.key,
}) : super(
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
transitionDuration: const Duration(milliseconds: 1000),
);
}
class MainScreen extends StatefulWidget {
final int initialIndex;
const MainScreen({super.key, required this.initialIndex});
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin {
late int _currentIndex;
late AnimationController _controller;
late Animation<Color?> _colorAnimation1;
late Animation<Color?> _colorAnimation2;
final List<List<Color>> _gradients = [
[Colors.blue.shade200, Colors.purple.shade200],
[Colors.purple.shade200, Colors.red.shade200],
];
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_setColorAnimation();
}
void _setColorAnimation() {
_colorAnimation1 = ColorTween(
begin: _gradients[_currentIndex][0],
end: _gradients[1 - _currentIndex][0],
).animate(_controller);
_colorAnimation2 = ColorTween(
begin: _gradients[_currentIndex][1],
end: _gradients[1 - _currentIndex][1],
).animate(_controller);
}
void _changePage() {
setState(() {
_currentIndex = 1 - _currentIndex;
});
_setColorAnimation();
_controller.forward(from: 0.0);
Future.delayed(const Duration(milliseconds: 500), () {
if (_currentIndex == 0) {
context.go('/');
} else {
context.go('/details');
}
});
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
_colorAnimation1.value!,
_colorAnimation2.value!,
],
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
title: Text(_currentIndex == 0 ? 'Home' : 'Details'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Center(
child: ElevatedButton(
onPressed: _changePage,
child: Text(_currentIndex == 0 ? 'Go to Details' : 'Go back to Home'),
),
),
),
);
},
);
}
}
おまけのロジック
アニメーションの持続時間の延長:
CustomFadeTransitionPage の transitionDuration を1秒(1000ミリ秒)に設定しました。
グラデーションアニメーションの追加:
AnimationController と ColorTween を使用して、グラデーション色のアニメーションを実装しました。
ページ遷移ロジックの修正:
_changePage メソッドでアニメーションを開始し、500ミリ秒後にページ遷移を行うようにしました。これにより、グラデーションの変化が始まってからページ遷移が行われるため、よりスムーズな視覚効果が得られます。
AnimatedBuilder の使用:
グラデーションの変化をリアルタイムで反映するために AnimatedBuilder を使用しています。
この実装により、以下の効果が得られます:
グラデーションの変化が1秒かけてスムーズに行われます。
ページ遷移のフェードアニメーションも1秒間続きます。
グラデーションの変化が始まってから少し遅れてページ遷移が始まるため、視覚的な連続性が向上します。
この方法では、グラデーションの変化とページ遷移のタイミングを細かく制御できるようになりました。ユーザーにとって、よりスムーズで魅力的な画面遷移体験を提供できるはずです。
さらに改善や調整が必要な点があれば、お知らせください。
Discussion