Line data Source code
1 : import 'package:flutter/material.dart';
2 :
3 : /// A customizable list tile button that wraps content in a rounded container
4 : /// and provides tap and long-press callbacks. Ideal for creating interactive
5 : /// list items with consistent styling.
6 : ///
7 : /// Example usage:
8 : /// ```dart
9 : /// ListTileButton(
10 : /// onPressed: () {},
11 : /// leading: Icon(Icons.star),
12 : /// body: Text('List Tile Button'),
13 : /// );
14 : /// ```
15 : class ListTileButton extends StatelessWidget {
16 : // Behavior
17 :
18 : /// Callback when the tile is tapped.
19 : final VoidCallback? onPressed;
20 :
21 : /// Callback when the tile is long-pressed.
22 : final VoidCallback? onLongPress;
23 :
24 : // Layout
25 :
26 : /// External margin around the tile.
27 : final EdgeInsetsGeometry? margin;
28 :
29 : /// Internal padding within the tile.
30 : final EdgeInsetsGeometry? padding;
31 :
32 : /// Padding for the [body] within the [ListTile].
33 : final EdgeInsetsGeometry? bodyPadding;
34 :
35 : /// Padding around the [leading] widget.
36 : final EdgeInsetsGeometry? leadingPadding;
37 :
38 : /// Padding around the [trailing] widget.
39 : final EdgeInsetsGeometry? trailingPadding;
40 :
41 : // Content
42 :
43 : /// Widget to display at the start of the tile.
44 : final Widget? leading;
45 :
46 : /// Factor to scale the size of the leading widget.
47 : final double leadingSizeFactor;
48 :
49 : /// The primary content of the tile.
50 : final Widget? body;
51 :
52 : /// Additional content displayed below the [body].
53 : final Widget? subtitle;
54 :
55 : /// Widget to display at the end of the tile.
56 : final Widget? trailing;
57 :
58 : // Style
59 :
60 : /// Background color of the tile.
61 : final Color? backgroundColor;
62 :
63 : /// Border color of the tile.
64 : final Color? borderColor;
65 :
66 : /// Border radius of the tile's rounded corners.
67 : final double borderRadius;
68 :
69 : /// Elevation of the tile's shadow.
70 : final double? elevation;
71 :
72 : // Visual Aspects
73 :
74 : /// Visual density of the tile to control compactness.
75 : final VisualDensity? visualDensity;
76 :
77 : /// Alignment of the [body] within the tile.
78 : final ListTileTitleAlignment? bodyAlignment;
79 :
80 : // Constraints
81 :
82 : /// Minimum height of the tile.
83 : final double? minHeight;
84 :
85 : /// Creates a [ListTileButton] with customizable content and styling.
86 2 : const ListTileButton({
87 : super.key,
88 : this.onPressed,
89 : this.onLongPress,
90 : this.margin,
91 : this.padding,
92 : this.bodyPadding,
93 : this.leadingPadding,
94 : this.trailingPadding,
95 : this.leading,
96 : this.leadingSizeFactor = 1.0,
97 : required this.body,
98 : this.subtitle,
99 : this.trailing,
100 : this.backgroundColor,
101 : this.borderColor,
102 : this.borderRadius = 10,
103 : this.elevation,
104 : this.visualDensity,
105 : this.bodyAlignment,
106 : this.minHeight,
107 : });
108 :
109 2 : @override
110 : Widget build(BuildContext context) {
111 : Widget? leadingWidget;
112 2 : if (leading != null) {
113 1 : leadingWidget = Padding(
114 1 : padding: leadingPadding ?? EdgeInsets.zero,
115 1 : child: Center(
116 1 : child: SizedBox(
117 : key: const Key('leading_wrapper'),
118 : // Added Key for testing
119 2 : width: 24.0 * leadingSizeFactor,
120 2 : height: 24.0 * leadingSizeFactor,
121 1 : child: FittedBox(
122 : fit: BoxFit.contain,
123 : alignment: Alignment.center,
124 1 : child: leading,
125 : ),
126 : ),
127 : ),
128 : );
129 : }
130 :
131 : Widget? trailingWidget;
132 2 : if (trailing != null) {
133 2 : trailingWidget = Padding(
134 2 : padding: trailingPadding ?? const EdgeInsets.only(right: 12),
135 2 : child: Container(
136 : alignment: Alignment.center,
137 : height: double.infinity,
138 2 : child: trailing,
139 : ),
140 : );
141 : }
142 :
143 2 : return RoundedContainer(
144 2 : margin: margin,
145 2 : borderColor: borderColor,
146 2 : backgroundColor: backgroundColor,
147 2 : elevation: elevation,
148 2 : borderRadius: borderRadius,
149 2 : child: Material(
150 : type: MaterialType.transparency,
151 2 : child: InkWell(
152 4 : borderRadius: BorderRadius.circular(borderRadius),
153 2 : onTap: onPressed,
154 2 : onLongPress: onLongPress,
155 2 : child: Padding(
156 2 : padding: padding ?? const EdgeInsets.all(8),
157 2 : child: ConstrainedBox(
158 2 : constraints: BoxConstraints(
159 2 : minHeight: minHeight ?? 50.0,
160 : ),
161 2 : child: IntrinsicHeight(
162 2 : child: Row(
163 2 : children: [
164 1 : if (leadingWidget != null) leadingWidget,
165 2 : Expanded(
166 2 : child: ListTile(
167 2 : titleAlignment: bodyAlignment,
168 2 : visualDensity: visualDensity ?? VisualDensity.compact,
169 : contentPadding:
170 2 : bodyPadding ?? const EdgeInsets.only(left: 8),
171 : minVerticalPadding: 0,
172 : minLeadingWidth: 0,
173 2 : title: body,
174 2 : subtitle: subtitle,
175 : ),
176 : ),
177 2 : if (trailingWidget != null) trailingWidget,
178 : ],
179 : ),
180 : ),
181 : ),
182 : ),
183 : ),
184 : ),
185 : );
186 : }
187 : }
188 :
189 : /// A convenience widget that combines an icon with a [ListTileButton].
190 : ///
191 : /// Example usage:
192 : /// ```dart
193 : /// IconListTileButton(
194 : /// icon: Icons.settings,
195 : /// title: Text('Settings'),
196 : /// onPressed: () {},
197 : /// );
198 : /// ```
199 : class IconListTileButton extends StatelessWidget {
200 : // Behavior
201 :
202 : /// Callback when the tile is tapped.
203 : final VoidCallback? onPressed;
204 :
205 : // Layout
206 :
207 : /// External margin around the tile.
208 : final EdgeInsetsGeometry? margin;
209 :
210 : /// Internal padding within the tile.
211 : final EdgeInsetsGeometry? padding;
212 :
213 : /// Padding for the [body] within the [ListTile].
214 : final EdgeInsetsGeometry? bodyPadding;
215 :
216 : /// Padding around the [leading] widget.
217 : final EdgeInsetsGeometry? leadingPadding;
218 :
219 : /// Padding around the [trailing] widget.
220 : final EdgeInsetsGeometry? trailingPadding;
221 :
222 : // Content
223 :
224 : /// Icon to display at the start of the tile.
225 : final IconData icon;
226 :
227 : /// The primary content of the tile.
228 : final Widget title;
229 :
230 : /// Additional content displayed below the [title].
231 : final Widget? subtitle;
232 :
233 : /// Widget to display at the end of the tile.
234 : final Widget? trailing;
235 :
236 : // Style
237 :
238 : /// Background color of the tile.
239 : final Color? backgroundColor;
240 :
241 : /// Border color of the tile.
242 : final Color? borderColor;
243 :
244 : /// Color of the icon.
245 : final Color? iconColor;
246 :
247 : /// Factor to scale the size of the leading icon.
248 : final double leadingSizeFactor;
249 :
250 : /// Elevation of the tile's shadow.
251 : final double? elevation;
252 :
253 : /// Border radius of the tile's rounded corners.
254 : final double borderRadius;
255 :
256 : /// Creates an [IconListTileButton] with an icon and customizable content.
257 1 : const IconListTileButton({
258 : super.key,
259 : required this.icon,
260 : required this.title,
261 : this.subtitle,
262 : this.trailing,
263 : this.onPressed,
264 : this.backgroundColor,
265 : this.borderColor,
266 : this.iconColor,
267 : this.leadingSizeFactor = 1.0,
268 : this.margin,
269 : this.padding,
270 : this.bodyPadding,
271 : this.leadingPadding,
272 : this.trailingPadding,
273 : this.elevation,
274 : this.borderRadius = 10,
275 : });
276 :
277 1 : @override
278 : Widget build(BuildContext context) {
279 1 : return ListTileButton(
280 1 : margin: margin,
281 1 : padding: padding,
282 1 : bodyPadding: bodyPadding,
283 1 : leadingPadding: leadingPadding,
284 1 : trailingPadding: trailingPadding,
285 1 : backgroundColor: backgroundColor,
286 1 : borderColor: borderColor,
287 1 : elevation: elevation,
288 1 : borderRadius: borderRadius,
289 1 : body: title,
290 1 : subtitle: subtitle,
291 1 : trailing: trailing,
292 1 : onPressed: onPressed,
293 1 : leading: Icon(
294 1 : icon,
295 4 : color: iconColor ?? Theme.of(context).iconTheme.color,
296 : size: 24.0,
297 : ),
298 1 : leadingSizeFactor: leadingSizeFactor,
299 : bodyAlignment: ListTileTitleAlignment.threeLine,
300 : );
301 : }
302 : }
303 :
304 : /// A container with rounded corners and optional border and elevation.
305 : ///
306 : /// Used internally by [ListTileButton] to wrap content with consistent styling.
307 : ///
308 : /// Example usage:
309 : /// ```dart
310 : /// RoundedContainer(
311 : /// child: Text('Content'),
312 : /// backgroundColor: Colors.white,
313 : /// borderColor: Colors.grey,
314 : /// );
315 : /// ```
316 : class RoundedContainer extends StatelessWidget {
317 : // Layout
318 :
319 : /// External margin around the container.
320 : final EdgeInsetsGeometry? margin;
321 :
322 : /// Internal padding within the container.
323 : final EdgeInsetsGeometry? padding;
324 :
325 : // Content
326 :
327 : /// The widget below this widget in the tree.
328 : final Widget child;
329 :
330 : // Style
331 :
332 : /// Background color of the container.
333 : final Color? backgroundColor;
334 :
335 : /// Border color of the container.
336 : final Color? borderColor;
337 :
338 : /// Border radius of the container's rounded corners.
339 : final double borderRadius;
340 :
341 : /// Elevation of the container's shadow.
342 : final double? elevation;
343 :
344 : /// Creates a [RoundedContainer] with customizable styling.
345 2 : const RoundedContainer({
346 : super.key,
347 : required this.child,
348 : this.margin,
349 : this.padding,
350 : this.backgroundColor,
351 : this.borderColor,
352 : this.borderRadius = 10,
353 : this.elevation,
354 : });
355 :
356 2 : @override
357 : Widget build(BuildContext context) {
358 2 : return Padding(
359 2 : padding: margin ?? EdgeInsets.zero,
360 2 : child: Material(
361 2 : elevation: elevation ?? 0,
362 4 : borderRadius: BorderRadius.circular(borderRadius),
363 4 : color: backgroundColor ?? Theme.of(context).cardColor,
364 2 : child: Container(
365 2 : padding: padding,
366 2 : decoration: BoxDecoration(
367 4 : borderRadius: BorderRadius.circular(borderRadius),
368 2 : border: borderColor != null
369 0 : ? Border.all(color: borderColor!, width: 2)
370 : : null,
371 : ),
372 2 : child: child,
373 : ),
374 : ),
375 : );
376 : }
377 : }
378 :
379 : /// A widget that displays two buttons side by side, typically used at the bottom
380 : /// of a sheet or dialog for actions like "Confirm" and "Cancel".
381 : ///
382 : /// Example usage:
383 : /// ```dart
384 : /// DoubleListTileButtons(
385 : /// firstButton: ElevatedButton(
386 : /// onPressed: () {},
387 : /// child: Text('Cancel'),
388 : /// ),
389 : /// secondButton: ElevatedButton(
390 : /// onPressed: () {},
391 : /// child: Text('Confirm'),
392 : /// ),
393 : /// );
394 : /// ```
395 : class DoubleListTileButtons extends StatelessWidget {
396 : /// The first button to display.
397 : final Widget firstButton;
398 :
399 : /// The second button to display.
400 : final Widget secondButton;
401 :
402 : /// Padding around the buttons.
403 : final EdgeInsetsGeometry padding;
404 :
405 : /// Whether the buttons should expand to fill the available width.
406 : final bool expanded;
407 :
408 : /// Space between the two buttons.
409 : final double? space;
410 :
411 : /// Creates a [DoubleListTileButtons] widget.
412 2 : const DoubleListTileButtons({
413 : super.key,
414 : required this.firstButton,
415 : required this.secondButton,
416 : this.padding = EdgeInsets.zero,
417 : this.expanded = true,
418 : this.space,
419 : });
420 :
421 2 : @override
422 : Widget build(BuildContext context) {
423 2 : return Padding(
424 2 : padding: padding,
425 2 : child: Row(
426 : mainAxisSize: MainAxisSize.min,
427 : mainAxisAlignment: MainAxisAlignment.spaceEvenly,
428 : crossAxisAlignment: CrossAxisAlignment.center,
429 2 : children: expanded
430 2 : ? [
431 4 : Expanded(child: firstButton),
432 4 : SizedBox(width: space ?? 8),
433 4 : Expanded(child: secondButton),
434 : ]
435 0 : : [
436 0 : firstButton,
437 0 : SizedBox(width: space ?? 8),
438 0 : secondButton,
439 : ],
440 : ),
441 : );
442 : }
443 : }
|