CW-376-picker-ui-issue (#919)

* feat: use common modal widget for repeated picker logic and display * refactor: rename widget * refactor: clear wrapper logic from picker widget and move title to hasTitle * Minor code readability enhancements [skip ci] --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

Rafael Saes committed May 10, 2023 at 09:19 UTC e28e2fbdde43dd6a00910c51c485fb7c8cc52ecd
4 files changed +300 -295
lib/src/screens/dashboard/widgets/filter_widget.dart
+84 -84
@@ -1,13 +1,9 @@
1 -import 'dart:ui';
2 -import 'package:cake_wallet/palette.dart';
1 import 'package:cake_wallet/src/screens/dashboard/widgets/filter_tile.dart';
2 import 'package:cake_wallet/src/widgets/section_divider.dart';
3 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
4 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
7 -import 'package:flutter/cupertino.dart';
5 import 'package:flutter/material.dart';
9 -import 'package:cake_wallet/src/widgets/alert_background.dart';
10 -import 'package:cake_wallet/src/widgets/alert_close_button.dart';
6 +import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart';
7 import 'package:cake_wallet/generated/i18n.dart';
8 import 'package:flutter_mobx/flutter_mobx.dart';
9 //import 'package:date_range_picker/date_range_picker.dart' as date_rage_picker;
@@ -20,89 +16,93 @@ class FilterWidget extends StatelessWidget {
16 @override
17 Widget build(BuildContext context) {
18 const sectionDivider = const SectionDivider();
23 - return AlertBackground(
24 - child: Stack(
25 - alignment: Alignment.center,
26 - children: <Widget>[
27 - Column(
28 - mainAxisSize: MainAxisSize.min,
29 - children: <Widget>[
30 - Padding(
31 - padding: EdgeInsets.only(left: 24, right: 24, top: 24),
32 - child: ClipRRect(
33 - borderRadius: BorderRadius.all(Radius.circular(24)),
34 - child: Container(
35 - color: Theme.of(context).textTheme!.bodyText1!.decorationColor!,
36 - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
37 - Padding(
38 - padding: EdgeInsets.all(24.0),
39 - child: Text(
40 - S.of(context).filter_by,
41 - style: TextStyle(
42 - color: Theme.of(context).primaryTextTheme.overline!.color!,
43 - fontSize: 16,
44 - fontFamily: 'Lato',
45 - decoration: TextDecoration.none,
46 - ),
19 + return PickerWrapperWidget(
20 + children: [
21 + Padding(
22 + padding: EdgeInsets.only(left: 24, right: 24, top: 24),
23 + child: ClipRRect(
24 + borderRadius: BorderRadius.all(Radius.circular(24)),
25 + child: Container(
26 + color: Theme.of(context).textTheme!.bodyText1!.decorationColor!,
27 + child: Column(
28 + crossAxisAlignment: CrossAxisAlignment.start,
29 + children: [
30 + Padding(
31 + padding: EdgeInsets.all(24.0),
32 + child: Text(
33 + S.of(context).filter_by,
34 + style: TextStyle(
35 + color: Theme.of(context)
36 + .primaryTextTheme
37 + .overline!
38 + .color!,
39 + fontSize: 16,
40 + fontFamily: 'Lato',
41 + decoration: TextDecoration.none,
42 ),
43 ),
49 - sectionDivider,
50 - ListView.separated(
51 - padding: EdgeInsets.zero,
52 - shrinkWrap: true,
53 - physics: const NeverScrollableScrollPhysics(),
54 - itemCount: dashboardViewModel.filterItems.length,
55 - separatorBuilder: (context, _) => sectionDivider,
56 - itemBuilder: (_, index1) {
57 - final title = dashboardViewModel.filterItems.keys.elementAt(index1);
58 - final section = dashboardViewModel.filterItems.values.elementAt(index1);
59 - return Column(
60 - crossAxisAlignment: CrossAxisAlignment.start,
61 - children: <Widget>[
62 - Padding(
63 - padding: EdgeInsets.only(top: 20, left: 24, right: 24),
64 - child: Text(
65 - title,
66 - style: TextStyle(
67 - color: Theme.of(context).primaryTextTheme!.headline6!.color!,
68 - fontSize: 16,
69 - fontFamily: 'Lato',
70 - fontWeight: FontWeight.bold,
71 - decoration: TextDecoration.none),
72 - ),
44 + ),
45 + sectionDivider,
46 + ListView.separated(
47 + padding: EdgeInsets.zero,
48 + shrinkWrap: true,
49 + physics: const NeverScrollableScrollPhysics(),
50 + itemCount: dashboardViewModel.filterItems.length,
51 + separatorBuilder: (context, _) => sectionDivider,
52 + itemBuilder: (_, index1) {
53 + final title = dashboardViewModel.filterItems.keys
54 + .elementAt(index1);
55 + final section = dashboardViewModel.filterItems.values
56 + .elementAt(index1);
57 + return Column(
58 + crossAxisAlignment: CrossAxisAlignment.start,
59 + children: <Widget>[
60 + Padding(
61 + padding:
62 + EdgeInsets.only(top: 20, left: 24, right: 24),
63 + child: Text(
64 + title,
65 + style: TextStyle(
66 + color: Theme.of(context)
67 + .primaryTextTheme!
68 + .headline6!
69 + .color!,
70 + fontSize: 16,
71 + fontFamily: 'Lato',
72 + fontWeight: FontWeight.bold,
73 + decoration: TextDecoration.none),
74 ),
74 - ListView.builder(
75 - padding: EdgeInsets.symmetric(vertical: 8.0),
76 - shrinkWrap: true,
77 - physics: const NeverScrollableScrollPhysics(),
78 - itemCount: section.length,
79 - itemBuilder: (_, index2) {
80 - final item = section[index2];
81 - final content = Observer(
82 - builder: (_) => StandardCheckbox(
83 - value: item.value(),
84 - caption: item.caption,
85 - gradientBackground: true,
86 - borderColor: Theme.of(context).dividerColor,
87 - iconColor: Colors.white,
88 - onChanged: (value) => item.onChanged(),
89 - ));
90 - return FilterTile(child: content);
91 - },
92 - )
93 - ],
94 - );
95 - },
96 - ),
97 - ]),
98 - ),
99 - ),
100 - ),
101 - ],
75 + ),
76 + ListView.builder(
77 + padding: EdgeInsets.symmetric(vertical: 8.0),
78 + shrinkWrap: true,
79 + physics: const NeverScrollableScrollPhysics(),
80 + itemCount: section.length,
81 + itemBuilder: (_, index2) {
82 + final item = section[index2];
83 + final content = Observer(
84 + builder: (_) => StandardCheckbox(
85 + value: item.value(),
86 + caption: item.caption,
87 + gradientBackground: true,
88 + borderColor:
89 + Theme.of(context).dividerColor,
90 + iconColor: Colors.white,
91 + onChanged: (value) =>
92 + item.onChanged(),
93 + ));
94 + return FilterTile(child: content);
95 + },
96 + )
97 + ],
98 + );
99 + },
100 + ),
101 + ]),
102 + ),
103 ),
103 - AlertCloseButton()
104 - ],
105 - ),
104 + )
105 + ],
106 );
107 }
108 }
lib/src/widgets/check_box_picker.dart
+49 -63
@@ -1,8 +1,7 @@
1 import 'package:cake_wallet/palette.dart';
2 import 'package:cake_wallet/utils/responsive_layout_util.dart';
3 import 'package:flutter/material.dart';
4 -import 'package:cake_wallet/src/widgets/alert_background.dart';
5 -import 'package:cake_wallet/src/widgets/alert_close_button.dart';
4 +import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart';
5
6 class CheckBoxPicker extends StatefulWidget {
7 CheckBoxPicker({
@@ -32,73 +31,57 @@ class CheckBoxPickerState extends State<CheckBoxPicker> {
31
32 @override
33 Widget build(BuildContext context) {
35 - return AlertBackground(
36 - child: Column(
37 - children: [
38 - Expanded(
39 - child: Stack(
40 - alignment: Alignment.center,
41 - children: [
42 - Column(
34 + return PickerWrapperWidget(
35 + children: [
36 + if (widget.title.isNotEmpty)
37 + Container(
38 + padding: EdgeInsets.symmetric(horizontal: 24),
39 + child: Text(
40 + widget.title,
41 + textAlign: TextAlign.center,
42 + style: TextStyle(
43 + fontSize: 18,
44 + fontFamily: 'Lato',
45 + fontWeight: FontWeight.bold,
46 + decoration: TextDecoration.none,
47 + color: Colors.white,
48 + ),
49 + ),
50 + ),
51 + Padding(
52 + padding: EdgeInsets.only(left: 24, right: 24, top: 24),
53 + child: ClipRRect(
54 + borderRadius: BorderRadius.all(Radius.circular(30)),
55 + child: Container(
56 + color: Theme.of(context).accentTextTheme.headline6!.color!,
57 + child: ConstrainedBox(
58 + constraints: BoxConstraints(
59 + maxHeight: MediaQuery.of(context).size.height * 0.65,
60 + maxWidth: ResponsiveLayoutUtil.kPopupWidth,
61 + ),
62 + child: Column(
63 mainAxisSize: MainAxisSize.min,
44 - children: <Widget>[
45 - if (widget.title.isNotEmpty)
46 - Container(
47 - padding: EdgeInsets.symmetric(horizontal: 24),
48 - child: Text(
49 - widget.title,
50 - textAlign: TextAlign.center,
51 - style: TextStyle(
52 - fontSize: 18,
53 - fontFamily: 'Lato',
54 - fontWeight: FontWeight.bold,
55 - decoration: TextDecoration.none,
56 - color: Colors.white,
57 - ),
58 - ),
59 - ),
60 - Padding(
61 - padding: EdgeInsets.only(left: 24, right: 24, top: 24),
62 - child: ClipRRect(
63 - borderRadius: BorderRadius.all(Radius.circular(30)),
64 - child: Container(
65 - color: Theme.of(context).accentTextTheme.headline6!.color!,
66 - child: ConstrainedBox(
67 - constraints: BoxConstraints(
68 - maxHeight: MediaQuery.of(context).size.height * 0.65,
69 - maxWidth: ResponsiveLayoutUtil.kPopupWidth,
70 - ),
71 - child: Column(
72 - mainAxisSize: MainAxisSize.min,
73 - children: [
74 - Flexible(
75 - child: Stack(
76 - alignment: Alignment.center,
77 - children: <Widget>[
78 - items.length > 3
79 - ? Scrollbar(
80 - controller: controller,
81 - child: itemsList(),
82 - )
83 - : itemsList(),
84 - ],
85 - ),
86 - ),
87 - ],
88 - ),
89 - ),
90 - ),
64 + children: [
65 + Flexible(
66 + child: Stack(
67 + alignment: Alignment.center,
68 + children: <Widget>[
69 + items.length > 3
70 + ? Scrollbar(
71 + controller: controller,
72 + child: itemsList(),
73 + )
74 + : itemsList(),
75 + ],
76 ),
77 ),
78 ],
79 ),
95 - SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight),
96 - AlertCloseButton(),
97 - ],
80 + ),
81 ),
82 ),
100 - ],
101 - ),
83 + ),
84 + ],
85 );
86 }
87
@@ -111,7 +94,10 @@ class CheckBoxPickerState extends State<CheckBoxPicker> {
94 shrinkWrap: true,
95 separatorBuilder: (context, index) => widget.isSeparated
96 ? Divider(
114 - color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!,
97 + color: Theme.of(context)
98 + .accentTextTheme
99 + .headline6!
100 + .backgroundColor!,
101 height: 1,
102 )
103 : const SizedBox(),
lib/src/widgets/picker.dart
+106 -148
@@ -2,9 +2,8 @@
2
3 import 'package:cake_wallet/utils/responsive_layout_util.dart';
4 import 'package:flutter/material.dart';
5 -import 'package:cake_wallet/src/widgets/alert_background.dart';
6 -import 'package:cake_wallet/src/widgets/alert_close_button.dart';
5 import 'package:cw_core/currency.dart';
6 +import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart';
7
8 class Picker<Item> extends StatefulWidget {
9 Picker({
@@ -114,171 +113,130 @@ class _PickerState<Item> extends State<Picker<Item>> {
113 final mq = MediaQuery.of(context);
114 final bottom = mq.viewInsets.bottom;
115 final height = mq.size.height - bottom;
117 - final screenCenter = height / 2;
116
119 - double closeButtonBottom = 60;
117 double containerHeight = height * 0.65;
118 if (bottom > 0) {
119 // increase a bit or it gets too squished in the top
120 containerHeight = height * 0.75;
124 -
125 - final containerCenter = containerHeight / 2;
126 - final containerBottom = screenCenter - containerCenter;
127 -
128 - final hasTitle = widget.title == null || widget.title!.isEmpty;
129 -
130 - // position the close button right below the search container
131 - closeButtonBottom = closeButtonBottom -
132 - containerBottom +
133 - (hasTitle ? padding : padding / 1.5);
121 }
122
136 - return AlertBackground(
137 - child: Column(
138 - children: [
139 - Expanded(
140 - flex: 1,
141 - child: Stack(
142 - alignment: Alignment.center,
143 - children: <Widget>[
144 - Column(
123 + return PickerWrapperWidget(
124 + hasTitle: widget.title?.isNotEmpty ?? false,
125 + children: [
126 + if (widget.title?.isNotEmpty ?? false)
127 + Container(
128 + padding: EdgeInsets.symmetric(horizontal: padding),
129 + child: Text(
130 + widget.title!,
131 + textAlign: TextAlign.center,
132 + style: TextStyle(
133 + fontSize: 18,
134 + fontFamily: 'Lato',
135 + fontWeight: FontWeight.bold,
136 + decoration: TextDecoration.none,
137 + color: Colors.white,
138 + ),
139 + ),
140 + ),
141 + Padding(
142 + padding: EdgeInsets.symmetric(horizontal: padding),
143 + child: ClipRRect(
144 + borderRadius: BorderRadius.all(Radius.circular(30)),
145 + child: Container(
146 + color: Theme.of(context).accentTextTheme.headline6!.color!,
147 + child: ConstrainedBox(
148 + constraints: BoxConstraints(
149 + maxHeight: containerHeight,
150 + maxWidth: ResponsiveLayoutUtil.kPopupWidth,
151 + ),
152 + child: Column(
153 mainAxisSize: MainAxisSize.min,
146 - mainAxisAlignment: MainAxisAlignment.center,
147 - children: <Widget>[
148 - if (widget.title?.isNotEmpty ?? false)
149 - Container(
150 - padding: EdgeInsets.symmetric(horizontal: padding),
151 - child: Text(
152 - widget.title!,
153 - textAlign: TextAlign.center,
154 + children: [
155 + if (widget.hintText != null)
156 + Padding(
157 + padding: const EdgeInsets.all(16),
158 + child: TextFormField(
159 + controller: searchController,
160 style: TextStyle(
155 - fontSize: 18,
156 - fontFamily: 'Lato',
157 - fontWeight: FontWeight.bold,
158 - decoration: TextDecoration.none,
159 - color: Colors.white,
161 + color: Theme.of(context)
162 + .primaryTextTheme
163 + .headline6!
164 + .color!),
165 + decoration: InputDecoration(
166 + hintText: widget.hintText,
167 + prefixIcon:
168 + Image.asset("assets/images/search_icon.png"),
169 + filled: true,
170 + fillColor: Theme.of(context)
171 + .accentTextTheme
172 + .headline3!
173 + .color!,
174 + alignLabelWithHint: false,
175 + contentPadding: const EdgeInsets.symmetric(
176 + vertical: 4, horizontal: 16),
177 + enabledBorder: OutlineInputBorder(
178 + borderRadius: BorderRadius.circular(14),
179 + borderSide: const BorderSide(
180 + color: Colors.transparent,
181 + )),
182 + focusedBorder: OutlineInputBorder(
183 + borderRadius: BorderRadius.circular(14),
184 + borderSide: const BorderSide(
185 + color: Colors.transparent,
186 + )),
187 ),
188 ),
189 ),
163 - Padding(
164 - padding: EdgeInsets.symmetric(horizontal: padding),
165 - child: ClipRRect(
166 - borderRadius: BorderRadius.all(Radius.circular(30)),
167 - child: Container(
168 - color: Theme.of(context)
169 - .accentTextTheme
170 - .headline6!
171 - .color!,
172 - child: ConstrainedBox(
173 - constraints: BoxConstraints(
174 - maxHeight: containerHeight,
175 - maxWidth: ResponsiveLayoutUtil.kPopupWidth,
176 - ),
177 - child: Column(
178 - mainAxisSize: MainAxisSize.min,
179 - children: [
180 - if (widget.hintText != null)
181 - Padding(
182 - padding: const EdgeInsets.all(16),
183 - child: TextFormField(
184 - controller: searchController,
185 - style: TextStyle(
186 - color: Theme.of(context)
187 - .primaryTextTheme
188 - .headline6!
189 - .color!),
190 - decoration: InputDecoration(
191 - hintText: widget.hintText,
192 - prefixIcon: Image.asset(
193 - "assets/images/search_icon.png"),
194 - filled: true,
195 - fillColor: Theme.of(context)
196 - .accentTextTheme
197 - .headline3!
198 - .color!,
199 - alignLabelWithHint: false,
200 - contentPadding:
201 - const EdgeInsets.symmetric(
202 - vertical: 4, horizontal: 16),
203 - enabledBorder: OutlineInputBorder(
204 - borderRadius:
205 - BorderRadius.circular(14),
206 - borderSide: const BorderSide(
207 - color: Colors.transparent,
208 - )),
209 - focusedBorder: OutlineInputBorder(
210 - borderRadius:
211 - BorderRadius.circular(14),
212 - borderSide: const BorderSide(
213 - color: Colors.transparent,
214 - )),
215 - ),
190 + Divider(
191 + color: Theme.of(context)
192 + .accentTextTheme
193 + .headline6!
194 + .backgroundColor!,
195 + height: 1,
196 + ),
197 + if (widget.selectedAtIndex != -1)
198 + buildSelectedItem(widget.selectedAtIndex),
199 + Flexible(
200 + child: Stack(
201 + alignment: Alignment.center,
202 + children: <Widget>[
203 + filteredItems.length > 3
204 + ? Scrollbar(
205 + controller: controller,
206 + child: itemsList(),
207 + )
208 + : itemsList(),
209 + (widget.description?.isNotEmpty ?? false)
210 + ? Positioned(
211 + bottom: padding,
212 + left: padding,
213 + right: padding,
214 + child: Text(
215 + widget.description!,
216 + textAlign: TextAlign.center,
217 + style: TextStyle(
218 + fontSize: 12,
219 + fontWeight: FontWeight.w500,
220 + fontFamily: 'Lato',
221 + decoration: TextDecoration.none,
222 + color: Theme.of(context)
223 + .primaryTextTheme
224 + .headline6!
225 + .color!,
226 ),
227 ),
218 - Divider(
219 - color: Theme.of(context)
220 - .accentTextTheme
221 - .headline6!
222 - .backgroundColor!,
223 - height: 1,
224 - ),
225 - if (widget.selectedAtIndex != -1)
226 - buildSelectedItem(widget.selectedAtIndex),
227 - Flexible(
228 - child: Stack(
229 - alignment: Alignment.center,
230 - children: <Widget>[
231 - filteredItems.length > 3
232 - ? Scrollbar(
233 - controller: controller,
234 - child: itemsList(),
235 - )
236 - : itemsList(),
237 - (widget.description?.isNotEmpty ?? false)
238 - ? Positioned(
239 - bottom: padding,
240 - left: padding,
241 - right: padding,
242 - child: Text(
243 - widget.description!,
244 - textAlign: TextAlign.center,
245 - style: TextStyle(
246 - fontSize: 12,
247 - fontWeight: FontWeight.w500,
248 - fontFamily: 'Lato',
249 - decoration:
250 - TextDecoration.none,
251 - color: Theme.of(context)
252 - .primaryTextTheme
253 - .headline6!
254 - .color!,
255 - ),
256 - ),
257 - )
258 - : Offstage(),
259 - ],
260 - ),
261 - ),
262 - ],
263 - ),
264 - ),
265 - ),
228 + )
229 + : Offstage(),
230 + ],
231 ),
267 - )
232 + ),
233 ],
234 ),
270 - SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight),
271 - AlertCloseButton(bottom: closeButtonBottom),
272 - ],
235 + ),
236 ),
237 ),
275 - // gives the extra spacing using MediaQuery.viewInsets.bottom
276 - // to simulate a keyboard area
277 - SizedBox(
278 - height: bottom,
279 - )
280 - ],
281 - ),
238 + )
239 + ],
240 );
241 }
242
lib/src/widgets/picker_wrapper_widget.dart new
+61
@@ -0,0 +1,61 @@
1 +import 'package:cake_wallet/utils/responsive_layout_util.dart';
2 +import 'package:flutter/material.dart';
3 +import 'package:cake_wallet/src/widgets/alert_background.dart';
4 +import 'package:cake_wallet/src/widgets/alert_close_button.dart';
5 +
6 +class PickerWrapperWidget extends StatelessWidget {
7 + PickerWrapperWidget({required this.children, this.hasTitle = false});
8 +
9 + final List<Widget> children;
10 + final bool hasTitle;
11 +
12 + @override
13 + Widget build(BuildContext context) {
14 + final double padding = 24;
15 +
16 + final mq = MediaQuery.of(context);
17 + final bottom = mq.viewInsets.bottom;
18 + final height = mq.size.height - bottom;
19 + final screenCenter = height / 2;
20 +
21 + double closeButtonBottom = 60;
22 + double containerHeight = height * 0.65;
23 + if (bottom > 0) {
24 + // increase a bit or it gets too squished in the top
25 + containerHeight = height * 0.75;
26 +
27 + final containerCenter = containerHeight / 2;
28 + final containerBottom = screenCenter - containerCenter;
29 +
30 + // position the close button right below the search container
31 + closeButtonBottom = closeButtonBottom -
32 + containerBottom + (!hasTitle ? padding : padding / 1.5);
33 + }
34 +
35 + return AlertBackground(
36 + child: Column(
37 + children: [
38 + Expanded(
39 + flex: 1,
40 + child: Stack(
41 + alignment: Alignment.center,
42 + children: <Widget>[
43 + Column(
44 + mainAxisSize: MainAxisSize.min,
45 + children: children,
46 + ),
47 + SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight),
48 + AlertCloseButton(bottom: closeButtonBottom),
49 + ],
50 + ),
51 + ),
52 + // gives the extra spacing using MediaQuery.viewInsets.bottom
53 + // to simulate a keyboard area
54 + SizedBox(
55 + height: bottom,
56 + )
57 + ],
58 + ),
59 + );
60 + }
61 +}