New Button Universe (#59702)
This commit is contained in:
parent
ea777fea1b
commit
dc31d89c56
@ -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';
|
||||
|
439
packages/flutter/lib/src/material/button_style.dart
Normal file
439
packages/flutter/lib/src/material/button_style.dart
Normal file
@ -0,0 +1,439 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
470
packages/flutter/lib/src/material/button_style_button.dart
Normal file
470
packages/flutter/lib/src/material/button_style_button.dart
Normal file
@ -0,0 +1,470 @@
|
||||
// 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
425
packages/flutter/lib/src/material/contained_button.dart
Normal file
425
packages/flutter/lib/src/material/contained_button.dart
Normal file
@ -0,0 +1,425 @@
|
||||
// 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],
|
||||
);
|
||||
}
|
||||
}
|
127
packages/flutter/lib/src/material/contained_button_theme.dart
Normal file
127
packages/flutter/lib/src/material/contained_button_theme.dart
Normal file
@ -0,0 +1,127 @@
|
||||
// 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)';
|
||||
}
|
||||
|
349
packages/flutter/lib/src/material/outlined_button.dart
Normal file
349
packages/flutter/lib/src/material/outlined_button.dart
Normal file
@ -0,0 +1,349 @@
|
||||
// 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],
|
||||
);
|
||||
}
|
||||
}
|
127
packages/flutter/lib/src/material/outlined_button_theme.dart
Normal file
127
packages/flutter/lib/src/material/outlined_button_theme.dart
Normal file
@ -0,0 +1,127 @@
|
||||
// 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;
|
||||
}
|
387
packages/flutter/lib/src/material/text_button.dart
Normal file
387
packages/flutter/lib/src/material/text_button.dart
Normal file
@ -0,0 +1,387 @@
|
||||
// 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],
|
||||
);
|
||||
}
|
||||
}
|
127
packages/flutter/lib/src/material/text_button_theme.dart
Normal file
127
packages/flutter/lib/src/material/text_button_theme.dart
Normal file
@ -0,0 +1,127 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
153
packages/flutter/test/material/button_style_test.dart
Normal file
153
packages/flutter/test/material/button_style_test.dart
Normal file
@ -0,0 +1,153 @@
|
||||
// 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())
|
||||
);
|
||||
});
|
||||
}
|
930
packages/flutter/test/material/contained_button_test.dart
Normal file
930
packages/flutter/test/material/contained_button_test.dart
Normal file
@ -0,0 +1,930 @@
|
||||
// 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;
|
||||
}
|
182
packages/flutter/test/material/contained_button_theme_test.dart
Normal file
182
packages/flutter/test/material/contained_button_theme_test.dart
Normal file
@ -0,0 +1,182 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
1204
packages/flutter/test/material/outlined_button_test.dart
Normal file
1204
packages/flutter/test/material/outlined_button_test.dart
Normal file
File diff suppressed because it is too large
Load Diff
183
packages/flutter/test/material/outlined_button_theme_test.dart
Normal file
183
packages/flutter/test/material/outlined_button_theme_test.dart
Normal file
@ -0,0 +1,183 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
980
packages/flutter/test/material/text_button_test.dart
Normal file
980
packages/flutter/test/material/text_button_test.dart
Normal file
@ -0,0 +1,980 @@
|
||||
// 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;
|
||||
}
|
180
packages/flutter/test/material/text_button_theme_test.dart
Normal file
180
packages/flutter/test/material/text_button_theme_test.dart
Normal file
@ -0,0 +1,180 @@
|
||||
// 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));
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user