Line data Source code
1 : // Copyright 2014 The Flutter Authors.
2 : // Copyright 2021 Suragch.
3 : // All rights reserved.
4 : // Use of this source code is governed by a BSD-style license that can be
5 : // found in the LICENSE file.
6 :
7 : import 'dart:math' as math;
8 : import 'dart:ui' show lerpDouble;
9 :
10 : import 'package:flutter/foundation.dart';
11 : import 'package:flutter/widgets.dart';
12 : import 'package:flutter/material.dart'
13 : show
14 : ButtonStyle,
15 : ButtonStyleButton,
16 : ColorScheme,
17 : ElevatedButtonTheme,
18 : InkRipple,
19 : InteractiveInkFeatureFactory,
20 : MaterialState,
21 : MaterialStateProperty,
22 : MaterialTapTargetSize,
23 : Theme,
24 : ThemeData,
25 : VisualDensity,
26 : kThemeChangeDuration;
27 :
28 : /// A vertical Material Design "elevated button".
29 : ///
30 : /// Use elevated buttons to add dimension to otherwise mostly flat
31 : /// layouts, e.g. in long busy lists of content, or in tall
32 : /// spaces. Avoid using elevated buttons on already-elevated content
33 : /// such as dialogs or cards.
34 : ///
35 : /// An elevated button is a label [child] displayed on a [Material]
36 : /// widget whose [Material.elevation] increases when the button is
37 : /// pressed. The label's [MongolText] and [Icon] widgets are displayed in
38 : /// [style]'s [ButtonStyle.foregroundColor] and the button's filled
39 : /// background is the [ButtonStyle.backgroundColor].
40 : ///
41 : /// The elevated button's default style is defined by
42 : /// [defaultStyleOf]. The style of this elevated button can be
43 : /// overridden with its [style] parameter. The style of all elevated
44 : /// buttons in a subtree can be overridden with the
45 : /// [ElevatedButtonTheme], and the style of all of the elevated
46 : /// buttons in an app can be overridden with the [Theme]'s
47 : /// [ThemeData.elevatedButtonTheme] property.
48 : ///
49 : /// The static [styleFrom] method is a convenient way to create a
50 : /// elevated button [ButtonStyle] from simple values.
51 : ///
52 : /// If [onPressed] and [onLongPress] callbacks are null, then the
53 : /// button will be disabled.
54 : ///
55 : /// {@tool dartpad --template=stateful_widget_scaffold}
56 : ///
57 : /// This sample produces an enabled and a disabled ElevatedButton.
58 : ///
59 : /// ```dart
60 : /// @override
61 : /// Widget build(BuildContext context) {
62 : /// final ButtonStyle style =
63 : /// MongolElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20));
64 : ///
65 : /// return Center(
66 : /// child: Column(
67 : /// mainAxisSize: MainAxisSize.min,
68 : /// children: <Widget>[
69 : /// MongolElevatedButton(
70 : /// style: style,
71 : /// onPressed: null,
72 : /// child: const Text('Disabled'),
73 : /// ),
74 : /// const SizedBox(height: 30),
75 : /// MongolElevatedButton(
76 : /// style: style,
77 : /// onPressed: () {},
78 : /// child: const Text('Enabled'),
79 : /// ),
80 : /// ],
81 : /// ),
82 : /// );
83 : /// }
84 : ///
85 : /// ```
86 : /// {@end-tool}
87 : ///
88 : /// See also:
89 : ///
90 : /// * [MongolTextButton], a simple flat button without a shadow.
91 : /// * [MongolOutlinedButton], a [MongolTextButton] with a border outline.
92 : /// * <https://material.io/design/components/buttons.html>
93 : class MongolElevatedButton extends ButtonStyleButton {
94 : /// Create a MongolElevatedButton.
95 : ///
96 : /// The [autofocus] and [clipBehavior] arguments must not be null.
97 0 : const MongolElevatedButton({
98 : Key? key,
99 : required VoidCallback? onPressed,
100 : VoidCallback? onLongPress,
101 : ButtonStyle? style,
102 : FocusNode? focusNode,
103 : bool autofocus = false,
104 : Clip clipBehavior = Clip.none,
105 : required Widget? child,
106 0 : }) : super(
107 : key: key,
108 : onPressed: onPressed,
109 : onLongPress: onLongPress,
110 : style: style,
111 : focusNode: focusNode,
112 : autofocus: autofocus,
113 : clipBehavior: clipBehavior,
114 : child: child,
115 : );
116 :
117 : /// Create an elevated button from a pair of widgets that serve as the button's
118 : /// [icon] and [label].
119 : ///
120 : /// The icon and label are arranged in a column and padded by 12 logical pixels
121 : /// at the start, and 16 at the end, with an 8 pixel gap in between.
122 : ///
123 : /// The [icon] and [label] arguments must not be null.
124 : factory MongolElevatedButton.icon({
125 : Key? key,
126 : required VoidCallback? onPressed,
127 : VoidCallback? onLongPress,
128 : ButtonStyle? style,
129 : FocusNode? focusNode,
130 : bool? autofocus,
131 : Clip? clipBehavior,
132 : required Widget icon,
133 : required Widget label,
134 : }) = _MongolElevatedButtonWithIcon;
135 :
136 : /// A static convenience method that constructs an elevated button
137 : /// [ButtonStyle] given simple values.
138 : ///
139 : /// The [onPrimary], and [onSurface] colors are used to create a
140 : /// [MaterialStateProperty] [ButtonStyle.foregroundColor] value in the same
141 : /// way that [defaultStyleOf] uses the [ColorScheme] colors with the same
142 : /// names. Specify a value for [onPrimary] to specify the color of the
143 : /// button's text and icons as well as the overlay colors used to indicate the
144 : /// hover, focus, and pressed states. Use [primary] for the button's background
145 : /// fill color and [onSurface] to specify the button's disabled text, icon,
146 : /// and fill color.
147 : ///
148 : /// The button's elevations are defined relative to the [elevation]
149 : /// parameter. The disabled elevation is the same as the parameter
150 : /// value, [elevation] + 2 is used when the button is hovered
151 : /// or focused, and elevation + 6 is used when the button is pressed.
152 : ///
153 : /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
154 : /// parameters are used to construct [ButtonStyle].mouseCursor.
155 : ///
156 : /// All of the other parameters are either used directly or used to
157 : /// create a [MaterialStateProperty] with a single value for all
158 : /// states.
159 : ///
160 : /// All parameters default to null, by default this method returns
161 : /// a [ButtonStyle] that doesn't override anything.
162 : ///
163 : /// For example, to override the default text and icon colors for a
164 : /// [MongolElevatedButton], as well as its overlay color, with all of the
165 : /// standard opacity adjustments for the pressed, focused, and
166 : /// hovered states, one could write:
167 : ///
168 : /// ```dart
169 : /// MongolElevatedButton(
170 : /// style: MongolElevatedButton.styleFrom(primary: Colors.green),
171 : /// )
172 : /// ```
173 0 : static ButtonStyle styleFrom({
174 : Color? primary,
175 : Color? onPrimary,
176 : Color? onSurface,
177 : Color? shadowColor,
178 : double? elevation,
179 : TextStyle? textStyle,
180 : EdgeInsetsGeometry? padding,
181 : Size? minimumSize,
182 : Size? fixedSize,
183 : Size? maximumSize,
184 : BorderSide? side,
185 : OutlinedBorder? shape,
186 : MouseCursor? enabledMouseCursor,
187 : MouseCursor? disabledMouseCursor,
188 : VisualDensity? visualDensity,
189 : MaterialTapTargetSize? tapTargetSize,
190 : Duration? animationDuration,
191 : bool? enableFeedback,
192 : AlignmentGeometry? alignment,
193 : InteractiveInkFeatureFactory? splashFactory,
194 : }) {
195 : final MaterialStateProperty<Color?>? backgroundColor =
196 : (onSurface == null && primary == null)
197 : ? null
198 0 : : _ElevatedButtonDefaultBackground(primary, onSurface);
199 : final MaterialStateProperty<Color?>? foregroundColor =
200 : (onSurface == null && onPrimary == null)
201 : ? null
202 0 : : _ElevatedButtonDefaultForeground(onPrimary, onSurface);
203 : final MaterialStateProperty<Color?>? overlayColor =
204 0 : (onPrimary == null) ? null : _ElevatedButtonDefaultOverlay(onPrimary);
205 : final MaterialStateProperty<double>? elevationValue =
206 0 : (elevation == null) ? null : _ElevatedButtonDefaultElevation(elevation);
207 : final MaterialStateProperty<MouseCursor?>? mouseCursor =
208 : (enabledMouseCursor == null && disabledMouseCursor == null)
209 : ? null
210 0 : : _ElevatedButtonDefaultMouseCursor(
211 : enabledMouseCursor, disabledMouseCursor);
212 :
213 0 : return ButtonStyle(
214 0 : textStyle: MaterialStateProperty.all<TextStyle?>(textStyle),
215 : backgroundColor: backgroundColor,
216 : foregroundColor: foregroundColor,
217 : overlayColor: overlayColor,
218 0 : shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
219 : elevation: elevationValue,
220 0 : padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
221 0 : minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
222 0 : fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
223 : // TODO: add when becomes available in stable channel
224 : //maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
225 0 : side: ButtonStyleButton.allOrNull<BorderSide>(side),
226 0 : shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
227 : mouseCursor: mouseCursor,
228 : visualDensity: visualDensity,
229 : tapTargetSize: tapTargetSize,
230 : animationDuration: animationDuration,
231 : enableFeedback: enableFeedback,
232 : alignment: alignment,
233 : splashFactory: splashFactory,
234 : );
235 : }
236 :
237 : /// Defines the button's default appearance.
238 : ///
239 : /// The button [child]'s [MongolText] and [Icon] widgets are rendered with
240 : /// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
241 : /// the style's overlay color when the button is focused, hovered
242 : /// or pressed. The button's background color becomes its [Material]
243 : /// color.
244 : ///
245 : /// All of the ButtonStyle's defaults appear below. In this list
246 : /// "Theme.foo" is shorthand for `Theme.of(context).foo`. Color
247 : /// scheme values like "onSurface(0.38)" are shorthand for
248 : /// `onSurface.withOpacity(0.38)`. [MaterialStateProperty] valued
249 : /// properties that are not followed by a sublist have the same
250 : /// value for all states, otherwise the values are as specified for
251 : /// each state, and "others" means all other states.
252 : ///
253 : /// The `textScaleFactor` is the value of
254 : /// `MediaQuery.of(context).textScaleFactor` and the names of the
255 : /// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been
256 : /// abbreviated for readability.
257 : ///
258 : /// The color of the [ButtonStyle.textStyle] is not used, the
259 : /// [ButtonStyle.foregroundColor] color is used instead.
260 : ///
261 : /// * `textStyle` - Theme.textTheme.button
262 : /// * `backgroundColor`
263 : /// * disabled - Theme.colorScheme.onSurface(0.12)
264 : /// * others - Theme.colorScheme.primary
265 : /// * `foregroundColor`
266 : /// * disabled - Theme.colorScheme.onSurface(0.38)
267 : /// * others - Theme.colorScheme.onPrimary
268 : /// * `overlayColor`
269 : /// * hovered - Theme.colorScheme.onPrimary(0.08)
270 : /// * focused or pressed - Theme.colorScheme.onPrimary(0.24)
271 : /// * `shadowColor` - Theme.shadowColor
272 : /// * `elevation`
273 : /// * disabled - 0
274 : /// * default - 2
275 : /// * hovered or focused - 4
276 : /// * pressed - 8
277 : /// * `padding`
278 : /// * textScaleFactor <= 1 - vertical(16)
279 : /// * `1 < textScaleFactor <= 2` - lerp(vertical(16), vertical(8))
280 : /// * `2 < textScaleFactor <= 3` - lerp(vertical(8), vertical(4))
281 : /// * `3 < textScaleFactor` - vertical(4)
282 : /// * `minimumSize` - Size(36, 64)
283 : /// * `fixedSize` - null
284 : /// * `maximumSize` - Size.infinite
285 : /// * `side` - null
286 : /// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
287 : /// * `mouseCursor`
288 : /// * disabled - SystemMouseCursors.forbidden
289 : /// * others - SystemMouseCursors.click
290 : /// * `visualDensity` - theme.visualDensity
291 : /// * `tapTargetSize` - theme.materialTapTargetSize
292 : /// * `animationDuration` - kThemeChangeDuration
293 : /// * `enableFeedback` - true
294 : /// * `alignment` - Alignment.center
295 : /// * `splashFactory` - InkRipple.splashFactory
296 : ///
297 : /// The default padding values for the [ElevatedButton.icon] factory are slightly different:
298 : ///
299 : /// * `padding`
300 : /// * `textScaleFactor <= 1` - start(12) end(16)
301 : /// * `1 < textScaleFactor <= 2` - lerp(start(12) end(16), vertical(8))
302 : /// * `2 < textScaleFactor <= 3` - lerp(vertical(8), vertical(4))
303 : /// * `3 < textScaleFactor` - vertical(4)
304 : ///
305 : /// The default value for `side`, which defines the appearance of the button's
306 : /// outline, is null. That means that the outline is defined by the button
307 : /// shape's [OutlinedBorder.side]. Typically the default value of an
308 : /// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
309 0 : @override
310 : ButtonStyle defaultStyleOf(BuildContext context) {
311 0 : final ThemeData theme = Theme.of(context);
312 0 : final ColorScheme colorScheme = theme.colorScheme;
313 :
314 0 : final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
315 : const EdgeInsets.symmetric(vertical: 16),
316 : const EdgeInsets.symmetric(vertical: 8),
317 : const EdgeInsets.symmetric(vertical: 4),
318 0 : MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
319 : );
320 :
321 0 : return styleFrom(
322 0 : primary: colorScheme.primary,
323 0 : onPrimary: colorScheme.onPrimary,
324 0 : onSurface: colorScheme.onSurface,
325 0 : shadowColor: theme.shadowColor,
326 : elevation: 2,
327 0 : textStyle: theme.textTheme.button,
328 : padding: scaledPadding,
329 : minimumSize: const Size(36, 64),
330 : maximumSize: Size.infinite,
331 : side: null,
332 : shape: const RoundedRectangleBorder(
333 : borderRadius: BorderRadius.all(Radius.circular(4))),
334 : enabledMouseCursor: SystemMouseCursors.click,
335 : disabledMouseCursor: SystemMouseCursors.forbidden,
336 0 : visualDensity: theme.visualDensity,
337 0 : tapTargetSize: theme.materialTapTargetSize,
338 : animationDuration: kThemeChangeDuration,
339 : enableFeedback: true,
340 : alignment: Alignment.center,
341 : splashFactory: InkRipple.splashFactory,
342 : );
343 : }
344 :
345 : /// Returns the [ElevatedButtonThemeData.style] of the closest
346 : /// [ElevatedButtonTheme] ancestor.
347 0 : @override
348 : ButtonStyle? themeStyleOf(BuildContext context) {
349 0 : return ElevatedButtonTheme.of(context).style;
350 : }
351 : }
352 :
353 : @immutable
354 : class _ElevatedButtonDefaultBackground extends MaterialStateProperty<Color?>
355 : with Diagnosticable {
356 0 : _ElevatedButtonDefaultBackground(this.primary, this.onSurface);
357 :
358 : final Color? primary;
359 : final Color? onSurface;
360 :
361 0 : @override
362 : Color? resolve(Set<MaterialState> states) {
363 0 : if (states.contains(MaterialState.disabled)) {
364 0 : return onSurface?.withOpacity(0.12);
365 : }
366 0 : return primary;
367 : }
368 : }
369 :
370 : @immutable
371 : class _ElevatedButtonDefaultForeground extends MaterialStateProperty<Color?>
372 : with Diagnosticable {
373 0 : _ElevatedButtonDefaultForeground(this.onPrimary, this.onSurface);
374 :
375 : final Color? onPrimary;
376 : final Color? onSurface;
377 :
378 0 : @override
379 : Color? resolve(Set<MaterialState> states) {
380 0 : if (states.contains(MaterialState.disabled)) {
381 0 : return onSurface?.withOpacity(0.38);
382 : }
383 0 : return onPrimary;
384 : }
385 : }
386 :
387 : @immutable
388 : class _ElevatedButtonDefaultOverlay extends MaterialStateProperty<Color?>
389 : with Diagnosticable {
390 0 : _ElevatedButtonDefaultOverlay(this.onPrimary);
391 :
392 : final Color onPrimary;
393 :
394 0 : @override
395 : Color? resolve(Set<MaterialState> states) {
396 0 : if (states.contains(MaterialState.hovered)) {
397 0 : return onPrimary.withOpacity(0.08);
398 : }
399 0 : if (states.contains(MaterialState.focused) ||
400 0 : states.contains(MaterialState.pressed)) {
401 0 : return onPrimary.withOpacity(0.24);
402 : }
403 : return null;
404 : }
405 : }
406 :
407 : @immutable
408 : class _ElevatedButtonDefaultElevation extends MaterialStateProperty<double>
409 : with Diagnosticable {
410 0 : _ElevatedButtonDefaultElevation(this.elevation);
411 :
412 : final double elevation;
413 :
414 0 : @override
415 : double resolve(Set<MaterialState> states) {
416 0 : if (states.contains(MaterialState.disabled)) return 0;
417 0 : if (states.contains(MaterialState.hovered)) return elevation + 2;
418 0 : if (states.contains(MaterialState.focused)) return elevation + 2;
419 0 : if (states.contains(MaterialState.pressed)) return elevation + 6;
420 0 : return elevation;
421 : }
422 : }
423 :
424 : @immutable
425 : class _ElevatedButtonDefaultMouseCursor
426 : extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
427 0 : _ElevatedButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
428 :
429 : final MouseCursor? enabledCursor;
430 : final MouseCursor? disabledCursor;
431 :
432 0 : @override
433 : MouseCursor? resolve(Set<MaterialState> states) {
434 0 : if (states.contains(MaterialState.disabled)) return disabledCursor;
435 0 : return enabledCursor;
436 : }
437 : }
438 :
439 : class _MongolElevatedButtonWithIcon extends MongolElevatedButton {
440 0 : _MongolElevatedButtonWithIcon({
441 : Key? key,
442 : required VoidCallback? onPressed,
443 : VoidCallback? onLongPress,
444 : ButtonStyle? style,
445 : FocusNode? focusNode,
446 : bool? autofocus,
447 : Clip? clipBehavior,
448 : required Widget icon,
449 : required Widget label,
450 0 : }) : super(
451 : key: key,
452 : onPressed: onPressed,
453 : onLongPress: onLongPress,
454 : style: style,
455 : focusNode: focusNode,
456 : autofocus: autofocus ?? false,
457 : clipBehavior: clipBehavior ?? Clip.none,
458 0 : child: _MongolElevatedButtonWithIconChild(icon: icon, label: label),
459 : );
460 :
461 0 : @override
462 : ButtonStyle defaultStyleOf(BuildContext context) {
463 0 : final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
464 : const EdgeInsets.fromLTRB(0, 12, 0, 16),
465 : const EdgeInsets.symmetric(vertical: 8),
466 : const EdgeInsets.fromLTRB(0, 8, 0, 4),
467 0 : MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
468 : );
469 0 : return super.defaultStyleOf(context).copyWith(
470 0 : padding: MaterialStateProperty.all<EdgeInsetsGeometry>(scaledPadding),
471 : );
472 : }
473 : }
474 :
475 : class _MongolElevatedButtonWithIconChild extends StatelessWidget {
476 0 : const _MongolElevatedButtonWithIconChild(
477 : {Key? key, required this.label, required this.icon})
478 0 : : super(key: key);
479 :
480 : final Widget label;
481 : final Widget icon;
482 :
483 0 : @override
484 : Widget build(BuildContext context) {
485 0 : final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
486 : final double gap =
487 0 : scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
488 0 : return Column(
489 : mainAxisSize: MainAxisSize.min,
490 0 : children: <Widget>[icon, SizedBox(height: gap), Flexible(child: label)],
491 : );
492 : }
493 : }
|