Skip to content

Commit c58b352

Browse files
committed
refactor: make all bottom sheet contents use dynamic height
Use `IndexStack` to replace `TabBarView` and enable `shrinkWrap` on `ListView`s to make all contents in bottom sheet using dynamic height instead of an always expanded one. This commit also makes bottom sheet avoid Android status bar.
1 parent 7bc3068 commit c58b352

File tree

4 files changed

+174
-142
lines changed

4 files changed

+174
-142
lines changed

lib/features/editor/widgets/color_bottom_sheet.dart

Lines changed: 134 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class _ColorBottomSheetState extends State<_ColorBottomSheet> with SingleTickerP
5656
late final TabController _tabController;
5757
late final TextEditingController _customColorValueController;
5858

59+
int currentIndex = 0;
60+
5961
String? _customTabErrorText;
6062

6163
Color _advancedTabColor = Colors.transparent;
@@ -64,6 +66,15 @@ class _ColorBottomSheetState extends State<_ColorBottomSheet> with SingleTickerP
6466

6567
List<Color> _recentCustomColors = [];
6668

69+
void onCurrentIndexChanged() {
70+
if (_tabController.indexIsChanging) {
71+
final newIndex = _tabController.index;
72+
setState(() {
73+
currentIndex = newIndex;
74+
});
75+
}
76+
}
77+
6778
void _updateCustomColorPreview(String? v) {
6879
final tr = context.t.colorPickerDialog.tabs.custom;
6980

@@ -109,114 +120,106 @@ class _ColorBottomSheetState extends State<_ColorBottomSheet> with SingleTickerP
109120
Widget _buildAdvancedTab() {
110121
final tr = context.t.colorPickerDialog.tabs.advanced;
111122

112-
return Column(
113-
children: [
114-
Expanded(
115-
child: SingleChildScrollView(
116-
child: ColorPicker(
117-
padding: EdgeInsets.zero,
118-
// Use the dialogPickerColor as start and active color.
119-
color: _advancedTabColor,
120-
// Update the dialogPickerColor using the callback.
121-
onColorChanged: (color) => setState(() => _advancedTabColor = color),
122-
borderRadius: 15,
123-
spacing: 5,
124-
runSpacing: 5,
125-
wheelDiameter: 155,
126-
heading: Text(tr.selectColor, style: Theme.of(context).textTheme.titleSmall),
127-
subheading: Text(tr.selectColorShade, style: Theme.of(context).textTheme.titleSmall),
128-
showMaterialName: true,
129-
showColorName: true,
130-
showColorCode: true,
131-
copyPasteBehavior: const ColorPickerCopyPasteBehavior(longPressMenu: true),
132-
materialNameTextStyle: Theme.of(context).textTheme.bodySmall,
133-
colorNameTextStyle: Theme.of(context).textTheme.bodySmall,
134-
colorCodeTextStyle: Theme.of(context).textTheme.bodySmall,
135-
pickersEnabled: const <ColorPickerType, bool>{
136-
ColorPickerType.both: true,
137-
ColorPickerType.primary: false,
138-
ColorPickerType.accent: false,
139-
ColorPickerType.bw: false,
140-
ColorPickerType.custom: false,
141-
ColorPickerType.customSecondary: false,
142-
ColorPickerType.wheel: false,
143-
},
144-
showEditIconButton: true,
145-
// customColorSwatchesAndNames: colorsNameMap,
146-
),
123+
return Align(
124+
child: Column(
125+
mainAxisSize: .min,
126+
children: [
127+
ColorPicker(
128+
padding: EdgeInsets.zero,
129+
// Use the dialogPickerColor as start and active color.
130+
color: _advancedTabColor,
131+
// Update the dialogPickerColor using the callback.
132+
onColorChanged: (color) => setState(() => _advancedTabColor = color),
133+
borderRadius: 15,
134+
spacing: 5,
135+
runSpacing: 5,
136+
wheelDiameter: 155,
137+
heading: Text(tr.selectColor, style: Theme.of(context).textTheme.titleSmall),
138+
subheading: Text(tr.selectColorShade, style: Theme.of(context).textTheme.titleSmall),
139+
showMaterialName: true,
140+
showColorName: true,
141+
showColorCode: true,
142+
copyPasteBehavior: const ColorPickerCopyPasteBehavior(longPressMenu: true),
143+
materialNameTextStyle: Theme.of(context).textTheme.bodySmall,
144+
colorNameTextStyle: Theme.of(context).textTheme.bodySmall,
145+
colorCodeTextStyle: Theme.of(context).textTheme.bodySmall,
146+
pickersEnabled: const <ColorPickerType, bool>{
147+
ColorPickerType.both: true,
148+
ColorPickerType.primary: false,
149+
ColorPickerType.accent: false,
150+
ColorPickerType.bw: false,
151+
ColorPickerType.custom: false,
152+
ColorPickerType.customSecondary: false,
153+
ColorPickerType.wheel: false,
154+
},
155+
showEditIconButton: true,
156+
// customColorSwatchesAndNames: colorsNameMap,
147157
),
148-
),
149-
sizedBoxW4H4,
150-
FilledButton(
151-
onPressed: _advancedTabColor != Colors.transparent
152-
? () => Navigator.of(context).pop(PickColorResult.picked(_advancedTabColor))
153-
: null,
154-
child: Text(context.t.general.ok),
155-
),
156-
],
158+
sizedBoxW4H4,
159+
FilledButton(
160+
onPressed: _advancedTabColor != Colors.transparent
161+
? () => Navigator.of(context).pop(PickColorResult.picked(_advancedTabColor))
162+
: null,
163+
child: Text(context.t.general.ok),
164+
),
165+
],
166+
),
157167
);
158168
}
159169

160170
Widget _buildCustomTab(BuildContext context) {
161171
final tr = context.t.colorPickerDialog.tabs.custom;
162172

163173
return Column(
174+
mainAxisSize: .min,
164175
children: [
165-
Expanded(
166-
child: SingleChildScrollView(
167-
child: Column(
168-
children: [
169-
sizedBoxW8H8,
170-
Row(
171-
children: [
172-
Expanded(
173-
child: TextField(
174-
controller: _customColorValueController,
175-
decoration: InputDecoration(errorText: _customTabErrorText, labelText: tr.colorValue),
176-
onChanged: _updateCustomColorPreview,
177-
),
178-
),
179-
sizedBoxW12H12,
180-
Container(
181-
width: 40,
182-
height: 40,
183-
decoration: BoxDecoration(
184-
borderRadius: const BorderRadius.all(Radius.circular(15)),
185-
color: _customTabColor,
186-
),
176+
sizedBoxW8H8,
177+
Row(
178+
children: [
179+
Expanded(
180+
child: TextField(
181+
controller: _customColorValueController,
182+
decoration: InputDecoration(errorText: _customTabErrorText, labelText: tr.colorValue),
183+
onChanged: _updateCustomColorPreview,
184+
),
185+
),
186+
sizedBoxW12H12,
187+
Container(
188+
width: 40,
189+
height: 40,
190+
decoration: BoxDecoration(
191+
borderRadius: const BorderRadius.all(Radius.circular(15)),
192+
color: _customTabColor,
193+
),
194+
),
195+
],
196+
),
197+
sizedBoxW4H4,
198+
Tips(tr.formatTip, enablePadding: false),
199+
sizedBoxW4H4,
200+
Text(tr.recentColor, style: Theme.of(context).textTheme.titleSmall),
201+
sizedBoxW4H4,
202+
Wrap(
203+
spacing: 4,
204+
children: _recentCustomColors
205+
.map(
206+
(e) => GestureDetector(
207+
onTap: () => setState(() {
208+
final value = e.hex.toLowerCase();
209+
_updateCustomColorPreview(value);
210+
_customColorValueController.text = value;
211+
}),
212+
child: Container(
213+
width: 40,
214+
height: 40,
215+
decoration: BoxDecoration(
216+
borderRadius: const BorderRadius.all(Radius.circular(15)),
217+
color: e,
187218
),
188-
],
189-
),
190-
sizedBoxW4H4,
191-
Tips(tr.formatTip, enablePadding: false),
192-
sizedBoxW4H4,
193-
Text(tr.recentColor, style: Theme.of(context).textTheme.titleSmall),
194-
sizedBoxW4H4,
195-
Wrap(
196-
spacing: 4,
197-
children: _recentCustomColors
198-
.map(
199-
(e) => GestureDetector(
200-
onTap: () => setState(() {
201-
final value = e.hex.toLowerCase();
202-
_updateCustomColorPreview(value);
203-
_customColorValueController.text = value;
204-
}),
205-
child: Container(
206-
width: 40,
207-
height: 40,
208-
decoration: BoxDecoration(
209-
borderRadius: const BorderRadius.all(Radius.circular(15)),
210-
color: e,
211-
),
212-
),
213-
),
214-
)
215-
.toList(),
219+
),
216220
),
217-
],
218-
),
219-
),
221+
)
222+
.toList(),
220223
),
221224
sizedBoxW4H4,
222225
FilledButton(
@@ -268,7 +271,7 @@ class _ColorBottomSheetState extends State<_ColorBottomSheet> with SingleTickerP
268271
@override
269272
void initState() {
270273
super.initState();
271-
_tabController = TabController(length: 3, vsync: this);
274+
_tabController = TabController(length: 3, vsync: this)..addListener(onCurrentIndexChanged);
272275
_customColorValueController = TextEditingController();
273276
if (widget.initialColor != null) {
274277
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -287,7 +290,9 @@ class _ColorBottomSheetState extends State<_ColorBottomSheet> with SingleTickerP
287290

288291
@override
289292
void dispose() {
290-
_tabController.dispose();
293+
_tabController
294+
..removeListener(onCurrentIndexChanged)
295+
..dispose();
291296
_customColorValueController.dispose();
292297
super.dispose();
293298
}
@@ -296,38 +301,38 @@ class _ColorBottomSheetState extends State<_ColorBottomSheet> with SingleTickerP
296301
Widget build(BuildContext context) {
297302
final tr = context.t.colorPickerDialog;
298303

299-
return ConstrainedBox(
300-
constraints: const BoxConstraints(maxHeight: 450),
301-
child: Column(
302-
mainAxisSize: MainAxisSize.min,
303-
children: [
304-
TabBar(
305-
controller: _tabController,
306-
tabs: [
307-
Tab(text: tr.tabs.normal.title),
308-
Tab(text: tr.tabs.advanced.title),
309-
Tab(text: tr.tabs.custom.title),
310-
],
311-
),
312-
sizedBoxW4H4,
313-
Expanded(
314-
child: Padding(
315-
padding: edgeInsetsL12R12,
316-
child: TabBarView(
317-
controller: _tabController,
318-
children: [_buildNormalTab(), _buildAdvancedTab(), _buildCustomTab(context)],
319-
),
304+
return SingleChildScrollView(
305+
child: Padding(
306+
padding: edgeInsetsL12R12,
307+
child: Column(
308+
mainAxisSize: MainAxisSize.min,
309+
spacing: 4,
310+
children: [
311+
TabBar(
312+
controller: _tabController,
313+
tabs: [
314+
Tab(text: tr.tabs.normal.title),
315+
Tab(text: tr.tabs.advanced.title),
316+
Tab(text: tr.tabs.custom.title),
317+
],
320318
),
321-
),
322-
sizedBoxW4H4,
323-
Align(
324-
alignment: Alignment.centerRight,
325-
child: TextButton(
326-
onPressed: () => Navigator.of(context).pop(PickColorResult.clearColor()),
327-
child: Text(context.t.general.reset),
319+
IndexedStack(
320+
index: currentIndex,
321+
children: [
322+
_buildNormalTab(),
323+
_buildAdvancedTab(),
324+
_buildCustomTab(context),
325+
],
328326
),
329-
),
330-
],
327+
Align(
328+
alignment: Alignment.centerRight,
329+
child: TextButton(
330+
onPressed: () => Navigator.of(context).pop(PickColorResult.clearColor()),
331+
child: Text(context.t.general.reset),
332+
),
333+
),
334+
],
335+
),
331336
),
332337
);
333338
}

lib/features/editor/widgets/emoji_bottom_sheet.dart

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,31 @@ class _EmojiBottomSheet extends StatefulWidget {
3434

3535
class _EmojiBottomSheetState extends State<_EmojiBottomSheet> with SingleTickerProviderStateMixin {
3636
TabController? tabController;
37+
int sheetIndex = 0;
38+
39+
void onSheetIndexChanged() {
40+
if (tabController?.indexIsChanging ?? false) {
41+
final currIndex = tabController?.index;
42+
if (currIndex != null) {
43+
setState(() {
44+
sheetIndex = currIndex;
45+
});
46+
}
47+
}
48+
}
3749

3850
@override
3951
void dispose() {
40-
tabController?.dispose();
52+
tabController
53+
?..removeListener(onSheetIndexChanged)
54+
..dispose();
4155
super.dispose();
4256
}
4357

4458
/// When calling this function, assume all emoji is available.
4559
Widget _buildEmojiTab(BuildContext context, EmojiState state) {
4660
final emojiGroupList = state.emojiGroupList!;
47-
tabController ??= TabController(length: emojiGroupList.length, vsync: this);
61+
tabController ??= TabController(length: emojiGroupList.length, vsync: this)..addListener(onSheetIndexChanged);
4862

4963
final tabs = emojiGroupList.map((e) => Tab(child: Text(e.name)));
5064
final tabViews = emojiGroupList.map(
@@ -75,14 +89,20 @@ class _EmojiBottomSheetState extends State<_EmojiBottomSheet> with SingleTickerP
7589
),
7690
);
7791

78-
return Column(
79-
children: [
80-
TabBar(isScrollable: true, tabAlignment: TabAlignment.start, controller: tabController, tabs: tabs.toList()),
81-
sizedBoxW12H12,
82-
Expanded(
83-
child: TabBarView(controller: tabController, children: tabViews.toList()),
84-
),
85-
],
92+
return SingleChildScrollView(
93+
child: Column(
94+
mainAxisSize: .min,
95+
children: [
96+
TabBar(isScrollable: true, tabAlignment: TabAlignment.start, controller: tabController, tabs: tabs.toList()),
97+
sizedBoxW12H12,
98+
// Use IndexStack instead of TabBarView to make contents having minimum height for the bottom sheet.
99+
// But it stucks more than TabBarView.
100+
IndexedStack(
101+
index: sheetIndex,
102+
children: tabViews.toList(),
103+
),
104+
],
105+
),
86106
);
87107
}
88108

@@ -115,7 +135,7 @@ class _EmojiBottomSheetState extends State<_EmojiBottomSheet> with SingleTickerP
115135
EmojiStatus.success => _buildEmojiTab(context, state),
116136
};
117137

118-
return ConstrainedBox(constraints: const BoxConstraints(maxHeight: 400), child: body);
138+
return body;
119139
},
120140
),
121141
);

lib/features/forum/widgets/thread_filter_chip.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class ThreadChip extends StatelessWidget {
4949
builder: (_) => BlocProvider.value(
5050
value: context.read<ForumBloc>(),
5151
child: BlocBuilder<ForumBloc, ForumState>(
52-
builder: (_, state) => ListView(children: sheetItemBuilder(context, state)),
52+
builder: (_, state) => ListView(shrinkWrap: true, children: sheetItemBuilder(context, state)),
5353
),
5454
),
5555
);

0 commit comments

Comments
 (0)