Instantly share code, notes, and snippets.
Created
May 11, 2020 18:57
-
Save HansMuller/68835e67ea588a15f7234f8129bb05c6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2014 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
// For more information see "Updating the Material Buttons and their Themes #54776" | |
// https://github.com/flutter/flutter/issues/54776 | |
import 'dart:math' as math; | |
import 'dart:ui' show lerpDouble; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/gestures.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'package:flutter/material.dart'; | |
// To simplify running the flutter_buttons repo demo in DartPad the repo's | |
// contents have been inserted below. In an ordinary app it would be imported using: | |
// import 'package:flutter_buttons/flutter_buttons.dart'; | |
// ---------------------------------------------------------------------------- | |
// lib/button_style.dart | |
class ButtonStyle with Diagnosticable { | |
const ButtonStyle({ | |
this.textStyle, | |
this.backgroundColor, | |
this.foregroundColor, | |
this.overlayColor, | |
this.elevation, | |
this.padding, | |
this.minimumSize, | |
this.side, | |
this.shape, | |
this.visualDensity, | |
this.tapTargetSize, | |
}); | |
final MaterialStateProperty<TextStyle> textStyle; | |
final MaterialStateProperty<Color> backgroundColor; | |
final MaterialStateProperty<Color> foregroundColor; | |
final MaterialStateProperty<Color> overlayColor; | |
final MaterialStateProperty<double> elevation; | |
final MaterialStateProperty<EdgeInsetsGeometry> padding; | |
final MaterialStateProperty<Size> minimumSize; | |
final MaterialStateProperty<BorderSide> side; | |
final MaterialStateProperty<ShapeBorder> shape; | |
final VisualDensity visualDensity; | |
final MaterialTapTargetSize tapTargetSize; | |
/// Returns a copy of this ButtonStyle with the given fields replaced with | |
/// the new values. | |
ButtonStyle copyWith({ | |
MaterialStateProperty<TextStyle> textStyle, | |
MaterialStateProperty<Color> backgroundColor, | |
MaterialStateProperty<Color> foregroundColor, | |
MaterialStateProperty<Color> overlayColor, | |
MaterialStateProperty<double> elevation, | |
MaterialStateProperty<EdgeInsetsGeometry> padding, | |
MaterialStateProperty<Size> minimumSize, | |
MaterialStateProperty<BorderSide> side, | |
MaterialStateProperty<ShapeBorder> shape, | |
VisualDensity visualDensity, | |
MaterialTapTargetSize tapTargetSize, | |
}) { | |
return ButtonStyle( | |
textStyle: textStyle ?? this.textStyle, | |
backgroundColor: backgroundColor ?? this.backgroundColor, | |
foregroundColor: foregroundColor ?? this.foregroundColor, | |
overlayColor: overlayColor ?? this.overlayColor, | |
elevation: elevation ?? this.elevation, | |
padding: padding ?? this.padding, | |
minimumSize: minimumSize ?? this.minimumSize, | |
side: side ?? this.side, | |
shape: shape ?? this.shape, | |
visualDensity: visualDensity ?? this.visualDensity, | |
tapTargetSize: tapTargetSize ?? this.tapTargetSize, | |
); | |
} | |
/// Returns a copy of this ButtonStyle where the non-null fields in [style] | |
/// have replaced the corresponding fields in this ButtonStyle. | |
ButtonStyle merge(ButtonStyle style) { | |
if (style == null) | |
return this; | |
return copyWith( | |
textStyle: style.textStyle ?? textStyle, | |
backgroundColor: style.backgroundColor ?? backgroundColor, | |
foregroundColor: style.foregroundColor ?? foregroundColor, | |
overlayColor: style.overlayColor ?? overlayColor, | |
elevation: style.elevation ?? elevation, | |
padding: style.padding ?? padding, | |
minimumSize: style.minimumSize ?? minimumSize, | |
side: style.side ?? side, | |
shape: style.shape ?? shape, | |
visualDensity: style.visualDensity ?? visualDensity, | |
tapTargetSize: style.tapTargetSize ?? tapTargetSize, | |
); | |
} | |
@override | |
int get hashCode { | |
return hashValues( | |
textStyle, | |
backgroundColor, | |
foregroundColor, | |
overlayColor, | |
elevation, | |
padding, | |
minimumSize, | |
side, | |
shape, | |
visualDensity, | |
tapTargetSize, | |
); | |
} | |
@override | |
bool operator ==(Object other) { | |
if (identical(this, other)) | |
return true; | |
if (other.runtimeType != runtimeType) | |
return false; | |
return other is ButtonStyle | |
&& other.textStyle == textStyle | |
&& other.backgroundColor == backgroundColor | |
&& other.foregroundColor == foregroundColor | |
&& other.overlayColor == overlayColor | |
&& other.elevation == elevation | |
&& other.padding == padding | |
&& other.minimumSize == minimumSize | |
&& other.side == side | |
&& other.shape == shape | |
&& other.visualDensity == visualDensity | |
&& other.tapTargetSize == tapTargetSize; | |
} | |
@override | |
String toStringShort() { | |
final List <String> overrides = <String>[ | |
if (textStyle != null) 'textStyle', | |
if (backgroundColor != null) 'backgroundColor', | |
if (foregroundColor != null) 'foregroundColor', | |
if (overlayColor != null) 'overlayColor', | |
if (elevation != null) 'elevation', | |
if (padding != null) 'padding', | |
if (minimumSize != null) 'minimumSize', | |
if (side != null) 'side', | |
if (shape != null) 'shape', | |
if (visualDensity != null) 'visualDensity', | |
if (tapTargetSize != null) 'tapTargetSize', | |
]; | |
final String overridesString = overrides.isEmpty | |
? 'no overrides' | |
: 'overrides ${overrides.join(", ")}'; | |
return '${super.toStringShort()}($overridesString)'; | |
} | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle>>('textStyle', textStyle, defaultValue: null)); | |
properties.add(DiagnosticsProperty<MaterialStateProperty<Color>>('backgroundColor', backgroundColor, defaultValue: null)); | |
properties.add(DiagnosticsProperty<MaterialStateProperty<Color>>('foregroundColor', foregroundColor, defaultValue: null)); | |
properties.add(DiagnosticsProperty<MaterialStateProperty<Color>>('overlayColor', overlayColor, defaultValue: null)); | |
properties.add(DiagnosticsProperty<MaterialStateProperty<double>>('elevation', elevation, defaultValue: null)); | |
properties.add(DiagnosticsProperty<MaterialStateProperty<EdgeInsetsGeometry>>('padding', padding, defaultValue: null)); | |
properties.add(DiagnosticsProperty<MaterialStateProperty<Size>>('minimumSize', minimumSize, defaultValue: null)); | |
properties.add(DiagnosticsProperty<MaterialStateProperty<BorderSide>>('side', side, defaultValue: null)); | |
properties.add(DiagnosticsProperty<MaterialStateProperty<ShapeBorder>>('shape', shape, defaultValue: null)); | |
properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null)); | |
properties.add(EnumProperty<MaterialTapTargetSize>('tapTargetSize', tapTargetSize, defaultValue: null)); | |
} | |
static ButtonStyle lerp(ButtonStyle a, ButtonStyle b, double t) { | |
assert (t != null); | |
if (a == null && b == null) | |
return null; | |
return ButtonStyle( | |
textStyle: _lerpTextStyles(a?.textStyle, b?.textStyle, t), | |
backgroundColor: _lerpColors(a?.backgroundColor, b?.backgroundColor, t), | |
foregroundColor: _lerpColors(a?.foregroundColor, b?.foregroundColor, t), | |
overlayColor: _lerpColors(a?.overlayColor, b?.overlayColor, t), | |
elevation: _lerpDoubles(a?.elevation, b?.elevation, t), | |
padding: _lerpInsets(a?.padding, b?.padding, t), | |
minimumSize: _lerpSizes(a?.minimumSize, b?.minimumSize, t), | |
side: _lerpSides(a?.side, b?.side, t), | |
shape: _lerpShapes(a?.shape, b?.shape, t), | |
visualDensity: t < 0.5 ? a.visualDensity : b.visualDensity, | |
tapTargetSize: t < 0.5 ? a.tapTargetSize : b.tapTargetSize, | |
); | |
} | |
static MaterialStateProperty<TextStyle> _lerpTextStyles(MaterialStateProperty<TextStyle> a, MaterialStateProperty<TextStyle> b, double t) { | |
if (a == null && b == null) | |
return null; | |
return _LerpTextStyles(a, b, t); | |
} | |
static MaterialStateProperty<Color> _lerpColors(MaterialStateProperty<Color> a, MaterialStateProperty<Color> b, double t) { | |
if (a == null && b == null) | |
return null; | |
return _LerpColors(a, b, t); | |
} | |
static MaterialStateProperty<double> _lerpDoubles(MaterialStateProperty<double> a, MaterialStateProperty<double> b, double t) { | |
if (a == null && b == null) | |
return null; | |
return _LerpDoubles(a, b, t); | |
} | |
static MaterialStateProperty<EdgeInsetsGeometry> _lerpInsets(MaterialStateProperty<EdgeInsetsGeometry> a, MaterialStateProperty<EdgeInsetsGeometry> b, double t) { | |
if (a == null && b == null) | |
return null; | |
return _LerpInsets(a, b, t); | |
} | |
static MaterialStateProperty<Size> _lerpSizes(MaterialStateProperty<Size> a, MaterialStateProperty<Size> b, double t) { | |
if (a == null && b == null) | |
return null; | |
return _LerpSizes(a, b, t); | |
} | |
static MaterialStateProperty<BorderSide> _lerpSides(MaterialStateProperty<BorderSide> a, MaterialStateProperty<BorderSide> b, double t) { | |
if (a == null && b == null) | |
return null; | |
return _LerpSides(a, b, t); | |
} | |
static MaterialStateProperty<ShapeBorder> _lerpShapes(MaterialStateProperty<ShapeBorder> a, MaterialStateProperty<ShapeBorder> b, double t) { | |
if (a == null && b == null) | |
return null; | |
return _LerpShapes(a, b, t); | |
} | |
} | |
class _LerpTextStyles implements MaterialStateProperty<TextStyle> { | |
const _LerpTextStyles(this.a, this.b, this.t); | |
final MaterialStateProperty<TextStyle> a; | |
final MaterialStateProperty<TextStyle> b; | |
final double t; | |
@override | |
TextStyle resolve(Set<MaterialState> states) { | |
final TextStyle resolvedA = a?.resolve(states); | |
final TextStyle resolvedB = b?.resolve(states); | |
return TextStyle.lerp(resolvedA, resolvedB, t); | |
} | |
} | |
class _LerpColors implements MaterialStateProperty<Color> { | |
const _LerpColors(this.a, this.b, this.t); | |
final MaterialStateProperty<Color> a; | |
final MaterialStateProperty<Color> b; | |
final double t; | |
@override | |
Color resolve(Set<MaterialState> states) { | |
final Color resolvedA = a?.resolve(states); | |
final Color resolvedB = b?.resolve(states); | |
return Color.lerp(resolvedA, resolvedB, t); | |
} | |
} | |
class _LerpDoubles implements MaterialStateProperty<double> { | |
const _LerpDoubles(this.a, this.b, this.t); | |
final MaterialStateProperty<double> a; | |
final MaterialStateProperty<double> b; | |
final double t; | |
@override | |
double resolve(Set<MaterialState> states) { | |
final double resolvedA = a?.resolve(states); | |
final double resolvedB = b?.resolve(states); | |
return lerpDouble(resolvedA, resolvedB, t); | |
} | |
} | |
class _LerpInsets implements MaterialStateProperty<EdgeInsetsGeometry> { | |
const _LerpInsets(this.a, this.b, this.t); | |
final MaterialStateProperty<EdgeInsetsGeometry> a; | |
final MaterialStateProperty<EdgeInsetsGeometry> b; | |
final double t; | |
@override | |
EdgeInsetsGeometry resolve(Set<MaterialState> states) { | |
final EdgeInsetsGeometry resolvedA = a?.resolve(states); | |
final EdgeInsetsGeometry resolvedB = b?.resolve(states); | |
return EdgeInsetsGeometry.lerp(resolvedA, resolvedB, t); | |
} | |
} | |
class _LerpSizes implements MaterialStateProperty<Size> { | |
const _LerpSizes(this.a, this.b, this.t); | |
final MaterialStateProperty<Size> a; | |
final MaterialStateProperty<Size> b; | |
final double t; | |
@override | |
Size resolve(Set<MaterialState> states) { | |
final Size resolvedA = a?.resolve(states); | |
final Size resolvedB = b?.resolve(states); | |
return Size.lerp(resolvedA, resolvedB, t); | |
} | |
} | |
class _LerpSides implements MaterialStateProperty<BorderSide> { | |
const _LerpSides(this.a, this.b, this.t); | |
final MaterialStateProperty<BorderSide> a; | |
final MaterialStateProperty<BorderSide> b; | |
final double t; | |
@override | |
BorderSide resolve(Set<MaterialState> states) { | |
final BorderSide resolvedA = a?.resolve(states); | |
final BorderSide resolvedB = b?.resolve(states); | |
return BorderSide.lerp(resolvedA, resolvedB, t); | |
} | |
} | |
class _LerpShapes implements MaterialStateProperty<ShapeBorder> { | |
const _LerpShapes(this.a, this.b, this.t); | |
final MaterialStateProperty<ShapeBorder> a; | |
final MaterialStateProperty<ShapeBorder> b; | |
final double t; | |
@override | |
ShapeBorder resolve(Set<MaterialState> states) { | |
final ShapeBorder resolvedA = a?.resolve(states); | |
final ShapeBorder resolvedB = b?.resolve(states); | |
return ShapeBorder.lerp(resolvedA, resolvedB, t); | |
} | |
} | |
// lib/text_button_theme.dart | |
class TextButtonThemeData with Diagnosticable { | |
const TextButtonThemeData({ this.style }); | |
final ButtonStyle style; | |
/// Linearly interpolate between two text button themes. | |
static TextButtonThemeData lerp(TextButtonThemeData a, TextButtonThemeData b, double t) { | |
assert (t != null); | |
if (a == null && b == null) | |
return null; | |
return TextButtonThemeData( | |
style: ButtonStyle.lerp(a?.style, b?.style, t), | |
); | |
} | |
@override | |
int get hashCode { | |
return style.hashCode; | |
} | |
@override | |
bool operator ==(Object other) { | |
if (identical(this, other)) | |
return true; | |
if (other.runtimeType != runtimeType) | |
return false; | |
return other is TextButtonThemeData && other.style == style; | |
} | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties.add(DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null)); | |
} | |
} | |
class TextButtonTheme extends InheritedTheme { | |
const TextButtonTheme({ | |
Key key, | |
@required this.data, | |
Widget child, | |
}) : assert(data != null), super(key: key, child: child); | |
final TextButtonThemeData data; | |
/// The closest instance of this class that encloses the given context. | |
/// | |
/// If there is no enclosing [TextButtonsTheme] widget, then | |
/// [ThemeData.textButtonTheme] is used. | |
/// | |
/// Typical usage is as follows: | |
/// | |
/// ```dart | |
/// TextButtonTheme theme = TextButtonTheme.of(context); | |
/// ``` | |
static TextButtonThemeData of(BuildContext context) { | |
final TextButtonTheme buttonTheme = context.dependOnInheritedWidgetOfExactType<TextButtonTheme>(); | |
return buttonTheme?.data; | |
} | |
@override | |
Widget wrap(BuildContext context, Widget child) { | |
final TextButtonTheme ancestorTheme = context.findAncestorWidgetOfExactType<TextButtonTheme>(); | |
return identical(this, ancestorTheme) ? child : TextButtonTheme(data: data, child: child); | |
} | |
@override | |
bool updateShouldNotify(TextButtonTheme oldWidget) => data != oldWidget.data; | |
} | |
// lib/contained_button_theme.dart | |
class ContainedButtonThemeData with Diagnosticable { | |
const ContainedButtonThemeData({ this.style }); | |
final ButtonStyle style; | |
/// Linearly interpolate between two text button themes. | |
static ContainedButtonThemeData lerp(ContainedButtonThemeData a, ContainedButtonThemeData b, double t) { | |
assert (t != null); | |
if (a == null && b == null) | |
return null; | |
return ContainedButtonThemeData( | |
style: ButtonStyle.lerp(a?.style, b?.style, t), | |
); | |
} | |
@override | |
int get hashCode { | |
return style.hashCode; | |
} | |
@override | |
bool operator ==(Object other) { | |
if (identical(this, other)) | |
return true; | |
if (other.runtimeType != runtimeType) | |
return false; | |
return other is ContainedButtonThemeData && other.style == style; | |
} | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties.add(DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null)); | |
} | |
} | |
class ContainedButtonTheme extends InheritedTheme { | |
const ContainedButtonTheme({ | |
Key key, | |
@required this.data, | |
Widget child, | |
}) : assert(data != null), super(key: key, child: child); | |
final ContainedButtonThemeData data; | |
/// The closest instance of this class that encloses the given context. | |
/// | |
/// If there is no enclosing [ContainedButtonsTheme] widget, then | |
/// [ThemeData.textButtonTheme] is used. | |
/// | |
/// Typical usage is as follows: | |
/// | |
/// ```dart | |
/// ContainedButtonTheme theme = ContainedButtonTheme.of(context); | |
/// ``` | |
static ContainedButtonThemeData of(BuildContext context) { | |
final ContainedButtonTheme buttonTheme = context.dependOnInheritedWidgetOfExactType<ContainedButtonTheme>(); | |
return buttonTheme?.data; | |
} | |
@override | |
Widget wrap(BuildContext context, Widget child) { | |
final ContainedButtonTheme ancestorTheme = context.findAncestorWidgetOfExactType<ContainedButtonTheme>(); | |
return identical(this, ancestorTheme) ? child : ContainedButtonTheme(data: data, child: child); | |
} | |
@override | |
bool updateShouldNotify(ContainedButtonTheme oldWidget) => data != oldWidget.data; | |
} | |
// lib/outlined_button_theme.dart | |
class OutlinedButtonThemeData with Diagnosticable { | |
const OutlinedButtonThemeData({ this.style }); | |
final ButtonStyle style; | |
/// Linearly interpolate between two text button themes. | |
static OutlinedButtonThemeData lerp(OutlinedButtonThemeData a, OutlinedButtonThemeData b, double t) { | |
assert (t != null); | |
if (a == null && b == null) | |
return null; | |
return OutlinedButtonThemeData( | |
style: ButtonStyle.lerp(a?.style, b?.style, t), | |
); | |
} | |
@override | |
int get hashCode { | |
return style.hashCode; | |
} | |
@override | |
bool operator ==(Object other) { | |
if (identical(this, other)) | |
return true; | |
if (other.runtimeType != runtimeType) | |
return false; | |
return other is OutlinedButtonThemeData && other.style == style; | |
} | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties.add(DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null)); | |
} | |
} | |
class OutlinedButtonTheme extends InheritedTheme { | |
const OutlinedButtonTheme({ | |
Key key, | |
@required this.data, | |
Widget child, | |
}) : assert(data != null), super(key: key, child: child); | |
final OutlinedButtonThemeData data; | |
/// The closest instance of this class that encloses the given context. | |
/// | |
/// If there is no enclosing [OutlinedButtonsTheme] widget, then | |
/// [ThemeData.textButtonTheme] is used. | |
/// | |
/// Typical usage is as follows: | |
/// | |
/// ```dart | |
/// OutlinedButtonTheme theme = OutlinedButtonTheme.of(context); | |
/// ``` | |
static OutlinedButtonThemeData of(BuildContext context) { | |
final OutlinedButtonTheme buttonTheme = context.dependOnInheritedWidgetOfExactType<OutlinedButtonTheme>(); | |
return buttonTheme?.data; | |
} | |
@override | |
Widget wrap(BuildContext context, Widget child) { | |
final OutlinedButtonTheme ancestorTheme = context.findAncestorWidgetOfExactType<OutlinedButtonTheme>(); | |
return identical(this, ancestorTheme) ? child : OutlinedButtonTheme(data: data, child: child); | |
} | |
@override | |
bool updateShouldNotify(OutlinedButtonTheme oldWidget) => data != oldWidget.data; | |
} | |
// button_style_buttons.dart | |
// Temporary proxy for TBD static MaterialStateProperty<T>.all(T value) | |
class MaterialStatePropertyAll<T> implements MaterialStateProperty<T> { | |
const MaterialStatePropertyAll(this.value); | |
final T value; | |
@override | |
T resolve(Set<MaterialState> states) => value; | |
@override | |
String toString() { | |
return '$runtimeType($value)'; | |
} | |
} | |
// Temporary proxy for TBD ShapeBorder.withSide(BorderSide side) | |
shapeBorderWithSide(ShapeBorder shape, BorderSide side) { | |
if (shape == null) | |
return shape; | |
if (shape is RoundedRectangleBorder) { | |
return RoundedRectangleBorder( | |
borderRadius: shape.borderRadius, | |
side: side, | |
); | |
} | |
if (shape is BeveledRectangleBorder) { | |
return BeveledRectangleBorder( | |
borderRadius: shape.borderRadius, | |
side: side, | |
); | |
} | |
if (shape is StadiumBorder) { | |
return StadiumBorder( | |
side: side, | |
); | |
} | |
if (shape is CircleBorder) { | |
return CircleBorder( | |
side: side, | |
); | |
} | |
if (shape is ContinuousRectangleBorder) { | |
return ContinuousRectangleBorder( | |
borderRadius: shape.borderRadius, | |
side: side, | |
); | |
} | |
if (shape is CircleBorder) { | |
return CircleBorder( | |
side: side, | |
); | |
} | |
return shape; | |
} | |
abstract class _ButtonStyleButton extends StatefulWidget { | |
const _ButtonStyleButton({ | |
Key key, | |
@required this.onPressed, | |
@required this.onLongPress, | |
@required this.style, | |
@required this.focusNode, | |
@required this.autofocus, | |
@required this.clipBehavior, | |
@required this.enableFeedback, | |
@required this.animationDuration, | |
@required this.child, | |
}) : assert(autofocus != null), | |
assert(clipBehavior != null), | |
assert(enableFeedback != null), | |
assert(animationDuration != null), | |
super(key: key); | |
final VoidCallback onPressed; | |
final VoidCallback onLongPress; | |
final ButtonStyle style; | |
final Clip clipBehavior; | |
final FocusNode focusNode; | |
final bool autofocus; | |
final bool enableFeedback; | |
final Duration animationDuration; | |
final Widget child; | |
// Returns a ButtonStyle that's based on the Theme's | |
// textTheme and colorScheme. Concrete button subclasses must | |
// resolve the button's actual visual parameters by combining this | |
// style with the widget's style and the button theme's style. | |
// | |
// Defined here rather than in the State subclass to ensure that | |
// the default style can only depend on the BuildContext. | |
ButtonStyle _defaultStyleOf(BuildContext context); | |
/// Whether the button is enabled or disabled. | |
/// | |
/// Buttons are disabled by default. To enable a button, set its [onPressed] | |
/// or [onLongPress] properties to a non-null value. | |
bool get enabled => onPressed != null || onLongPress != null; | |
} | |
abstract class _ButtonStyleState<T extends _ButtonStyleButton> extends State<T> { | |
final Set<MaterialState> _states = <MaterialState>{}; | |
bool get _hovered => _states.contains(MaterialState.hovered); | |
bool get _focused => _states.contains(MaterialState.focused); | |
bool get _pressed => _states.contains(MaterialState.pressed); | |
bool get _disabled => _states.contains(MaterialState.disabled); | |
void _updateState(MaterialState state, bool value) { | |
value ? _states.add(state) : _states.remove(state); | |
} | |
void _handleHighlightChanged(bool value) { | |
if (_pressed != value) { | |
setState(() { | |
_updateState(MaterialState.pressed, value); | |
}); | |
} | |
} | |
void _handleHoveredChanged(bool value) { | |
if (_hovered != value) { | |
setState(() { | |
_updateState(MaterialState.hovered, value); | |
}); | |
} | |
} | |
void _handleFocusedChanged(bool value) { | |
if (_focused != value) { | |
setState(() { | |
_updateState(MaterialState.focused, value); | |
}); | |
} | |
} | |
@override | |
void initState() { | |
super.initState(); | |
_updateState(MaterialState.disabled, !widget.enabled); | |
} | |
@override | |
void didUpdateWidget(T oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
_updateState(MaterialState.disabled, !widget.enabled); | |
// If the button is disabled while a press gesture is currently ongoing, | |
// InkWell makes a call to handleHighlightChanged. This causes an exception | |
// because it calls setState in the middle of a build. To preempt this, we | |
// manually update pressed to false when this situation occurs. | |
if (_disabled && _pressed) { | |
_handleHighlightChanged(false); | |
} | |
} | |
T _resolve<T>( | |
MaterialStateProperty<T> widgetValue, | |
MaterialStateProperty<T> themeValue, | |
MaterialStateProperty<T> defaultValue) | |
{ | |
assert(defaultValue != null); | |
return widgetValue?.resolve(_states) ?? themeValue?.resolve(_states) ?? defaultValue.resolve(_states); | |
} | |
ButtonStyle themeStyleFor(BuildContext context); | |
@override | |
Widget build(BuildContext context) { | |
final ButtonStyle widgetStyle = widget.style; | |
final ButtonStyle themeStyle = themeStyleFor(context); | |
final ButtonStyle defaultStyle = widget._defaultStyleOf(context); | |
final TextStyle resolvedTextStyle = _resolve<TextStyle>( | |
widgetStyle?.textStyle, themeStyle?.textStyle, defaultStyle.textStyle, | |
); | |
final Color resolvedBackgroundColor = _resolve<Color>( | |
widgetStyle?.backgroundColor, themeStyle?.backgroundColor, defaultStyle.backgroundColor, | |
); | |
final Color resolvedForegroundColor = _resolve<Color>( | |
widgetStyle?.foregroundColor, themeStyle?.foregroundColor, defaultStyle.foregroundColor, | |
); | |
final double resolvedElevation = _resolve<double>( | |
widgetStyle?.elevation, themeStyle?.elevation, defaultStyle.elevation, | |
); | |
final EdgeInsetsGeometry resolvedPadding = _resolve<EdgeInsetsGeometry>( | |
widgetStyle?.padding, themeStyle?.padding, defaultStyle.padding, | |
); | |
final Size resolvedMinimumSize = _resolve<Size>( | |
widgetStyle?.minimumSize, themeStyle?.minimumSize, defaultStyle.minimumSize, | |
); | |
final BorderSide resolvedSide = _resolve<BorderSide>( | |
widgetStyle?.side, themeStyle?.side, defaultStyle.side, | |
); | |
final ShapeBorder resolvedShape = _resolve<ShapeBorder>( | |
widgetStyle?.shape, themeStyle?.shape, defaultStyle.shape, | |
); | |
Color resolveOverlayColor(MaterialState state) { | |
final Set<MaterialState> states = <MaterialState>{ state }; | |
return widgetStyle?.overlayColor?.resolve(states) | |
?? themeStyle?.overlayColor?.resolve(states) | |
?? defaultStyle.overlayColor.resolve(states); | |
} | |
final Color pressedColor = resolveOverlayColor(MaterialState.pressed); | |
final Color hoveredColor = resolveOverlayColor(MaterialState.hovered); | |
final Color focusedColor = resolveOverlayColor(MaterialState.focused); | |
final VisualDensity resolvedVisualDensity = widgetStyle?.visualDensity ?? defaultStyle.visualDensity; | |
final MaterialTapTargetSize resolvedTapTargetSize = widgetStyle?.tapTargetSize ?? defaultStyle.tapTargetSize; | |
final Offset densityAdjustment = resolvedVisualDensity.baseSizeAdjustment; | |
final BoxConstraints effectiveConstraints = resolvedVisualDensity.effectiveConstraints( | |
BoxConstraints( | |
minWidth: resolvedMinimumSize.width, | |
minHeight: resolvedMinimumSize.height, | |
), | |
); | |
final EdgeInsetsGeometry padding = resolvedPadding.add( | |
EdgeInsets.only( | |
left: densityAdjustment.dx, | |
top: densityAdjustment.dy, | |
right: densityAdjustment.dx, | |
bottom: densityAdjustment.dy, | |
), | |
).clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity); | |
final Widget result = ConstrainedBox( | |
constraints: effectiveConstraints, | |
child: Material( | |
elevation: resolvedElevation, | |
textStyle: resolvedTextStyle?.copyWith(color: resolvedForegroundColor), | |
shape: shapeBorderWithSide(resolvedShape, resolvedSide), // resolvedShape?.withSide(resolvedSide) | |
color: resolvedBackgroundColor, | |
type: resolvedBackgroundColor == null ? MaterialType.transparency : MaterialType.button, | |
animationDuration: widget.animationDuration, | |
clipBehavior: widget.clipBehavior, | |
child: InkWell( | |
onTap: widget.onPressed, | |
onLongPress: widget.onLongPress, | |
onHighlightChanged: _handleHighlightChanged, | |
onHover: _handleHoveredChanged, | |
enableFeedback: widget.enableFeedback, | |
focusNode: widget.focusNode, | |
canRequestFocus: widget.enabled, | |
onFocusChange: _handleFocusedChanged, | |
autofocus: widget.autofocus, | |
splashFactory: InkRipple.splashFactory, | |
highlightColor: Colors.transparent, | |
splashColor: pressedColor, | |
focusColor: focusedColor, | |
hoverColor: hoveredColor, | |
customBorder: resolvedShape, | |
child: IconTheme.merge( | |
data: IconThemeData(color: resolvedForegroundColor), | |
child: Padding( | |
padding: padding, | |
child: Center( | |
widthFactor: 1.0, | |
heightFactor: 1.0, | |
child: widget.child, | |
), | |
), | |
), | |
), | |
), | |
); | |
Size minSize; | |
switch (resolvedTapTargetSize) { | |
case MaterialTapTargetSize.padded: | |
minSize = Size( | |
kMinInteractiveDimension + densityAdjustment.dx, | |
kMinInteractiveDimension + densityAdjustment.dy, | |
); | |
assert(minSize.width >= 0.0); | |
assert(minSize.height >= 0.0); | |
break; | |
case MaterialTapTargetSize.shrinkWrap: | |
minSize = Size.zero; | |
break; | |
} | |
return Semantics( | |
container: true, | |
button: true, | |
enabled: widget.enabled, | |
child: _InputPadding( | |
minSize: minSize, | |
child: result, | |
), | |
); | |
} | |
} | |
/// A material design "Text Button". | |
/// | |
/// A text button is a text label displayed on a (zero elevation) [Material] | |
/// widget that reacts to touches by filling with color. | |
/// | |
/// Use text buttons on toolbars, in dialogs, or inline with other content but | |
/// offset from that content with padding so that the button's presence is | |
/// obvious. Text buttons intentionally do not have visible borders and must | |
/// therefore rely on their position relative to other content for context. In | |
/// dialogs and cards, they should be grouped together in one of the bottom | |
/// corners. Avoid using text buttons where they would blend in with other | |
/// content, for example in the middle of lists. | |
/// | |
/// Material design text buttons have an all-caps label, some internal padding, | |
/// and some defined dimensions. To have a part of your application be | |
/// interactive, with ink splashes, without also committing to these stylistic | |
/// choices, consider using [InkWell] instead. | |
/// | |
/// If the [onPressed] and [onLongPress] callbacks are null, then this button will be disabled, | |
/// will not react to touch, and will be colored as specified by | |
/// the [disabledColor] property instead of the [color] property. If you are | |
/// trying to change the button's [color] and it is not having any effect, check | |
/// that you are passing a non-null [onPressed] handler. | |
/// | |
/// Text buttons have a minimum size of 88.0 by 36.0 which can be overridden | |
/// with [ButtonTheme]. | |
/// | |
/// The [clipBehavior] argument must not be null. | |
/// | |
/// {@tool snippet} | |
/// | |
/// This example shows a simple [TextButton]. | |
/// | |
/// ![A simple TextButton](https://flutter.github.io/assets-for-api-docs/assets/material/text_button.png) | |
/// | |
/// ```dart | |
/// TextButton( | |
/// onPressed: () { | |
/// /*...*/ | |
/// }, | |
/// child: Text( | |
/// "Text Button", | |
/// ), | |
/// ) | |
/// ``` | |
/// {@end-tool} | |
/// | |
/// See also: | |
/// | |
/// * [ContainedButton], a filled button whose material elevates when pressed. | |
/// * [DropdownButton], which offers the user a choice of a number of options. | |
/// * [SimpleDialogOption], which is used in [SimpleDialog]s. | |
/// * [IconButton], to create buttons that just contain icons. | |
/// * [InkWell], which implements the ink splash part of a text button. | |
/// * <https://material.io/design/components/buttons.html> | |
class TextButton extends _ButtonStyleButton { | |
/// Create a TextButton. | |
/// | |
/// The [autofocus] and [clipBehavior] arguments must not be null. | |
const TextButton({ | |
Key key, | |
@required VoidCallback onPressed, | |
VoidCallback onLongPress, | |
ButtonStyle style, | |
FocusNode focusNode, | |
bool autofocus = false, | |
Clip clipBehavior = Clip.none, | |
bool enableFeedback = true, | |
Duration animationDuration = kThemeChangeDuration, | |
@required Widget child, | |
}) : super( | |
key: key, | |
onPressed: onPressed, | |
onLongPress: onLongPress, | |
style: style, | |
focusNode: focusNode, | |
autofocus: autofocus, | |
clipBehavior: clipBehavior, | |
enableFeedback: enableFeedback, | |
animationDuration: animationDuration, | |
child: child, | |
); | |
factory TextButton.icon({ | |
Key key, | |
@required VoidCallback onPressed, | |
VoidCallback onLongPress, | |
ButtonStyle style, | |
FocusNode focusNode, | |
bool autofocus = false, | |
Clip clipBehavior = Clip.none, | |
bool enableFeedback = true, | |
Duration animationDuration = kThemeChangeDuration, | |
@required Widget icon, | |
@required Widget label, | |
}) { | |
assert(icon != null); | |
assert(label != null); | |
return TextButton( | |
key: key, | |
onPressed: onPressed, | |
onLongPress: onLongPress, | |
style: style, | |
focusNode: focusNode, | |
autofocus: autofocus, | |
clipBehavior: clipBehavior, | |
enableFeedback: enableFeedback, | |
animationDuration: animationDuration, | |
child: Row( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
icon, | |
const SizedBox(width: 8.0), | |
label, | |
], | |
), | |
); | |
} | |
@override | |
_TextButtonState createState() => _TextButtonState(); | |
static ButtonStyle styleFrom({ | |
Color primary, | |
Color onSurface, | |
Color backgroundColor, | |
TextStyle textStyle, | |
double elevation, | |
EdgeInsetsGeometry padding, | |
Size minimumSize, | |
BorderSide side, | |
ShapeBorder shape, | |
VisualDensity visualDensity, | |
MaterialTapTargetSize tapTargetSize, | |
}) { | |
final MaterialStateProperty<Color> foregroundColor = (onSurface == null && primary == null) | |
? null | |
: MaterialStateProperty.resolveWith<Color>( | |
(Set<MaterialState> states) { | |
if (states.contains(MaterialState.disabled)) | |
return onSurface?.withOpacity(0.38); | |
return primary; | |
}, | |
); | |
final MaterialStateProperty<Color> overlayColor = (primary == null) | |
? null | |
: MaterialStateProperty.resolveWith<Color>( | |
(Set<MaterialState> states) { | |
if (states.contains(MaterialState.hovered)) | |
return primary?.withOpacity(0.04); | |
if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) | |
return primary?.withOpacity(0.12); | |
return null; | |
} | |
); | |
return ButtonStyle( | |
textStyle: MaterialStatePropertyAll<TextStyle>(textStyle), | |
foregroundColor: foregroundColor, | |
backgroundColor: MaterialStatePropertyAll<Color>(backgroundColor), | |
overlayColor: overlayColor, | |
elevation: MaterialStatePropertyAll<double>(elevation), | |
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(padding), | |
minimumSize: MaterialStatePropertyAll<Size>(minimumSize), | |
side: MaterialStatePropertyAll<BorderSide>(side), | |
shape: MaterialStatePropertyAll<ShapeBorder>(shape), | |
visualDensity: visualDensity, | |
tapTargetSize: tapTargetSize, | |
); | |
} | |
@override | |
ButtonStyle _defaultStyleOf(BuildContext context) { | |
final ThemeData theme = Theme.of(context); | |
final ColorScheme colorScheme = theme.colorScheme; | |
return styleFrom( | |
primary: colorScheme.primary, | |
onSurface: colorScheme.onSurface, | |
backgroundColor: Colors.transparent, | |
textStyle: theme.textTheme.button, | |
elevation: 0, | |
padding: EdgeInsets.all(8), | |
minimumSize: Size(0, 36), | |
side: BorderSide.none, | |
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))), | |
visualDensity: theme.visualDensity, | |
tapTargetSize: theme.materialTapTargetSize, | |
); | |
} | |
} | |
class _TextButtonState extends _ButtonStyleState<TextButton> { | |
@override | |
ButtonStyle themeStyleFor(BuildContext context) { | |
return TextButtonTheme.of(context)?.style; | |
} | |
} | |
/// A material design "contained button". | |
/// | |
/// A contained button is based on a [Material] widget whose [Material.elevation] | |
/// increases when the button is pressed. | |
/// | |
/// Use contained buttons to add dimension to otherwise mostly flat layouts, e.g. | |
/// in long busy lists of content, or in wide spaces. Avoid using contained buttons | |
/// on already-contained content such as dialogs or cards. | |
/// | |
/// If [onPressed] and [onLongPress] callbacks are null, then the button will be disabled and by | |
/// default will resemble a flat button in the [disabledColor]. If you are | |
/// trying to change the button's [color] and it is not having any effect, check | |
/// that you are passing a non-null [onPressed] or [onLongPress] callbacks. | |
/// | |
/// If you want an ink-splash effect for taps, but don't want to use a button, | |
/// consider using [InkWell] directly. | |
/// | |
/// Contained buttons have a minimum size of 88.0 by 36.0 which can be overridden | |
/// with [ButtonTheme]. | |
/// | |
/// {@tool dartpad --template=stateless_widget_scaffold} | |
/// | |
/// This sample shows how to render a disabled ContainedButton, an enabled ContainedButton | |
/// and lastly a ContainedButton with gradient background. | |
/// | |
/// ![Three contained buttons, one enabled, another disabled, and the last one | |
/// styled with a blue gradient background](https://flutter.github.io/assets-for-api-docs/assets/material/contained_button.png) | |
/// | |
/// ```dart | |
/// Widget build(BuildContext context) { | |
/// return Center( | |
/// child: Column( | |
/// mainAxisSize: MainAxisSize.min, | |
/// children: <Widget>[ | |
/// const ContainedButton( | |
/// onPressed: null, | |
/// style: ContainedButton.styleFrom( | |
/// textStyle: TextStyle(fontSize: 20), | |
/// ), | |
/// child: Text('Disabled Button'), | |
/// ), | |
/// const SizedBox(height: 30), | |
/// const ContainedButton( | |
/// onPressed: () {}, | |
/// style: ContainedButton.styleFrom( | |
/// textStyle: TextStyle(fontSize: 20), | |
/// ), | |
/// child: Text('Enabled Button'), | |
/// ), | |
/// const SizedBox(height: 30), | |
/// ContainedButton( | |
/// onPressed: () {}, | |
/// style: ContainedButton.styleFrom( | |
/// textStyle: TextStyle(fontSize: 20), | |
/// textColor: Colors.white, | |
/// const EdgeInsets.all(0.0), | |
/// ), | |
/// child: Container( | |
/// decoration: const BoxDecoration( | |
/// gradient: LinearGradient( | |
/// colors: <Color>[ | |
/// Color(0xFF0D47A1), | |
/// Color(0xFF1976D2), | |
/// Color(0xFF42A5F5), | |
/// ], | |
/// ), | |
/// ), | |
/// padding: const EdgeInsets.all(10.0), | |
/// child: const Text('Gradient Button'), | |
/// ), | |
/// ), | |
/// ], | |
/// ), | |
/// ); | |
/// } | |
/// ``` | |
/// {@end-tool} | |
/// | |
/// See also: | |
/// | |
/// * [TextButton], a material design button without a shadow. | |
/// * [DropdownButton], a button that shows options to select from. | |
/// * [FloatingActionButton], the round button in material applications. | |
/// * [IconButton], to create buttons that just contain icons. | |
/// * [InkWell], which implements the ink splash part of a flat button. | |
/// * <https://material.io/design/components/buttons.html> | |
class ContainedButton extends _ButtonStyleButton { | |
/// Create a ContainedButton. | |
/// | |
/// The [autofocus] and [clipBehavior] arguments must not be null. | |
const ContainedButton({ | |
Key key, | |
@required VoidCallback onPressed, | |
VoidCallback onLongPress, | |
ButtonStyle style, | |
FocusNode focusNode, | |
bool autofocus = false, | |
Clip clipBehavior = Clip.none, | |
bool enableFeedback = true, | |
Duration animationDuration = kThemeChangeDuration, | |
@required Widget child, | |
}) : super( | |
key: key, | |
onPressed: onPressed, | |
onLongPress: onLongPress, | |
style: style, | |
focusNode: focusNode, | |
autofocus: autofocus, | |
clipBehavior: clipBehavior, | |
enableFeedback: enableFeedback, | |
animationDuration: animationDuration, | |
child: child, | |
); | |
factory ContainedButton.icon({ | |
Key key, | |
@required VoidCallback onPressed, | |
VoidCallback onLongPress, | |
ButtonStyle style, | |
FocusNode focusNode, | |
bool autofocus = false, | |
Clip clipBehavior = Clip.none, | |
bool enableFeedback = true, | |
Duration animationDuration = kThemeChangeDuration, | |
@required Widget icon, | |
@required Widget label, | |
}) { | |
assert(icon != null); | |
assert(label != null); | |
const ButtonStyle paddingStyle = ButtonStyle( | |
padding: MaterialStatePropertyAll<EdgeInsets>(EdgeInsets.only(left: 12, right: 16)), | |
); | |
return ContainedButton( | |
key: key, | |
onPressed: onPressed, | |
onLongPress: onLongPress, | |
style: style == null ? paddingStyle : style.merge(paddingStyle), | |
focusNode: focusNode, | |
autofocus: autofocus, | |
clipBehavior: clipBehavior, | |
enableFeedback: enableFeedback, | |
animationDuration: animationDuration, | |
child: Row( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
icon, | |
const SizedBox(width: 8.0), | |
label, | |
], | |
), | |
); | |
} | |
@override | |
_ContainedButtonState createState() => _ContainedButtonState(); | |
static ButtonStyle styleFrom({ | |
Color primary, | |
Color onPrimary, | |
Color onSurface, | |
double elevation, | |
TextStyle textStyle, | |
EdgeInsetsGeometry padding, | |
Size minimumSize, | |
BorderSide side, | |
ShapeBorder shape, | |
VisualDensity visualDensity, | |
MaterialTapTargetSize tapTargetSize, | |
}) { | |
final MaterialStateProperty<Color> backgroundColor = (onSurface == null && primary == null) | |
? null | |
: MaterialStateProperty.resolveWith<Color>( | |
(Set<MaterialState> states) { | |
if (states.contains(MaterialState.disabled)) | |
return onSurface?.withOpacity(0.12); | |
return primary; | |
} | |
); | |
final MaterialStateProperty<Color> foregroundColor = (onSurface == null && onPrimary == null) | |
? null | |
: MaterialStateProperty.resolveWith<Color>( | |
(Set<MaterialState> states) { | |
if (states.contains(MaterialState.disabled)) | |
return onSurface?.withOpacity(0.38); | |
return onPrimary; | |
} | |
); | |
final MaterialStateProperty<Color> overlayColor = (onPrimary == null) | |
? null | |
: MaterialStateProperty.resolveWith<Color>( | |
(Set<MaterialState> states) { | |
if (states.contains(MaterialState.hovered)) | |
return onPrimary?.withOpacity(0.08); | |
if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) | |
return onPrimary?.withOpacity(0.24); | |
return null; | |
} | |
); | |
final MaterialStateProperty<double> elevationValue = (elevation == null) | |
? null | |
: MaterialStateProperty.resolveWith<double>( | |
(Set<MaterialState> states) { | |
if (states.contains(MaterialState.disabled)) return 0; | |
if (states.contains(MaterialState.hovered)) return elevation + 2; | |
if (states.contains(MaterialState.focused)) return elevation + 2; | |
if (states.contains(MaterialState.pressed)) return elevation + 6; | |
return elevation; | |
} | |
); | |
return ButtonStyle( | |
textStyle: MaterialStatePropertyAll<TextStyle>(textStyle), | |
backgroundColor: backgroundColor, | |
foregroundColor: foregroundColor, | |
overlayColor: overlayColor, | |
elevation: elevationValue, | |
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(padding), | |
minimumSize: MaterialStatePropertyAll<Size>(minimumSize), | |
side: MaterialStatePropertyAll<BorderSide>(side), | |
shape: MaterialStatePropertyAll<ShapeBorder>(shape), | |
visualDensity: visualDensity, | |
tapTargetSize: tapTargetSize, | |
); | |
} | |
@override | |
ButtonStyle _defaultStyleOf(BuildContext context) { | |
final ThemeData theme = Theme.of(context); | |
final ColorScheme colorScheme = theme.colorScheme; | |
return styleFrom( | |
primary: colorScheme.primary, | |
onPrimary: colorScheme.onPrimary, | |
onSurface: colorScheme.onSurface, | |
elevation: 2, | |
textStyle: theme.textTheme.button, | |
padding: const EdgeInsets.symmetric(horizontal: 16), | |
minimumSize: const Size(64, 36), | |
side: BorderSide.none, | |
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))), | |
visualDensity: theme.visualDensity, | |
tapTargetSize: theme.materialTapTargetSize, | |
); | |
} | |
} | |
class _ContainedButtonState extends _ButtonStyleState<ContainedButton> { | |
@override | |
ButtonStyle themeStyleFor(BuildContext context) { | |
return ContainedButtonTheme.of(context)?.style; | |
} | |
} | |
/// Essentially a [TextButton] with a thin grey rounded rectangle border. | |
/// | |
/// If the [onPressed] or [onLongPress] callbacks are null, then the button will be disabled and by | |
/// default will resemble a flat button in the [disabledColor]. | |
/// | |
/// If you want an ink-splash effect for taps, but don't want to use a button, | |
/// consider using [InkWell] directly. | |
/// | |
/// See also: | |
/// | |
/// * [ContainedButton], a filled material design button with a shadow. | |
/// * [TextButton], a material design button without a shadow. | |
/// * [DropdownButton], a button that shows options to select from. | |
/// * [FloatingActionButton], the round button in material applications. | |
/// * [IconButton], to create buttons that just contain icons. | |
/// * [InkWell], which implements the ink splash part of a flat button. | |
/// * <https://material.io/design/components/buttons.html> | |
class OutlinedButton extends _ButtonStyleButton { | |
/// Create an OutlinedButton. | |
/// | |
/// The [autofocus] and [clipBehavior] arguments must not be null. | |
const OutlinedButton({ | |
Key key, | |
@required VoidCallback onPressed, | |
VoidCallback onLongPress, | |
ButtonStyle style, | |
FocusNode focusNode, | |
bool autofocus = false, | |
Clip clipBehavior = Clip.none, | |
bool enableFeedback = true, | |
Duration animationDuration = kThemeChangeDuration, | |
@required Widget child, | |
}) : super( | |
key: key, | |
onPressed: onPressed, | |
onLongPress: onLongPress, | |
style: style, | |
focusNode: focusNode, | |
autofocus: autofocus, | |
clipBehavior: clipBehavior, | |
enableFeedback: enableFeedback, | |
animationDuration: animationDuration, | |
child: child, | |
); | |
factory OutlinedButton.icon({ | |
Key key, | |
@required VoidCallback onPressed, | |
VoidCallback onLongPress, | |
ButtonStyle style, | |
FocusNode focusNode, | |
bool autofocus = false, | |
Clip clipBehavior = Clip.none, | |
bool enableFeedback = true, | |
Duration animationDuration = kThemeChangeDuration, | |
@required Widget icon, | |
@required Widget label, | |
}) { | |
assert(icon != null); | |
assert(label != null); | |
const ButtonStyle paddingStyle = ButtonStyle( | |
padding: MaterialStatePropertyAll<EdgeInsets>(EdgeInsets.only(left: 12, right: 16)), | |
); | |
return OutlinedButton( | |
key: key, | |
onPressed: onPressed, | |
onLongPress: onLongPress, | |
style: style == null ? paddingStyle : style.merge(paddingStyle), | |
focusNode: focusNode, | |
autofocus: autofocus, | |
clipBehavior: clipBehavior, | |
enableFeedback: enableFeedback, | |
animationDuration: animationDuration, | |
child: Row( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
icon, | |
const SizedBox(width: 8.0), | |
label, | |
], | |
), | |
); | |
} | |
@override | |
_OutlinedButtonState createState() => _OutlinedButtonState(); | |
static ButtonStyle styleFrom({ | |
Color primary, | |
Color onSurface, | |
Color backgroundColor, | |
TextStyle textStyle, | |
double elevation, | |
EdgeInsetsGeometry padding, | |
Size minimumSize, | |
BorderSide side, | |
ShapeBorder shape, | |
VisualDensity visualDensity, | |
MaterialTapTargetSize tapTargetSize, | |
}) { | |
final MaterialStateProperty<Color> foregroundColor = (onSurface == null && primary == null) | |
? null | |
: MaterialStateProperty.resolveWith<Color>( | |
(Set<MaterialState> states) { | |
if (states.contains(MaterialState.disabled)) | |
return onSurface?.withOpacity(0.38); | |
return primary; | |
}, | |
); | |
final MaterialStateProperty<Color> overlayColor = (primary == null) | |
? null | |
: MaterialStateProperty.resolveWith<Color>( | |
(Set<MaterialState> states) { | |
if (states.contains(MaterialState.hovered)) | |
return primary?.withOpacity(0.04); | |
if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) | |
return primary?.withOpacity(0.12); | |
return null; | |
} | |
); | |
return ButtonStyle( | |
textStyle: MaterialStatePropertyAll<TextStyle>(textStyle), | |
foregroundColor: foregroundColor, | |
backgroundColor: MaterialStatePropertyAll<Color>(backgroundColor), | |
overlayColor: overlayColor, | |
elevation: MaterialStatePropertyAll<double>(elevation), | |
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(padding), | |
minimumSize: MaterialStatePropertyAll<Size>(minimumSize), | |
side: MaterialStatePropertyAll<BorderSide>(side), | |
shape: MaterialStatePropertyAll<ShapeBorder>(shape), | |
visualDensity: visualDensity, | |
tapTargetSize: tapTargetSize, | |
); | |
} | |
@override | |
ButtonStyle _defaultStyleOf(BuildContext context) { | |
final ThemeData theme = Theme.of(context); | |
final ColorScheme colorScheme = theme.colorScheme; | |
return styleFrom( | |
primary: colorScheme.primary, | |
onSurface: colorScheme.onSurface, | |
backgroundColor: Colors.transparent, | |
textStyle: theme.textTheme.button, | |
elevation: 0, | |
padding: const EdgeInsets.symmetric(horizontal: 16), | |
minimumSize: Size(64, 36), | |
side: BorderSide( | |
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12), | |
width: 1, | |
), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.all(Radius.circular(4)), | |
), | |
visualDensity: theme.visualDensity, | |
tapTargetSize: theme.materialTapTargetSize, | |
); | |
} | |
} | |
class _OutlinedButtonState extends _ButtonStyleState<OutlinedButton> { | |
@override | |
ButtonStyle themeStyleFor(BuildContext context) { | |
return OutlinedButtonTheme.of(context)?.style; | |
} | |
} | |
/// A widget to pad the area around a [MaterialButton]'s inner [Material]. | |
/// | |
/// Redirect taps that occur in the padded area around the child to the center | |
/// of the child. This increases the size of the button and the button's | |
/// "tap target", but not its material or its ink splashes. | |
class _InputPadding extends SingleChildRenderObjectWidget { | |
const _InputPadding({ | |
Key key, | |
Widget child, | |
this.minSize, | |
}) : super(key: key, child: child); | |
final Size minSize; | |
@override | |
RenderObject createRenderObject(BuildContext context) { | |
return _RenderInputPadding(minSize); | |
} | |
@override | |
void updateRenderObject(BuildContext context, covariant _RenderInputPadding renderObject) { | |
renderObject.minSize = minSize; | |
} | |
} | |
class _RenderInputPadding extends RenderShiftedBox { | |
_RenderInputPadding(this._minSize, [RenderBox child]) : super(child); | |
Size get minSize => _minSize; | |
Size _minSize; | |
set minSize(Size value) { | |
if (_minSize == value) | |
return; | |
_minSize = value; | |
markNeedsLayout(); | |
} | |
@override | |
double computeMinIntrinsicWidth(double height) { | |
if (child != null) | |
return math.max(child.getMinIntrinsicWidth(height), minSize.width); | |
return 0.0; | |
} | |
@override | |
double computeMinIntrinsicHeight(double width) { | |
if (child != null) | |
return math.max(child.getMinIntrinsicHeight(width), minSize.height); | |
return 0.0; | |
} | |
@override | |
double computeMaxIntrinsicWidth(double height) { | |
if (child != null) | |
return math.max(child.getMaxIntrinsicWidth(height), minSize.width); | |
return 0.0; | |
} | |
@override | |
double computeMaxIntrinsicHeight(double width) { | |
if (child != null) | |
return math.max(child.getMaxIntrinsicHeight(width), minSize.height); | |
return 0.0; | |
} | |
@override | |
void performLayout() { | |
final BoxConstraints constraints = this.constraints; | |
if (child != null) { | |
child.layout(constraints, parentUsesSize: true); | |
final double height = math.max(child.size.width, minSize.width); | |
final double width = math.max(child.size.height, minSize.height); | |
size = constraints.constrain(Size(height, width)); | |
final BoxParentData childParentData = child.parentData as BoxParentData; | |
childParentData.offset = Alignment.center.alongOffset(size - child.size as Offset); | |
} else { | |
size = Size.zero; | |
} | |
} | |
@override | |
bool hitTest(BoxHitTestResult result, { Offset position }) { | |
if (super.hitTest(result, position: position)) { | |
return true; | |
} | |
final Offset center = child.size.center(Offset.zero); | |
return result.addWithRawTransform( | |
transform: MatrixUtils.forceToPoint(center), | |
position: center, | |
hitTest: (BoxHitTestResult result, Offset position) { | |
assert(position == center); | |
return child.hitTest(result, position: center); | |
}, | |
); | |
} | |
} | |
// The demo code follows. The code above the line is from the flutter_buttons | |
// repo. In an ordinary app it would be imported using: | |
// import 'package:flutter_buttons/flutter_buttons.dart'; | |
// ---------------------------------------------------------------------------- | |
class DialogButtons extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
void dismissDialog() { | |
Navigator.of(context).pop(); | |
} | |
void showDemoDialog(String message, ButtonStyle style1, [ButtonStyle style2]) { | |
showDialog( | |
context: context, | |
builder: (BuildContext context) { | |
return AlertDialog( | |
title: Text('AlertDialog Title'), | |
content: Text(message), | |
actions: <Widget>[ | |
OutlinedButton( | |
style: style1, | |
onPressed: () { dismissDialog(); }, | |
child: Text('Approve'), | |
), | |
OutlinedButton( | |
style: style2 ?? style1, | |
onPressed: () { dismissDialog(); }, | |
child: Text('Really Approve'), | |
), | |
], | |
); | |
}, | |
); | |
} | |
return Center( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
ContainedButton( | |
onPressed: () { | |
showDemoDialog( | |
'Stadium shaped action buttons, default ouline.', | |
OutlinedButton.styleFrom(shape: StadiumBorder()), | |
); | |
}, | |
child: Text('Show an AlertDialog'), | |
), | |
SizedBox(height: 16), | |
ContainedButton( | |
onPressed: () { | |
showDemoDialog( | |
'One Stadium shaped action button, with a heavy, primary color ' | |
'outline.', | |
OutlinedButton.styleFrom(shape: StadiumBorder()), | |
OutlinedButton.styleFrom( | |
shape: StadiumBorder(), | |
side: BorderSide( | |
width: 2, | |
color: Theme.of(context).colorScheme.primary, | |
), | |
), | |
); | |
}, | |
child: Text('Show another AlertDialog'), | |
), | |
SizedBox(height: 16), | |
ContainedButton( | |
onPressed: () { | |
showDemoDialog( | |
'Stadium shaped action buttons, with a heavy, primary color ' | |
'outline when the button is focused or hovered', | |
OutlinedButton.styleFrom( | |
shape: StadiumBorder(), | |
).copyWith( | |
side: MaterialStateProperty.resolveWith<BorderSide>((Set<MaterialState> states) { | |
if (states.contains(MaterialState.hovered) || states.contains(MaterialState.focused)) { | |
return BorderSide( | |
width: 2, | |
color: Theme.of(context).colorScheme.primary, | |
); | |
} | |
return null; // defer to the default | |
}, | |
)), | |
); | |
}, | |
child: Text('Show yet another AlertDialog'), | |
), | |
] | |
), | |
); | |
} | |
} | |
class IndividuallySizedButtons extends StatefulWidget { | |
@override | |
_IndividuallySizedButtonsState createState() => _IndividuallySizedButtonsState(); | |
} | |
class _IndividuallySizedButtonsState extends State<IndividuallySizedButtons> { | |
bool _textButtonFlag = false; | |
bool _containedButtonFlag = false; | |
bool _outlinedButtonFlag = false; | |
@override | |
Widget build(BuildContext context) { | |
final TextTheme textTheme = Theme.of(context).textTheme; | |
const Widget spacer = SizedBox(height: 16); | |
return SingleChildScrollView( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
spacer, | |
TextButton( | |
style: TextButton.styleFrom( | |
textStyle: _textButtonFlag ? textTheme.headline2 : textTheme.headline4, | |
), | |
onPressed: () { | |
setState(() { | |
_textButtonFlag = !_textButtonFlag; | |
}); | |
}, | |
child: Text('TEXT'), | |
), | |
spacer, | |
ContainedButton( | |
style: ContainedButton.styleFrom( | |
textStyle: _containedButtonFlag ? textTheme.headline2 : textTheme.headline4, | |
), | |
onPressed: () { | |
setState(() { | |
_containedButtonFlag = !_containedButtonFlag; | |
}); | |
}, | |
child: Text('CONTAINED'), | |
), | |
spacer, | |
OutlinedButton( | |
style: OutlinedButton.styleFrom( | |
textStyle: _outlinedButtonFlag ? textTheme.headline2 : textTheme.headline4, | |
), | |
onPressed: () { | |
setState(() { | |
_outlinedButtonFlag = !_outlinedButtonFlag; | |
}); | |
}, | |
child: Text('OUTLINED'), | |
), | |
spacer, | |
], | |
), | |
); | |
} | |
} | |
class ShapeButtons extends StatefulWidget { | |
@override | |
_ShapeButtonsState createState() => _ShapeButtonsState(); | |
} | |
class _ShapeButtonsState extends State<ShapeButtons> { | |
int shapeIndex = 0; | |
@override | |
Widget build(BuildContext context) { | |
final List<ShapeBorder> buttonShapes = <ShapeBorder>[ | |
RoundedRectangleBorder( | |
borderRadius: BorderRadius.all(Radius.circular(4)), | |
), | |
BeveledRectangleBorder( | |
borderRadius: BorderRadius.all(Radius.circular(8)), | |
), | |
StadiumBorder(), | |
]; | |
return TextButtonTheme( | |
data: TextButtonThemeData( | |
style: TextButton.styleFrom(shape: buttonShapes[shapeIndex]), | |
), | |
child: ContainedButtonTheme( | |
data: ContainedButtonThemeData( | |
style: ContainedButton.styleFrom(shape: buttonShapes[shapeIndex]), | |
), | |
child: OutlinedButtonTheme( | |
data: OutlinedButtonThemeData( | |
style: OutlinedButton.styleFrom(shape: buttonShapes[shapeIndex]), | |
), | |
child: OverflowBox( | |
maxWidth: double.infinity, | |
child: DefaultButtons( | |
onPressed: () { | |
setState(() { | |
shapeIndex = (shapeIndex + 1) % buttonShapes.length; | |
}); | |
}, | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
class TextStyleButtons extends StatefulWidget { | |
@override | |
_TextStyleButtonsState createState() => _TextStyleButtonsState(); | |
} | |
class _TextStyleButtonsState extends State<TextStyleButtons> { | |
bool _flag = false; | |
@override | |
Widget build(BuildContext context) { | |
final TextTheme textTheme = Theme.of(context).textTheme; | |
final TextStyle style = _flag ? textTheme.headline1 : textTheme.headline4; | |
return TextButtonTheme( | |
data: TextButtonThemeData( | |
style: TextButton.styleFrom(textStyle: style), | |
), | |
child: ContainedButtonTheme( | |
data: ContainedButtonThemeData( | |
style: ContainedButton.styleFrom(textStyle: style), | |
), | |
child: OutlinedButtonTheme( | |
data: OutlinedButtonThemeData( | |
style: OutlinedButton.styleFrom(textStyle: style), | |
), | |
child: OverflowBox( | |
maxWidth: double.infinity, | |
child: DefaultButtons( | |
iconSize: _flag ? 64 : 24, | |
onPressed: () { | |
setState(() { | |
_flag = !_flag; | |
}); | |
}, | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
class TextColorButtons extends StatefulWidget { | |
@override | |
_TextColorButtonsState createState() => _TextColorButtonsState(); | |
} | |
class _TextColorButtonsState extends State<TextColorButtons> { | |
int index = 0; | |
static const List<Color> foregroundColors = <Color>[ | |
Colors.red, | |
Colors.purple, | |
Colors.indigo, | |
Colors.teal, | |
Colors.lime, | |
Colors.deepOrange, | |
Colors.yellow, | |
]; | |
static const List<Color> backgroundColors = <Color>[ | |
Colors.lightBlue, | |
Colors.yellow, | |
Colors.grey, | |
Colors.amber, | |
Colors.orange, | |
Colors.blue, | |
Colors.purple, | |
]; | |
@override | |
Widget build(BuildContext context) { | |
final Color foregroundColor = foregroundColors[index]; | |
final Color backgroundColor = backgroundColors[index]; | |
return TextButtonTheme( | |
data: TextButtonThemeData( | |
style: TextButton.styleFrom(primary: foregroundColor), | |
), | |
child: ContainedButtonTheme( | |
data: ContainedButtonThemeData( | |
style: ContainedButton.styleFrom( | |
primary: backgroundColor, | |
onPrimary: foregroundColor, | |
), | |
), | |
child: OutlinedButtonTheme( | |
data: OutlinedButtonThemeData( | |
style: TextButton.styleFrom(primary: foregroundColor), | |
), | |
child: DefaultButtons( | |
onPressed: () { | |
setState(() { | |
index = (index + 1) % foregroundColors.length; | |
}); | |
}, | |
), | |
), | |
), | |
); | |
} | |
} | |
class DefaultButtons extends StatelessWidget { | |
const DefaultButtons({ Key key, this.onPressed, this.iconSize = 18 }) : super(key: key); | |
final VoidCallback onPressed; | |
final double iconSize; | |
@override | |
Widget build(BuildContext context) { | |
const Widget spacer = SizedBox(height: 4); | |
final Widget icon = Icon(Icons.star, size: iconSize); | |
return SingleChildScrollView( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
TextButton( | |
onPressed: onPressed ?? () { }, | |
child: Text('TEXT'), | |
), | |
spacer, | |
TextButton.icon( | |
onPressed: onPressed ?? () { }, | |
icon: icon, | |
label: Text('TEXT'), | |
), | |
spacer, | |
TextButton.icon( | |
onPressed: null, | |
icon: icon, | |
label: Text('DISABLED'), | |
), | |
spacer, | |
ContainedButton( | |
onPressed: onPressed ?? () { }, | |
child: Text('CONTAINED'), | |
), | |
spacer, | |
ContainedButton.icon( | |
onPressed: onPressed ?? () { }, | |
icon: icon, | |
label: Text('CONTAINED'), | |
), | |
spacer, | |
ContainedButton.icon( | |
onPressed: null, | |
icon: icon, | |
label: Text('DISABLED'), | |
), | |
spacer, | |
OutlinedButton( | |
onPressed: onPressed ?? () { }, | |
child: Text('OUTLINED'), | |
), | |
OutlinedButton.icon( | |
onPressed: onPressed ?? () { }, | |
icon: icon, | |
label: Text('OUTLINED'), | |
), | |
spacer, | |
OutlinedButton.icon( | |
onPressed: null, | |
icon: icon, | |
label: Text('DISABLED'), | |
), | |
spacer, | |
], | |
), | |
); | |
} | |
} | |
class ButtonDemo { | |
const ButtonDemo({ Key key, this.title, this.description, this.builder }); | |
final String title; | |
final String description; | |
final WidgetBuilder builder; | |
} | |
final List<ButtonDemo> allButtonDemos = <ButtonDemo>[ | |
ButtonDemo( | |
title: 'Default Buttons', | |
description: 'Enabled and disabled buttons in their default configurations.', | |
builder: (BuildContext context) => DefaultButtons(), | |
), | |
ButtonDemo( | |
title: 'TextColor Buttons', | |
description: | |
'Use TextButtonTheme, ContainedButtonTheme, OutlinedButtonTheme to ' | |
'override the text color of all buttons. The background color for ' | |
'ContainedButtons does not change.', | |
builder: (BuildContext context) => TextColorButtons(), | |
), | |
ButtonDemo( | |
title: 'TextStyle Buttons', | |
description: | |
'Use TextButtonTheme, ContainedButtonTheme, OutlinedButtonTheme to override ' | |
'the default text style of the buttons. Press any button to toggle the text ' | |
'style size to an even bigger value.', | |
builder: (BuildContext context) => TextStyleButtons(), | |
), | |
ButtonDemo( | |
title: 'Individually Sized Buttons', | |
description: | |
'Sets the ButtonStyle parameter of individual buttons to override their ' | |
'default text style/ Press any button to toggle its text text style size.', | |
builder: (BuildContext context) => IndividuallySizedButtons(), | |
), | |
ButtonDemo( | |
title: 'Button Shapes', | |
description: | |
'Use TextButtonTheme, ContainedButtonTheme, OutlinedButtonTheme to ' | |
'override the shape all buttons.', | |
builder: (BuildContext context) => ShapeButtons(), | |
), | |
ButtonDemo( | |
title: 'Dialog Buttons', | |
description: | |
'Use ButtonStyle to configure the shape of a dialog\'s action buttons.', | |
builder: (BuildContext context) => DialogButtons(), | |
), | |
]; | |
class Home extends StatefulWidget { | |
const Home({ Key key, this.toggleThemeMode }) : super(key: key); | |
final VoidCallback toggleThemeMode; | |
@override | |
_HomeState createState() => _HomeState(); | |
} | |
class _HomeState extends State<Home> { | |
PageController _pageController; | |
int _currentPage = 0; | |
@override | |
void initState() { | |
super.initState(); | |
_pageController = PageController(initialPage: _currentPage); | |
_pageController.addListener(pageChanged); | |
} | |
@override | |
void dispose() { | |
_pageController.dispose(); | |
super.dispose(); | |
} | |
void pageChanged() { | |
if (_pageController.hasClients) { | |
setState(() { | |
_currentPage = _pageController.page.floor(); | |
}); | |
} | |
} | |
void changePage(int delta) { | |
if (_pageController.hasClients) { | |
const Duration duration = Duration(milliseconds: 300); | |
_pageController.animateToPage(_currentPage + delta, duration: duration, curve: Curves.easeInOut); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
final ColorScheme colorScheme = Theme.of(context).colorScheme; | |
final ButtonStyle actionButtonStyle = TextButton.styleFrom( | |
primary: colorScheme.onPrimary, | |
); | |
return Scaffold( | |
appBar: AppBar( | |
title: Text( | |
allButtonDemos[_currentPage].title, | |
style: TextStyle(color: colorScheme.onPrimary) | |
), | |
backgroundColor: colorScheme.primary, | |
actions: <Widget>[ | |
TextButton( | |
style: actionButtonStyle, | |
onPressed: widget.toggleThemeMode, | |
child: Icon(Icons.stars), | |
), | |
TextButton( | |
style: actionButtonStyle, | |
onPressed: () { changePage(-1); }, | |
child: Icon(Icons.arrow_back), | |
), | |
TextButton( | |
style: actionButtonStyle, | |
onPressed: () { changePage(1); }, | |
child: Icon(Icons.arrow_forward), | |
), | |
], | |
), | |
body: Column( | |
children: <Widget>[ | |
Padding( | |
padding: EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 8), | |
child: Container( | |
height: 98, | |
color: colorScheme.onSurface.withOpacity(0.1), | |
alignment: Alignment.center, | |
child: Padding( | |
padding: EdgeInsets.all(12), | |
child: Text( | |
allButtonDemos[_currentPage].description, | |
maxLines: 4, | |
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.7)), | |
), | |
), | |
), | |
), | |
Expanded( | |
child: PageView.builder( | |
controller: _pageController, | |
itemCount: allButtonDemos.length, | |
itemBuilder: (BuildContext context, int index) { | |
return allButtonDemos[index].builder(context); | |
}, | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class App extends StatefulWidget { | |
@override | |
_AppState createState() => _AppState(); | |
} | |
class _AppState extends State<App> { | |
ThemeMode _themeMode = ThemeMode.light; | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData.from(colorScheme: ColorScheme.light()), | |
darkTheme: ThemeData.from(colorScheme: ColorScheme.dark()), | |
themeMode: _themeMode, | |
debugShowCheckedModeBanner: false, | |
home: Home( | |
toggleThemeMode: () { | |
setState(() { | |
_themeMode = _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; | |
}); | |
} | |
), | |
); | |
} | |
} | |
void main() { | |
runApp(App()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment