Unverified Commit dc31d89c authored by Hans Muller's avatar Hans Muller Committed by GitHub

New Button Universe (#59702)

parent ea777fea
......@@ -36,6 +36,8 @@ export 'src/material/bottom_sheet_theme.dart';
export 'src/material/button.dart';
export 'src/material/button_bar.dart';
export 'src/material/button_bar_theme.dart';
export 'src/material/button_style.dart';
export 'src/material/button_style_button.dart';
export 'src/material/button_theme.dart';
export 'src/material/card.dart';
export 'src/material/card_theme.dart';
......@@ -47,6 +49,8 @@ export 'src/material/circle_avatar.dart';
export 'src/material/color_scheme.dart';
export 'src/material/colors.dart';
export 'src/material/constants.dart';
export 'src/material/contained_button.dart';
export 'src/material/contained_button_theme.dart';
export 'src/material/data_table.dart';
export 'src/material/data_table_source.dart';
export 'src/material/debug.dart';
......@@ -88,6 +92,8 @@ export 'src/material/mergeable_material.dart';
export 'src/material/navigation_rail.dart';
export 'src/material/navigation_rail_theme.dart';
export 'src/material/outline_button.dart';
export 'src/material/outlined_button.dart';
export 'src/material/outlined_button_theme.dart';
export 'src/material/page.dart';
export 'src/material/page_transitions_theme.dart';
export 'src/material/paginated_data_table.dart';
......@@ -117,6 +123,8 @@ export 'src/material/tab_bar_theme.dart';
export 'src/material/tab_controller.dart';
export 'src/material/tab_indicator.dart';
export 'src/material/tabs.dart';
export 'src/material/text_button.dart';
export 'src/material/text_button_theme.dart';
export 'src/material/text_field.dart';
export 'src/material/text_form_field.dart';
export 'src/material/text_selection.dart';
......
// 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.
// @dart = 2.8
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'material_state.dart';
import 'theme_data.dart';
/// The visual properties that most buttons have in common.
///
/// Buttons and their themes have a ButtonStyle property which defines the visual
/// properties whose default values are to be overidden. The default values are
/// defined by the invidual button widgets and are typically based on overall
/// theme's [ThemeData.colorScheme] and [ThemeData.textTheme].
///
/// All of the ButtonStyle properties are null by default.
///
/// Many of the ButtonStyle properties are [MaterialStateProperty] objects which
/// resolve to different values depending on the button's state. For example
/// the [Color] properties are defined with `MaterialStateProperty<Color>` and
/// can resolve to different colors depending on if the button is pressed,
/// hovered, focused, disabled, etc.
///
/// These properties can override the default value for just one state or all of
/// them. For example to create a [ContainedButton] whose background color is the
/// color scheme’s primary color with 50% opacity, but only when the button is
/// pressed, one could write:
///
/// ```dart
/// ContainedButton(
/// style: ButtonStyle(
/// backgroundColor: MaterialStateProperty.resolveWith<Color>(
/// (Set<MaterialState> states) {
/// if (states.contains(MaterialState.pressed))
/// return Theme.of(context).colorScheme.primary.withOpacity(0.5);
/// return null; // Use the component's default.
/// },
/// ),
/// ),
/// )
///```
///
/// In this case the background color for all other button states would fallback
/// to the ContainedButton’s default values. To unconditionally set the button's
/// [backgroundColor] for all states one could write:
///
/// ```dart
/// ContainedButton(
/// style: ButtonStyle(
/// backgroundColor: MaterialStateProperty.all<Color>(Colors.green),
/// ),
/// )
///```
///
/// Configuring a ButtonStyle directly makes it possible to very
/// precisely control the button’s visual attributes for all states.
/// This level of control is typically required when a custom
/// “branded” look and feel is desirable. However, in many cases it’s
/// useful to make relatively sweeping changes based on a few initial
/// parameters with simple values. The button styleFrom() methods
/// enable such sweeping changes. See for example:
/// [TextButton.styleFrom], [ContainedButton.styleFrom],
/// [OutlinedButton.styleFrom].
///
/// For example, to override the default text and icon colors for a
/// [TextButton], as well as its overlay color, with all of the
/// standard opacity adjustments for the pressed, focused, and
/// hovered states, one could write:
///
/// ```dart
/// TextButton(
/// style: TextButton.styleFrom(primary: Colors.green),
/// )
///```
///
/// To configure all of the application's text buttons in the same
/// way, specify the overall theme's `textButtonTheme`:
/// ```dart
/// MaterialApp(
/// theme: ThemeData(
/// textButtonTheme: TextButtonThemeData(
/// style: TextButton.styleFrom(primary: Colors.green),
/// ),
/// ),
/// home: MyAppHome(),
/// )
///```
/// See also:
///
/// * [TextButtonTheme], the theme for [TextButton]s.
/// * [ContainedButtonTheme], the theme for [ContainedButton]s.
/// * [OutlinedButtonTheme], the theme for [OutlinedButton]s.
@immutable
class ButtonStyle with Diagnosticable {
/// Create a [ButtonStyle].
const ButtonStyle({
this.textStyle,
this.backgroundColor,
this.foregroundColor,
this.overlayColor,
this.shadowColor,
this.elevation,
this.padding,
this.minimumSize,
this.side,
this.shape,
this.mouseCursor,
this.visualDensity,
this.tapTargetSize,
this.animationDuration,
this.enableFeedback,
});
/// The style for a button's [Text] widget descendants.
///
/// The color of the [textStyle] is typically not used directly, the
/// [foreground] color is used instead.
final MaterialStateProperty<TextStyle> textStyle;
/// The button's background fill color.
final MaterialStateProperty<Color> backgroundColor;
/// The color for the button's [Text] and [Icon] widget descendants.
///
/// This color is typically used instead of the color of the [textStyle]. All
/// of the components that compute defaults from [ButtonStyle] values
/// compute a default [foregroundColor] and use that instead of the
/// [textStyle]'s color.
final MaterialStateProperty<Color> foregroundColor;
/// The highlight color that's typically used to indicate that
/// the button is focused, hovered, or pressed.
final MaterialStateProperty<Color> overlayColor;
/// The shadow color of the button's [Material].
///
/// The material's elevation shadow can be difficult to see for
/// dark themes, so by default the button classes add a
/// semi-transparent overlay to indicate elevation. See
/// [ThemeData.applyElevationOverlayColor].
final MaterialStateProperty<Color> shadowColor;
/// The elevation of the button's [Material].
final MaterialStateProperty<double> elevation;
/// The padding between the button's boundary and its child.
final MaterialStateProperty<EdgeInsetsGeometry> padding;
/// The minimum size of the button itself.
///
/// The size of the rectangle the button lies within may be larger
/// per [tapTargetSize].
final MaterialStateProperty<Size> minimumSize;
/// The color and weight of the button's outline.
///
/// This value is combined with [shape] to create a shape decorated
/// with an outline.
final MaterialStateProperty<BorderSide> side;
/// The shape of the button's underlying [Material].
///
/// This shape is combined with [side] to create a shape decorated
/// with an outline.
final MaterialStateProperty<OutlinedBorder> shape;
/// The cursor for a mouse pointer when it enters or is hovering over
/// this button's [InkWell].
final MaterialStateProperty<MouseCursor> mouseCursor;
/// Defines how compact the button's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all widgets
/// within a [Theme].
final VisualDensity visualDensity;
/// Configures the minimum size of the area within which the button may be pressed.
///
/// If the [tapTargetSize] is larger than [minimumSize], the button will include
/// a transparent margin that responds to taps.
///
/// Always defaults to [ThemeData.materialTapTargetSize].
final MaterialTapTargetSize tapTargetSize;
/// Defines the duration of animated changes for [shape] and [elevation].
///
/// Typically the component default value is [kThemeChangeDuration].
final Duration animationDuration;
/// Whether detected gestures should provide acoustic and/or haptic feedback.
///
/// For example, on Android a tap will produce a clicking sound and a
/// long-press will produce a short vibration, when feedback is enabled.
///
/// Typically the component default value is true.
///
/// See also:
///
/// * [Feedback] for providing platform-specific feedback to certain actions.
final bool enableFeedback;
/// 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<Color> shadowColor,
MaterialStateProperty<double> elevation,
MaterialStateProperty<EdgeInsetsGeometry> padding,
MaterialStateProperty<Size> minimumSize,
MaterialStateProperty<BorderSide> side,
MaterialStateProperty<OutlinedBorder> shape,
MaterialStateProperty<MouseCursor> mouseCursor,
VisualDensity visualDensity,
MaterialTapTargetSize tapTargetSize,
Duration animationDuration,
bool enableFeedback,
}) {
return ButtonStyle(
textStyle: textStyle ?? this.textStyle,
backgroundColor: backgroundColor ?? this.backgroundColor,
foregroundColor: foregroundColor ?? this.foregroundColor,
overlayColor: overlayColor ?? this.overlayColor,
shadowColor: shadowColor ?? this.shadowColor,
elevation: elevation ?? this.elevation,
padding: padding ?? this.padding,
minimumSize: minimumSize ?? this.minimumSize,
side: side ?? this.side,
shape: shape ?? this.shape,
mouseCursor: mouseCursor ?? this.mouseCursor,
visualDensity: visualDensity ?? this.visualDensity,
tapTargetSize: tapTargetSize ?? this.tapTargetSize,
animationDuration: animationDuration ?? this.animationDuration,
enableFeedback: enableFeedback ?? this.enableFeedback,
);
}
/// Returns a copy of this ButtonStyle where the non-null fields in [style]
/// have replaced the corresponding null fields in this ButtonStyle.
///
/// In other words, [style] is used to fill in unspecified (null) fields
/// this ButtonStyle.
ButtonStyle merge(ButtonStyle style) {
if (style == null)
return this;
return copyWith(
textStyle: textStyle ?? style.textStyle,
backgroundColor: backgroundColor ?? style.backgroundColor,
foregroundColor: foregroundColor ?? style.foregroundColor,
overlayColor: overlayColor ?? style.overlayColor,
shadowColor: shadowColor ?? style.shadowColor,
elevation: elevation ?? style.elevation,
padding: padding ?? style.padding,
minimumSize: minimumSize ?? style.minimumSize,
side: side ?? style.side,
shape: shape ?? style.shape,
mouseCursor: mouseCursor ?? style.mouseCursor,
visualDensity: visualDensity ?? style.visualDensity,
tapTargetSize: tapTargetSize ?? style.tapTargetSize,
animationDuration: animationDuration ?? style.animationDuration,
enableFeedback: enableFeedback ?? style.enableFeedback,
);
}
@override
int get hashCode {
return hashValues(
textStyle,
backgroundColor,
foregroundColor,
overlayColor,
shadowColor,
elevation,
padding,
minimumSize,
side,
shape,
mouseCursor,
visualDensity,
tapTargetSize,
animationDuration,
enableFeedback,
);
}
@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.shadowColor == shadowColor
&& other.elevation == elevation
&& other.padding == padding
&& other.minimumSize == minimumSize
&& other.side == side
&& other.shape == shape
&& other.mouseCursor == mouseCursor
&& other.visualDensity == visualDensity
&& other.tapTargetSize == tapTargetSize
&& other.animationDuration == animationDuration
&& other.enableFeedback == enableFeedback;
}
@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<Color>>('shadowColor', shadowColor, 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<OutlinedBorder>>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor>>('mouseCursor', mouseCursor, defaultValue: null));
properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null));
properties.add(EnumProperty<MaterialTapTargetSize>('tapTargetSize', tapTargetSize, defaultValue: null));
properties.add(DiagnosticsProperty<Duration>('animationDuration', animationDuration, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
}
/// Linearly interpolate between two [ButtonStyle]s.
static ButtonStyle lerp(ButtonStyle a, ButtonStyle b, double t) {
assert (t != null);
if (a == null && b == null)
return null;
return ButtonStyle(
textStyle: _lerpProperties<TextStyle>(a?.textStyle, b?.textStyle, t, TextStyle.lerp),
backgroundColor: _lerpProperties<Color>(a?.backgroundColor, b?.backgroundColor, t, Color.lerp),
foregroundColor: _lerpProperties<Color>(a?.foregroundColor, b?.foregroundColor, t, Color.lerp),
overlayColor: _lerpProperties<Color>(a?.overlayColor, b?.overlayColor, t, Color.lerp),
shadowColor: _lerpProperties<Color>(a?.shadowColor, b?.shadowColor, t, Color.lerp),
elevation: _lerpProperties<double>(a?.elevation, b?.elevation, t, lerpDouble),
padding: _lerpProperties<EdgeInsetsGeometry>(a?.padding, b?.padding, t, EdgeInsetsGeometry.lerp),
minimumSize: _lerpProperties<Size>(a?.minimumSize, b?.minimumSize, t, Size.lerp),
side: _lerpSides(a?.side, b?.side, t),
shape: _lerpShapes(a?.shape, b?.shape, t),
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
visualDensity: t < 0.5 ? a.visualDensity : b.visualDensity,
tapTargetSize: t < 0.5 ? a.tapTargetSize : b.tapTargetSize,
animationDuration: t < 0.5 ? a.animationDuration : b.animationDuration,
enableFeedback: t < 0.5 ? a.enableFeedback : b.enableFeedback,
);
}
static MaterialStateProperty<T> _lerpProperties<T>(MaterialStateProperty<T> a, MaterialStateProperty<T> b, double t, T Function(T, T, double) lerpFunction ) {
// Avoid creating a _LerpProperties object for a common case.
if (a == null && b == null)
return null;
return _LerpProperties<T>(a, b, t, lerpFunction);
}
// Special case because BorderSide.lerp() doesn't support null arguments
static MaterialStateProperty<BorderSide> _lerpSides(MaterialStateProperty<BorderSide> a, MaterialStateProperty<BorderSide> b, double t) {
if (a == null && b == null)
return null;
return _LerpSides(a, b, t);
}
// TODO(hansmuller): OutlinedBorder needs a lerp method - https://github.com/flutter/flutter/issues/60555.
static MaterialStateProperty<OutlinedBorder> _lerpShapes(MaterialStateProperty<OutlinedBorder> a, MaterialStateProperty<OutlinedBorder> b, double t) {
if (a == null && b == null)
return null;
return _LerpShapes(a, b, t);
}
}
class _LerpProperties<T> implements MaterialStateProperty<T> {
const _LerpProperties(this.a, this.b, this.t, this.lerpFunction);
final MaterialStateProperty<T> a;
final MaterialStateProperty<T> b;
final double t;
final T Function(T, T, double) lerpFunction;
@override
T resolve(Set<MaterialState> states) {
final T resolvedA = a?.resolve(states);
final T resolvedB = b?.resolve(states);
return lerpFunction(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);
if (resolvedA == null && resolvedB == null)
return null;
if (resolvedA == null)
return BorderSide.lerp(BorderSide(width: 0, color: resolvedB.color.withAlpha(0)), resolvedB, t);
if (resolvedB == null)
return BorderSide.lerp(BorderSide(width: 0, color: resolvedA.color.withAlpha(0)), resolvedA, t);
return BorderSide.lerp(resolvedA, resolvedB, t);
}
}
class _LerpShapes implements MaterialStateProperty<OutlinedBorder> {
const _LerpShapes(this.a, this.b, this.t);
final MaterialStateProperty<OutlinedBorder> a;
final MaterialStateProperty<OutlinedBorder> b;
final double t;
@override
OutlinedBorder resolve(Set<MaterialState> states) {
final OutlinedBorder resolvedA = a?.resolve(states);
final OutlinedBorder resolvedB = b?.resolve(states);
return ShapeBorder.lerp(resolvedA, resolvedB, t) as OutlinedBorder;
}
}
// 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.
// @dart = 2.8
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'colors.dart';
import 'constants.dart';
import 'ink_ripple.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_state.dart';
import 'theme_data.dart';
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
///
/// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
///
/// See also:
///
/// * [TextButton], a simple ButtonStyleButton without a shadow.
/// * [ContainedButton], a filled ButtonStyleButton whose material elevates when pressed.
/// * [OutlinedButton], similar to [TextButton], but with an outline.
abstract class ButtonStyleButton extends StatefulWidget {
/// Create a [ButtonStyleButton].
const ButtonStyleButton({
Key key,
@required this.onPressed,
@required this.onLongPress,
@required this.style,
@required this.focusNode,
@required this.autofocus,
@required this.clipBehavior,
@required this.child,
}) : assert(autofocus != null),
assert(clipBehavior != null),
super(key: key);
/// Called when the button is tapped or otherwise activated.
///
/// If this callback and [onLongPress] are null, then the button will be disabled.
///
/// See also:
///
/// * [enabled], which is true if the button is enabled.
final VoidCallback onPressed;
/// Called when the button is long-pressed.
///
/// If this callback and [onPressed] are null, then the button will be disabled.
///
/// See also:
///
/// * [enabled], which is true if the button is enabled.
final VoidCallback onLongPress;
/// Customizes this button's appearance.
///
/// Non-null properties of this style override the corresponding
/// properties in [themeStyleOf] and [defaultStyleOf]. [MaterialStateProperty]s
/// that resolve to non-null values will similarly override the corresponding
/// [MaterialStateProperty]s in [themeStyleOf] and [defaultStyleOf].
///
/// Null by default.
final ButtonStyle style;
/// {@macro flutter.widgets.Clip}
///
/// Defaults to [Clip.none], and must not be null.
final Clip clipBehavior;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode focusNode;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// Typically the button's label.
final Widget child;
/// Returns a non-null [ButtonStyle] that's based primarily on the [Theme]'s
/// [ThemeData.textTheme] and [ThemeData.colorScheme].
///
/// The returned style can be overriden by the [style] parameter and
/// by the style returned by [themeStyleOf]. For example the default
/// style of the [TextButton] subclass can be overidden with its
/// [TextButton.style] constructor parameter, or with a
/// [TextButtonTheme].
///
/// Concrete button subclasses should return a ButtonStyle that
/// has no null properties, and where all of the [MaterialStateProperty]
/// properties resolve to non-null values.
///
/// See also:
///
/// * [themeStyleOf], Returns the ButtonStyle of this button's component theme.
@protected
ButtonStyle defaultStyleOf(BuildContext context);
/// Returns the ButtonStyle that belongs to the button's component theme.
///
/// The returned style can be overriden by the [style] parameter.
///
/// Concrete button subclasses should return the ButtonStyle for the
/// nearest subclass-specific inherited theme, and if no such theme
/// exists, then the same value from the overall [Theme].
///
/// See also:
///
/// * [defaultStyleOf], Returns the default [ButtonStyle] for this button.
@protected
ButtonStyle themeStyleOf(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;
@override
_ButtonStyleState createState() => _ButtonStyleState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
properties.add(DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
}
/// Returns null if [value] is null, otherwise `MaterialStateProperty.all<T>(value)`.
///
/// A convenience method for subclasses.
static MaterialStateProperty<T> allOrNull<T>(T value) => value == null ? null : MaterialStateProperty.all<T>(value);
/// Returns an interpolated value based on the [textScaleFactor] parameter:
///
/// * 0 - 1 [geometry1x]
/// * 1 - 2 lerp([geometry1x], [geometry2x], [textScaleFactor] - 1)
/// * 2 - 3 lerp([geometry2x], [geometry3x], [textScaleFactor] - 2)
/// * otherwise [geometry3x]
///
/// A convenience method for subclasses.
static EdgeInsetsGeometry scaledPadding(
EdgeInsetsGeometry geometry1x,
EdgeInsetsGeometry geometry2x,
EdgeInsetsGeometry geometry3x,
double textScaleFactor,
) {
assert(geometry1x != null);
assert(geometry2x != null);
assert(geometry3x != null);
assert(textScaleFactor != null);
if (textScaleFactor <= 1) {
return geometry1x;
} else if (textScaleFactor >= 3) {
return geometry3x;
} else if (textScaleFactor <= 2) {
return EdgeInsetsGeometry.lerp(geometry1x, geometry2x, textScaleFactor - 1);
}
return EdgeInsetsGeometry.lerp(geometry2x, geometry3x, textScaleFactor - 2);
}
}
/// The base [State] class for buttons whose style is defined by a [ButtonStyle] object.
///
/// See also:
///
/// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State].
/// * [TextButton], a simple button without a shadow.
/// * [ContainedButton], a filled button whose material elevates when pressed.
/// * [OutlinedButton], similar to [TextButton], but with an outline.
class _ButtonStyleState extends State<ButtonStyleButton> {
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(ButtonStyleButton 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);
}
}
@override
Widget build(BuildContext context) {
final ButtonStyle widgetStyle = widget.style;
final ButtonStyle themeStyle = widget.themeStyleOf(context);
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
assert(defaultStyle != null);
T effectiveValue<T>(T Function(ButtonStyle style) getProperty) {
final T widgetValue = getProperty(widgetStyle);
final T themeValue = getProperty(themeStyle);
final T defaultValue = getProperty(defaultStyle);
return widgetValue ?? themeValue ?? defaultValue;
}
T resolve<T>(MaterialStateProperty<T> Function(ButtonStyle style) getProperty) {
return effectiveValue(
(ButtonStyle style) => getProperty(style)?.resolve(_states),
);
}
final TextStyle resolvedTextStyle = resolve<TextStyle>((ButtonStyle style) => style?.textStyle);
final Color resolvedBackgroundColor = resolve<Color>((ButtonStyle style) => style?.backgroundColor);
final Color resolvedForegroundColor = resolve<Color>((ButtonStyle style) => style?.foregroundColor);
final Color resolvedShadowColor = resolve<Color>((ButtonStyle style) => style?.shadowColor);
final double resolvedElevation = resolve<double>((ButtonStyle style) => style?.elevation);
final EdgeInsetsGeometry resolvedPadding = resolve<EdgeInsetsGeometry>((ButtonStyle style) => style?.padding);
final Size resolvedMinimumSize = resolve<Size>((ButtonStyle style) => style?.minimumSize);
final BorderSide resolvedSide = resolve<BorderSide>((ButtonStyle style) => style?.side);
final OutlinedBorder resolvedShape = resolve<OutlinedBorder>((ButtonStyle style) => style?.shape);
final MaterialStateMouseCursor resolvedMouseCursor = _MouseCursor(
(Set<MaterialState> states) => effectiveValue((ButtonStyle style) => style?.mouseCursor?.resolve(states)),
);
final MaterialStateProperty<Color> overlayColor = MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) => effectiveValue((ButtonStyle style) => style?.overlayColor?.resolve(states)),
);
final VisualDensity resolvedVisualDensity = effectiveValue((ButtonStyle style) => style?.visualDensity);
final MaterialTapTargetSize resolvedTapTargetSize = effectiveValue((ButtonStyle style) => style?.tapTargetSize);
final Duration resolvedAnimationDuration = effectiveValue((ButtonStyle style) => style?.animationDuration);
final bool resolvedEnableFeedback = effectiveValue((ButtonStyle style) => style?.enableFeedback);
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: resolvedShape.copyWith(side: resolvedSide),
color: resolvedBackgroundColor,
shadowColor: resolvedShadowColor,
type: resolvedBackgroundColor == null ? MaterialType.transparency : MaterialType.button,
animationDuration: resolvedAnimationDuration,
clipBehavior: widget.clipBehavior,
child: InkWell(
onTap: widget.onPressed,
onLongPress: widget.onLongPress,
onHighlightChanged: _handleHighlightChanged,
onHover: _handleHoveredChanged,
mouseCursor: resolvedMouseCursor,
enableFeedback: resolvedEnableFeedback,
focusNode: widget.focusNode,
canRequestFocus: widget.enabled,
onFocusChange: _handleFocusedChanged,
autofocus: widget.autofocus,
splashFactory: InkRipple.splashFactory,
overlayColor: overlayColor,
highlightColor: Colors.transparent,
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,
),
);
}
}
class _MouseCursor extends MaterialStateMouseCursor {
const _MouseCursor(this.resolveCallback);
final MaterialPropertyResolver<MouseCursor> resolveCallback;
@override
MouseCursor resolve(Set<MaterialState> states) => resolveCallback(states);
@override
String get debugDescription => 'ButtonStyleButton_MouseCursor';
}
/// 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);
},
);
}
}
// 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.
// @dart = 2.8
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'button_style_button.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'contained_button_theme.dart';
import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
/// A Material Design "contained button".
///
/// 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.
///
/// A contained button is a label [child] displayed on a [Material]
/// widget whose [Material.elevation] increases when the button is
/// pressed. The label's [Text] and [Icon] widgets are displayed in
/// [style]'s [ButtonStyle.onForegroundColor] and the button's filled
/// background is the [ButtonStyle.backgroundColor].
///
/// The contained button's default style is defined by
/// [defaultStyleOf]. The style of this contained button can be
/// overridden with its [style] parameter. The style of all contained
/// buttons in a subtree can be overridden with the
/// [ContainedButtonTheme], and the style of all of the contained
/// buttons in an app can be overridden with the [Theme]'s
/// [ThemeData.containedButtonTheme] property.
///
/// The static [styleFrom] method is a convenient way to create a
/// contained button [ButtonStyle] from simple values.
///
/// If [onPressed] and [onLongPress] callbacks are null, then the
/// button will be disabled.
///
/// See also:
///
/// * [TextButton], a simple flat button without a shadow.
/// * [OutlinedButton], a [TextButton] with a border outline.
/// * <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,
@required Widget child,
}) : super(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
style: style,
focusNode: focusNode,
autofocus: autofocus,
clipBehavior: clipBehavior,
child: child,
);
/// Create a contained button from a pair of widgets that serve as the button's
/// [icon] and [label].
///
/// The icon and label are arranged in a row and padded by 12 logical pixels
/// at the start, and 16 at the end, with an 8 pixel gap in between.
///
/// The [icon] and [label] arguments must not be null.
factory ContainedButton.icon({
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
ButtonStyle style,
FocusNode focusNode,
bool autofocus,
Clip clipBehavior,
@required Widget icon,
@required Widget label,
}) = _ContainedButtonWithIcon;
/// A static convenience method that constructs a contained button
/// [ButtonStyle] given simple values.
///
/// The [onPrimary], and [onSurface] colors are used to to create a
/// [MaterialStateProperty] [foreground] value in the same way that
/// [defaultStyleOf] uses the [ColorScheme] colors with the same
/// names. Specify a value for [onPrimary] to specify the color of the
/// button's text and icons as well as the overlay colors used to
/// indicate the hover, focus, and pressed states. Use primary for
/// the button's background fill color and [onSurface]
/// to specify the button's disabled text, icon, and fill color.
///
/// The button's elevations are defined relative to the [elevation]
/// parameter. The disabled elevation is the same as the parameter
/// value, [elevation] + 2 is used when the button is hovered
/// or focused, and elevation + 6 is used when the button is pressed.
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle].mouseCursor.
///
/// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all
/// states.
///
/// All parameters default to null, by default this method returns
/// a [ButtonStyle] that doesn't override anything.
///
/// For example, to override the default text and icon colors for a
/// [ContainedButton], as well as its overlay color, with all of the
/// standard opacity adjustments for the pressed, focused, and
/// hovered states, one could write:
///
/// ```dart
/// ContainedButton(
/// style: TextButton.styleFrom(primary: Colors.green),
/// )
/// ```
static ButtonStyle styleFrom({
Color primary,
Color onPrimary,
Color onSurface,
Color shadowColor,
double elevation,
TextStyle textStyle,
EdgeInsetsGeometry padding,
Size minimumSize,
BorderSide side,
OutlinedBorder shape,
MouseCursor enabledMouseCursor,
MouseCursor disabledMouseCursor,
VisualDensity visualDensity,
MaterialTapTargetSize tapTargetSize,
Duration animationDuration,
bool enableFeedback,
}) {
final MaterialStateProperty<Color> backgroundColor = (onSurface == null && primary == null)
? null
: _ContainedButtonDefaultBackground(primary, onSurface);
final MaterialStateProperty<Color> foregroundColor = (onSurface == null && onPrimary == null)
? null
: _ContainedButtonDefaultForeground(onPrimary, onSurface);
final MaterialStateProperty<Color> overlayColor = (onPrimary == null)
? null
: _ContainedButtonDefaultOverlay(onPrimary);
final MaterialStateProperty<double> elevationValue = (elevation == null)
? null
: _ContainedButtonDefaultElevation(elevation);
final MaterialStateProperty<MouseCursor> mouseCursor = (enabledMouseCursor == null && disabledMouseCursor == null)
? null
: _ContainedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle(
textStyle: MaterialStateProperty.all<TextStyle>(textStyle),
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
overlayColor: overlayColor,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
elevation: elevationValue,
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
);
}
/// Defines the button's default appearance.
///
/// The button [child]'s [Text] and [Icon] widgets are rendered with
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
/// the style's overlay color when the button is focused, hovered
/// or pressed. The button's background color becomes its [Material]
/// color.
///
/// All of the ButtonStyle's defaults appear below. In this list
/// "Theme.foo" is shorthand for `Theme.of(context).foo`. Color
/// scheme values like "onSurface(0.38)" are shorthand for
/// `onSurface.withOpacity(0.38)`. [MaterialStateProperty] valued
/// properties that are not followed by by a sublist have the same
/// value for all states, otherwise the values are as specified for
/// each state, and "others" means all other states.
///
/// The `textScaleFactor` is the value of
/// `MediaQuery.of(context).textScaleFactor` and the names of the
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been
/// abbreviated for readability.
///
/// The color of the [textStyle] is not used, the [foreground] color
/// is used instead.
///
/// * `textStyle` - Theme.textTheme.button
/// * `backgroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.12)
/// * others - Theme.colorScheme.primary
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.onPrimary
/// * `overlayColor`
/// * hovered - Theme.colorScheme.onPrimary(0.08)
/// * focused or pressed - Theme.colorScheme.onPrimary(0.24)
/// * `shadowColor` - Colors.black
/// * `elevation`
/// * disabled - 0
/// * hovered or focused - 2
/// * pressed - 6
/// * `padding`
/// * textScaleFactor <= 1 - horizontal(16)
/// * `1 < textScaleFactor <= 2` - lerp(horizontal(16), horizontal(8))
/// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
/// * `3 < textScaleFactor` - horizontal(4)
/// * `minimumSize` - Size(64, 36)
/// * `side` - BorderSide.none
/// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
/// * `mouseCursor`
/// * disabled - SystemMouseCursors.forbidden
/// * others - SystemMouseCursors.click
/// * `visualDensity` - theme.visualDensity
/// * `tapTargetSize` - theme.materialTapTargetSize
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
///
/// The default padding values for the [ContainedButton.icon] factory are slightly different:
///
/// * `padding`
/// * `textScaleFactor <= 1` - start(12) end(16)
/// * `1 < textScaleFactor <= 2` - lerp(start(12) end(16), horizontal(8))
/// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
/// * `3 < textScaleFactor` - horizontal(4)
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
const EdgeInsets.symmetric(horizontal: 16),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1,
);
return styleFrom(
primary: colorScheme.primary,
onPrimary: colorScheme.onPrimary,
onSurface: colorScheme.onSurface,
shadowColor: Colors.black,
elevation: 2,
textStyle: theme.textTheme.button,
padding: scaledPadding,
minimumSize: const Size(64, 36),
side: BorderSide.none,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
enabledMouseCursor: SystemMouseCursors.click,
disabledMouseCursor: SystemMouseCursors.forbidden,
visualDensity: theme.visualDensity,
tapTargetSize: theme.materialTapTargetSize,
animationDuration: kThemeChangeDuration,
enableFeedback: true,
);
}
/// Returns the [ContainedButtonThemeData.style] of the closest
/// [ContainedButtonTheme] ancestor.
@override
ButtonStyle themeStyleOf(BuildContext context) {
return ContainedButtonTheme.of(context)?.style;
}
}
@immutable
class _ContainedButtonDefaultBackground extends MaterialStateProperty<Color> with Diagnosticable {
_ContainedButtonDefaultBackground(this.primary, this.onSurface);
final Color primary;
final Color onSurface;
@override
Color resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return onSurface?.withOpacity(0.12);
return primary;
}
}
@immutable
class _ContainedButtonDefaultForeground extends MaterialStateProperty<Color> with Diagnosticable {
_ContainedButtonDefaultForeground(this.onPrimary, this.onSurface);
final Color onPrimary;
final Color onSurface;
@override
Color resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return onSurface?.withOpacity(0.38);
return onPrimary;
}
}
@immutable
class _ContainedButtonDefaultOverlay extends MaterialStateProperty<Color> with Diagnosticable {
_ContainedButtonDefaultOverlay(this.onPrimary);
final Color onPrimary;
@override
Color resolve(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;
}
}
@immutable
class _ContainedButtonDefaultElevation extends MaterialStateProperty<double> with Diagnosticable {
_ContainedButtonDefaultElevation(this.elevation);
final double elevation;
@override
double resolve(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;
}
}
@immutable
class _ContainedButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor> with Diagnosticable {
_ContainedButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor enabledCursor;
final MouseCursor disabledCursor;
@override
MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return disabledCursor;
return enabledCursor;
}
}
class _ContainedButtonWithIcon extends ContainedButton {
_ContainedButtonWithIcon({
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
ButtonStyle style,
FocusNode focusNode,
bool autofocus,
Clip clipBehavior,
@required Widget icon,
@required Widget label,
}) : assert(icon != null),
assert(label != null),
super(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
style: style,
focusNode: focusNode,
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _ContainedButtonWithIconChild(icon: icon, label: label),
);
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0),
MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1,
);
return super.defaultStyleOf(context).copyWith(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(scaledPadding)
);
}
}
class _ContainedButtonWithIconChild extends StatelessWidget {
const _ContainedButtonWithIconChild({ Key key, this.label, this.icon }) : super(key: key);
final Widget label;
final Widget icon;
@override
Widget build(BuildContext context) {
final double scale = MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1;
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1));
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, SizedBox(width: gap), label],
);
}
}
// 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.
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'theme.dart';
/// A [ButtonStyle] that overrides the default appearance of
/// [ContainedButton]s when it's used with [ContainedButtonTheme] or with the
/// overall [Theme]'s [ThemeData.containedButtonTheme].
///
/// The [style]'s properties override [ContainedButton]'s default style,
/// i.e. the [ButtonStyle] returned by [ContainedButton.defaultStyleOf]. Only
/// the style's non-null property values or resolved non-null
/// [MaterialStateProperty] values are used.
///
/// See also:
///
/// * [ContainedButtonTheme], the theme which is configured with this class.
/// * [ContainedButton.defaultStyleOf], which returns the default [ButtonStyle]
/// for text buttons.
/// * [ContainedButton.styleOf], which converts simple values into a
/// [ButtonStyle] that's consistent with [ContainedButton]'s defaults.
/// * [MaterialStateProperty.resolve], "resolve" a material state property
/// to a simple value based on a set of [MaterialState]s.
/// * [ThemeData.containedButtonTheme], which can be used to override the default
/// [ButtonStyle] for [ContainedButton]s below the overall [Theme].
@immutable
class ContainedButtonThemeData with Diagnosticable {
/// Creates a [ContainedButtonThemeData].
///
/// The [style] may be null.
const ContainedButtonThemeData({ this.style });
/// Overrides for [ContainedButton]'s default style.
///
/// Non-null properties or non-null resolved [MaterialStateProperty]
/// values override the [ButtonStyle] returned by
/// [ContainedButton.defaultStyleOf].
///
/// If [style] is null, then this theme doesn't override anything.
final ButtonStyle style;
/// Linearly interpolate between two contained 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));
}
}
/// Overrides the default [ButtonStyle] of its [ContainedButton] descendants.
///
/// See also:
///
/// * [ContainedButtonThemeData], which is used to configure this theme.
/// * [ContainedButton.defaultStyleOf], which returns the default [ButtonStyle]
/// for text buttons.
/// * [ContainedButton.styleOf], which converts simple values into a
/// [ButtonStyle] that's consistent with [ContainedButton]'s defaults.
/// * [ThemeData.containedButtonTheme], which can be used to override the default
/// [ButtonStyle] for [ContainedButton]s below the overall [Theme].
class ContainedButtonTheme extends InheritedTheme {
/// Create a [ContainedButtonTheme].
///
/// The [data] parameter must not be null.
const ContainedButtonTheme({
Key key,
@required this.data,
Widget child,
}) : assert(data != null), super(key: key, child: child);
/// The configuration of this theme.
final ContainedButtonThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [ContainedButtonsTheme] widget, then
/// [ThemeData.containedButtonTheme] 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 ?? Theme.of(context).containedButtonTheme;
}
@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;
}
......@@ -362,4 +362,7 @@ class _MaterialStatePropertyAll<T> implements MaterialStateProperty<T> {
@override
T resolve(Set<MaterialState> states) => value;
@override
String toString() => 'MaterialStateProperty.all($value)';
}
// 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.
// @dart = 2.8
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'button_style_button.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'material_state.dart';
import 'outlined_button_theme.dart';
import 'theme.dart';
import 'theme_data.dart';
/// A Material Design "Outlined Button"; essentially a [TextButton]
/// with an outlined border.
///
/// Outlined buttons are medium-emphasis buttons. They contain actions
/// that are important, but they aren’t the primary action in an app.
///
/// An outlined button is a label [child] displayed on a (zero
/// elevation) [Material] widget. The label's [Text] and [Icon]
/// widgets are displayed in the [style]'s
/// [ButtonStyle.foregroundColor] and the outline's weight and color
/// are defined by [ButtonStyle.side]. The button reacts to touches
/// by filling with the [style]'s [ButtonStyle.backgroundColor].
///
/// The outlined button's default style is defined by [defaultStyleOf].
/// The style of this outline button can be overridden with its [style]
/// parameter. The style of all text buttons in a subtree can be
/// overridden with the [OutlinedButtonTheme] and the style of all of the
/// outlined buttons in an app can be overridden with the [Theme]'s
/// [ThemeData.outlinedButtonTheme] property.
///
/// The static [styleFrom] method is a convenient way to create a
/// outlined button [ButtonStyle] from simple values.
///
/// See also:
///
/// * [ContainedButton], a filled material design button with a shadow.
/// * [TextButton], a material design button without a shadow.
/// * <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,
@required Widget child,
}) : super(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
style: style,
focusNode: focusNode,
autofocus: autofocus,
clipBehavior: clipBehavior,
child: child,
);
/// Create a text button from a pair of widgets that serve as the button's
/// [icon] and [label].
///
/// The icon and label are arranged in a row and padded by 12 logical pixels
/// at the start, and 16 at the end, with an 8 pixel gap in between.
///
/// The [icon] and [label] arguments must not be null.
factory OutlinedButton.icon({
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
ButtonStyle style,
FocusNode focusNode,
bool autofocus,
Clip clipBehavior,
@required Widget icon,
@required Widget label,
}) = _OutlinedButtonWithIcon;
/// A static convenience method that constructs an outlined button
/// [ButtonStyle] given simple values.
///
/// The [primary], and [onSurface] colors are used to to create a
/// [MaterialStateProperty] [foreground] value in the same way that
/// [defaultStyleOf] uses the [ColorScheme] colors with the same
/// names. Specify a value for [primary] to specify the color of the
/// button's text and icons as well as the overlay colors used to
/// indicate the hover, focus, and pressed states. Use [onSurface]
/// to specify the button's disabled text and icon color.
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle].mouseCursor.
///
/// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all
/// states.
///
/// All parameters default to null, by default this method returns
/// a [ButtonStyle] that doesn't override anything.
///
/// For example, to override the default shape and outline for an
/// [OutlinedButton], one could write:
///
/// ```dart
/// OutlinedButton(
/// style: OutlinedButton.styleFrom(
/// shape: StadiumBorder(),
/// side: BorderSide(width: 2, color: Colors.green),
/// ),
/// )
/// ```
static ButtonStyle styleFrom({
Color primary,
Color onSurface,
Color backgroundColor,
Color shadowColor,
double elevation,
TextStyle textStyle,
EdgeInsetsGeometry padding,
Size minimumSize,
BorderSide side,
OutlinedBorder shape,
MouseCursor enabledMouseCursor,
MouseCursor disabledMouseCursor,
VisualDensity visualDensity,
MaterialTapTargetSize tapTargetSize,
Duration animationDuration,
bool enableFeedback,
}) {
final MaterialStateProperty<Color> foregroundColor = (onSurface == null && primary == null)
? null
: _OutlinedButtonDefaultForeground(primary, onSurface);
final MaterialStateProperty<Color> overlayColor = (primary == null)
? null
: _OutlinedButtonDefaultOverlay(primary);
final MaterialStateProperty<MouseCursor> mouseCursor = (enabledMouseCursor == null && disabledMouseCursor == null)
? null
: _OutlinedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
foregroundColor: foregroundColor,
backgroundColor: ButtonStyleButton.allOrNull<Color>(backgroundColor),
overlayColor: overlayColor,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
);
}
/// Defines the button's default appearance.
///
/// With the exception of [ButtonStyle.side], which defines the
/// outline, and [ButtonStyle.padding], the returned style is the
/// same as for [TextButton].
///
/// The button [child]'s [Text] and [Icon] widgets are rendered with
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
/// the style's overlay color when the button is focused, hovered
/// or pressed. The button's background color becomes its [Material]
/// color and is transparent by default.
///
/// All of the ButtonStyle's defaults appear below. In this list
/// "Theme.foo" is shorthand for `Theme.of(context).foo`. Color
/// scheme values like "onSurface(0.38)" are shorthand for
/// `onSurface.withOpacity(0.38)`. [MaterialStateProperty] valued
/// properties that are not followed by by a sublist have the same
/// value for all states, otherwise the values are as specified for
/// each state and "others" means all other states.
///
/// The color of the [textStyle] is not used, the [foreground] color
/// is used instead.
///
/// * `textStyle` - Theme.textTheme.button
/// * `backgroundColor` - transparent
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary
/// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.04)
/// * focused or pressed - Theme.colorScheme.primary(0.12)
/// * `shadowColor` - Colors.black
/// * `elevation` - 0
/// * `padding`
/// * `textScaleFactor <= 1` - horizontal(16)
/// * `1 < textScaleFactor <= 2` - lerp(horizontal(16), horizontal(8))
/// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
/// * `3 < textScaleFactor` - horizontal(4)
/// * `minimumSize` - Size(64, 36)
/// * `side` - BorderSide(width: 1, color: Theme.colorScheme.onSurface(0.12))
/// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
/// * `mouseCursor`
/// * disabled - SystemMouseCursors.forbidden
/// * others - SystemMouseCursors.click
/// * `visualDensity` - theme.visualDensity
/// * `tapTargetSize` - theme.materialTapTargetSize
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
const EdgeInsets.symmetric(horizontal: 16),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1,
);
return styleFrom(
primary: colorScheme.primary,
onSurface: colorScheme.onSurface,
backgroundColor: Colors.transparent,
shadowColor: Colors.black,
elevation: 0,
textStyle: theme.textTheme.button,
padding: scaledPadding,
minimumSize: const Size(64, 36),
side: BorderSide(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
width: 1,
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
enabledMouseCursor: SystemMouseCursors.click,
disabledMouseCursor: SystemMouseCursors.forbidden,
visualDensity: theme.visualDensity,
tapTargetSize: theme.materialTapTargetSize,
animationDuration: kThemeChangeDuration,
enableFeedback: true,
);
}
@override
ButtonStyle themeStyleOf(BuildContext context) {
return OutlinedButtonTheme.of(context)?.style;
}
}
@immutable
class _OutlinedButtonDefaultForeground extends MaterialStateProperty<Color> with Diagnosticable {
_OutlinedButtonDefaultForeground(this.primary, this.onSurface);
final Color primary;
final Color onSurface;
@override
Color resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return onSurface?.withOpacity(0.38);
return primary;
}
}
@immutable
class _OutlinedButtonDefaultOverlay extends MaterialStateProperty<Color> with Diagnosticable {
_OutlinedButtonDefaultOverlay(this.primary);
final Color primary;
@override
Color resolve(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;
}
}
@immutable
class _OutlinedButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor> with Diagnosticable {
_OutlinedButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor enabledCursor;
final MouseCursor disabledCursor;
@override
MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return disabledCursor;
return enabledCursor;
}
}
class _OutlinedButtonWithIcon extends OutlinedButton {
_OutlinedButtonWithIcon({
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
ButtonStyle style,
FocusNode focusNode,
bool autofocus,
Clip clipBehavior,
@required Widget icon,
@required Widget label,
}) : assert(icon != null),
assert(label != null),
super(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
style: style,
focusNode: focusNode,
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _OutlinedButtonWithIconChild(icon: icon, label: label),
);
}
class _OutlinedButtonWithIconChild extends StatelessWidget {
const _OutlinedButtonWithIconChild({ Key key, this.label, this.icon }) : super(key: key);
final Widget label;
final Widget icon;
@override
Widget build(BuildContext context) {
final double scale = MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1;
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1));
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, SizedBox(width: gap), label],
);
}
}
// 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.
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'theme.dart';
/// A [ButtonStyle] that overrides the default appearance of
/// [OutlinedButton]s when it's used with [OutlinedButtonTheme] or with the
/// overall [Theme]'s [ThemeData.outlinedButtonTheme].
///
/// The [style]'s properties override [OutlinedButton]'s default style,
/// i.e. the [ButtonStyle] returned by [OutlinedButton.defaultStyleOf]. Only
/// the style's non-null property values or resolved non-null
/// [MaterialStateProperty] values are used.
///
/// See also:
///
/// * [OutlinedButtonTheme], the theme which is configured with this class.
/// * [OutlinedButton.defaultStyleOf], which returns the default [ButtonStyle]
/// for text buttons.
/// * [OutlinedButton.styleOf], which converts simple values into a
/// [ButtonStyle] that's consistent with [OutlinedButton]'s defaults.
/// * [MaterialStateProperty.resolve], "resolve" a material state property
/// to a simple value based on a set of [MaterialState]s.
/// * [ThemeData.outlinedButtonTheme], which can be used to override the default
/// [ButtonStyle] for [OutlinedButton]s below the overall [Theme].
@immutable
class OutlinedButtonThemeData with Diagnosticable {
/// Creates a [OutlinedButtonThemeData].
///
/// The [style] may be null.
const OutlinedButtonThemeData({ this.style });
/// Overrides for [OutlinedButton]'s default style.
///
/// Non-null properties or non-null resolved [MaterialStateProperty]
/// values override the [ButtonStyle] returned by
/// [OutlinedButton.defaultStyleOf].
///
/// If [style] is null, then this theme doesn't override anything.
final ButtonStyle style;
/// Linearly interpolate between two outlined 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));
}
}
/// Overrides the default [ButtonStyle] of its [OutlinedButton] descendants.
///
/// See also:
///
/// * [OutlinedButtonThemeData], which is used to configure this theme.
/// * [OutlinedButton.defaultStyleOf], which returns the default [ButtonStyle]
/// for text buttons.
/// * [OutlinedButton.styleOf], which converts simple values into a
/// [ButtonStyle] that's consistent with [OutlinedButton]'s defaults.
/// * [ThemeData.outlinedButtonTheme], which can be used to override the default
/// [ButtonStyle] for [OutlinedButton]s below the overall [Theme].
class OutlinedButtonTheme extends InheritedTheme {
/// Create a [OutlinedButtonTheme].
///
/// The [data] parameter must not be null.
const OutlinedButtonTheme({
Key key,
@required this.data,
Widget child,
}) : assert(data != null), super(key: key, child: child);
/// The configuration of this theme.
final OutlinedButtonThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [OutlinedButtonsTheme] widget, then
/// [ThemeData.outlinedButtonTheme] 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 ?? Theme.of(context).outlinedButtonTheme;
}
@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;
}
// 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.
// @dart = 2.8
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'button_style_button.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'material_state.dart';
import 'text_button_theme.dart';
import 'theme.dart';
import 'theme_data.dart';
/// A Material Design "Text Button".
///
/// 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 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.
///
/// A text button is a label [child] displayed on a (zero elevation)
/// [Material] widget. The label's [Text] and [Icon] widgets are
/// displayed in the [style]'s [ButtonStyle.foregroundColor]. The
/// button reacts to touches by filling with the [style]'s
/// [ButtonStyle.backgroundColor].
///
/// The text button's default style is defined by [defaultStyleOf].
/// The style of this text button can be overridden with its [style]
/// parameter. The style of all text buttons in a subtree can be
/// overridden with the [TextButtonTheme] and the style of all of the
/// text buttons in an app can be overridden with the [Theme]'s
/// [ThemeData.textButtonTheme] property.
///
/// The static [styleFrom] method is a convenient way to create a
/// text button [ButtonStyle] from simple values.
///
/// If the [onPressed] and [onLongPress] callbacks are null, then this
/// button will be disabled, it will not react to touch.
///
/// See also:
///
/// * [OutlinedButton], a [TextButton] with a border outline.
/// * [ContainedButton], a filled button whose material elevates when pressed.
/// * <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,
@required Widget child,
}) : super(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
style: style,
focusNode: focusNode,
autofocus: autofocus,
clipBehavior: clipBehavior,
child: child,
);
/// Create a text button from a pair of widgets that serve as the button's
/// [icon] and [label].
///
/// The icon and label are arranged in a row and padded by 8 logical pixels
/// at the ends, with an 8 pixel gap in between.
///
/// The [icon] and [label] arguments must not be null.
factory TextButton.icon({
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
ButtonStyle style,
FocusNode focusNode,
bool autofocus,
Clip clipBehavior,
@required Widget icon,
@required Widget label,
}) = _TextButtonWithIcon;
/// A static convenience method that constructs a text button
/// [ButtonStyle] given simple values.
///
/// The [primary], and [onSurface] colors are used to to create a
/// [MaterialStateProperty] [foreground] value in the same way that
/// [defaultStyleOf] uses the [ColorScheme] colors with the same
/// names. Specify a value for [primary] to specify the color of the
/// button's text and icons as well as the overlay colors used to
/// indicate the hover, focus, and pressed states. Use [onSurface]
/// to specify the button's disabled text and icon color.
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle].mouseCursor.
///
/// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all
/// states.
///
/// All parameters default to null. By default this method returns
/// a [ButtonStyle] that doesn't override anything.
///
/// For example, to override the default text and icon colors for a
/// [TextButton], as well as its overlay color, with all of the
/// standard opacity adjustments for the pressed, focused, and
/// hovered states, one could write:
///
/// ```dart
/// TextButton(
/// style: TextButton.styleFrom(primary: Colors.green),
/// )
/// ```
static ButtonStyle styleFrom({
Color primary,
Color onSurface,
Color backgroundColor,
Color shadowColor,
double elevation,
TextStyle textStyle,
EdgeInsetsGeometry padding,
Size minimumSize,
BorderSide side,
OutlinedBorder shape,
MouseCursor enabledMouseCursor,
MouseCursor disabledMouseCursor,
VisualDensity visualDensity,
MaterialTapTargetSize tapTargetSize,
Duration animationDuration,
bool enableFeedback,
}) {
final MaterialStateProperty<Color> foregroundColor = (onSurface == null && primary == null)
? null
: _TextButtonDefaultForeground(primary, onSurface);
final MaterialStateProperty<Color> overlayColor = (primary == null)
? null
: _TextButtonDefaultOverlay(primary);
final MaterialStateProperty<MouseCursor> mouseCursor = (enabledMouseCursor == null && disabledMouseCursor == null)
? null
: _TextButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
backgroundColor: ButtonStyleButton.allOrNull<Color>(backgroundColor),
foregroundColor: foregroundColor,
overlayColor: overlayColor,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
);
}
/// Defines the button's default appearance.
///
/// The button [child]'s [Text] and [Icon] widgets are rendered with
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
/// the style's overlay color when the button is focused, hovered
/// or pressed. The button's background color becomes its [Material]
/// color and is transparent by default.
///
/// All of the ButtonStyle's defaults appear below.
///
/// In this list "Theme.foo" is shorthand for
/// `Theme.of(context).foo`. Color scheme values like
/// "onSurface(0.38)" are shorthand for
/// `onSurface.withOpacity(0.38)`. [MaterialStateProperty] valued
/// properties that are not followed by by a sublist have the same
/// value for all states, otherwise the values are as specified for
/// each state and "others" means all other states.
///
/// The `textScaleFactor` is the value of
/// `MediaQuery.of(context).textScaleFactor` and the names of the
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been
/// abbreviated for readability.
///
/// The color of the [textStyle] is not used, the [foreground] color
/// is used instead.
///
/// * `textStyle` - Theme.textTheme.button
/// * `backgroundColor` - transparent
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary
/// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.04)
/// * focused or pressed - Theme.colorScheme.primary(0.12)
/// * `shadowColor` - Colors.black
/// * `elevation` - 0
/// * `padding`
/// * `textScaleFactor <= 1` - all(8)
/// * `1 < textScaleFactor <= 2` - lerp(all(8), horizontal(8))
/// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
/// * `3 < textScaleFactor` - horizontal(4)
/// * `minimumSize` - Size(64, 36)
/// * `side` - BorderSide.none
/// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
/// * `mouseCursor`
/// * disabled - SystemMouseCursors.forbidden
/// * others - SystemMouseCursors.click
/// * `visualDensity` - theme.visualDensity
/// * `tapTargetSize` - theme.materialTapTargetSize
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
///
/// The default padding values for the [TextButton.icon] factory are slightly different:
///
/// * `padding`
/// * `textScaleFactor <= 1` - all(8)
/// * `1 < textScaleFactor <= 2 `- lerp(all(8), horizontal(4))
/// * `2 < textScaleFactor` - horizontal(4)
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1,
);
return styleFrom(
primary: colorScheme.primary,
onSurface: colorScheme.onSurface,
backgroundColor: Colors.transparent,
shadowColor: Colors.black,
elevation: 0,
textStyle: theme.textTheme.button,
padding: scaledPadding,
minimumSize: const Size(64, 36),
side: BorderSide.none,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
enabledMouseCursor: SystemMouseCursors.click,
disabledMouseCursor: SystemMouseCursors.forbidden,
visualDensity: theme.visualDensity,
tapTargetSize: theme.materialTapTargetSize,
animationDuration: kThemeChangeDuration,
enableFeedback: true,
);
}
/// Returns the [TextButtonThemeData.style] of the closest
/// [TextButtonTheme] ancestor.
@override
ButtonStyle themeStyleOf(BuildContext context) {
return TextButtonTheme.of(context)?.style;
}
}
@immutable
class _TextButtonDefaultForeground extends MaterialStateProperty<Color> {
_TextButtonDefaultForeground(this.primary, this.onSurface);
final Color primary;
final Color onSurface;
@override
Color resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return onSurface?.withOpacity(0.38);
return primary;
}
@override
String toString() {
return '{disabled: ${onSurface?.withOpacity(0.38)}, otherwise: $primary}';
}
}
@immutable
class _TextButtonDefaultOverlay extends MaterialStateProperty<Color> {
_TextButtonDefaultOverlay(this.primary);
final Color primary;
@override
Color resolve(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;
}
@override
String toString() {
return '{hovered: ${primary?.withOpacity(0.04)}, focused,pressed: ${primary?.withOpacity(0.12)}, otherwise: null}';
}
}
@immutable
class _TextButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor> with Diagnosticable {
_TextButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor enabledCursor;
final MouseCursor disabledCursor;
@override
MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return disabledCursor;
return enabledCursor;
}
}
class _TextButtonWithIcon extends TextButton {
_TextButtonWithIcon({
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
ButtonStyle style,
FocusNode focusNode,
bool autofocus,
Clip clipBehavior,
@required Widget icon,
@required Widget label,
}) : assert(icon != null),
assert(label != null),
super(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
style: style,
focusNode: focusNode,
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _TextButtonWithIconChild(icon: icon, label: label),
);
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 4),
const EdgeInsets.symmetric(horizontal: 4),
MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1,
);
return super.defaultStyleOf(context).copyWith(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(scaledPadding)
);
}
}
class _TextButtonWithIconChild extends StatelessWidget {
const _TextButtonWithIconChild({ Key key, this.label, this.icon }) : super(key: key);
final Widget label;
final Widget icon;
@override
Widget build(BuildContext context) {
final double scale = MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1;
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1));
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, SizedBox(width: gap), label],
);
}
}
// 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.
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'theme.dart';
/// A [ButtonStyle] that overrides the default appearance of
/// [TextButton]s when it's used with [TextButtonTheme] or with the
/// overall [Theme]'s [ThemeData.textButtonTheme].
///
/// The [style]'s properties override [TextButton]'s default style,
/// i.e. the [ButtonStyle] returned by [TextButton.defaultStyleOf]. Only
/// the style's non-null property values or resolved non-null
/// [MaterialStateProperty] values are used.
///
/// See also:
///
/// * [TextButtonTheme], the theme which is configured with this class.
/// * [TextButton.defaultStyleOf], which returns the default [ButtonStyle]
/// for text buttons.
/// * [TextButton.styleOf], which converts simple values into a
/// [ButtonStyle] that's consistent with [TextButton]'s defaults.
/// * [MaterialStateProperty.resolve], "resolve" a material state property
/// to a simple value based on a set of [MaterialState]s.
/// * [ThemeData.textButtonTheme], which can be used to override the default
/// [ButtonStyle] for [TextButton]s below the overall [Theme].
@immutable
class TextButtonThemeData with Diagnosticable {
/// Creates a [TextButtonThemeData].
///
/// The [style] may be null.
const TextButtonThemeData({ this.style });
/// Overrides for [TextButton]'s default style.
///
/// Non-null properties or non-null resolved [MaterialStateProperty]
/// values override the [ButtonStyle] returned by
/// [TextButton.defaultStyleOf].
///
/// If [style] is null, then this theme doesn't override anything.
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));
}
}
/// Overrides the default [ButtonStyle] of its [TextButton] descendants.
///
/// See also:
///
/// * [TextButtonThemeData], which is used to configure this theme.
/// * [TextButton.defaultStyleOf], which returns the default [ButtonStyle]
/// for text buttons.
/// * [TextButton.styleOf], which converts simple values into a
/// [ButtonStyle] that's consistent with [TextButton]'s defaults.
/// * [ThemeData.textButtonTheme], which can be used to override the default
/// [ButtonStyle] for [TextButton]s below the overall [Theme].
class TextButtonTheme extends InheritedTheme {
/// Create a [TextButtonTheme].
///
/// The [data] parameter must not be null.
const TextButtonTheme({
Key key,
@required this.data,
Widget child,
}) : assert(data != null), super(key: key, child: child);
/// The configuration of this theme.
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 ?? Theme.of(context).textButtonTheme;
}
@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;
}
......@@ -22,6 +22,7 @@ import 'card_theme.dart';
import 'chip_theme.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'contained_button_theme.dart';
import 'dialog_theme.dart';
import 'divider_theme.dart';
import 'floating_action_button_theme.dart';
......@@ -29,11 +30,13 @@ import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart';
import 'navigation_rail_theme.dart';
import 'outlined_button_theme.dart';
import 'page_transitions_theme.dart';
import 'popup_menu_theme.dart';
import 'slider_theme.dart';
import 'snack_bar_theme.dart';
import 'tab_bar_theme.dart';
import 'text_button_theme.dart';
import 'text_theme.dart';
import 'time_picker_theme.dart';
import 'toggle_buttons_theme.dart';
......@@ -271,6 +274,9 @@ class ThemeData with Diagnosticable {
ButtonBarThemeData buttonBarTheme,
BottomNavigationBarThemeData bottomNavigationBarTheme,
TimePickerThemeData timePickerTheme,
TextButtonThemeData textButtonTheme,
ContainedButtonThemeData containedButtonTheme,
OutlinedButtonThemeData outlinedButtonTheme,
bool fixTextFieldOutlineLabel,
}) {
assert(colorScheme?.brightness == null || brightness == null || colorScheme.brightness == brightness);
......@@ -383,7 +389,9 @@ class ThemeData with Diagnosticable {
buttonBarTheme ??= const ButtonBarThemeData();
bottomNavigationBarTheme ??= const BottomNavigationBarThemeData();
timePickerTheme ??= const TimePickerThemeData();
textButtonTheme ??= const TextButtonThemeData();
containedButtonTheme ??= const ContainedButtonThemeData();
outlinedButtonTheme ??= const OutlinedButtonThemeData();
fixTextFieldOutlineLabel ??= false;
return ThemeData.raw(
......@@ -452,6 +460,9 @@ class ThemeData with Diagnosticable {
buttonBarTheme: buttonBarTheme,
bottomNavigationBarTheme: bottomNavigationBarTheme,
timePickerTheme: timePickerTheme,
textButtonTheme: textButtonTheme,
containedButtonTheme: containedButtonTheme,
outlinedButtonTheme: outlinedButtonTheme,
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel,
);
}
......@@ -532,6 +543,9 @@ class ThemeData with Diagnosticable {
@required this.buttonBarTheme,
@required this.bottomNavigationBarTheme,
@required this.timePickerTheme,
@required this.textButtonTheme,
@required this.containedButtonTheme,
@required this.outlinedButtonTheme,
@required this.fixTextFieldOutlineLabel,
}) : assert(visualDensity != null),
assert(primaryColor != null),
......@@ -595,6 +609,9 @@ class ThemeData with Diagnosticable {
assert(buttonBarTheme != null),
assert(bottomNavigationBarTheme != null),
assert(timePickerTheme != null),
assert(textButtonTheme != null),
assert(containedButtonTheme != null),
assert(outlinedButtonTheme != null),
assert(fixTextFieldOutlineLabel != null);
/// Create a [ThemeData] based on the colors in the given [colorScheme] and
......@@ -1044,6 +1061,18 @@ class ThemeData with Diagnosticable {
/// A theme for customizing the appearance and layout of time picker widgets.
final TimePickerThemeData timePickerTheme;
/// A theme for customizing the appearance and internal layout of
/// [TextButton]s.
final TextButtonThemeData textButtonTheme;
/// A theme for customizing the appearance and internal layout of
/// [ContainedButton]s
final ContainedButtonThemeData containedButtonTheme;
/// A theme for customizing the appearance and internal layout of
/// [OutlinedButton]s.
final OutlinedButtonThemeData outlinedButtonTheme;
/// A temporary flag to allow apps to opt-in to a
/// [small fix](https://github.com/flutter/flutter/issues/54028) for the Y
/// coordinate of the floating label in a [TextField] [OutlineInputBorder].
......@@ -1126,6 +1155,9 @@ class ThemeData with Diagnosticable {
ButtonBarThemeData buttonBarTheme,
BottomNavigationBarThemeData bottomNavigationBarTheme,
TimePickerThemeData timePickerTheme,
TextButtonThemeData textButtonTheme,
ContainedButtonThemeData containedButtonTheme,
OutlinedButtonThemeData outlinedButtonTheme,
bool fixTextFieldOutlineLabel,
}) {
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
......@@ -1195,6 +1227,9 @@ class ThemeData with Diagnosticable {
buttonBarTheme: buttonBarTheme ?? this.buttonBarTheme,
bottomNavigationBarTheme: bottomNavigationBarTheme ?? this.bottomNavigationBarTheme,
timePickerTheme: timePickerTheme ?? this.timePickerTheme,
textButtonTheme: textButtonTheme ?? this.textButtonTheme,
containedButtonTheme: containedButtonTheme ?? this.containedButtonTheme,
outlinedButtonTheme: outlinedButtonTheme ?? this.outlinedButtonTheme,
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel ?? this.fixTextFieldOutlineLabel,
);
}
......@@ -1342,6 +1377,9 @@ class ThemeData with Diagnosticable {
buttonBarTheme: ButtonBarThemeData.lerp(a.buttonBarTheme, b.buttonBarTheme, t),
bottomNavigationBarTheme: BottomNavigationBarThemeData.lerp(a.bottomNavigationBarTheme, b.bottomNavigationBarTheme, t),
timePickerTheme: TimePickerThemeData.lerp(a.timePickerTheme, b.timePickerTheme, t),
textButtonTheme: TextButtonThemeData.lerp(a.textButtonTheme, b.textButtonTheme, t),
containedButtonTheme: ContainedButtonThemeData.lerp(a.containedButtonTheme, b.containedButtonTheme, t),
outlinedButtonTheme: OutlinedButtonThemeData.lerp(a.outlinedButtonTheme, b.outlinedButtonTheme, t),
fixTextFieldOutlineLabel: t < 0.5 ? a.fixTextFieldOutlineLabel : b.fixTextFieldOutlineLabel,
);
}
......@@ -1417,6 +1455,9 @@ class ThemeData with Diagnosticable {
&& other.buttonBarTheme == buttonBarTheme
&& other.bottomNavigationBarTheme == bottomNavigationBarTheme
&& other.timePickerTheme == timePickerTheme
&& other.textButtonTheme == textButtonTheme
&& other.containedButtonTheme == containedButtonTheme
&& other.outlinedButtonTheme == outlinedButtonTheme
&& other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel;
}
......@@ -1491,6 +1532,9 @@ class ThemeData with Diagnosticable {
buttonBarTheme,
bottomNavigationBarTheme,
timePickerTheme,
textButtonTheme,
containedButtonTheme,
outlinedButtonTheme,
fixTextFieldOutlineLabel,
];
return hashList(values);
......@@ -1562,6 +1606,9 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<ButtonBarThemeData>('buttonBarTheme', buttonBarTheme, defaultValue: defaultData.buttonBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<TimePickerThemeData>('timePickerTheme', timePickerTheme, defaultValue: defaultData.timePickerTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<BottomNavigationBarThemeData>('bottomNavigationBarTheme', bottomNavigationBarTheme, defaultValue: defaultData.bottomNavigationBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<TextButtonThemeData>('textButtonTheme', textButtonTheme, defaultValue: defaultData.textButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ContainedButtonThemeData>('containedButtonTheme', containedButtonTheme, defaultValue: defaultData.containedButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<OutlinedButtonThemeData>('outlinedButtonTheme', outlinedButtonTheme, defaultValue: defaultData.outlinedButtonTheme, level: DiagnosticLevel.debug));
}
}
......
// 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.
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('ButtonStyle copyWith, merge, ==, hashCode basics', () {
expect(const ButtonStyle(), const ButtonStyle().copyWith());
expect(const ButtonStyle().merge(const ButtonStyle()), const ButtonStyle());
expect(const ButtonStyle().hashCode, const ButtonStyle().copyWith().hashCode);
});
test('ButtonStyle defaults', () {
const ButtonStyle style = ButtonStyle();
expect(style.textStyle, null);
expect(style.backgroundColor, null);
expect(style.foregroundColor, null);
expect(style.overlayColor, null);
expect(style.elevation, null);
expect(style.padding, null);
expect(style.minimumSize, null);
expect(style.side, null);
expect(style.shape, null);
expect(style.mouseCursor, null);
expect(style.visualDensity, null);
expect(style.tapTargetSize, null);
expect(style.animationDuration, null);
expect(style.enableFeedback, null);
});
testWidgets('Default ButtonStyle debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const ButtonStyle().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('ButtonStyle debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
ButtonStyle(
textStyle: MaterialStateProperty.all<TextStyle>(const TextStyle(fontSize: 10.0)),
backgroundColor: MaterialStateProperty.all<Color>(const Color(0xfffffff1)),
foregroundColor: MaterialStateProperty.all<Color>(const Color(0xfffffff2)),
overlayColor: MaterialStateProperty.all<Color>(const Color(0xfffffff3)),
elevation: MaterialStateProperty.all<double>(1.5),
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.all(1.0)),
minimumSize: MaterialStateProperty.all<Size>(const Size(1.0, 2.0)),
side: MaterialStateProperty.all<BorderSide>(const BorderSide(width: 4.0, color: Color(0xfffffff4))),
shape: MaterialStateProperty.all<OutlinedBorder>(const StadiumBorder()),
mouseCursor: MaterialStateProperty.all<MouseCursor>(SystemMouseCursors.forbidden),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
animationDuration: const Duration(seconds: 1),
enableFeedback: true,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[
'textStyle: MaterialStateProperty.all(TextStyle(inherit: true, size: 10.0))',
'backgroundColor: MaterialStateProperty.all(Color(0xfffffff1))',
'foregroundColor: MaterialStateProperty.all(Color(0xfffffff2))',
'overlayColor: MaterialStateProperty.all(Color(0xfffffff3))',
'elevation: MaterialStateProperty.all(1.5)',
'padding: MaterialStateProperty.all(EdgeInsets.all(1.0))',
'minimumSize: MaterialStateProperty.all(Size(1.0, 2.0))',
'side: MaterialStateProperty.all(BorderSide(Color(0xfffffff4), 4.0, BorderStyle.solid))',
'shape: MaterialStateProperty.all(StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none)))',
'mouseCursor: MaterialStateProperty.all(SystemMouseCursor(forbidden))',
'tapTargetSize: shrinkWrap',
'animationDuration: 0:00:01.000000',
'enableFeedback: true',
]);
});
testWidgets('ButtonStyle copyWith, merge', (WidgetTester tester) async {
final MaterialStateProperty<TextStyle> textStyle = MaterialStateProperty.all<TextStyle>(const TextStyle(fontSize: 10));
final MaterialStateProperty<Color> backgroundColor = MaterialStateProperty.all<Color>(const Color(0xfffffff1));
final MaterialStateProperty<Color> foregroundColor = MaterialStateProperty.all<Color>(const Color(0xfffffff2));
final MaterialStateProperty<Color> overlayColor = MaterialStateProperty.all<Color>(const Color(0xfffffff3));
final MaterialStateProperty<double> elevation = MaterialStateProperty.all<double>(1);
final MaterialStateProperty<EdgeInsets> padding = MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.all(1));
final MaterialStateProperty<Size> minimumSize = MaterialStateProperty.all<Size>(const Size(1, 2));
final MaterialStateProperty<BorderSide> side = MaterialStateProperty.all<BorderSide>(const BorderSide());
final MaterialStateProperty<OutlinedBorder> shape = MaterialStateProperty.all<OutlinedBorder>(const StadiumBorder());
final MaterialStateProperty<MouseCursor> mouseCursor = MaterialStateProperty.all<MouseCursor>(SystemMouseCursors.forbidden);
const VisualDensity visualDensity = VisualDensity.compact;
const MaterialTapTargetSize tapTargetSize = MaterialTapTargetSize.shrinkWrap;
const Duration animationDuration = Duration(seconds: 1);
const bool enableFeedback = true;
final ButtonStyle style = ButtonStyle(
textStyle: textStyle,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
overlayColor: overlayColor,
elevation: elevation,
padding: padding,
minimumSize: minimumSize,
side: side,
shape: shape,
mouseCursor: mouseCursor,
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
);
expect(
style,
const ButtonStyle().copyWith(
textStyle: textStyle,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
overlayColor: overlayColor,
elevation: elevation,
padding: padding,
minimumSize: minimumSize,
side: side,
shape: shape,
mouseCursor: mouseCursor,
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
),
);
expect(
style,
const ButtonStyle().merge(style),
);
expect(
style.copyWith(),
style.merge(const ButtonStyle())
);
});
}
// 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.
// @dart = 2.8
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('ContainedButton defaults', (WidgetTester tester) async {
final Finder rawButtonMaterial = find.descendant(
of: find.byType(ContainedButton),
matching: find.byType(Material),
);
const ColorScheme colorScheme = ColorScheme.light();
// Enabled ContainedButton
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: Center(
child: ContainedButton(
onPressed: () { },
child: const Text('button'),
),
),
),
);
Material material = tester.widget<Material>(rawButtonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, colorScheme.primary);
expect(material.elevation, 2);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)));
expect(material.textStyle.color, colorScheme.onPrimary);
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
final Offset center = tester.getCenter(find.byType(ContainedButton));
await tester.startGesture(center);
await tester.pumpAndSettle();
// Only elevation changes when enabled and pressed.
material = tester.widget<Material>(rawButtonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, colorScheme.primary);
expect(material.elevation, 8);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)));
expect(material.textStyle.color, colorScheme.onPrimary);
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
// Disabled ContainedButton
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: const Center(
child: ContainedButton(
onPressed: null,
child: Text('button'),
),
),
),
);
material = tester.widget<Material>(rawButtonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, colorScheme.onSurface.withOpacity(0.12));
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)));
expect(material.textStyle.color, colorScheme.onSurface.withOpacity(0.38));
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
});
testWidgets('Default ContainedButton meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Scaffold(
body: Center(
child: ContainedButton(
child: const Text('ContainedButton'),
onPressed: () { },
focusNode: focusNode,
),
),
),
),
);
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(ContainedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
},
skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
semanticsEnabled: true,
);
testWidgets('ContainedButton uses stateful color for text color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ContainedButtonTheme(
data: ContainedButtonThemeData(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
),
child: Builder(
builder: (BuildContext context) {
return ContainedButton(
child: const Text('ContainedButton'),
onPressed: () {},
focusNode: focusNode,
);
},
),
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('ContainedButton')).text.style.color;
}
// Default, not disabled.
expect(textColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byType(ContainedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(textColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(textColor(), pressedColor);
});
testWidgets('ContainedButton uses stateful color for icon color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final Key buttonKey = UniqueKey();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ContainedButtonTheme(
data: ContainedButtonThemeData(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
),
child: Builder(
builder: (BuildContext context) {
return ContainedButton.icon(
key: buttonKey,
icon: const Icon(Icons.add),
label: const Text('ContainedButton'),
onPressed: () {},
focusNode: focusNode,
);
},
),
),
),
),
),
);
Color iconColor() => _iconStyle(tester, Icons.add).color;
// Default, not disabled.
expect(iconColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(iconColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byKey(buttonKey));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(iconColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(iconColor(), pressedColor);
});
testWidgets('ContainedButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async {
bool wasPressed;
Finder containedButton;
Widget buildFrame({ VoidCallback onPressed, VoidCallback onLongPress }) {
return Directionality(
textDirection: TextDirection.ltr,
child: ContainedButton(
child: const Text('button'),
onPressed: onPressed,
onLongPress: onLongPress,
),
);
}
// onPressed not null, onLongPress null.
wasPressed = false;
await tester.pumpWidget(
buildFrame(onPressed: () { wasPressed = true; }, onLongPress: null),
);
containedButton = find.byType(ContainedButton);
expect(tester.widget<ContainedButton>(containedButton).enabled, true);
await tester.tap(containedButton);
expect(wasPressed, true);
// onPressed null, onLongPress not null.
wasPressed = false;
await tester.pumpWidget(
buildFrame(onPressed: null, onLongPress: () { wasPressed = true; }),
);
containedButton = find.byType(ContainedButton);
expect(tester.widget<ContainedButton>(containedButton).enabled, true);
await tester.longPress(containedButton);
expect(wasPressed, true);
// onPressed null, onLongPress null.
await tester.pumpWidget(
buildFrame(onPressed: null, onLongPress: null),
);
containedButton = find.byType(ContainedButton);
expect(tester.widget<ContainedButton>(containedButton).enabled, false);
});
testWidgets('ContainedButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async {
bool didPressButton = false;
bool didLongPressButton = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ContainedButton(
onPressed: () {
didPressButton = true;
},
onLongPress: () {
didLongPressButton = true;
},
child: const Text('button'),
),
),
);
final Finder containedButton = find.byType(ContainedButton);
expect(tester.widget<ContainedButton>(containedButton).enabled, true);
expect(didPressButton, isFalse);
await tester.tap(containedButton);
expect(didPressButton, isTrue);
expect(didLongPressButton, isFalse);
await tester.longPress(containedButton);
expect(didLongPressButton, isTrue);
});
testWidgets('Does ContainedButton work with hover', (WidgetTester tester) async {
const Color hoverColor = Color(0xff001122);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ContainedButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
return states.contains(MaterialState.hovered) ? hoverColor : null;
}),
),
onPressed: () { },
child: const Text('button'),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(ContainedButton)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
await gesture.removePointer();
});
testWidgets('Does ContainedButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
final FocusNode focusNode = FocusNode(debugLabel: 'ContainedButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ContainedButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
return states.contains(MaterialState.focused) ? focusColor : null;
}),
),
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
focusNode.requestFocus();
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
});
testWidgets('Does ContainedButton work with autofocus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
Color getOverlayColor(Set<MaterialState> states) {
return states.contains(MaterialState.focused) ? focusColor : null;
}
final FocusNode focusNode = FocusNode(debugLabel: 'ContainedButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ContainedButton(
autofocus: true,
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color>(getOverlayColor),
),
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
});
testWidgets('Does ContainedButton contribute semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: ContainedButton(
style: ButtonStyle(
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the semantics tree's rect and transform
// match the original version of this test.
minimumSize: MaterialStateProperty.all<Size>(const Size(88, 36)),
),
onPressed: () { },
child: const Text('ABC'),
),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
),
ignoreId: true,
));
semantics.dispose();
});
testWidgets('ContainedButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
final ButtonStyle style = ButtonStyle(
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the corresponding button size matches
// the original version of this test.
minimumSize: MaterialStateProperty.all<Size>(const Size(88, 36)),
);
Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) {
return Theme(
data: ThemeData(materialTapTargetSize: tapTargetSize),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: ContainedButton(
key: key,
style: style,
child: const SizedBox(width: 50.0, height: 8.0),
onPressed: () { },
),
),
),
),
);
}
final Key key1 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded, key1));
expect(tester.getSize(find.byKey(key1)), const Size(88.0, 48.0));
final Key key2 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2));
expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0));
});
testWidgets('ContainedButton has no clip by default', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: ContainedButton(
onPressed: () { /* to make sure the button is enabled */ },
child: const Text('button'),
),
),
),
);
expect(
tester.renderObject(find.byType(ContainedButton)),
paintsExactlyCountTimes(#clipPath, 0),
);
});
testWidgets('ContainedButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
const Key childKey = Key('test child');
Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
return await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: ContainedButton(
style: ButtonStyle(
visualDensity: visualDensity,
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the corresponding button size matches
// the original version of this test.
minimumSize: MaterialStateProperty.all<Size>(const Size(88, 36)),
),
key: key,
onPressed: () {},
child: useText
? const Text('Text', key: childKey)
: Container(key: childKey, width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
),
);
}
await buildTest(const VisualDensity());
final RenderBox box = tester.renderObject(find.byKey(key));
Rect childRect = tester.getRect(find.byKey(childKey));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(132, 100)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(156, 124)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(108, 100)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(88, 48)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(112, 60)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(76, 36)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
});
testWidgets('ContainedButton.icon responds to applied padding', (WidgetTester tester) async {
const Key buttonKey = Key('test');
const Key labelKey = Key('label');
await tester.pumpWidget(
// When textDirection is set to TextDirection.ltr, the label appears on the
// right side of the icon. This is important in determining whether the
// horizontal padding is applied correctly later on
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: ContainedButton.icon(
key: buttonKey,
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.fromLTRB(16, 5, 10, 12)),
),
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text(
'Hello',
key: labelKey,
),
),
),
),
),
);
final Rect paddingRect = tester.getRect(find.byType(Padding));
final Rect labelRect = tester.getRect(find.byKey(labelKey));
final Rect iconRect = tester.getRect(find.byType(Icon));
// The right padding should be applied on the right of the label, whereas the
// left padding should be applied on the left side of the icon.
expect(paddingRect.right, labelRect.right + 10);
expect(paddingRect.left, iconRect.left - 16);
// Use the taller widget to check the top and bottom padding.
final Rect tallerWidget = iconRect.height > labelRect.height ? iconRect : labelRect;
expect(paddingRect.top, tallerWidget.top - 5);
expect(paddingRect.bottom, tallerWidget.bottom + 12);
});
group('Default ContainedButton padding for textScaleFactor, textDirection', () {
const ValueKey<String> buttonKey = ValueKey<String>('button');
const ValueKey<String> labelKey = ValueKey<String>('label');
const ValueKey<String> iconKey = ValueKey<String>('icon');
const List<double> textScaleFactorOptions = <double>[0.5, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0];
const List<TextDirection> textDirectionOptions = <TextDirection>[TextDirection.ltr, TextDirection.rtl];
const List<Widget> iconOptions = <Widget>[null, Icon(Icons.add, size: 18, key: iconKey)];
// Expected values for each textScaleFactor.
final Map<double, double> paddingWithoutIconStart = <double, double>{
0.5: 16,
1: 16,
1.25: 14,
1.5: 12,
2: 8,
2.5: 6,
3: 4,
4: 4,
};
final Map<double, double> paddingWithoutIconEnd = <double, double>{
0.5: 16,
1: 16,
1.25: 14,
1.5: 12,
2: 8,
2.5: 6,
3: 4,
4: 4,
};
final Map<double, double> paddingWithIconStart = <double, double>{
0.5: 12,
1: 12,
1.25: 11,
1.5: 10,
2: 8,
2.5: 8,
3: 8,
4: 8,
};
final Map<double, double> paddingWithIconEnd = <double, double>{
0.5: 16,
1: 16,
1.25: 14,
1.5: 12,
2: 8,
2.5: 6,
3: 4,
4: 4,
};
final Map<double, double> paddingWithIconGap = <double, double>{
0.5: 8,
1: 8,
1.25: 7,
1.5: 6,
2: 4,
2.5: 4,
3: 4,
4: 4,
};
Rect globalBounds(RenderBox renderBox) {
final Offset topLeft = renderBox.localToGlobal(Offset.zero);
return topLeft & renderBox.size;
}
/// Computes the padding between two [Rect]s, one inside the other.
EdgeInsets paddingBetween({ Rect parent, Rect child }) {
assert (parent.intersect(child) == child);
return EdgeInsets.fromLTRB(
child.left - parent.left,
child.top - parent.top,
parent.right - child.right,
parent.bottom - child.bottom,
);
}
for (final double textScaleFactor in textScaleFactorOptions) {
for (final TextDirection textDirection in textDirectionOptions) {
for (final Widget icon in iconOptions) {
final String testName = 'ContainedButton'
', text scale $textScaleFactor'
'${icon != null ? ", with icon" : ""}'
'${textDirection == TextDirection.rtl ? ", RTL" : ""}';
testWidgets(testName, (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: textScaleFactor,
),
child: Directionality(
textDirection: textDirection,
child: Scaffold(
body: Center(
child: icon == null
? ContainedButton(
key: buttonKey,
onPressed: () {},
child: const Text('button', key: labelKey),
)
: ContainedButton.icon(
key: buttonKey,
onPressed: () {},
icon: icon,
label: const Text('button', key: labelKey),
),
),
),
),
);
},
),
),
);
final Element paddingElement = tester.element(
find.descendant(
of: find.byKey(buttonKey),
matching: find.byType(Padding),
),
);
expect(Directionality.of(paddingElement), textDirection);
final Padding paddingWidget = paddingElement.widget as Padding;
// Compute expected padding, and check.
final double expectedStart = icon != null
? paddingWithIconStart[textScaleFactor]
: paddingWithoutIconStart[textScaleFactor];
final double expectedEnd = icon != null
? paddingWithIconEnd[textScaleFactor]
: paddingWithoutIconEnd[textScaleFactor];
final EdgeInsets expectedPadding = EdgeInsetsDirectional.fromSTEB(expectedStart, 0, expectedEnd, 0)
.resolve(textDirection);
expect(paddingWidget.padding.resolve(textDirection), expectedPadding);
// Measure padding in terms of the difference between the button and its label child
// and check that.
final RenderBox labelRenderBox = tester.renderObject<RenderBox>(find.byKey(labelKey));
final Rect labelBounds = globalBounds(labelRenderBox);
final RenderBox iconRenderBox = icon == null ? null : tester.renderObject<RenderBox>(find.byKey(iconKey));
final Rect iconBounds = icon == null ? null : globalBounds(iconRenderBox);
final Rect childBounds = icon == null ? labelBounds : labelBounds.expandToInclude(iconBounds);
// We measure the `InkResponse` descendant of the button
// element, because the button has a larger `RenderBox`
// which accommodates the minimum tap target with a height
// of 48.
final RenderBox buttonRenderBox = tester.renderObject<RenderBox>(
find.descendant(
of: find.byKey(buttonKey),
matching: find.byWidgetPredicate(
(Widget widget) => widget is InkResponse,
),
),
);
final Rect buttonBounds = globalBounds(buttonRenderBox);
final EdgeInsets visuallyMeasuredPadding = paddingBetween(
parent: buttonBounds,
child: childBounds,
);
// Since there is a requirement of a minimum width of 64
// and a minimum height of 36 on material buttons, the visual
// padding of smaller buttons may not match their settings.
// Therefore, we only test buttons that are large enough.
if (buttonBounds.width > 64) {
expect(
visuallyMeasuredPadding.left,
expectedPadding.left,
);
expect(
visuallyMeasuredPadding.right,
expectedPadding.right,
);
}
if (buttonBounds.height > 36) {
expect(
visuallyMeasuredPadding.top,
expectedPadding.top,
);
expect(
visuallyMeasuredPadding.bottom,
expectedPadding.bottom,
);
}
// Check the gap between the icon and the label
if (icon != null) {
final double gapWidth = textDirection == TextDirection.ltr
? labelBounds.left - iconBounds.right
: iconBounds.left - labelBounds.right;
expect(gapWidth, paddingWithIconGap[textScaleFactor]);
}
// Check the text's height - should be consistent with the textScaleFactor.
final RenderBox textRenderObject = tester.renderObject<RenderBox>(
find.descendant(
of: find.byKey(labelKey),
matching: find.byElementPredicate(
(Element element) => element.widget is RichText,
),
),
);
final double textHeight = textRenderObject.paintBounds.size.height;
final double expectedTextHeight = 14 * textScaleFactor;
expect(textHeight, moreOrLessEquals(expectedTextHeight, epsilon: 0.5));
});
}
}
}
});
testWidgets('Override ContainedButton default padding', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: 2,
),
child: Scaffold(
body: Center(
child: ContainedButton(
style: ContainedButton.styleFrom(padding: const EdgeInsets.all(22)),
onPressed: () {},
child: const Text('ContainedButton')
),
),
),
);
},
),
),
);
final Padding paddingWidget = tester.widget<Padding>(
find.descendant(
of: find.byType(ContainedButton),
matching: find.byType(Padding),
),
);
expect(paddingWidget.padding, const EdgeInsets.all(22));
});
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style;
}
// 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.
// @dart = 2.8
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Passing no ContainedButtonTheme returns defaults', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: Scaffold(
body: Center(
child: ContainedButton(
onPressed: () { },
child: const Text('button'),
),
),
),
),
);
final Finder buttonMaterial = find.descendant(
of: find.byType(ContainedButton),
matching: find.byType(Material),
);
final Material material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, colorScheme.primary);
expect(material.elevation, 2);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)));
expect(material.textStyle.color, colorScheme.onPrimary);
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
});
group('[Theme, TextTheme, ContainedButton style overrides]', () {
const Color primaryColor = Color(0xff000001);
const Color onSurfaceColor = Color(0xff000002);
const Color shadowColor = Color(0xff000004);
const Color onPrimaryColor = Color(0xff000005);
const double elevation = 1;
const TextStyle textStyle = TextStyle(fontSize: 12.0);
const EdgeInsets padding = EdgeInsets.all(3);
const Size minimumSize = Size(200, 200);
const BorderSide side = BorderSide(color: Colors.green, width: 2);
const OutlinedBorder shape = RoundedRectangleBorder(side: side, borderRadius: BorderRadius.all(Radius.circular(2)));
const MouseCursor enabledMouseCursor = SystemMouseCursors.text;
const MouseCursor disabledMouseCursor = SystemMouseCursors.grab;
const MaterialTapTargetSize tapTargetSize = MaterialTapTargetSize.shrinkWrap;
const Duration animationDuration = Duration(milliseconds: 25);
const bool enableFeedback = false;
final ButtonStyle style = ContainedButton.styleFrom(
primary: primaryColor,
onPrimary: onPrimaryColor,
onSurface: onSurfaceColor,
shadowColor: shadowColor,
elevation: elevation,
textStyle: textStyle,
padding: padding,
minimumSize: minimumSize,
side: side,
shape: shape,
enabledMouseCursor: enabledMouseCursor,
disabledMouseCursor: disabledMouseCursor,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
);
Widget buildFrame({ ButtonStyle buttonStyle, ButtonStyle themeStyle, ButtonStyle overallStyle }) {
final Widget child = Builder(
builder: (BuildContext context) {
return ContainedButton(
style: buttonStyle,
onPressed: () { },
child: const Text('button'),
);
},
);
return MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()).copyWith(
containedButtonTheme: ContainedButtonThemeData(style: overallStyle),
),
home: Scaffold(
body: Center(
// If the ContainedButtonTheme widget is present, it's used
// instead of the Theme's ThemeData.containedButtonTheme.
child: themeStyle == null ? child : ContainedButtonTheme(
data: ContainedButtonThemeData(style: themeStyle),
child: child,
),
),
),
);
}
final Finder findMaterial = find.descendant(
of: find.byType(ContainedButton),
matching: find.byType(Material),
);
final Finder findInkWell = find.descendant(
of: find.byType(ContainedButton),
matching: find.byType(InkWell),
);
const Set<MaterialState> enabled = <MaterialState>{};
const Set<MaterialState> disabled = <MaterialState>{ MaterialState.disabled };
const Set<MaterialState> hovered = <MaterialState>{ MaterialState.hovered };
const Set<MaterialState> focused = <MaterialState>{ MaterialState.focused };
const Set<MaterialState> pressed = <MaterialState>{ MaterialState.pressed };
void checkButton(WidgetTester tester) {
final Material material = tester.widget<Material>(findMaterial);
final InkWell inkWell = tester.widget<InkWell>(findInkWell);
expect(material.textStyle.color, onPrimaryColor);
expect(material.textStyle.fontSize, 12);
expect(material.color, primaryColor);
expect(material.shadowColor, shadowColor);
expect(material.elevation, elevation);
expect(MaterialStateProperty.resolveAs<MouseCursor>(inkWell.mouseCursor, enabled), enabledMouseCursor);
expect(MaterialStateProperty.resolveAs<MouseCursor>(inkWell.mouseCursor, disabled), disabledMouseCursor);
expect(inkWell.overlayColor.resolve(hovered), onPrimaryColor.withOpacity(0.08));
expect(inkWell.overlayColor.resolve(focused), onPrimaryColor.withOpacity(0.24));
expect(inkWell.overlayColor.resolve(pressed), onPrimaryColor.withOpacity(0.24));
expect(inkWell.enableFeedback, enableFeedback);
expect(material.borderRadius, null);
expect(material.shape, shape);
expect(material.animationDuration, animationDuration);
expect(tester.getSize(find.byType(ContainedButton)), const Size(200, 200));
}
testWidgets('Button style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(themeStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(overallStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
// Same as the previous tests with empty ButtonStyle's instead of null.
testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: null, overallStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
});
}
// 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.
// @dart = 2.8
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('OutlinedButton defaults', (WidgetTester tester) async {
final Finder rawButtonMaterial = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(Material),
);
const ColorScheme colorScheme = ColorScheme.light();
// Enabled OutlinedButton
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: Center(
child: OutlinedButton(
onPressed: () { },
child: const Text('button'),
),
),
),
);
Material material = tester.widget<Material>(rawButtonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(
side: BorderSide(
width: 1,
color: colorScheme.onSurface.withOpacity(0.12),
),
borderRadius: BorderRadius.circular(4.0),
));
expect(material.textStyle.color, colorScheme.primary);
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
final Offset center = tester.getCenter(find.byType(OutlinedButton));
await tester.startGesture(center);
await tester.pumpAndSettle();
// No change vs enabled and not pressed.
material = tester.widget<Material>(rawButtonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(
side: BorderSide(
width: 1,
color: colorScheme.onSurface.withOpacity(0.12),
),
borderRadius: BorderRadius.circular(4.0),
));
expect(material.textStyle.color, colorScheme.primary);
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
// Disabled OutlinedButton
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: const Center(
child: OutlinedButton(
onPressed: null,
child: Text('button'),
),
),
),
);
material = tester.widget<Material>(rawButtonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(
side: BorderSide(
width: 1,
color: colorScheme.onSurface.withOpacity(0.12),
),
borderRadius: BorderRadius.circular(4.0),
));
expect(material.textStyle.color, colorScheme.onSurface.withOpacity(0.38));
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
});
testWidgets('Does OutlinedButton work with hover', (WidgetTester tester) async {
const Color hoverColor = Color(0xff001122);
Color getOverlayColor(Set<MaterialState> states) {
return states.contains(MaterialState.hovered) ? hoverColor : null;
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color>(getOverlayColor),
),
onPressed: () { },
child: const Text('button'),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(OutlinedButton)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
gesture.removePointer();
});
testWidgets('Does OutlinedButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
Color getOverlayColor(Set<MaterialState> states) {
return states.contains(MaterialState.focused) ? focusColor : null;
}
final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color>(getOverlayColor),
),
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
focusNode.requestFocus();
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
});
testWidgets('Does OutlinedButton work with autofocus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
Color getOverlayColor(Set<MaterialState> states) {
return states.contains(MaterialState.focused) ? focusColor : null;
}
final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
autofocus: true,
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color>(getOverlayColor),
),
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
});
testWidgets('Default OutlinedButton meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Scaffold(
body: Center(
child: OutlinedButton(
child: const Text('OutlinedButton'),
onPressed: () {},
focusNode: focusNode,
),
),
),
),
);
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(OutlinedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
await gesture.removePointer();
},
skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
semanticsEnabled: true,
);
testWidgets('OutlinedButton with colored theme meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
Color getTextColor(Set<MaterialState> states) {
final Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Colors.blue[900];
}
return Colors.blue[800];
}
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue)),
home: Scaffold(
backgroundColor: Colors.white,
body: Center(
child: OutlinedButtonTheme(
data: OutlinedButtonThemeData(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
),
child: Builder(
builder: (BuildContext context) {
return OutlinedButton(
child: const Text('OutlinedButton'),
onPressed: () {},
focusNode: focusNode,
);
},
),
),
),
),
),
);
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(OutlinedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
},
skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
semanticsEnabled: true,
);
testWidgets('OutlinedButton uses stateful color for text color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: OutlinedButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
onPressed: () {},
focusNode: focusNode,
child: const Text('OutlinedButton'),
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('OutlinedButton')).text.style.color;
}
// Default, not disabled.
expect(textColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byType(OutlinedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(textColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(textColor(), pressedColor);
});
testWidgets('OutlinedButton uses stateful color for icon color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final Key buttonKey = UniqueKey();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
Color getIconColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: OutlinedButton.icon(
key: buttonKey,
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getIconColor),
),
icon: const Icon(Icons.add),
label: const Text('OutlinedButton'),
onPressed: () {},
focusNode: focusNode,
),
),
),
),
);
Color iconColor() => _iconStyle(tester, Icons.add).color;
// Default, not disabled.
expect(iconColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(iconColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byKey(buttonKey));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(iconColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(iconColor(), pressedColor);
});
testWidgets('OutlinedButton uses stateful color for border color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
BorderSide getBorderSide(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return const BorderSide(color: pressedColor, width: 1);
}
if (states.contains(MaterialState.hovered)) {
return const BorderSide(color: hoverColor, width: 1);
}
if (states.contains(MaterialState.focused)) {
return const BorderSide(color: focusedColor, width: 1);
}
return const BorderSide(color: defaultColor, width: 1);
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: OutlinedButton(
style: ButtonStyle(
side: MaterialStateProperty.resolveWith<BorderSide>(getBorderSide),
),
onPressed: () {},
focusNode: focusNode,
child: const Text('OutlinedButton'),
),
),
),
),
);
final Finder outlinedButton = find.byType(OutlinedButton);
// Default, not disabled.
expect(outlinedButton, paints..drrect(color: defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(outlinedButton, paints..drrect(color: focusedColor));
// Hovered.
final Offset center = tester.getCenter(find.byType(OutlinedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(outlinedButton, paints..drrect(color: hoverColor));
// Highlighted (pressed).
await gesture.down(center);
await tester.pumpAndSettle();
expect(outlinedButton, paints..drrect(color: pressedColor));
});
testWidgets('OutlinedButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async {
bool wasPressed;
Finder outlinedButton;
Widget buildFrame({ VoidCallback onPressed, VoidCallback onLongPress }) {
return Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
child: const Text('button'),
onPressed: onPressed,
onLongPress: onLongPress,
),
);
}
// onPressed not null, onLongPress null.
wasPressed = false;
await tester.pumpWidget(
buildFrame(onPressed: () { wasPressed = true; }, onLongPress: null),
);
outlinedButton = find.byType(OutlinedButton);
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, true);
await tester.tap(outlinedButton);
expect(wasPressed, true);
// onPressed null, onLongPress not null.
wasPressed = false;
await tester.pumpWidget(
buildFrame(onPressed: null, onLongPress: () { wasPressed = true; }),
);
outlinedButton = find.byType(OutlinedButton);
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, true);
await tester.longPress(outlinedButton);
expect(wasPressed, true);
// onPressed null, onLongPress null.
await tester.pumpWidget(
buildFrame(onPressed: null, onLongPress: null),
);
outlinedButton = find.byType(OutlinedButton);
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, false);
});
testWidgets("Outline button doesn't crash if disabled during a gesture", (WidgetTester tester) async {
Widget buildFrame(VoidCallback onPressed) {
return Directionality(
textDirection: TextDirection.ltr,
child: Theme(
data: ThemeData(),
child: Center(
child: OutlinedButton(onPressed: onPressed, child: const Text('button')),
),
),
);
}
await tester.pumpWidget(buildFrame(() {}));
await tester.press(find.byType(OutlinedButton));
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(null));
await tester.pumpAndSettle();
});
testWidgets('OutlinedButton shape and border component overrides', (WidgetTester tester) async {
const Color fillColor = Color(0xFF00FF00);
const BorderSide disabledBorderSide = BorderSide(color: Color(0xFFFF0000), width: 3);
const BorderSide enabledBorderSide = BorderSide(color: Color(0xFFFF00FF), width: 4);
const BorderSide pressedBorderSide = BorderSide(color: Color(0xFF0000FF), width: 5);
Widget buildFrame({ VoidCallback onPressed }) {
return Directionality(
textDirection: TextDirection.ltr,
child: Theme(
data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
child: Container(
alignment: Alignment.topLeft,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: const RoundedRectangleBorder(), // default border radius is 0
backgroundColor: fillColor,
).copyWith(
side: MaterialStateProperty.resolveWith<BorderSide>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return disabledBorderSide;
if (states.contains(MaterialState.pressed))
return pressedBorderSide;
return enabledBorderSide;
}),
),
clipBehavior: Clip.antiAlias,
onPressed: onPressed,
child: const Text('button'),
),
),
),
);
}
// 116 = 16 + 'button'.length * 14 + 16, horizontal padding = 16
const Rect clipRect = Rect.fromLTRB(0.0, 0.0, 116.0, 36.0);
final Path clipPath = Path()..addRect(clipRect);
final Finder outlinedButton = find.byType(OutlinedButton);
BorderSide getBorderSide() {
final OutlinedBorder border = tester.widget<Material>(
find.descendant(of: outlinedButton, matching: find.byType(Material))
).shape as OutlinedBorder;
return border.side;
}
// Pump a button with a null onPressed callback to make it disabled.
await tester.pumpWidget(
buildFrame(onPressed: null),
);
// Expect that the button is disabled and painted with the disabled border color.
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, false);
expect(getBorderSide(), disabledBorderSide);
_checkPhysicalLayer(
tester.element(outlinedButton),
fillColor,
clipPath: clipPath,
clipRect: clipRect,
);
// Pump a new button with a no-op onPressed callback to make it enabled.
await tester.pumpWidget(
buildFrame(onPressed: () {}),
);
// Wait for the border color to change from disabled to enabled.
await tester.pumpAndSettle();
expect(getBorderSide(), enabledBorderSide);
final Offset center = tester.getCenter(outlinedButton);
final TestGesture gesture = await tester.startGesture(center);
await tester.pump(); // start gesture
// Wait for the border's color to change to pressed
await tester.pump(const Duration(milliseconds: 200));
expect(getBorderSide(), pressedBorderSide);
_checkPhysicalLayer(
tester.element(outlinedButton),
fillColor,
clipPath: clipPath,
clipRect: clipRect,
);
// Tap gesture completes, button returns to its initial configuration.
await gesture.up();
await tester.pumpAndSettle();
expect(getBorderSide(), enabledBorderSide);
_checkPhysicalLayer(
tester.element(outlinedButton),
fillColor,
clipPath: clipPath,
clipRect: clipRect,
);
});
testWidgets('OutlinedButton has no clip by default', (WidgetTester tester) async {
final GlobalKey buttonKey = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: OutlinedButton(
key: buttonKey,
onPressed: () {},
child: const Text('ABC'),
),
),
),
),
);
expect(
tester.renderObject(find.byKey(buttonKey)),
paintsExactlyCountTimes(#clipPath, 0),
);
});
testWidgets('OutlinedButton contributes semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: OutlinedButton(
style: ButtonStyle(
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the corresponding button size matches
// the original version of this test.
minimumSize: MaterialStateProperty.all<Size>(const Size(88, 36)),
),
onPressed: () {},
child: const Text('ABC'),
),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
),
ignoreId: true,
));
semantics.dispose();
});
testWidgets('OutlinedButton scales textScaleFactor', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: MediaQuery(
data: const MediaQueryData(textScaleFactor: 1.0),
child: Center(
child: OutlinedButton(
style: ButtonStyle(
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the corresponding button size matches
// the original version of this test.
minimumSize: MaterialStateProperty.all<Size>(const Size(88, 36)),
),
onPressed: () {},
child: const Text('ABC'),
),
),
),
),
),
);
expect(tester.getSize(find.byType(OutlinedButton)), equals(const Size(88.0, 48.0)));
expect(tester.getSize(find.byType(Text)), equals(const Size(42.0, 14.0)));
// textScaleFactor expands text, but not button.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: MediaQuery(
data: const MediaQueryData(textScaleFactor: 1.3),
child: Center(
child: OutlinedButton(
style: ButtonStyle(
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the corresponding button size matches
// the original version of this test.
minimumSize: MaterialStateProperty.all<Size>(const Size(88, 36)),
),
onPressed: () {},
child: const Text('ABC'),
),
),
),
),
),
);
expect(tester.getSize(find.byType(OutlinedButton)), equals(const Size(88.0, 48.0)));
// Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
expect(tester.getSize(find.byType(Text)).width, isIn(<double>[54.0, 55.0]));
expect(tester.getSize(find.byType(Text)).height, isIn(<double>[18.0, 19.0]));
// Set text scale large enough to expand text and button.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: MediaQuery(
data: const MediaQueryData(textScaleFactor: 3.0),
child: Center(
child: OutlinedButton(
onPressed: () {},
child: const Text('ABC'),
),
),
),
),
),
);
// Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
expect(tester.getSize(find.byType(OutlinedButton)).width, isIn(<double>[133.0, 134.0]));
expect(tester.getSize(find.byType(OutlinedButton)).height, equals(48.0));
expect(tester.getSize(find.byType(Text)).width, isIn(<double>[126.0, 127.0]));
expect(tester.getSize(find.byType(Text)).height, equals(42.0));
});
testWidgets('OutlinedButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async {
bool didPressButton = false;
bool didLongPressButton = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
onPressed: () {
didPressButton = true;
},
onLongPress: () {
didLongPressButton = true;
},
child: const Text('button'),
),
),
);
final Finder outlinedButton = find.byType(OutlinedButton);
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, true);
expect(didPressButton, isFalse);
await tester.tap(outlinedButton);
expect(didPressButton, isTrue);
expect(didLongPressButton, isFalse);
await tester.longPress(outlinedButton);
expect(didLongPressButton, isTrue);
});
testWidgets('OutlinedButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
const Key childKey = Key('test child');
Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
return await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: OutlinedButton(
style: ButtonStyle(visualDensity: visualDensity),
key: key,
onPressed: () {},
child: useText
? const Text('Text', key: childKey)
: Container(key: childKey, width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
),
);
}
await buildTest(const VisualDensity());
final RenderBox box = tester.renderObject(find.byKey(key));
Rect childRect = tester.getRect(find.byKey(childKey));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(132, 100)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(156, 124)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(108, 100)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(88, 48)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(112, 60)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(64, 36)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
});
group('Default OutlinedButton padding for textScaleFactor, textDirection', () {
const ValueKey<String> buttonKey = ValueKey<String>('button');
const ValueKey<String> labelKey = ValueKey<String>('label');
const ValueKey<String> iconKey = ValueKey<String>('icon');
const List<double> textScaleFactorOptions = <double>[0.5, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0];
const List<TextDirection> textDirectionOptions = <TextDirection>[TextDirection.ltr, TextDirection.rtl];
const List<Widget> iconOptions = <Widget>[null, Icon(Icons.add, size: 18, key: iconKey)];
// Expected values for each textScaleFactor.
final Map<double, double> paddingVertical = <double, double>{
0.5: 0,
1: 0,
1.25: 0,
1.5: 0,
2: 0,
2.5: 0,
3: 0,
4: 0,
};
final Map<double, double> paddingWithIconGap = <double, double>{
0.5: 8,
1: 8,
1.25: 7,
1.5: 6,
2: 4,
2.5: 4,
3: 4,
4: 4,
};
final Map<double, double> paddingHorizontal = <double, double>{
0.5: 16,
1: 16,
1.25: 14,
1.5: 12,
2: 8,
2.5: 6,
3: 4,
4: 4,
};
Rect globalBounds(RenderBox renderBox) {
final Offset topLeft = renderBox.localToGlobal(Offset.zero);
return topLeft & renderBox.size;
}
/// Computes the padding between two [Rect]s, one inside the other.
EdgeInsets paddingBetween({ Rect parent, Rect child }) {
assert (parent.intersect(child) == child);
return EdgeInsets.fromLTRB(
child.left - parent.left,
child.top - parent.top,
parent.right - child.right,
parent.bottom - child.bottom,
);
}
for (final double textScaleFactor in textScaleFactorOptions) {
for (final TextDirection textDirection in textDirectionOptions) {
for (final Widget icon in iconOptions) {
final String testName = 'OutlinedButton'
', text scale $textScaleFactor'
'${icon != null ? ", with icon" : ""}'
'${textDirection == TextDirection.rtl ? ", RTL" : ""}';
testWidgets(testName, (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: textScaleFactor,
),
child: Directionality(
textDirection: textDirection,
child: Scaffold(
body: Center(
child: icon == null
? OutlinedButton(
key: buttonKey,
onPressed: () {},
child: const Text('button', key: labelKey),
)
: OutlinedButton.icon(
key: buttonKey,
onPressed: () {},
icon: icon,
label: const Text('button', key: labelKey),
),
),
),
),
);
},
),
),
);
final Element paddingElement = tester.element(
find.descendant(
of: find.byKey(buttonKey),
matching: find.byType(Padding),
),
);
expect(Directionality.of(paddingElement), textDirection);
final Padding paddingWidget = paddingElement.widget as Padding;
// Compute expected padding, and check.
final double expectedPaddingTop = paddingVertical[textScaleFactor];
final double expectedPaddingBottom = paddingVertical[textScaleFactor];
final double expectedPaddingStart = paddingHorizontal[textScaleFactor];
final double expectedPaddingEnd = expectedPaddingStart;
final EdgeInsets expectedPadding = EdgeInsetsDirectional.fromSTEB(
expectedPaddingStart,
expectedPaddingTop,
expectedPaddingEnd,
expectedPaddingBottom,
).resolve(textDirection);
expect(paddingWidget.padding.resolve(textDirection), expectedPadding);
// Measure padding in terms of the difference between the button and its label child
// and check that.
final RenderBox labelRenderBox = tester.renderObject<RenderBox>(find.byKey(labelKey));
final Rect labelBounds = globalBounds(labelRenderBox);
final RenderBox iconRenderBox = icon == null ? null : tester.renderObject<RenderBox>(find.byKey(iconKey));
final Rect iconBounds = icon == null ? null : globalBounds(iconRenderBox);
final Rect childBounds = icon == null ? labelBounds : labelBounds.expandToInclude(iconBounds);
// We measure the `InkResponse` descendant of the button
// element, because the button has a larger `RenderBox`
// which accommodates the minimum tap target with a height
// of 48.
final RenderBox buttonRenderBox = tester.renderObject<RenderBox>(
find.descendant(
of: find.byKey(buttonKey),
matching: find.byWidgetPredicate(
(Widget widget) => widget is InkResponse,
),
),
);
final Rect buttonBounds = globalBounds(buttonRenderBox);
final EdgeInsets visuallyMeasuredPadding = paddingBetween(
parent: buttonBounds,
child: childBounds,
);
// Since there is a requirement of a minimum width of 64
// and a minimum height of 36 on material buttons, the visual
// padding of smaller buttons may not match their settings.
// Therefore, we only test buttons that are large enough.
if (buttonBounds.width > 64) {
expect(
visuallyMeasuredPadding.left,
expectedPadding.left,
);
expect(
visuallyMeasuredPadding.right,
expectedPadding.right,
);
}
if (buttonBounds.height > 36) {
expect(
visuallyMeasuredPadding.top,
expectedPadding.top,
);
expect(
visuallyMeasuredPadding.bottom,
expectedPadding.bottom,
);
}
// Check the gap between the icon and the label
if (icon != null) {
final double gapWidth = textDirection == TextDirection.ltr
? labelBounds.left - iconBounds.right
: iconBounds.left - labelBounds.right;
expect(gapWidth, paddingWithIconGap[textScaleFactor]);
}
// Check the text's height - should be consistent with the textScaleFactor.
final RenderBox textRenderObject = tester.renderObject<RenderBox>(
find.descendant(
of: find.byKey(labelKey),
matching: find.byElementPredicate(
(Element element) => element.widget is RichText,
),
),
);
final double textHeight = textRenderObject.paintBounds.size.height;
final double expectedTextHeight = 14 * textScaleFactor;
expect(textHeight, moreOrLessEquals(expectedTextHeight, epsilon: 0.5));
});
}
}
}
});
testWidgets('Override OutlinedButton default padding', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: 2,
),
child: Scaffold(
body: Center(
child: OutlinedButton(
style: OutlinedButton.styleFrom(padding: const EdgeInsets.all(22)),
onPressed: () {},
child: const Text('OutlinedButton')
),
),
),
);
},
),
),
);
final Padding paddingWidget = tester.widget<Padding>(
find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(Padding),
),
);
expect(paddingWidget.padding, const EdgeInsets.all(22));
});
}
PhysicalModelLayer _findPhysicalLayer(Element element) {
expect(element, isNotNull);
RenderObject object = element.renderObject;
while (object != null && object is! RenderRepaintBoundary && object is! RenderView) {
object = object.parent as RenderObject;
}
expect(object.debugLayer, isNotNull);
expect(object.debugLayer.firstChild, isA<PhysicalModelLayer>());
final PhysicalModelLayer layer = object.debugLayer.firstChild as PhysicalModelLayer;
final Layer child = layer.firstChild;
return child is PhysicalModelLayer ? child : layer;
}
void _checkPhysicalLayer(Element element, Color expectedColor, { Path clipPath, Rect clipRect }) {
final PhysicalModelLayer expectedLayer = _findPhysicalLayer(element);
expect(expectedLayer.elevation, 0.0);
expect(expectedLayer.color, expectedColor);
if (clipPath != null) {
expect(clipRect, isNotNull);
expect(expectedLayer.clipPath, coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)));
}
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style;
}
// 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.
// @dart = 2.8
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Passing no OutlinedButtonTheme returns defaults', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: Scaffold(
body: Center(
child: OutlinedButton(
onPressed: () { },
child: const Text('button'),
),
),
),
),
);
final Finder buttonMaterial = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(Material),
);
final Material material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(
side: BorderSide(width: 1, color: colorScheme.onSurface.withOpacity(0.12)),
borderRadius: BorderRadius.circular(4.0),
));
expect(material.textStyle.color, colorScheme.primary);
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
});
group('[Theme, TextTheme, OutlinedButton style overrides]', () {
const Color primaryColor = Color(0xff000001);
const Color onSurfaceColor = Color(0xff000002);
const Color backgroundColor = Color(0xff000003);
const Color shadowColor = Color(0xff000004);
const double elevation = 3;
const TextStyle textStyle = TextStyle(fontSize: 12.0);
const EdgeInsets padding = EdgeInsets.all(3);
const Size minimumSize = Size(200, 200);
const BorderSide side = BorderSide(color: Colors.green, width: 2);
const OutlinedBorder shape = RoundedRectangleBorder(side: side, borderRadius: BorderRadius.all(Radius.circular(2)));
const MouseCursor enabledMouseCursor = SystemMouseCursors.text;
const MouseCursor disabledMouseCursor = SystemMouseCursors.grab;
const MaterialTapTargetSize tapTargetSize = MaterialTapTargetSize.shrinkWrap;
const Duration animationDuration = Duration(milliseconds: 25);
const bool enableFeedback = false;
final ButtonStyle style = OutlinedButton.styleFrom(
primary: primaryColor,
onSurface: onSurfaceColor,
backgroundColor: backgroundColor,
shadowColor: shadowColor,
elevation: elevation,
textStyle: textStyle,
padding: padding,
minimumSize: minimumSize,
side: side,
shape: shape,
enabledMouseCursor: enabledMouseCursor,
disabledMouseCursor: disabledMouseCursor,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
);
Widget buildFrame({ ButtonStyle buttonStyle, ButtonStyle themeStyle, ButtonStyle overallStyle }) {
final Widget child = Builder(
builder: (BuildContext context) {
return OutlinedButton(
style: buttonStyle,
onPressed: () { },
child: const Text('button'),
);
},
);
return MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()).copyWith(
outlinedButtonTheme: OutlinedButtonThemeData(style: overallStyle),
),
home: Scaffold(
body: Center(
// If the OutlinedButtonTheme widget is present, it's used
// instead of the Theme's ThemeData.outlinedButtonTheme.
child: themeStyle == null ? child : OutlinedButtonTheme(
data: OutlinedButtonThemeData(style: themeStyle),
child: child,
),
),
),
);
}
final Finder findMaterial = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(Material),
);
final Finder findInkWell = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(InkWell),
);
const Set<MaterialState> enabled = <MaterialState>{};
const Set<MaterialState> disabled = <MaterialState>{ MaterialState.disabled };
const Set<MaterialState> hovered = <MaterialState>{ MaterialState.hovered };
const Set<MaterialState> focused = <MaterialState>{ MaterialState.focused };
void checkButton(WidgetTester tester) {
final Material material = tester.widget<Material>(findMaterial);
final InkWell inkWell = tester.widget<InkWell>(findInkWell);
expect(material.textStyle.color, primaryColor);
expect(material.textStyle.fontSize, 12);
expect(material.color, backgroundColor);
expect(material.shadowColor, shadowColor);
expect(material.elevation, elevation);
expect(MaterialStateProperty.resolveAs<MouseCursor>(inkWell.mouseCursor, enabled), enabledMouseCursor);
expect(MaterialStateProperty.resolveAs<MouseCursor>(inkWell.mouseCursor, disabled), disabledMouseCursor);
expect(inkWell.overlayColor.resolve(hovered), primaryColor.withOpacity(0.04));
expect(inkWell.overlayColor.resolve(focused), primaryColor.withOpacity(0.12));
expect(inkWell.enableFeedback, enableFeedback);
expect(material.borderRadius, null);
expect(material.shape, shape);
expect(material.animationDuration, animationDuration);
expect(tester.getSize(find.byType(OutlinedButton)), const Size(200, 200));
}
testWidgets('Button style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(themeStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(overallStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
// Same as the previous tests with empty ButtonStyle's instead of null.
testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: null, overallStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
});
}
// 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.
// @dart = 2.8
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('TextButton defaults', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
// Enabled TextButton
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: Center(
child: TextButton(
onPressed: () { },
child: const Text('button'),
),
),
),
);
final Finder buttonMaterial = find.descendant(
of: find.byType(TextButton),
matching: find.byType(Material),
);
Material material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)));
expect(material.textStyle.color, colorScheme.primary);
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
final Offset center = tester.getCenter(find.byType(TextButton));
await tester.startGesture(center);
await tester.pumpAndSettle();
material = tester.widget<Material>(buttonMaterial);
// No change vs enabled and not pressed.
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)));
expect(material.textStyle.color, colorScheme.primary);
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
// Disabled TextButton
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: const Center(
child: TextButton(
onPressed: null,
child: Text('button'),
),
),
),
);
material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)));
expect(material.textStyle.color, colorScheme.onSurface.withOpacity(0.38));
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
});
testWidgets('Default TextButton meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Scaffold(
body: Center(
child: TextButton(
child: const Text('TextButton'),
onPressed: () { },
focusNode: focusNode,
),
),
),
),
);
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(TextButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
await gesture.removePointer();
},
skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
semanticsEnabled: true,
);
testWidgets('TextButton with colored theme meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
Color getTextColor(Set<MaterialState> states) {
final Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Colors.blue[900];
}
return Colors.blue[800];
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: TextButtonTheme(
data: TextButtonThemeData(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
),
child: Builder(
builder: (BuildContext context) {
return TextButton(
child: const Text('TextButton'),
onPressed: () {},
focusNode: focusNode,
);
},
),
),
),
),
),
);
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(TextButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
},
skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
semanticsEnabled: true,
);
testWidgets('TextButton uses stateful color for text color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
onPressed: () {},
focusNode: focusNode,
child: const Text('TextButton'),
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('TextButton')).text.style.color;
}
// Default, not disabled.
expect(textColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byType(TextButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(textColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(textColor(), pressedColor);
});
testWidgets('TextButton uses stateful color for icon color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final Key buttonKey = UniqueKey();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: TextButton.icon(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
key: buttonKey,
icon: const Icon(Icons.add),
label: const Text('TextButton'),
onPressed: () {},
focusNode: focusNode,
),
),
),
),
);
Color iconColor() => _iconStyle(tester, Icons.add).color;
// Default, not disabled.
expect(iconColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(iconColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byKey(buttonKey));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(iconColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(iconColor(), pressedColor);
});
testWidgets('TextButton has no clip by default', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: TextButton(
child: Container(),
onPressed: () { /* to make sure the button is enabled */ },
),
),
),
);
expect(
tester.renderObject(find.byType(TextButton)),
paintsExactlyCountTimes(#clipPath, 0),
);
});
testWidgets('Does TextButton work with hover', (WidgetTester tester) async {
const Color hoverColor = Color(0xff001122);
Color getOverlayColor(Set<MaterialState> states) {
return states.contains(MaterialState.hovered) ? hoverColor : null;
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: TextButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color>(getOverlayColor),
),
child: Container(),
onPressed: () { /* to make sure the button is enabled */ },
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(TextButton)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
});
testWidgets('Does TextButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
Color getOverlayColor(Set<MaterialState> states) {
return states.contains(MaterialState.focused) ? focusColor : null;
}
final FocusNode focusNode = FocusNode(debugLabel: 'TextButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color>(getOverlayColor),
),
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
WidgetsBinding.instance.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
focusNode.requestFocus();
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
});
testWidgets('Does TextButton contribute semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: TextButton(
style: ButtonStyle(
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the semantics tree's rect and transform
// match the original version of this test.
minimumSize: MaterialStateProperty.all<Size>(const Size(88, 36)),
),
onPressed: () { },
child: const Text('ABC'),
),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
),
ignoreId: true,
));
semantics.dispose();
});
testWidgets('Does TextButton scale with font scale changes', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: MediaQuery(
data: const MediaQueryData(textScaleFactor: 1.0),
child: Center(
child: TextButton(
onPressed: () { },
child: const Text('ABC'),
),
),
),
),
),
);
expect(tester.getSize(find.byType(TextButton)), equals(const Size(64.0, 48.0)));
expect(tester.getSize(find.byType(Text)), equals(const Size(42.0, 14.0)));
// textScaleFactor expands text, but not button.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: MediaQuery(
data: const MediaQueryData(textScaleFactor: 1.3),
child: Center(
child: TextButton(
onPressed: () { },
child: const Text('ABC'),
),
),
),
),
),
);
// Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
expect(tester.getSize(find.byType(TextButton)).width, isIn(<double>[70.0, 71.0]));
expect(tester.getSize(find.byType(TextButton)).height, isIn(<double>[47.0, 48.0]));
expect(tester.getSize(find.byType(Text)).width, isIn(<double>[54.0, 55.0]));
expect(tester.getSize(find.byType(Text)).height, isIn(<double>[18.0, 19.0]));
// Set text scale large enough to expand text and button.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: MediaQuery(
data: const MediaQueryData(textScaleFactor: 3.0),
child: Center(
child: TextButton(
onPressed: () { },
child: const Text('ABC'),
),
),
),
),
),
);
// Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
expect(tester.getSize(find.byType(TextButton)).width, isIn(<double>[133.0, 134.0]));
expect(tester.getSize(find.byType(TextButton)).height, equals(48.0));
expect(tester.getSize(find.byType(Text)).width, isIn(<double>[126.0, 127.0]));
expect(tester.getSize(find.byType(Text)).height, equals(42.0));
});
testWidgets('TextButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) {
return Theme(
data: ThemeData(materialTapTargetSize: tapTargetSize),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: TextButton(
key: key,
child: const SizedBox(width: 50.0, height: 8.0),
onPressed: () { },
),
),
),
),
);
}
final Key key1 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded, key1));
expect(tester.getSize(find.byKey(key1)), const Size(66.0, 48.0));
final Key key2 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2));
expect(tester.getSize(find.byKey(key2)), const Size(66.0, 36.0));
});
testWidgets('TextButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async {
bool wasPressed;
Finder textButton;
Widget buildFrame({ VoidCallback onPressed, VoidCallback onLongPress }) {
return Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
child: const Text('button'),
onPressed: onPressed,
onLongPress: onLongPress,
),
);
}
// onPressed not null, onLongPress null.
wasPressed = false;
await tester.pumpWidget(
buildFrame(onPressed: () { wasPressed = true; }, onLongPress: null),
);
textButton = find.byType(TextButton);
expect(tester.widget<TextButton>(textButton).enabled, true);
await tester.tap(textButton);
expect(wasPressed, true);
// onPressed null, onLongPress not null.
wasPressed = false;
await tester.pumpWidget(
buildFrame(onPressed: null, onLongPress: () { wasPressed = true; }),
);
textButton = find.byType(TextButton);
expect(tester.widget<TextButton>(textButton).enabled, true);
await tester.longPress(textButton);
expect(wasPressed, true);
// onPressed null, onLongPress null.
await tester.pumpWidget(
buildFrame(onPressed: null, onLongPress: null),
);
textButton = find.byType(TextButton);
expect(tester.widget<TextButton>(textButton).enabled, false);
});
testWidgets('TextButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async {
bool didPressButton = false;
bool didLongPressButton = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
onPressed: () {
didPressButton = true;
},
onLongPress: () {
didLongPressButton = true;
},
child: const Text('button'),
),
),
);
final Finder textButton = find.byType(TextButton);
expect(tester.widget<TextButton>(textButton).enabled, true);
expect(didPressButton, isFalse);
await tester.tap(textButton);
expect(didPressButton, isTrue);
expect(didLongPressButton, isFalse);
await tester.longPress(textButton);
expect(didLongPressButton, isTrue);
});
testWidgets('TextButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
const Key childKey = Key('test child');
Future<void> buildTest(VisualDensity visualDensity, { bool useText = false }) async {
return await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: TextButton(
style: ButtonStyle(
visualDensity: visualDensity,
),
key: key,
onPressed: () {},
child: useText
? const Text('Text', key: childKey)
: Container(key: childKey, width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
),
);
}
await buildTest(const VisualDensity());
final RenderBox box = tester.renderObject(find.byKey(key));
Rect childRect = tester.getRect(find.byKey(childKey));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(116, 116)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(140, 140)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(100, 100)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(72, 48)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(96, 60)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(56, 36)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
});
group('Default TextButton padding for textScaleFactor, textDirection', () {
const ValueKey<String> buttonKey = ValueKey<String>('button');
const ValueKey<String> labelKey = ValueKey<String>('label');
const ValueKey<String> iconKey = ValueKey<String>('icon');
const List<double> textScaleFactorOptions = <double>[0.5, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0];
const List<TextDirection> textDirectionOptions = <TextDirection>[TextDirection.ltr, TextDirection.rtl];
const List<Widget> iconOptions = <Widget>[null, Icon(Icons.add, size: 18, key: iconKey)];
// Expected values for each textScaleFactor.
final Map<double, double> paddingVertical = <double, double>{
0.5: 8,
1: 8,
1.25: 6,
1.5: 4,
2: 0,
2.5: 0,
3: 0,
4: 0,
};
final Map<double, double> paddingWithIconGap = <double, double>{
0.5: 8,
1: 8,
1.25: 7,
1.5: 6,
2: 4,
2.5: 4,
3: 4,
4: 4,
};
final Map<double, double> textPaddingWithoutIconHorizontal = <double, double>{
0.5: 8,
1: 8,
1.25: 8,
1.5: 8,
2: 8,
2.5: 6,
3: 4,
4: 4,
};
final Map<double, double> textPaddingWithIconHorizontal = <double, double>{
0.5: 8,
1: 8,
1.25: 7,
1.5: 6,
2: 4,
2.5: 4,
3: 4,
4: 4,
};
Rect globalBounds(RenderBox renderBox) {
final Offset topLeft = renderBox.localToGlobal(Offset.zero);
return topLeft & renderBox.size;
}
/// Computes the padding between two [Rect]s, one inside the other.
EdgeInsets paddingBetween({ Rect parent, Rect child }) {
assert (parent.intersect(child) == child);
return EdgeInsets.fromLTRB(
child.left - parent.left,
child.top - parent.top,
parent.right - child.right,
parent.bottom - child.bottom,
);
}
for (final double textScaleFactor in textScaleFactorOptions) {
for (final TextDirection textDirection in textDirectionOptions) {
for (final Widget icon in iconOptions) {
final String testName = 'TextButton'
', text scale $textScaleFactor'
'${icon != null ? ", with icon" : ""}'
'${textDirection == TextDirection.rtl ? ", RTL" : ""}';
testWidgets(testName, (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: textScaleFactor,
),
child: Directionality(
textDirection: textDirection,
child: Scaffold(
body: Center(
child: icon == null
? TextButton(
key: buttonKey,
onPressed: () {},
child: const Text('button', key: labelKey),
)
: TextButton.icon(
key: buttonKey,
onPressed: () {},
icon: icon,
label: const Text('button', key: labelKey),
),
),
),
),
);
},
),
),
);
final Element paddingElement = tester.element(
find.descendant(
of: find.byKey(buttonKey),
matching: find.byType(Padding),
),
);
expect(Directionality.of(paddingElement), textDirection);
final Padding paddingWidget = paddingElement.widget as Padding;
// Compute expected padding, and check.
final double expectedPaddingTop = paddingVertical[textScaleFactor];
final double expectedPaddingBottom = paddingVertical[textScaleFactor];
final double expectedPaddingStart = icon != null
? textPaddingWithIconHorizontal[textScaleFactor]
: textPaddingWithoutIconHorizontal[textScaleFactor];
final double expectedPaddingEnd = expectedPaddingStart;
final EdgeInsets expectedPadding = EdgeInsetsDirectional.fromSTEB(
expectedPaddingStart,
expectedPaddingTop,
expectedPaddingEnd,
expectedPaddingBottom,
).resolve(textDirection);
expect(paddingWidget.padding.resolve(textDirection), expectedPadding);
// Measure padding in terms of the difference between the button and its label child
// and check that.
final RenderBox labelRenderBox = tester.renderObject<RenderBox>(find.byKey(labelKey));
final Rect labelBounds = globalBounds(labelRenderBox);
final RenderBox iconRenderBox = icon == null ? null : tester.renderObject<RenderBox>(find.byKey(iconKey));
final Rect iconBounds = icon == null ? null : globalBounds(iconRenderBox);
final Rect childBounds = icon == null ? labelBounds : labelBounds.expandToInclude(iconBounds);
// We measure the `InkResponse` descendant of the button
// element, because the button has a larger `RenderBox`
// which accommodates the minimum tap target with a height
// of 48.
final RenderBox buttonRenderBox = tester.renderObject<RenderBox>(
find.descendant(
of: find.byKey(buttonKey),
matching: find.byWidgetPredicate(
(Widget widget) => widget is InkResponse,
),
),
);
final Rect buttonBounds = globalBounds(buttonRenderBox);
final EdgeInsets visuallyMeasuredPadding = paddingBetween(
parent: buttonBounds,
child: childBounds,
);
// Since there is a requirement of a minimum width of 64
// and a minimum height of 36 on material buttons, the visual
// padding of smaller buttons may not match their settings.
// Therefore, we only test buttons that are large enough.
if (buttonBounds.width > 64) {
expect(
visuallyMeasuredPadding.left,
expectedPadding.left,
);
expect(
visuallyMeasuredPadding.right,
expectedPadding.right,
);
}
if (buttonBounds.height > 36) {
expect(
visuallyMeasuredPadding.top,
expectedPadding.top,
);
expect(
visuallyMeasuredPadding.bottom,
expectedPadding.bottom,
);
}
// Check the gap between the icon and the label
if (icon != null) {
final double gapWidth = textDirection == TextDirection.ltr
? labelBounds.left - iconBounds.right
: iconBounds.left - labelBounds.right;
expect(gapWidth, paddingWithIconGap[textScaleFactor]);
}
// Check the text's height - should be consistent with the textScaleFactor.
final RenderBox textRenderObject = tester.renderObject<RenderBox>(
find.descendant(
of: find.byKey(labelKey),
matching: find.byElementPredicate(
(Element element) => element.widget is RichText,
),
),
);
final double textHeight = textRenderObject.paintBounds.size.height;
final double expectedTextHeight = 14 * textScaleFactor;
expect(textHeight, moreOrLessEquals(expectedTextHeight, epsilon: 0.5));
});
}
}
}
});
testWidgets('Override TextButton default padding', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: 2,
),
child: Scaffold(
body: Center(
child: TextButton(
style: TextButton.styleFrom(padding: const EdgeInsets.all(22)),
onPressed: () {},
child: const Text('TextButton')
),
),
),
);
},
),
),
);
final Padding paddingWidget = tester.widget<Padding>(
find.descendant(
of: find.byType(TextButton),
matching: find.byType(Padding),
),
);
expect(paddingWidget.padding, const EdgeInsets.all(22));
});
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style;
}
// 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.
// @dart = 2.8
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Passing no TextButtonTheme returns defaults', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: Scaffold(
body: Center(
child: TextButton(
onPressed: () { },
child: const Text('button'),
),
),
),
),
);
final Finder buttonMaterial = find.descendant(
of: find.byType(TextButton),
matching: find.byType(Material),
);
final Material material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)));
expect(material.textStyle.color, colorScheme.primary);
expect(material.textStyle.fontFamily, 'Roboto');
expect(material.textStyle.fontSize, 14);
expect(material.textStyle.fontWeight, FontWeight.w500);
});
group('[Theme, TextTheme, TextButton style overrides]', () {
const Color primaryColor = Color(0xff000001);
const Color onSurfaceColor = Color(0xff000002);
const Color backgroundColor = Color(0xff000003);
const Color shadowColor = Color(0xff000004);
const double elevation = 3;
const TextStyle textStyle = TextStyle(fontSize: 12.0);
const EdgeInsets padding = EdgeInsets.all(3);
const Size minimumSize = Size(200, 200);
const BorderSide side = BorderSide(color: Colors.green, width: 2);
const OutlinedBorder shape = RoundedRectangleBorder(side: side, borderRadius: BorderRadius.all(Radius.circular(2)));
const MouseCursor enabledMouseCursor = SystemMouseCursors.text;
const MouseCursor disabledMouseCursor = SystemMouseCursors.grab;
const MaterialTapTargetSize tapTargetSize = MaterialTapTargetSize.shrinkWrap;
const Duration animationDuration = Duration(milliseconds: 25);
const bool enableFeedback = false;
final ButtonStyle style = TextButton.styleFrom(
primary: primaryColor,
onSurface: onSurfaceColor,
backgroundColor: backgroundColor,
shadowColor: shadowColor,
elevation: elevation,
textStyle: textStyle,
padding: padding,
minimumSize: minimumSize,
side: side,
shape: shape,
enabledMouseCursor: enabledMouseCursor,
disabledMouseCursor: disabledMouseCursor,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
);
Widget buildFrame({ ButtonStyle buttonStyle, ButtonStyle themeStyle, ButtonStyle overallStyle }) {
final Widget child = Builder(
builder: (BuildContext context) {
return TextButton(
style: buttonStyle,
onPressed: () { },
child: const Text('button'),
);
},
);
return MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()).copyWith(
textButtonTheme: TextButtonThemeData(style: overallStyle),
),
home: Scaffold(
body: Center(
// If the TextButtonTheme widget is present, it's used
// instead of the Theme's ThemeData.textButtonTheme.
child: themeStyle == null ? child : TextButtonTheme(
data: TextButtonThemeData(style: themeStyle),
child: child,
),
),
),
);
}
final Finder findMaterial = find.descendant(
of: find.byType(TextButton),
matching: find.byType(Material),
);
final Finder findInkWell = find.descendant(
of: find.byType(TextButton),
matching: find.byType(InkWell),
);
const Set<MaterialState> enabled = <MaterialState>{};
const Set<MaterialState> disabled = <MaterialState>{ MaterialState.disabled };
const Set<MaterialState> hovered = <MaterialState>{ MaterialState.hovered };
const Set<MaterialState> focused = <MaterialState>{ MaterialState.focused };
void checkButton(WidgetTester tester) {
final Material material = tester.widget<Material>(findMaterial);
final InkWell inkWell = tester.widget<InkWell>(findInkWell);
expect(material.textStyle.color, primaryColor);
expect(material.textStyle.fontSize, 12);
expect(material.color, backgroundColor);
expect(material.shadowColor, shadowColor);
expect(material.elevation, elevation);
expect(MaterialStateProperty.resolveAs<MouseCursor>(inkWell.mouseCursor, enabled), enabledMouseCursor);
expect(MaterialStateProperty.resolveAs<MouseCursor>(inkWell.mouseCursor, disabled), disabledMouseCursor);
expect(inkWell.overlayColor.resolve(hovered), primaryColor.withOpacity(0.04));
expect(inkWell.overlayColor.resolve(focused), primaryColor.withOpacity(0.12));
expect(inkWell.enableFeedback, enableFeedback);
expect(material.borderRadius, null);
expect(material.shape, shape);
expect(material.animationDuration, animationDuration);
expect(tester.getSize(find.byType(TextButton)), const Size(200, 200));
}
testWidgets('Button style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(themeStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(overallStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
// Same as the previous tests with empty ButtonStyle's instead of null.
testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: null, overallStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
});
}
......@@ -283,6 +283,9 @@ void main() {
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.start),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed),
timePickerTheme: const TimePickerThemeData(backgroundColor: Colors.black),
textButtonTheme: TextButtonThemeData(style: TextButton.styleFrom(primary: Colors.red)),
containedButtonTheme: ContainedButtonThemeData(style: ContainedButton.styleFrom(primary: Colors.green)),
outlinedButtonTheme: OutlinedButtonThemeData(style: OutlinedButton.styleFrom(primary: Colors.blue)),
fixTextFieldOutlineLabel: false,
);
......@@ -365,6 +368,9 @@ void main() {
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.end),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.shifting),
timePickerTheme: const TimePickerThemeData(backgroundColor: Colors.white),
textButtonTheme: const TextButtonThemeData(),
containedButtonTheme: const ContainedButtonThemeData(),
outlinedButtonTheme: const OutlinedButtonThemeData(),
fixTextFieldOutlineLabel: true,
);
......@@ -433,6 +439,9 @@ void main() {
buttonBarTheme: otherTheme.buttonBarTheme,
bottomNavigationBarTheme: otherTheme.bottomNavigationBarTheme,
timePickerTheme: otherTheme.timePickerTheme,
textButtonTheme: otherTheme.textButtonTheme,
containedButtonTheme: otherTheme.containedButtonTheme,
outlinedButtonTheme: otherTheme.outlinedButtonTheme,
fixTextFieldOutlineLabel: otherTheme.fixTextFieldOutlineLabel,
);
......@@ -503,6 +512,9 @@ void main() {
expect(themeDataCopy.buttonBarTheme, equals(otherTheme.buttonBarTheme));
expect(themeDataCopy.bottomNavigationBarTheme, equals(otherTheme.bottomNavigationBarTheme));
expect(themeDataCopy.timePickerTheme, equals(otherTheme.timePickerTheme));
expect(themeDataCopy.textButtonTheme, equals(otherTheme.textButtonTheme));
expect(themeDataCopy.containedButtonTheme, equals(otherTheme.containedButtonTheme));
expect(themeDataCopy.outlinedButtonTheme, equals(otherTheme.outlinedButtonTheme));
expect(themeDataCopy.fixTextFieldOutlineLabel, equals(otherTheme.fixTextFieldOutlineLabel));
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment