Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:flutter/material.dart';
4 :
5 : /// Types of buttons available in [CustomActionButton].
6 : enum ButtonType { elevated, flat, minimal, longPress }
7 :
8 : /// A customizable button widget that can be configured as elevated, flat,
9 : /// minimal, or long-press button types. Provides a flexible API to adjust
10 : /// styles, colors, shapes, and behaviors.
11 : ///
12 : /// The [CustomActionButton] supports different visual styles through the
13 : /// [ButtonType] enum and offers factory constructors for convenience.
14 : ///
15 : /// Example usage:
16 : /// ```dart
17 : /// CustomActionButton.elevated(
18 : /// onPressed: () {},
19 : /// child: Text('Elevated Button'),
20 : /// );
21 : /// ```
22 : class CustomActionButton extends StatefulWidget {
23 : /// The callback that is called when the button is tapped.
24 : final VoidCallback? onPressed;
25 :
26 : /// The callback that is called when the button is long-pressed.
27 : /// Only used when [buttonType] is [ButtonType.longPress].
28 : final VoidCallback? onLongPress;
29 :
30 : /// The child widget to display inside the button.
31 : final Widget child;
32 :
33 : /// The type of button to display.
34 : final ButtonType? buttonType;
35 :
36 : /// The background color of the button.
37 : final Color? backgroundColor;
38 :
39 : /// The foreground color (text/icon color) of the button.
40 : final Color? foregroundColor;
41 :
42 : /// The shadow color of the button.
43 : final Color? shadowColor;
44 :
45 : /// The splash color of the button when tapped.
46 : final Color? splashColor;
47 :
48 : /// The background color of the button when it is disabled.
49 : final Color? disabledBackgroundColor;
50 :
51 : /// The border color of the button when it is disabled.
52 : final Color? disabledBorderColor;
53 :
54 : /// The border color of the button.
55 : final Color? borderColor;
56 :
57 : /// The elevation of the button.
58 : final double? elevation;
59 :
60 : /// The border radius of the button.
61 : final double? borderRadius;
62 :
63 : /// The width of the button.
64 : final double? width;
65 :
66 : /// The height of the button.
67 : final double? height;
68 :
69 : /// The shape of the button's material.
70 : final OutlinedBorder? shape;
71 :
72 : /// The amount of space to surround the child inside the button.
73 : final EdgeInsetsGeometry? padding;
74 :
75 : /// The external margin around the button.
76 : final EdgeInsetsGeometry? margin;
77 :
78 : /// The splash factory to define interaction effects.
79 : final InteractiveInkFeatureFactory? splashFactory;
80 :
81 : /// Creates a [CustomActionButton] with the given parameters.
82 4 : const CustomActionButton({
83 : super.key,
84 : required this.child,
85 : this.buttonType,
86 : this.onPressed,
87 : this.onLongPress,
88 : this.backgroundColor,
89 : this.foregroundColor,
90 : this.shadowColor,
91 : this.splashColor,
92 : this.disabledBackgroundColor,
93 : this.disabledBorderColor,
94 : this.borderColor,
95 : this.elevation,
96 : this.borderRadius,
97 : this.width,
98 : this.height,
99 : this.shape,
100 : this.padding,
101 : this.margin,
102 : this.splashFactory,
103 : });
104 :
105 : /// Creates an elevated button.
106 : ///
107 : /// The [onPressed] and [child] parameters are required.
108 1 : factory CustomActionButton.elevated({
109 : required VoidCallback? onPressed,
110 : required Widget child,
111 : Color? backgroundColor,
112 : Color? foregroundColor,
113 : Color? shadowColor,
114 : Color? splashColor,
115 : Color? disabledBackgroundColor,
116 : Color? disabledForegroundColor,
117 : Color? borderColor,
118 : double elevation = 2.0,
119 : double borderRadius = 8.0,
120 : BorderSide? side,
121 : OutlinedBorder? shape,
122 : double? width,
123 : double? height,
124 : EdgeInsetsGeometry? padding,
125 : EdgeInsetsGeometry? margin,
126 : InteractiveInkFeatureFactory? splashFactory,
127 : }) {
128 1 : return CustomActionButton(
129 : buttonType: ButtonType.elevated,
130 : onPressed: onPressed,
131 : foregroundColor: foregroundColor,
132 : backgroundColor: backgroundColor,
133 : shadowColor: shadowColor,
134 : splashColor: splashColor,
135 : disabledBackgroundColor: disabledBackgroundColor,
136 : disabledBorderColor: disabledForegroundColor,
137 : borderColor: borderColor,
138 : elevation: elevation,
139 : borderRadius: borderRadius,
140 : shape: shape,
141 : width: width,
142 : height: height,
143 : padding: padding,
144 : margin: margin,
145 : splashFactory: splashFactory,
146 : child: child,
147 : );
148 : }
149 :
150 : /// Creates a flat button.
151 : ///
152 : /// The [onPressed] and [child] parameters are required.
153 2 : factory CustomActionButton.flat({
154 : required VoidCallback? onPressed,
155 : required Widget child,
156 : Color? backgroundColor,
157 : Color? foregroundColor,
158 : Color? splashColor,
159 : Color? disabledBackgroundColor,
160 : Color? disabledForegroundColor,
161 : Color? borderColor,
162 : double borderRadius = 8.0,
163 : BorderSide? side,
164 : OutlinedBorder? shape,
165 : double? width,
166 : double? height,
167 : EdgeInsetsGeometry? padding,
168 : EdgeInsetsGeometry? margin,
169 : InteractiveInkFeatureFactory? splashFactory,
170 : }) {
171 2 : return CustomActionButton(
172 : buttonType: ButtonType.flat,
173 : onPressed: onPressed,
174 : foregroundColor: foregroundColor,
175 : backgroundColor: backgroundColor,
176 : splashColor: splashColor,
177 : disabledBackgroundColor: disabledBackgroundColor,
178 : disabledBorderColor: disabledForegroundColor,
179 : borderColor: borderColor,
180 : borderRadius: borderRadius,
181 : shape: shape,
182 : width: width,
183 : height: height,
184 : padding: padding,
185 : margin: margin,
186 : splashFactory: splashFactory,
187 : child: child,
188 : );
189 : }
190 :
191 : /// Creates a minimal button.
192 : ///
193 : /// The [onPressed] and [child] parameters are required.
194 1 : factory CustomActionButton.minimal({
195 : required VoidCallback? onPressed,
196 : required Widget child,
197 : Color? foregroundColor,
198 : Color? disabledForegroundColor,
199 : Color? borderColor,
200 : double? width,
201 : double? height,
202 : OutlinedBorder? shape,
203 : EdgeInsetsGeometry? padding,
204 : EdgeInsetsGeometry? margin,
205 : }) {
206 1 : return CustomActionButton(
207 : buttonType: ButtonType.minimal,
208 : onPressed: onPressed,
209 : foregroundColor: foregroundColor,
210 : disabledBorderColor: disabledForegroundColor,
211 : borderColor: borderColor,
212 : width: width,
213 : height: height,
214 : shape: shape,
215 : padding: padding,
216 : margin: margin,
217 : child: child,
218 : );
219 : }
220 :
221 : /// Creates a long-press button.
222 : ///
223 : /// The [onPressed], [onLongPress], and [child] parameters are required.
224 2 : factory CustomActionButton.longPress({
225 : required VoidCallback? onPressed,
226 : required VoidCallback? onLongPress,
227 : required Widget child,
228 : Color? backgroundColor,
229 : Color? foregroundColor,
230 : Color? shadowColor,
231 : Color? splashColor,
232 : Color? disabledBackgroundColor,
233 : Color? disabledForegroundColor,
234 : Color? borderColor,
235 : double elevation = 2.0,
236 : double borderRadius = 8.0,
237 : BorderSide? side,
238 : OutlinedBorder? shape,
239 : double? width,
240 : double? height,
241 : EdgeInsetsGeometry? padding,
242 : EdgeInsetsGeometry? margin,
243 : InteractiveInkFeatureFactory? splashFactory,
244 : }) {
245 2 : return CustomActionButton(
246 : buttonType: ButtonType.longPress,
247 : onPressed: onPressed,
248 : onLongPress: onLongPress,
249 : foregroundColor: foregroundColor,
250 : backgroundColor: backgroundColor,
251 : shadowColor: shadowColor,
252 : splashColor: splashColor,
253 : disabledBackgroundColor: disabledBackgroundColor,
254 : disabledBorderColor: disabledForegroundColor,
255 : borderColor: borderColor,
256 : elevation: elevation,
257 : borderRadius: borderRadius,
258 : shape: shape,
259 : width: width,
260 : height: height,
261 : padding: padding,
262 : margin: margin,
263 : splashFactory: splashFactory,
264 : child: child,
265 : );
266 : }
267 :
268 4 : @override
269 4 : State<CustomActionButton> createState() => _CustomActionButtonState();
270 : }
271 :
272 : class _CustomActionButtonState extends State<CustomActionButton> {
273 : Timer? _longPressTimer;
274 :
275 : /// Handles the long-press action by repeatedly invoking [widget.onLongPress]
276 : /// at a fixed interval.
277 1 : void _handleLongPress() {
278 2 : if (widget.onLongPress != null) {
279 2 : _longPressTimer = Timer.periodic(
280 : const Duration(milliseconds: 100),
281 1 : (timer) {
282 3 : widget.onLongPress?.call();
283 : },
284 : );
285 : }
286 : }
287 :
288 : /// Cancels the ongoing long-press action.
289 1 : void _cancelLongPress() {
290 2 : _longPressTimer?.cancel();
291 : }
292 :
293 4 : @override
294 : Widget build(BuildContext context) {
295 17 : if (widget.onPressed == null && widget.buttonType != ButtonType.longPress) {
296 2 : return _buildDisabledButton(context);
297 : }
298 :
299 8 : switch (widget.buttonType) {
300 4 : case ButtonType.minimal:
301 1 : return _buildMinimalButton(context);
302 4 : case ButtonType.longPress:
303 2 : return _buildLongPressButton(context);
304 3 : case ButtonType.elevated:
305 1 : return _buildElevatedButton(context);
306 : case ButtonType.flat:
307 : default:
308 3 : return _buildFlatButton(context);
309 : }
310 : }
311 :
312 : /// Builds a disabled button when [onPressed] is null.
313 2 : Widget _buildDisabledButton(BuildContext context) {
314 2 : final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
315 : overlayColor: Colors.transparent,
316 : surfaceTintColor: Colors.transparent,
317 4 : foregroundColor: widget.foregroundColor ?? Colors.transparent,
318 4 : backgroundColor: widget.disabledBackgroundColor ??
319 4 : widget.backgroundColor ??
320 2 : Theme.of(context).primaryColor,
321 4 : shadowColor: widget.shadowColor ?? Colors.black,
322 4 : padding: widget.padding ??
323 : const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
324 4 : shape: widget.shape ??
325 2 : RoundedRectangleBorder(
326 6 : borderRadius: BorderRadius.circular(widget.borderRadius ?? 8.0),
327 8 : side: (widget.disabledBorderColor ?? widget.borderColor) != null
328 0 : ? BorderSide(
329 0 : color: widget.disabledBorderColor ??
330 0 : widget.borderColor ??
331 : Colors.transparent,
332 : width: 1)
333 : : BorderSide.none,
334 : ),
335 4 : elevation: widget.elevation,
336 : );
337 :
338 2 : return Container(
339 4 : margin: widget.margin,
340 4 : width: widget.width,
341 4 : height: widget.height,
342 2 : child: AbsorbPointer(
343 : absorbing: true,
344 2 : child: ElevatedButton(
345 : style: buttonStyle,
346 0 : onPressed: () {},
347 4 : child: widget.child,
348 : ),
349 : ),
350 : );
351 : }
352 :
353 : /// Builds an elevated button style.
354 1 : Widget _buildElevatedButton(BuildContext context) {
355 1 : final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
356 2 : foregroundColor: widget.foregroundColor ?? Colors.white,
357 2 : backgroundColor: widget.backgroundColor ?? Theme.of(context).primaryColor,
358 2 : shadowColor: widget.shadowColor,
359 2 : padding: widget.padding ??
360 : const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
361 2 : shape: widget.shape ??
362 1 : RoundedRectangleBorder(
363 3 : borderRadius: BorderRadius.circular(widget.borderRadius ?? 8.0),
364 2 : side: widget.borderColor != null
365 0 : ? BorderSide(color: widget.borderColor!, width: 1)
366 : : BorderSide.none,
367 : ),
368 2 : elevation: widget.elevation ?? 2.0,
369 2 : overlayColor: widget.splashColor ?? Colors.transparent,
370 2 : splashFactory: widget.splashFactory,
371 : );
372 :
373 1 : return Container(
374 2 : margin: widget.margin,
375 2 : width: widget.width,
376 2 : height: widget.height,
377 1 : child: ElevatedButton(
378 : style: buttonStyle,
379 2 : onPressed: widget.onPressed,
380 2 : child: widget.child,
381 : ),
382 : );
383 : }
384 :
385 : /// Builds a flat button style.
386 3 : Widget _buildFlatButton(BuildContext context) {
387 3 : final ButtonStyle buttonStyle = TextButton.styleFrom(
388 6 : foregroundColor: widget.foregroundColor ?? Colors.white,
389 6 : backgroundColor: widget.backgroundColor ?? Theme.of(context).primaryColor,
390 9 : overlayColor: widget.splashColor ?? Colors.grey.withOpacity(0.2),
391 6 : padding: widget.padding ??
392 : const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
393 6 : shape: widget.shape ??
394 3 : RoundedRectangleBorder(
395 9 : borderRadius: BorderRadius.circular(widget.borderRadius ?? 8.0),
396 6 : side: widget.borderColor != null
397 0 : ? BorderSide(color: widget.borderColor!, width: 1)
398 : : BorderSide.none,
399 : ),
400 6 : splashFactory: widget.splashFactory ?? InkRipple.splashFactory,
401 : );
402 :
403 3 : return Container(
404 6 : margin: widget.margin,
405 6 : width: widget.width,
406 6 : height: widget.height,
407 3 : child: TextButton(
408 : style: buttonStyle,
409 6 : onPressed: widget.onPressed,
410 6 : child: widget.child,
411 : ),
412 : );
413 : }
414 :
415 : /// Builds a minimal button style.
416 1 : Widget _buildMinimalButton(BuildContext context) {
417 1 : final ButtonStyle buttonStyle = TextButton.styleFrom(
418 2 : foregroundColor: widget.foregroundColor ?? Colors.black,
419 2 : padding: widget.padding ??
420 : const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
421 2 : shape: widget.shape ?? const RoundedRectangleBorder(),
422 : backgroundColor: Colors.transparent,
423 2 : side: widget.borderColor != null
424 0 : ? BorderSide(color: widget.borderColor!, width: 1)
425 : : BorderSide.none,
426 1 : ).copyWith(
427 1 : overlayColor: WidgetStateProperty.all(Colors.transparent),
428 : splashFactory: NoSplash.splashFactory,
429 : );
430 :
431 1 : return Container(
432 2 : margin: widget.margin,
433 2 : width: widget.width,
434 2 : height: widget.height,
435 1 : child: TextButton(
436 : style: buttonStyle,
437 2 : onPressed: widget.onPressed,
438 2 : child: widget.child,
439 : ),
440 : );
441 : }
442 :
443 : /// Builds a button that supports long-press actions.
444 2 : Widget _buildLongPressButton(BuildContext context) {
445 2 : final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
446 4 : foregroundColor: widget.foregroundColor ?? Colors.white,
447 6 : backgroundColor: widget.backgroundColor ?? Theme.of(context).primaryColor,
448 4 : shadowColor: widget.shadowColor,
449 4 : padding: widget.padding ??
450 : const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
451 4 : shape: widget.shape ??
452 1 : RoundedRectangleBorder(
453 3 : borderRadius: BorderRadius.circular(widget.borderRadius ?? 8.0),
454 2 : side: widget.borderColor != null
455 0 : ? BorderSide(color: widget.borderColor!, width: 1)
456 : : BorderSide.none,
457 : ),
458 4 : elevation: widget.elevation ?? 2.0,
459 2 : ).copyWith(
460 4 : overlayColor: widget.splashColor != null
461 0 : ? WidgetStateProperty.all(widget.splashColor)
462 : : null,
463 4 : splashFactory: widget.splashFactory,
464 : );
465 :
466 2 : return Container(
467 4 : margin: widget.margin,
468 4 : width: widget.width,
469 4 : height: widget.height,
470 2 : child: GestureDetector(
471 4 : onTap: widget.onPressed,
472 2 : onLongPressStart: (_) => _handleLongPress(),
473 2 : onLongPressEnd: (_) => _cancelLongPress(),
474 2 : child: ElevatedButton(
475 : style: buttonStyle,
476 4 : onPressed: widget.onPressed,
477 4 : child: widget.child,
478 : ),
479 : ),
480 : );
481 : }
482 :
483 4 : @override
484 : void dispose() {
485 5 : _longPressTimer?.cancel();
486 4 : super.dispose();
487 : }
488 : }
|