Line data Source code
1 : import 'package:flutter/cupertino.dart';
2 : import 'package:flutter/material.dart';
3 :
4 : import '../buttons/custom_action_button.dart';
5 : import '../buttons/list_tile_button.dart';
6 :
7 : /// A class responsible for displaying customized modal bottom sheets.
8 : ///
9 : /// The [OpenCustomSheet] class provides a flexible and reusable way to present
10 : /// modal bottom sheets with various customization options, such as padding,
11 : /// colors, shapes, and default action buttons. It supports both scrollable and
12 : /// non-scrollable sheets, and includes factory constructors for common sheet
13 : /// configurations like confirmation dialogs and scrollable content areas.
14 : class OpenCustomSheet {
15 : /// Determines whether the sheet can be dismissed by tapping outside of it.
16 : ///
17 : /// Defaults to `true`.
18 : final bool barrierDismissible;
19 :
20 : /// The semantic label for the barrier.
21 : ///
22 : /// Useful for accessibility purposes. If not provided, defaults to null.
23 : final String? barrierLabel;
24 :
25 : /// The color of the modal barrier that darkens everything below the sheet.
26 : ///
27 : /// If not specified, it defaults to `Colors.black54`.
28 : final Color? barrierColor;
29 :
30 : /// Callback invoked when the sheet is closed.
31 : ///
32 : /// Receives the result passed to `Navigator.pop`.
33 : final Function(dynamic)? onClose;
34 :
35 : /// The main content of the sheet.
36 : ///
37 : /// A function that returns a widget. It can optionally receive a [ScrollController]
38 : /// if the sheet is scrollable.
39 : final Widget Function({ScrollController? scrollController}) body;
40 :
41 : /// Determines if the sheet is scrollable.
42 : ///
43 : /// If `true`, the sheet uses a [DraggableScrollableSheet] to allow scrolling.
44 : /// Defaults to `false`.
45 : final bool scrollable;
46 :
47 : /// Whether the sheet should expand to fill the available space.
48 : ///
49 : /// Defaults to `true`.
50 : final bool expand;
51 :
52 : /// The initial size of the sheet as a fraction of the screen height.
53 : ///
54 : /// Only applicable if [scrollable] is `true`. Defaults to `0.5`.
55 : final double initialChildSize;
56 :
57 : /// The minimum size the sheet can be dragged down to, as a fraction of the screen height.
58 : ///
59 : /// Only applicable if [scrollable] is `true`. Defaults to `0.25`.
60 : final double minChildSize;
61 :
62 : /// The maximum size the sheet can be dragged up to, as a fraction of the screen height.
63 : ///
64 : /// Only applicable if [scrollable] is `true`. Defaults to `1.0`.
65 : final double maxChildSize;
66 :
67 : /// The background color of the sheet.
68 : ///
69 : /// If not specified, it defaults to the theme's card color.
70 : final Color? backgroundColor;
71 :
72 : /// The color of the handle (the small bar at the top of the sheet) used for dragging.
73 : ///
74 : /// If not specified, it defaults to `CupertinoColors.inactiveGray`.
75 : final Color? handleColor;
76 :
77 : /// The shape of the sheet's border.
78 : ///
79 : /// Allows for customizing the sheet's outline and corners. If not specified,
80 : /// a default rounded rectangle is used.
81 : final ShapeBorder? sheetShape;
82 :
83 : /// The padding inside the sheet.
84 : ///
85 : /// Applies padding around the entire content of the sheet. If not specified,
86 : /// it defaults to `EdgeInsets.only(bottom: 16.0)`.
87 : final EdgeInsetsGeometry? sheetPadding;
88 :
89 : /// The background color of the first action button.
90 : ///
91 : /// Typically used for the cancellation or negative action. If not specified,
92 : /// it defaults to `Colors.red`.
93 : final Color? firstButtonColor;
94 :
95 : /// The background color of the second action button.
96 : ///
97 : /// Typically used for the confirmation or positive action. If not specified,
98 : /// it defaults to `Colors.green`.
99 : final Color? secondButtonColor;
100 :
101 : /// The text color of the first action button.
102 : ///
103 : /// If not specified, it defaults to `Colors.white`.
104 : final Color? firstButtonTextColor;
105 :
106 : /// The text color of the second action button.
107 : ///
108 : /// If not specified, it defaults to `Colors.white`.
109 : final Color? secondButtonTextColor;
110 :
111 : /// The text label for the confirmation button.
112 : ///
113 : /// If not specified, it defaults to `'Confirm'`.
114 : final String? confirmButtonText;
115 :
116 : /// The text label for the cancellation button.
117 : ///
118 : /// If not specified, it defaults to `'Close'`.
119 : final String? cancelButtonText;
120 :
121 : /// The padding around the action buttons.
122 : ///
123 : /// If not specified, it defaults to `EdgeInsets.symmetric(vertical: 10, horizontal: 8)`.
124 : final EdgeInsetsGeometry? padding;
125 :
126 : /// The spacing between the action buttons.
127 : ///
128 : /// If not specified, it defaults to `8.0`.
129 : final double? buttonSpacing;
130 :
131 : /// Determines whether to show default action buttons (Confirm and Cancel).
132 : ///
133 : /// If set to `true`, the sheet will include these buttons at the bottom.
134 : /// Defaults to `false`.
135 : final bool showDefaultButtons;
136 :
137 : /// Creates an instance of [OpenCustomSheet] with the specified properties.
138 : ///
139 : /// The [body] parameter is required and must not be null. Other parameters are optional
140 : /// and have default values if not specified.
141 1 : const OpenCustomSheet({
142 : this.barrierDismissible = true,
143 : this.barrierColor,
144 : this.barrierLabel,
145 : this.onClose,
146 : required this.body,
147 : this.scrollable = false,
148 : this.expand = true,
149 : this.initialChildSize = 0.5,
150 : this.minChildSize = 0.25,
151 : this.maxChildSize = 1.0,
152 : this.backgroundColor,
153 : this.handleColor,
154 : this.sheetShape,
155 : this.sheetPadding,
156 : this.firstButtonColor,
157 : this.secondButtonColor,
158 : this.firstButtonTextColor,
159 : this.secondButtonTextColor,
160 : this.confirmButtonText,
161 : this.cancelButtonText,
162 : this.padding,
163 : this.buttonSpacing,
164 : this.showDefaultButtons = false, // Changed default to false
165 : });
166 :
167 : /// Factory constructor to create a confirmation sheet with default action buttons.
168 : ///
169 : /// The confirmation sheet includes Confirm and Cancel buttons at the bottom.
170 : ///
171 : /// **Example usage:**
172 : /// ```dart
173 : /// OpenCustomSheet.openConfirmSheet(
174 : /// context,
175 : /// body: ({scrollController}) => Text('Are you sure you want to proceed?'),
176 : /// onClose: (result) {
177 : /// if (result == true) {
178 : /// // Handle confirmation
179 : /// } else {
180 : /// // Handle cancellation
181 : /// }
182 : /// },
183 : /// );
184 : /// ```
185 1 : factory OpenCustomSheet.openConfirmSheet(
186 : BuildContext context, {
187 : required Widget body,
188 : Function(dynamic)? onClose,
189 : Color? backgroundColor,
190 : Color? handleColor,
191 : bool barrierDismissible = true,
192 : Color? firstButtonColor,
193 : Color? secondButtonColor,
194 : Color? firstButtonTextColor,
195 : Color? secondButtonTextColor,
196 : String? confirmButtonText,
197 : String? cancelButtonText,
198 : EdgeInsetsGeometry? padding,
199 : double? buttonSpacing,
200 : }) {
201 1 : return OpenCustomSheet(
202 : barrierDismissible: barrierDismissible,
203 1 : barrierColor: Colors.black.withOpacity(0.5),
204 : onClose: onClose,
205 : backgroundColor: backgroundColor,
206 : handleColor: handleColor,
207 : padding: padding,
208 : buttonSpacing: buttonSpacing,
209 : firstButtonColor: firstButtonColor,
210 : secondButtonColor: secondButtonColor,
211 : firstButtonTextColor: firstButtonTextColor,
212 : secondButtonTextColor: secondButtonTextColor,
213 : confirmButtonText: confirmButtonText,
214 : cancelButtonText: cancelButtonText,
215 : showDefaultButtons: true,
216 : // Enable default buttons for confirm sheet
217 1 : body: ({scrollController}) => body,
218 : );
219 : }
220 :
221 : /// Factory constructor to create a scrollable sheet without default action buttons.
222 : ///
223 : /// This is useful for displaying large amounts of content that require scrolling.
224 : ///
225 : /// **Example usage:**
226 : /// ```dart
227 : /// OpenCustomSheet.scrollableSheet(
228 : /// context,
229 : /// body: ({scrollController}) => ListView.builder(
230 : /// controller: scrollController,
231 : /// itemCount: 50,
232 : /// itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
233 : /// ),
234 : /// );
235 : /// ```
236 1 : factory OpenCustomSheet.scrollableSheet(
237 : BuildContext context, {
238 : required Widget Function({ScrollController? scrollController}) body,
239 : Function(dynamic)? onClose,
240 : bool expand = true,
241 : double initialChildSize = 0.5,
242 : double minChildSize = 0.25,
243 : double maxChildSize = 1.0,
244 : Color? barrierColor,
245 : Color? backgroundColor,
246 : Color? handleColor,
247 : bool barrierDismissible = true,
248 : ShapeBorder? sheetShape,
249 : EdgeInsetsGeometry? sheetPadding,
250 : }) {
251 1 : return OpenCustomSheet(
252 : scrollable: true,
253 : expand: expand,
254 : initialChildSize: initialChildSize,
255 : minChildSize: minChildSize,
256 : maxChildSize: maxChildSize,
257 : onClose: onClose,
258 : barrierColor: barrierColor,
259 : barrierDismissible: barrierDismissible,
260 : backgroundColor: backgroundColor,
261 : handleColor: handleColor,
262 : sheetShape: sheetShape,
263 : sheetPadding: sheetPadding,
264 : showDefaultButtons: false,
265 : // Disable default buttons for scrollable sheet
266 : body: body,
267 : );
268 : }
269 :
270 : /// Displays the custom sheet using [showModalBottomSheet].
271 : ///
272 : /// This method configures the sheet based on the properties provided during
273 : /// instantiation, such as scrollability, padding, colors, and action buttons.
274 : ///
275 : /// After the sheet is closed, the [onClose] callback is invoked with the result.
276 1 : void show(BuildContext context) {
277 1 : showModalBottomSheet(
278 : backgroundColor: Colors.transparent,
279 1 : isDismissible: barrierDismissible,
280 1 : barrierColor: barrierColor,
281 1 : barrierLabel: barrierLabel,
282 : isScrollControlled: true,
283 : useSafeArea: true,
284 : context: context,
285 1 : builder: (context) {
286 1 : final mediaQuery = MediaQuery.of(context);
287 3 : final maxHeight = mediaQuery.size.height * 0.9;
288 1 : if (scrollable) {
289 1 : return DraggableScrollableSheet(
290 1 : expand: expand,
291 1 : initialChildSize: initialChildSize,
292 1 : minChildSize: minChildSize,
293 1 : maxChildSize: maxChildSize,
294 1 : builder: (context, scrollController) {
295 1 : return Container(
296 1 : decoration: BoxDecoration(
297 : borderRadius:
298 : const BorderRadius.vertical(top: Radius.circular(10)),
299 1 : color: backgroundColor ?? Theme.of(context).cardColor,
300 : ),
301 1 : padding: sheetPadding ?? const EdgeInsets.only(bottom: 16.0),
302 1 : child: Column(
303 1 : children: [
304 2 : if (handleColor != Colors.transparent)
305 2 : _buildHandle(handleColor),
306 1 : Expanded(
307 2 : child: body(scrollController: scrollController),
308 : ),
309 : ],
310 : ),
311 : );
312 : },
313 : );
314 : } else {
315 : // For non-scrollable sheet, enable scrolling only if necessary
316 1 : return Container(
317 1 : constraints: BoxConstraints(
318 : maxHeight: maxHeight,
319 : ),
320 1 : decoration: BoxDecoration(
321 : borderRadius:
322 : const BorderRadius.vertical(top: Radius.circular(10)),
323 3 : color: backgroundColor ?? Theme.of(context).cardColor,
324 : ),
325 1 : padding: sheetPadding ?? const EdgeInsets.only(bottom: 16.0),
326 1 : child: Column(
327 : mainAxisSize: MainAxisSize.min,
328 1 : children: [
329 2 : if (handleColor != Colors.transparent)
330 2 : _buildHandle(handleColor),
331 1 : Flexible(
332 1 : child: SingleChildScrollView(
333 1 : child: Padding(
334 : padding: const EdgeInsets.symmetric(
335 : horizontal: 30, vertical: 20),
336 2 : child: body(scrollController: null),
337 : ),
338 : ),
339 : ),
340 1 : if (showDefaultButtons)
341 1 : _buildButtons(
342 : context,
343 1 : firstButtonColor,
344 1 : secondButtonColor,
345 1 : firstButtonTextColor,
346 1 : secondButtonTextColor,
347 1 : buttonSpacing,
348 1 : confirmButtonText,
349 1 : cancelButtonText,
350 : ),
351 : ],
352 : ),
353 : );
354 : }
355 : },
356 2 : ).then((value) {
357 1 : if (onClose != null) {
358 2 : onClose!(value);
359 : }
360 : });
361 : }
362 :
363 : /// Builds the confirmation and cancellation buttons for the sheet.
364 : ///
365 : /// These buttons are displayed at the bottom of the sheet and handle user actions
366 : /// like confirming or cancelling an operation.
367 : ///
368 : /// - [context]: The build context.
369 : /// - [firstButtonColor]: The background color for the first button (e.g., Cancel).
370 : /// - [secondButtonColor]: The background color for the second button (e.g., Confirm).
371 : /// - [firstButtonTextColor]: The text color for the first button.
372 : /// - [secondButtonTextColor]: The text color for the second button.
373 : /// - [buttonSpacing]: The spacing between the buttons.
374 : /// - [confirmButtonText]: The label for the confirmation button.
375 : /// - [cancelButtonText]: The label for the cancellation button.
376 : ///
377 : /// **Example usage:**
378 : /// ```dart
379 : /// _buildButtons(
380 : /// context,
381 : /// Colors.red,
382 : /// Colors.green,
383 : /// Colors.white,
384 : /// Colors.white,
385 : /// 10.0,
386 : /// 'Yes',
387 : /// 'No',
388 : /// );
389 : /// ```
390 1 : static Widget _buildButtons(
391 : BuildContext context,
392 : Color? firstButtonColor,
393 : Color? secondButtonColor,
394 : Color? firstButtonTextColor,
395 : Color? secondButtonTextColor,
396 : double? buttonSpacing,
397 : String? confirmButtonText,
398 : String? cancelButtonText,
399 : ) {
400 1 : return Padding(
401 : padding: const EdgeInsets.only(top: 16.0),
402 1 : child: DoubleListTileButtons(
403 : padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
404 : space: buttonSpacing ?? 8,
405 1 : firstButton: CustomActionButton.flat(
406 : margin: EdgeInsets.zero,
407 : width: double.infinity,
408 2 : onPressed: () => Navigator.pop(context, false),
409 : backgroundColor: firstButtonColor ?? Colors.red,
410 1 : child: Text(
411 : cancelButtonText ?? 'Close',
412 4 : style: Theme.of(context).textTheme.labelLarge!.copyWith(
413 : color: firstButtonTextColor ?? Colors.white,
414 : ),
415 : ),
416 : ),
417 1 : secondButton: CustomActionButton.flat(
418 2 : onPressed: () => Navigator.pop(context, true),
419 : margin: EdgeInsets.zero,
420 : width: double.infinity,
421 : backgroundColor: secondButtonColor ?? Colors.green,
422 1 : child: Text(
423 : confirmButtonText ?? 'Confirm',
424 4 : style: Theme.of(context).textTheme.labelLarge!.copyWith(
425 : color: secondButtonTextColor ?? Colors.white,
426 : ),
427 : ),
428 : ),
429 : ),
430 : );
431 : }
432 :
433 : /// Builds the drag handle for the sheet.
434 : ///
435 : /// The handle provides a visual indicator that the sheet can be dragged.
436 : ///
437 : /// - [handleColor]: The color of the handle. If not specified, it defaults to
438 : /// `CupertinoColors.inactiveGray`.
439 : ///
440 : /// **Example usage:**
441 : /// ```dart
442 : /// _buildHandle(Colors.blue);
443 : /// ```
444 1 : static Widget _buildHandle(Color? handleColor) {
445 1 : return Center(
446 1 : child: Container(
447 : width: 150,
448 : height: 5,
449 1 : decoration: BoxDecoration(
450 : color: handleColor ?? CupertinoColors.inactiveGray,
451 1 : borderRadius: BorderRadius.circular(20),
452 : ),
453 : margin: const EdgeInsets.all(10),
454 : ),
455 : );
456 : }
457 : }
|