# Flutter底部彈窗怎么實現多項選擇
## 前言
在移動應用開發中,底部彈窗(Bottom Sheet)是一種常見的交互模式,特別適合用于展示多個選項或操作。當需要用戶從多個選項中進行選擇時,多項選擇的底部彈窗就顯得尤為重要。Flutter提供了靈活且強大的工具來實現這種交互。
本文將詳細介紹如何在Flutter中實現支持多項選擇的底部彈窗,涵蓋以下內容:
1. Flutter底部彈窗的基本概念
2. 實現簡單的底部彈窗
3. 添加多項選擇功能
4. 自定義底部彈窗樣式
5. 處理用戶選擇結果
6. 最佳實踐和常見問題
## 一、Flutter底部彈窗基礎
### 1.1 什么是底部彈窗
底部彈窗(Bottom Sheet)是從屏幕底部向上滑動的面板,通常用于:
- 展示額外內容
- 提供多個操作選項
- 收集用戶輸入
- 顯示詳細信息而不離開當前頁面
Flutter提供了兩種類型的底部彈窗:
- **Persistent Bottom Sheet**:持久化底部彈窗,與Scaffold關聯
- **Modal Bottom Sheet**:模態底部彈窗,覆蓋整個屏幕
對于多項選擇場景,我們通常使用Modal Bottom Sheet。
### 1.2 相關Widget
實現底部彈窗主要涉及以下Widget:
- `showModalBottomSheet`:顯示模態底部彈窗的主方法
- `BottomSheet`:底部彈窗的基礎Widget
- `ListTile`:常用于構建選項列表項
- `Checkbox`/`CheckboxListTile`:實現多選的核心組件
## 二、實現基礎底部彈窗
### 2.1 最簡單的底部彈窗
```dart
void showSimpleBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) {
return Container(
height: 200,
child: Column(
children: [
ListTile(title: Text('選項1')),
ListTile(title: Text('選項2')),
ListTile(title: Text('選項3')),
],
),
);
},
);
}
void showEnhancedBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) {
return Container(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('請選擇', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 16),
ListTile(title: Text('選項1')),
ListTile(title: Text('選項2')),
ListTile(title: Text('選項3')),
SizedBox(height: 16),
ElevatedButton(
child: Text('確認'),
onPressed: () => Navigator.pop(context),
),
],
),
);
},
);
}
class MultiSelectBottomSheet extends StatefulWidget {
@override
_MultiSelectBottomSheetState createState() => _MultiSelectBottomSheetState();
}
class _MultiSelectBottomSheetState extends State<MultiSelectBottomSheet> {
Map<String, bool> selections = {
'選項1': false,
'選項2': false,
'選項3': false,
'選項4': false,
};
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('多項選擇', style: Theme.of(context).textTheme.headline6),
SizedBox(height: 16),
...selections.keys.map((option) {
return CheckboxListTile(
title: Text(option),
value: selections[option],
onChanged: (value) {
setState(() {
selections[option] = value!;
});
},
);
}).toList(),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
child: Text('取消'),
onPressed: () => Navigator.pop(context),
),
ElevatedButton(
child: Text('確認'),
onPressed: () {
Navigator.pop(context, selections);
},
),
],
),
],
),
);
}
}
// 調用方式
void showMultiSelectBottomSheet(BuildContext context) async {
final result = await showModalBottomSheet(
context: context,
builder: (context) => MultiSelectBottomSheet(),
);
if (result != null) {
print('用戶選擇: $result');
}
}
更實用的實現是從外部傳入選項數據:
class MultiSelectBottomSheet extends StatefulWidget {
final List<String> options;
MultiSelectBottomSheet({required this.options});
@override
_MultiSelectBottomSheetState createState() => _MultiSelectBottomSheetState();
}
class _MultiSelectBottomSheetState extends State<MultiSelectBottomSheet> {
late Map<String, bool> selections;
@override
void initState() {
super.initState();
selections = {for (var option in widget.options) option: false};
}
// ...其余代碼保持不變...
}
對于大量選項,可以添加搜索框:
class SearchableMultiSelectBottomSheet extends StatefulWidget {
final List<String> options;
SearchableMultiSelectBottomSheet({required this.options});
@override
_SearchableMultiSelectBottomSheetState createState() => _SearchableMultiSelectBottomSheetState();
}
class _SearchableMultiSelectBottomSheetState extends State<SearchableMultiSelectBottomSheet> {
late Map<String, bool> selections;
late List<String> filteredOptions;
TextEditingController searchController = TextEditingController();
@override
void initState() {
super.initState();
selections = {for (var option in widget.options) option: false};
filteredOptions = widget.options;
}
void filterOptions(String query) {
setState(() {
filteredOptions = widget.options
.where((option) => option.toLowerCase().contains(query.toLowerCase()))
.toList();
});
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: searchController,
decoration: InputDecoration(
labelText: '搜索',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onChanged: filterOptions,
),
SizedBox(height: 16),
Expanded(
child: ListView(
shrinkWrap: true,
children: filteredOptions.map((option) {
return CheckboxListTile(
title: Text(option),
value: selections[option],
onChanged: (value) {
setState(() {
selections[option] = value!;
});
},
);
}).toList(),
),
),
// ...按鈕代碼...
],
),
);
}
}
showModalBottomSheet(
context: context,
builder: (context) => MultiSelectBottomSheet(options: options),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
backgroundColor: Colors.white,
elevation: 10,
isScrollControlled: true, // 允許內容高度超過屏幕一半
);
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Container(
height: MediaQuery.of(context).size.height * 0.9,
child: MultiSelectBottomSheet(options: options),
),
),
);
// 在確認按鈕中
ElevatedButton(
child: Text('確認'),
onPressed: () {
// 獲取所有選中的選項
final selectedOptions = selections.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
Navigator.pop(context, selectedOptions);
},
),
// 調用時處理結果
void showMultiSelectBottomSheet(BuildContext context) async {
final selectedOptions = await showModalBottomSheet<List<String>>(
context: context,
builder: (context) => MultiSelectBottomSheet(options: options),
);
if (selectedOptions != null && selectedOptions.isNotEmpty) {
// 處理用戶選擇
print('用戶選擇了: ${selectedOptions.join(', ')}');
}
}
class MultiSelectBottomSheet extends StatefulWidget {
final List<String> options;
final List<String>? initialSelections;
MultiSelectBottomSheet({
required this.options,
this.initialSelections,
});
@override
_MultiSelectBottomSheetState createState() => _MultiSelectBottomSheetState();
}
class _MultiSelectBottomSheetState extends State<MultiSelectBottomSheet> {
late Map<String, bool> selections;
@override
void initState() {
super.initState();
selections = {
for (var option in widget.options)
option: widget.initialSelections?.contains(option) ?? false
};
}
// ...
}
問題1:底部彈窗高度不足
解決方案:設置isScrollControlled: true
并使用ListView
問題2:鍵盤遮擋輸入內容
解決方案:使用MediaQuery.of(context).viewInsets.bottom
調整底部間距
問題3:性能問題(大量選項)
解決方案:
- 使用ListView.builder
替代Column
- 考慮分頁加載
- 添加搜索過濾功能
問題4:橫屏適配 解決方案:設置最大寬度約束
showModalBottomSheet(
context: context,
builder: (context) => ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 600, // 適合平板和橫屏模式
),
child: MultiSelectBottomSheet(options: options),
),
);
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter多項選擇底部彈窗',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
final List<String> options = [
'紅色', '藍色', '綠色', '黃色',
'紫色', '橙色', '黑色', '白色'
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('多項選擇底部彈窗示例')),
body: Center(
child: ElevatedButton(
child: Text('顯示多項選擇'),
onPressed: () => _showMultiSelectBottomSheet(context),
),
),
);
}
Future<void> _showMultiSelectBottomSheet(BuildContext context) async {
final selectedOptions = await showModalBottomSheet<List<String>>(
context: context,
isScrollControlled: true,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
return MultiSelectBottomSheet(
options: options,
onSelectionsChanged: (selections) {
// 可以實時獲取選擇變化
print('當前選擇: $selections');
},
);
},
),
);
if (selectedOptions != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('您選擇了: ${selectedOptions.join(', ')}')),
);
}
}
}
class MultiSelectBottomSheet extends StatefulWidget {
final List<String> options;
final Function(Map<String, bool>)? onSelectionsChanged;
final List<String>? initialSelections;
MultiSelectBottomSheet({
required this.options,
this.onSelectionsChanged,
this.initialSelections,
});
@override
_MultiSelectBottomSheetState createState() => _MultiSelectBottomSheetState();
}
class _MultiSelectBottomSheetState extends State<MultiSelectBottomSheet> {
late Map<String, bool> selections;
final TextEditingController _searchController = TextEditingController();
late List<String> _filteredOptions;
@override
void initState() {
super.initState();
selections = {
for (var option in widget.options)
option: widget.initialSelections?.contains(option) ?? false
};
_filteredOptions = widget.options;
_searchController.addListener(_filterOptions);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _filterOptions() {
final query = _searchController.text.toLowerCase();
setState(() {
_filteredOptions = widget.options
.where((option) => option.toLowerCase().contains(query))
.toList();
});
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.9,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('請選擇顏色', style: Theme.of(context).textTheme.headline6),
SizedBox(height: 12),
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(vertical: 12),
),
),
SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: _filteredOptions.length,
itemBuilder: (context, index) {
final option = _filteredOptions[index];
return CheckboxListTile(
title: Text(option),
value: selections[option],
onChanged: (value) {
setState(() {
selections[option] = value!;
widget.onSelectionsChanged?.call(selections);
});
},
secondary: Icon(Icons.color_lens, color: _getColor(option)),
);
},
),
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: OutlinedButton(
child: Text('取消'),
onPressed: () => Navigator.pop(context),
),
),
SizedBox(width: 16),
Expanded(
child: ElevatedButton(
child: Text('確認 (${selections.values.where((v) => v).length})'),
onPressed: () {
final selected = selections.entries
.where((e) => e.value)
.map((e) => e.key)
.toList();
Navigator.pop(context, selected);
},
),
),
],
),
],
),
);
}
Color? _getColor(String colorName) {
switch (colorName) {
case '紅色': return Colors.red;
case '藍色': return Colors.blue;
case '綠色': return Colors.green;
case '黃色': return Colors.yellow;
case '紫色': return Colors.purple;
case '橙色': return Colors.orange;
case '黑色': return Colors.black;
case '白色': return Colors.white;
default: return null;
}
}
}
通過本文,我們詳細探討了在Flutter中實現支持多項選擇的底部彈窗的全過程。從基礎實現到高級功能,從UI定制到性能優化,我們覆蓋了實際開發中可能遇到的各種場景。
關鍵要點總結:
1. 使用showModalBottomSheet
創建底部彈窗
2. 結合CheckboxListTile
實現多項選擇
3. 通過狀態管理跟蹤用戶選擇
4. 添加搜索功能提升用戶體驗
5. 自定義樣式以適應應用設計語言
這種多項選擇的底部彈窗可以廣泛應用于各種場景,如: - 商品篩選 - 標簽選擇 - 權限設置 - 多文件選擇等
希望本文能幫助你在Flutter應用中實現優雅且功能完善的多項選擇交互體驗! “`
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。