DropdownBottomSheet
典型都是使用 ListView
搭配 ListTile
實現,但 ListTile
自帶 Padding,可能會無法達成 Designer
的設計要求,可使用沒有自帶 Padding 的 GestureDector
取代 ListTile
。
Version
Flutter 3.24
Flutter
- Android 與 iOS 都成功使用
GestureDetector
實現兩層下拉選單,注意每個 item 之間都沒有 padding
GestureDetector
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
const Home({super.key});
State<Home> createState() => _Home();
}
class _Home extends State<Home> {
String _selectedText = 'Select Option';
void _showModalBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(16),
),
),
builder: (BuildContext context) {
var deviceHeight = MediaQuery.of(context).size.height;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
height: deviceHeight * 0.2,
child: ListView(
shrinkWrap: true,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 1');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text(
'Option 1',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Padding(
padding: const EdgeInsets.only(left: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 1.1');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 1.1'),
),
),
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 1.2');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 1.2'),
),
),
],
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 2');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text(
'Option 2',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Padding(
padding: const EdgeInsets.only(left: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 2.1');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 2.1'),
),
),
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 2.2');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 2.2'),
),
),
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 2.3');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 2.3'),
),
),
],
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 3');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text(
'Option 3',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Padding(
padding: const EdgeInsets.only(left: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 3.1');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 3.1'),
),
),
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 3.2');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 3.2'),
),
),
],
),
),
],
),
],
),
),
);
},
);
}
Widget build(BuildContext context) {
var appBar = AppBar(
title: const Text('DropdownBottomSheet'),
);
var dropdownBottomSheet = GestureDetector(
onTap: () => _showModalBottomSheet(context),
child: Container(
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_selectedText),
const Icon(Icons.arrow_drop_down),
],
),
),
);
return Scaffold(
appBar: appBar,
body: dropdownBottomSheet,
);
}
}
Line 194
var dropdownBottomSheet = GestureDetector(
onTap: () => _showModalBottomSheet(context),
child: Container(
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_selectedText),
const Icon(Icons.arrow_drop_down),
],
),
),
)
GestureDetector
:包裹整個Container
,當Container
被點擊時會觸發onTap()
onTap()
:馬上呼叫_showModalBottomSheet()
Line 101
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_selectedText),
const Icon(Icons.arrow_drop_down),
],
),
- 上方的下拉選單,其實是用
Text
與Icon
組合出來的
Line 13
void _showModalBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(16),
),
),
builder: (BuildContext context) {
var deviceHeight = MediaQuery.of(context).size.height;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
height: deviceHeight * 0.2,
child: ListView(
shrinkWrap: true,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 1');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text(
'Option 1',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Padding(
padding: const EdgeInsets.only(left: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 1.1');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 1.1'),
),
),
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 1.2');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 1.2'),
),
),
],
),
),
],
),
],
),
),
);
},
);
}
- 呼叫
showModalBottomSheet()
顯示ModalBottomSheet
Line 35
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 1');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text(
'Option 1',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
- 第一層選單
GestureDetector
:由於不使用ListTile
,只是透過GestureDetector
偵測點擊onTap()
:第一層選單被點擊時觸發Padding
:由於不像ListTile
自帶 padding,可自行使用 prototype 所設計的 paddingText
:選單文字
Line 53
Padding(
padding: const EdgeInsets.only(left: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() => _selectedText = 'Option 1.1');
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.zero,
child: Text('Option 1.1'),
),
),
],
),
- 第二層選單
Padding
:設定第二層選單的縮排Column
:第二層選單可包含多個選項GestureDetector
:由於不使用ListTile
,只是透過GestureDetector
偵測點擊onTap()
:第二層選單被點擊時觸發Padding
:由於不像ListTile
自帶 padding,可自行使用 prototype 所設計的 paddingText
:選單文字
Conclusion
- 在一般使用上,由
ListView
+ListTile
組合可完美實現DropdownBottomSheet
,但由於ListTile
自帶 padding,可能無法達成 designer 的要求 - 若要完美實現 designer 的 padding,可使用
ListView
+GestureDetector
組合,可自行精準設定 item 間的 padding