【Flutter】画面の一部を画面遷移させる
はじめに
タイトルの通り、画面の一部を画面遷移させたくて調べたことをまとめました。
今回は、以下のように2分割した画面をそれぞれ画面遷移できるようにします。
(右下に表示されるボタンの色とアイコンをそれぞれの画面で設定しています。)
また、実現方法として
- Navigatorを用いる
- AnimatedSwitcherを用いる(画面遷移とは言えないかも)
の2つを紹介します。見た目はどちらもあまり変わらないと思います。
パフォーマンス面等で違いがありそうですが、深いところまでは調べていませんので悪しからず。。。
(Flutterバージョン)
> flutter --version
Flutter 3.16.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 78666c8dc5 (3 weeks ago) • 2023-12-19 16:14:14 -0800
Engine • revision 3f3e560236
Tools • Dart 3.2.3 • DevTools 2.28.4
画面構成
ざっくりこんな感じです。
基本画面の実装
各画面の実装を見たい場合は以下もしくはGitHubリポジトリを確認してください。
各画面の実装
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'ボタン作成'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Color buttonColor = Colors.blue;
Icon buttonIcon = const Icon(Icons.add);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: xxxx // TODO:色選択の画面遷移を管理するWidget
),
Expanded(
child: xxxx // TODO:アイコン選択の画面遷移を管理するWidget
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
backgroundColor: buttonColor,
child: buttonIcon,
),
);
}
}
- 色選択画面
import 'package:flutter/material.dart';
class ColorSelectPage extends StatelessWidget {
const ColorSelectPage({super.key, required this.onSelect});
// ListTileタップ時に、タップした色と名前を通知するコールバック
final void Function(Color color, String colorName) onSelect;
static const _selectableColors = {
"red": Colors.red,
"orange": Colors.orange,
"yellow": Colors.yellow,
"lightGreen": Colors.lightGreen,
"green": Colors.green,
"blue": Colors.blue,
"purple": Colors.purple,
};
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.onInverseSurface,
title: const Text('色選択'),
),
body: Center(
child: ListView(
children: [
for (final color in _selectableColors.entries)
// 各色のListTile
ListTile(
title: Text(color.key),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () => onSelect(color.value, color.key),
)
],
),
),
);
}
}
- 色詳細画面
import 'package:flutter/material.dart';
class ColorDetailPage extends StatelessWidget {
const ColorDetailPage({
super.key,
required this.onSelect,
required this.color,
required this.colorName,
});
final void Function() onSelect;
final Color color;
final String colorName;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.onInverseSurface,
title: const Text('色詳細'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 実際に色を表示
Expanded(
flex: 2,
child: Container(color: color,),
),
// 色名
Expanded(
flex: 1,
child: Center(
child: Text(colorName),
),
),
// ボタン
Expanded(
flex: 1,
child: Center(
child: TextButton(
onPressed: onSelect,
child: const Text('Set'),
),
),
),
],
),
),
);
}
}
- アイコン選択画面
import 'package:flutter/material.dart';
class IconSelectPage extends StatelessWidget {
const IconSelectPage({super.key, required this.onSelect});
// ListTileタップ時に、タップしたアイコンと名前を通知するコールバック
final void Function(Icon icon, String iconName) onSelect;
static const _selectableIcons = {
"add": Icon(Icons.add),
"edit": Icon(Icons.edit),
"settings": Icon(Icons.settings),
"zoom_in": Icon(Icons.zoom_in),
"zoom_out": Icon(Icons.zoom_out),
"soccer": Icon(Icons.sports_soccer),
};
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.onInverseSurface,
title: const Text('アイコン選択'),
),
body: Center(
child: ListView(
children: [
for (final icon in _selectableIcons.entries)
// 各アイコンのListTile
ListTile(
title: Text(icon.key),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () => onSelect(icon.value, icon.key),
)
],
),
),
);
}
}
- アイコン詳細画面
import 'package:flutter/material.dart';
class IconDetailPage extends StatelessWidget {
const IconDetailPage({
super.key,
required this.onSelect,
required this.icon,
required this.iconName,
});
final void Function() onSelect;
final Icon icon;
final String iconName;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.onInverseSurface,
title: const Text('アイコン詳細'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// アイコン拡大表示
Expanded(
flex: 2,
child: FittedBox(
child: Icon(
icon.icon,
size: 100,
),
),
),
// アイコン名
Expanded(
flex: 1,
child: Center(
child: Text(iconName),
),
),
// ボタン
Expanded(
flex: 1,
child: Center(
child: TextButton(
onPressed: onSelect,
child: const Text('Set'),
),
),
),
],
),
),
);
}
}
Navigatorを用いる
自作した画面(ColorSelectPage
・ColorDetailPage
とIconSelectPage
・IconDetailPage
)を管理するNavigator
をそれぞれ作成し、MyHomePage
のColumn
(のExpand
)のchild
に指定してあげます。
class ColorPageNavigator extends StatelessWidget {
/* ... */
Widget build(BuildContext context) {
return Navigator(
onGenerateRoute: ((settings) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return ColorSelectPage(
/* ... */
);
}
);
}),
);
}
}
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
/* ... */
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
+ child: ColorPageNavigator(
+ /* ... */
+ ),
/* ... */
}
あとはSelectPage
のListTile
が押されたらDetailPage
をpush、DetailPage
のボタンが押されたらpopしてあげれば良いです。
(※以下、onSelect
は自作)
class ColorPageNavigator extends StatelessWidget {
/* ... */
+ // 色詳細ページをpushする
+ void _pushDetailPage(BuildContext context, Color color, String colorName) {
+ Navigator.push(
+ context,
+ PageRouteBuilder(
+ pageBuilder: (context, animation, secondaryAnimation) {
+ return ColorDetailPage(
+ color: color,
+ colorName: colorName,
+ // 色詳細ページでボタンが押されたらpopするようにする
+ onSelect: () {
+ Navigator.pop(context);
+ }
+ );
+ },
+ ),
+ );
+ }
Widget build(BuildContext context) {
return Navigator(
onGenerateRoute: ((settings) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return ColorSelectPage(
+ // 色選択画面のListTileが押されたら_pushDetailPageが呼ばれるようにする
+ onSelect: (color, colorName) => _pushDetailPage(context, color, colorName)
);
}
);
}),
);
}
}
仕組みは簡単ですが、Navigator
のonGenerateRoute
引数でMaterialPageRoute
を返してしまうと画面全体を覆うRoute
を返すことになってしまって目的に沿わないので注意です。
AnimatedSwitcherを用いる
AnimatedSwitcher
は、child
に指定したWidget
が変更される際のアニメーションを指定することができます。
setState()
を呼んでWidget
を変更することでいい感じにアニメーションしてくれます。
これにより、見た目上はあたかも画面遷移しているように見えます。
import 'package:flutter/material.dart';
import 'package:navigator_part_of_screen/color_detail_page.dart';
import 'package:navigator_part_of_screen/color_select_page.dart';
enum ColorPage {
select,
detail,
}
// Widgetを変更することで画面遷移のように見せる。そのため、StatefulWidgetを使う
class ColorPageSwitcher extends StatefulWidget {
const ColorPageSwitcher({super.key, required this.onSelect});
final void Function(Color color) onSelect;
State<ColorPageSwitcher> createState() => _ColorPageSwitcherState();
}
class _ColorPageSwitcherState extends State<ColorPageSwitcher> {
ColorPage _currentPage = ColorPage.select;
Color _color = Colors.blue;
String _colorName = 'blue';
Widget build(BuildContext context) {
Widget page;
switch (_currentPage) {
case ColorPage.select:
page = ColorSelectPage(
// ListTileをタップしたとき_currentPageを変更する
onSelect: (color, colorName) => setState(() {
_color = color;
_colorName = colorName;
_currentPage = ColorPage.detail;
}),
);
break;
case ColorPage.detail:
page = ColorDetailPage(
// ボタンを押したとき_currentPageを変更する
onSelect: () {
widget.onSelect(_color);
setState(() {
_currentPage = ColorPage.select;
});
},
color: _color,
colorName: _colorName,
);
break;
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
const Offset begin = Offset(1.0, 0.0);
const Offset end = Offset.zero;
final Animatable<Offset> tween = Tween(begin: begin, end: end)
.chain(CurveTween(curve: Curves.easeInOut));
final Animation<Offset> offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
child: page,
);
}
}
Widget
を変更しているので、Navigator
のようにWidget
をスタックさせているわけではありません。
メモリ上にWidget
をたくさん溜めることが無くなるのでこちらで実装する方が良いのかな...?
なお、ここでは書いていませんが戻るためのボタンは自分で作る必要があります。
まとめ
Navigator
、AnimatedSwitcher
を用いて画面の一部を画面遷移させる(ように見せる)ことができます。
作る際は各画面遷移で矛盾・不都合が起こらないように注意ください。
参考文献
Discussion