Last active
March 1, 2024 03:01
-
-
Save kumamotone/84195a839113d60152000e3c0e936fc4 to your computer and use it in GitHub Desktop.
showModalPickerSheet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Dialog Demo', | |
theme: ThemeData( | |
visualDensity: VisualDensity.adaptivePlatformDensity, | |
), | |
home: const ModalPickerSheetCatalogScreen(), | |
); | |
} | |
} | |
class ModalPickerSheetCatalogScreen extends StatelessWidget { | |
const ModalPickerSheetCatalogScreen({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Modal Picker Sheet Demo'), | |
), | |
body: ListView( | |
children: <Widget>[ | |
_buildPickerButton( | |
context, | |
'オプション1つ', | |
[ | |
const PickerOption( | |
key: 'destructive', | |
label: '退出する', | |
isDestructiveAction: true, | |
), | |
], | |
), | |
_buildPickerButton( | |
context, | |
'カメラ/ライブラリ', | |
[ | |
const PickerOption( | |
key: 'Camera', | |
label: 'カメラで撮影する', | |
), | |
const PickerOption( | |
key: 'Library', | |
label: 'ライブラリから選択する', | |
), | |
], | |
), | |
_buildPickerButton(context, '編集/削除', [ | |
const PickerOption( | |
key: 'edit', | |
label: '編集する', | |
), | |
const PickerOption( | |
key: 'delete', | |
label: '削除する', | |
isDestructiveAction: true, | |
), | |
]), | |
_buildPickerButton( | |
context, | |
'シェアメニュー', | |
[ | |
const PickerOption( | |
key: 'X', | |
label: 'X(Twitter) でシェア', | |
), | |
const PickerOption( | |
key: 'Facebook', | |
label: 'Facebook でシェア', | |
), | |
const PickerOption( | |
key: 'Other', | |
label: 'その他の方法でシェア', | |
), | |
const PickerOption( | |
key: 'Copy', | |
label: 'コピー', | |
), | |
], | |
), | |
_buildPickerButton( | |
context, | |
'ヘッダーあり', | |
[ | |
const PickerOption( | |
key: 'destructive', | |
label: '変更を破棄する', | |
isDestructiveAction: true, | |
), | |
], | |
title: '保存していない変更があります', | |
subTitle: '変更内容を破棄してもよろしいですか?', | |
), | |
_buildPickerButton( | |
context, | |
'全部入り', | |
[ | |
const PickerOption( | |
key: 'Default', | |
label: 'デフォルトのアクション', | |
caption: '注釈1', | |
isDefaultAction: true, | |
), | |
const PickerOption( | |
key: 'Normal', | |
label: '普通のアクション', | |
caption: '注釈2', | |
), | |
const PickerOption( | |
key: 'Destructive', | |
label: '破棄のアクション', | |
caption: '注釈3', | |
isDestructiveAction: true, | |
), | |
], | |
title: 'タイトル', | |
subTitle: 'サブタイトル', | |
), | |
], | |
), | |
); | |
} | |
Widget _buildPickerButton( | |
BuildContext context, | |
String buttonText, | |
List<PickerOption<dynamic>> options, { | |
String? title, | |
String? subTitle, | |
}) { | |
return Padding( | |
padding: const EdgeInsets.symmetric( | |
vertical: 12, | |
horizontal: 16, | |
), | |
child: ElevatedButton( | |
onPressed: () async { | |
final result = await showModalPickerSheet( | |
context: context, | |
options: options, | |
headerTitle: title, | |
headerMessage: subTitle, | |
cancelLabel: 'キャンセル', | |
); | |
if (result != null && context.mounted) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar( | |
content: Text('選択: $result'), | |
duration: const Duration(milliseconds: 500), | |
), | |
); | |
} | |
}, | |
child: Text(buttonText), | |
), | |
); | |
} | |
} | |
Future<T?> showModalPickerSheet<T>({ | |
required BuildContext context, | |
required List<PickerOption<T>> options, | |
String? headerTitle, | |
String? headerMessage, | |
String? cancelLabel, | |
}) { | |
final navigator = Navigator.of( | |
context, | |
rootNavigator: true, | |
); | |
return showModalBottomSheet<T>( | |
context: context, | |
backgroundColor: Colors.transparent, | |
builder: (context) => _ModalPickerSheet<T>( | |
title: headerTitle, | |
message: headerMessage, | |
options: options, | |
onOptionPressed: navigator.pop, | |
cancelLabel: cancelLabel, | |
onCancelPressed: cancelLabel != null ? navigator.pop : null, | |
), | |
); | |
} | |
class _ModalPickerSheet<T> extends StatelessWidget { | |
const _ModalPickerSheet({ | |
required this.options, | |
required this.onOptionPressed, | |
this.title, | |
this.message, | |
this.cancelLabel, | |
this.onCancelPressed, | |
}); | |
final String? title; | |
final String? message; | |
final List<PickerOption<T>> options; | |
final ValueSetter<T?> onOptionPressed; | |
final String? cancelLabel; | |
final VoidCallback? onCancelPressed; | |
List<Widget> buildHeader({ | |
required BuildContext context, | |
String? title, | |
String? subTitle, | |
}) { | |
if (title == null && subTitle == null) { | |
return [const SizedBox.shrink()]; | |
} else { | |
return [ | |
ListTile( | |
title: title != null ? Text(title) : null, | |
subtitle: subTitle != null | |
? Text( | |
subTitle, | |
style: Theme.of(context).textTheme.bodySmall, | |
) | |
: null, | |
), | |
const Divider(height: 1), | |
]; | |
} | |
} | |
List<Widget> buildOptionTiles({ | |
required BuildContext context, | |
required List<PickerOption<T>> options, | |
required ValueSetter<T?> onOptionPressed, | |
}) { | |
return options.map<Widget>((option) { | |
return Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
ListTile( | |
title: Text( | |
option.label, | |
style: TextStyle( | |
color: option.isDestructiveAction | |
? Theme.of(context).colorScheme.error | |
: null, | |
fontWeight: option.isDefaultAction ? FontWeight.bold : null, | |
), | |
), | |
subtitle: option.caption != null | |
? Text( | |
option.caption!, | |
style: Theme.of(context).textTheme.bodySmall, | |
) | |
: null, | |
onTap: () => onOptionPressed(option.key), | |
), | |
const Divider(height: 1), | |
], | |
); | |
}).toList(); | |
} | |
List<Widget> buildCancelTile({ | |
String? cancelLabel, | |
VoidCallback? onCancelPressed, | |
}) { | |
if (cancelLabel == null) { | |
return [const SizedBox.shrink()]; | |
} else { | |
return [ | |
const Divider(height: 1), | |
ListTile( | |
title: Text(cancelLabel), | |
onTap: onCancelPressed, | |
), | |
]; | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return SafeArea( | |
child: Padding( | |
padding: const EdgeInsets.all(8), | |
child: SingleChildScrollView( | |
child: Material( | |
shape: const RoundedRectangleBorder( | |
borderRadius: BorderRadius.all(Radius.circular(16)), | |
), | |
color: Theme.of(context).bottomSheetTheme.backgroundColor ?? | |
Theme.of(context).canvasColor, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
...buildHeader( | |
context: context, | |
title: title, | |
subTitle: message, | |
), | |
...buildOptionTiles( | |
context: context, | |
options: options, | |
onOptionPressed: onOptionPressed, | |
), | |
...buildCancelTile( | |
cancelLabel: cancelLabel, | |
onCancelPressed: onCancelPressed, | |
), | |
], | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
class PickerOption<T> { | |
const PickerOption({ | |
required this.label, | |
this.caption, | |
this.key, | |
this.isDefaultAction = false, | |
this.isDestructiveAction = false, | |
}); | |
final String label; | |
final String? caption; | |
final T? key; | |
final bool isDefaultAction; | |
final bool isDestructiveAction; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment