はじめに
ショッピングサイト等でよくある、商品の絞り込み毎に左→右へ画面遷移を繰り返すあのUIを、BottomSheetで実現したい。
イメージ参考:zozotownアプリ カテゴリー検索
完成図
① フローティングボタンタップで[絞り込み画面]表示
② [絞り込み画面]内のリストタップでシートを開いたまま[カテゴリー画面]を開く
③ [カテゴリー画面]の<ボタンタップで[絞り込み画面]に戻る
④ [カテゴリー画面]の×ボタンタップでシート自体pop
BottomSheetを表示する
以下より実装方法を記載していきます。
まず① フローティングボタンタップで[絞り込み画面]表示 の処理
BottomSheetをPushします。
Future<void> _incrementCounter() async {
await Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
barrierDismissible: true,
barrierColor: Colors.black.withOpacity(0.6),
pageBuilder: (_, __, ___) => const BottomSheetView(),
transitionDuration: const Duration(milliseconds: 500),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const Offset begin = Offset(0.0, 1.0); // 下から上にSheetを表示
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,
);
},
),
);
}
② [絞り込み画面]内のリストタップでシートを開いたまま[カテゴリー画面]を開く の処理
ここで[カテゴリー画面]をpush時にboolを引数で返せるように設定するawait Navigator.of(context).push<bool?>
③④でのpop処理時にtrue
orfalse
を渡すことでbottomSheet自体を閉じるか、一つ前の画面に戻るかを判定できる。
もちろん、bool
以外を返却地に設定し、pop処理時に任意の値を渡すことも可能
Future<void> _onTapCategoryButton() async {
final backButton = await Navigator.of(context).push<bool?>(
PageRouteBuilder(
opaque: false,
barrierDismissible: true,
pageBuilder: (context, animation, secondaryAnimation) =>
const CategoryBottomSheetView(),
),
);
if (backButton == null || backButton == false) {
if (mounted) Navigator.of(context).pop();
}
}
}
③ [カテゴリー画面]の<ボタンタップで[絞り込み画面]に戻る の処理
void _onPressedBackButton() {
Navigator.of(context).pop(true);
}
④ [カテゴリー画面]の×ボタンタップでシート自体pop
void _onPressedCloseButton() {
Navigator.of(context).pop(false);
}
コード全体
main.dart
import 'package:flutter/material.dart';
import 'package:test_project/bottomsheet_view.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Container(),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.egg_alt_sharp),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
Future<void> _incrementCounter() async {
await Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
barrierDismissible: true,
barrierColor: Colors.black.withOpacity(0.6),
pageBuilder: (_, __, ___) => const BottomSheetView(),
transitionDuration: const Duration(milliseconds: 500),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const Offset begin = Offset(0.0, 1.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,
);
},
),
);
}
}
bottomsheet_view.dart
import 'package:auto_size_text_plus/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:test_project/categorybottomsheet_view.dart';
class BottomSheetView extends StatefulWidget {
const BottomSheetView({Key? key}) : super(key: key);
@override
_BottomSheetView createState() => _BottomSheetView();
}
class _BottomSheetView extends State<BottomSheetView> {
_BottomSheetView() {}
@override
Widget build(final BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(12), topLeft: Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
height: MediaQuery.of(context).size.height * 0.8,
width: double.maxFinite,
child: Material(
type: MaterialType.transparency,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
//余白
const Divider(color: Colors.transparent),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(
Icons.arrow_back_ios_rounded,
size: 15,
),
color: Colors.transparent,
onPressed: () {},
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 9.0),
child: AutoSizeText("絞り込み"),
),
//余白
IconButton(
icon: const Icon(
Icons.arrow_back_ios_rounded,
size: 15,
),
color: Colors.transparent,
onPressed: () {},
),
],
),
ListTile(
onTap: _onTapCategoryButton,
title: const Text(
"カテゴリー",
style: TextStyle(fontSize: 15),
),
trailing: const Icon(
Icons.arrow_forward_ios_rounded,
color: Colors.grey,
size: 15,
),
// isThreeLine: true,
),
ListTile(
onTap: () {
// _onTapCreateReadyMadeClubButton(GolfclubCategoryType.hybridUtility);
},
title: const Text(
"ショップ",
style: TextStyle(fontSize: 15),
),
trailing: const Icon(
Icons.arrow_forward_ios_rounded,
color: Colors.grey,
size: 15,
),
// isThreeLine: true,
),
ListTile(
onTap: () {
// _onTapCreateReadyMadeClubButton(GolfclubCategoryType.iron);
},
title: const Text(
"オプション",
style: TextStyle(fontSize: 15),
),
trailing: const Icon(
Icons.arrow_forward_ios_rounded,
color: Colors.grey,
size: 15,
),
// isThreeLine: true,
),
const Divider(color: Colors.transparent),
],
),
),
),
),
);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
Future<void> _onTapCategoryButton() async {
final backButton = await Navigator.of(context).push<bool?>(
PageRouteBuilder(
opaque: false,
barrierDismissible: true,
pageBuilder: (context, animation, secondaryAnimation) =>
const CategoryBottomSheetView(),
),
);
if (backButton == null || backButton == false) {
if (mounted) Navigator.of(context).pop();
}
}
}
categorybottomsheet_view.dart
import 'package:auto_size_text_plus/auto_size_text.dart';
import 'package:flutter/material.dart';
class CategoryBottomSheetView extends StatefulWidget {
const CategoryBottomSheetView({Key? key}) : super(key: key);
@override
_CategoryBottomSheetView createState() => _CategoryBottomSheetView();
}
class _CategoryBottomSheetView extends State<CategoryBottomSheetView> {
_CategoryBottomSheetView() {}
@override
Widget build(final BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(12), topLeft: Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
height: MediaQuery.of(context).size.height * 0.8,
width: double.maxFinite,
child: Material(
type: MaterialType.transparency,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
//余白
const Divider(color: Colors.transparent),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(
Icons.arrow_back_ios_rounded,
size: 15,
),
color: Colors.black,
onPressed: _onPressedBackButton,
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 9.0),
child: AutoSizeText("カテゴリー"),
),
//中央揃えのため透明アイコンを配置
IconButton(
icon: const Icon(
Icons.close,
size: 21,
),
color: Colors.black,
onPressed: _onPressedCloseButton,
),
],
),
const ListTile(
onTap: null,
title: Text(
"トップス",
style: TextStyle(fontSize: 15),
),
),
ListTile(
onTap: () {},
title: const Text(
"ボトムス",
style: TextStyle(fontSize: 15),
),
),
const Divider(color: Colors.transparent),
],
),
),
),
),
);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
void _onPressedBackButton() {
Navigator.of(context).pop(true);
}
void _onPressedCloseButton() {
Navigator.of(context).pop(false);
}
}
参考