Implemented CupertinoButton new styles/sizes (fixes #92525) (#152845)

This PR fixes #92525 and introduces the following changes according to [latest iOS HIG](https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS):

- `CupertinoButton` now has a `size` property (type `enum CupertinoButtonSize`, values sm/md/lg, default `lg`) that allows the devs to apply new iOS 15+ button styles
- Previously `CupertinoButton` had a larger padding when no background color was specified. With the new HIG, that is no longer the case
- `CupertinoButton` now has a `.tinted` constructor that renders a translucent background (transparency % is brightness-dependent) and uses a different foreground color compared to `.filled`
- `CupertinoButton` now uses the `actionTextStyle` TextStyle from the given theme
- `CupertinoButton`'s child IconTheme's size will always be x1.2 the given TextStyle's size
- `CupertinoTextThemeData` now has a `actionSmallTextStyle` property to use with small buttons (including a default `_kDefaultActionSmallTextStyle` TextStyle)

Preview & example:

![image](https://github.com/user-attachments/assets/0985eb19-c091-41f5-bd98-0de196b7e403)

> **NOTE**: there is a discrepancy in dark mode button foreground color between the default CupertinoTheme and the HIG. A separate issue will be opened for this.

~EDIT: issue reported here https://github.com/flutter/flutter/issues/152846~
EDIT2: fixed by #153039 !

![image](https://github.com/user-attachments/assets/d671d7b4-bb2f-4b38-9464-ee1b04927304)

## Example
```dart
import 'package:flutter/cupertino.dart';

const Widget body = Row(
  mainAxisSize: MainAxisSize.min,
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Icon(
      CupertinoIcons.play_fill,
    ),
    Text("Play"),
  ],
);

void main() =>
  runApp(
    CupertinoApp(
      home: Container(
        child: Wrap(
        direction: Axis.horizontal,
        children: <Widget>[
          // header
          Text(''),
          Text('Plain'),
          Text('Grey'),
          Text('Tinted'),
          Text('Filled'),
          // small
          Text('Small'),
          CupertinoButton(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.small,
          ),
          CupertinoButton.tinted(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.small,
            color: CupertinoColors.systemGrey,
          ),
          CupertinoButton.tinted(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.small,
          ),
          CupertinoButton.filled(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.small,
          ),
          // medium
          Text('Medium'),
          CupertinoButton(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.medium,
          ),
          CupertinoButton.tinted(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.medium,
            color: CupertinoColors.systemGrey,
          ),
          CupertinoButton.tinted(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.medium,
          ),
          CupertinoButton.filled(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.medium,
          ),
          // large
          Text('Large'),
          CupertinoButton(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.large,
          ),
          CupertinoButton.tinted(
            child: body,
            onPressed: () {},
            color: CupertinoColors.systemGrey,
            size: CupertinoButtonSize.large,
          ),
          CupertinoButton.tinted(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.large,
          ),
          CupertinoButton.filled(
            child: body,
            onPressed: () {},
            size: CupertinoButtonSize.large,
          ),
        ].map((Widget w) => SizedBox(width: 110, height: 70, child: Center(child: w))).toList(),
      ),
      )
    ),
  );

```

*List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.*

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
This commit is contained in:
Jamie Kerber 2024-08-12 21:26:39 +02:00 committed by GitHub
parent 2f0415f37b
commit dae3a87d93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 314 additions and 62 deletions

View File

@ -63,7 +63,6 @@ class _EditableTextToolbarBuilderExampleAppState extends State<EditableTextToolb
// buttons depending on the platform. // buttons depending on the platform.
children: editableTextState.contextMenuButtonItems.map((ContextMenuButtonItem buttonItem) { children: editableTextState.contextMenuButtonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoButton( return CupertinoButton(
borderRadius: null,
color: const Color(0xffaaaa00), color: const Color(0xffaaaa00),
disabledColor: const Color(0xffaaaaff), disabledColor: const Color(0xffaaaaff),
onPressed: buttonItem.onPressed, onPressed: buttonItem.onPressed,

View File

@ -10,14 +10,33 @@ import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'constants.dart'; import 'constants.dart';
import 'text_theme.dart';
import 'theme.dart'; import 'theme.dart';
// Measured against iOS 12 in Xcode. // Measured against iOS (17) [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS).
const EdgeInsets _kButtonPadding = EdgeInsets.all(16.0);
const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric( /// The size of a [CupertinoButton].
vertical: 14.0, /// Based on the iOS (17) [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS).
horizontal: 64.0, enum CupertinoButtonSize {
); /// Displays a smaller button with round sides and smaller text (uses [CupertinoTextThemeData.actionSmallTextStyle]).
small,
/// Displays a medium sized button with round sides and regular-sized text.
medium,
/// Displays a (classic) large button with rounded edges and regular-sized text.
large,
}
/// The style of a [CupertinoButton] that changes the style of the button's background.
///
/// Based on the iOS Human Interface Guidelines (https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS).
enum _CupertinoButtonStyle {
/// No background or border, primary foreground color.
plain,
/// Translucent background, primary foreground color.
tinted,
/// Solid background, contrasting foreground color.
filled,
}
/// An iOS-style button. /// An iOS-style button.
/// ///
@ -48,12 +67,13 @@ class CupertinoButton extends StatefulWidget {
const CupertinoButton({ const CupertinoButton({
super.key, super.key,
required this.child, required this.child,
this.sizeStyle = CupertinoButtonSize.large,
this.padding, this.padding,
this.color, this.color,
this.disabledColor = CupertinoColors.quaternarySystemFill, this.disabledColor = CupertinoColors.quaternarySystemFill,
this.minSize = kMinInteractiveDimensionCupertino, this.minSize,
this.pressedOpacity = 0.4, this.pressedOpacity = 0.4,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)), this.borderRadius,
this.alignment = Alignment.center, this.alignment = Alignment.center,
this.focusColor, this.focusColor,
this.focusNode, this.focusNode,
@ -61,7 +81,34 @@ class CupertinoButton extends StatefulWidget {
this.autofocus = false, this.autofocus = false,
required this.onPressed, required this.onPressed,
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)), }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
_filled = false; _style = _CupertinoButtonStyle.plain;
/// Creates an iOS-style button with a tinted background.
///
/// The background color is derived from the [CupertinoTheme]'s `primaryColor` + transparency.
/// The foreground color is the [CupertinoTheme]'s `primaryColor`.
///
/// To specify a custom background color, use the [color] argument of the
/// default constructor.
///
/// To match the iOS "grey" button style, set [color] to [CupertinoColors.systemGrey].
const CupertinoButton.tinted({
super.key,
required this.child,
this.sizeStyle = CupertinoButtonSize.large,
this.padding,
this.color,
this.disabledColor = CupertinoColors.tertiarySystemFill,
this.minSize,
this.pressedOpacity = 0.4,
this.borderRadius,
this.alignment = Alignment.center,
this.focusColor,
this.focusNode,
this.onFocusChange,
this.autofocus = false,
required this.onPressed,
}) : _style = _CupertinoButtonStyle.tinted;
/// Creates an iOS-style button with a filled background. /// Creates an iOS-style button with a filled background.
/// ///
@ -72,11 +119,12 @@ class CupertinoButton extends StatefulWidget {
const CupertinoButton.filled({ const CupertinoButton.filled({
super.key, super.key,
required this.child, required this.child,
this.sizeStyle = CupertinoButtonSize.large,
this.padding, this.padding,
this.disabledColor = CupertinoColors.quaternarySystemFill, this.disabledColor = CupertinoColors.tertiarySystemFill,
this.minSize = kMinInteractiveDimensionCupertino, this.minSize,
this.pressedOpacity = 0.4, this.pressedOpacity = 0.4,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)), this.borderRadius,
this.alignment = Alignment.center, this.alignment = Alignment.center,
this.focusColor, this.focusColor,
this.focusNode, this.focusNode,
@ -85,7 +133,7 @@ class CupertinoButton extends StatefulWidget {
required this.onPressed, required this.onPressed,
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)), }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
color = null, color = null,
_filled = true; _style = _CupertinoButtonStyle.filled;
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
@ -133,9 +181,14 @@ class CupertinoButton extends StatefulWidget {
/// The radius of the button's corners when it has a background color. /// The radius of the button's corners when it has a background color.
/// ///
/// Defaults to round corners of 8 logical pixels. /// Defaults to [kCupertinoButtonSizeBorderRadius], based on [sizeStyle].
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
/// The size of the button.
///
/// Defaults to [CupertinoButtonSize.large].
final CupertinoButtonSize sizeStyle;
/// The alignment of the button's [child]. /// The alignment of the button's [child].
/// ///
/// Typically buttons are sized to be just big enough to contain the child and its /// Typically buttons are sized to be just big enough to contain the child and its
@ -166,7 +219,7 @@ class CupertinoButton extends StatefulWidget {
/// {@macro flutter.widgets.Focus.autofocus} /// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus; final bool autofocus;
final bool _filled; final _CupertinoButtonStyle _style;
/// Whether the button is enabled or disabled. Buttons are disabled by default. To /// Whether the button is enabled or disabled. Buttons are disabled by default. To
/// enable a button, set its [onPressed] property to a non-null value. /// enable a button, set its [onPressed] property to a non-null value.
@ -273,15 +326,24 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
final bool enabled = widget.enabled; final bool enabled = widget.enabled;
final CupertinoThemeData themeData = CupertinoTheme.of(context); final CupertinoThemeData themeData = CupertinoTheme.of(context);
final Color primaryColor = themeData.primaryColor; final Color primaryColor = themeData.primaryColor;
final Color? backgroundColor = widget.color == null final Color? backgroundColor = (
? (widget._filled ? primaryColor : null) widget.color == null
: CupertinoDynamicColor.maybeResolve(widget.color, context); ? widget._style != _CupertinoButtonStyle.plain
? primaryColor
final Color foregroundColor = backgroundColor != null : null
: CupertinoDynamicColor.maybeResolve(widget.color, context)
)?.withOpacity(
widget._style == _CupertinoButtonStyle.tinted
? CupertinoTheme.brightnessOf(context) == Brightness.light
? kCupertinoButtonTintedOpacityLight
: kCupertinoButtonTintedOpacityDark
: widget.color?.opacity ?? 1.0,
);
final Color foregroundColor = widget._style == _CupertinoButtonStyle.filled
? themeData.primaryContrastingColor ? themeData.primaryContrastingColor
: enabled : enabled
? primaryColor ? primaryColor
: CupertinoDynamicColor.resolve(CupertinoColors.placeholderText, context); : CupertinoDynamicColor.resolve(CupertinoColors.tertiaryLabel, context);
final Color effectiveFocusOutlineColor = widget.focusColor ?? final Color effectiveFocusOutlineColor = widget.focusColor ??
HSLColor HSLColor
@ -291,8 +353,17 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
.withSaturation(kCupertinoFocusColorSaturation) .withSaturation(kCupertinoFocusColorSaturation)
.toColor(); .toColor();
final TextStyle textStyle = themeData.textTheme.textStyle.copyWith(color: foregroundColor); final TextStyle textStyle = (
final IconThemeData iconTheme = IconTheme.of(context).copyWith(color: foregroundColor); widget.sizeStyle == CupertinoButtonSize.small
? themeData.textTheme.actionSmallTextStyle
: themeData.textTheme.actionTextStyle
).copyWith(color: foregroundColor);
final IconThemeData iconTheme = IconTheme.of(context).copyWith(
color: foregroundColor,
size: textStyle.fontSize != null
? textStyle.fontSize! * 1.2
: kCupertinoButtonDefaultIconSize,
);
return MouseRegion( return MouseRegion(
cursor: enabled && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer, cursor: enabled && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
@ -311,12 +382,10 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
child: Semantics( child: Semantics(
button: true, button: true,
child: ConstrainedBox( child: ConstrainedBox(
constraints: widget.minSize == null constraints: BoxConstraints(
? const BoxConstraints() minWidth: widget.minSize ?? kCupertinoButtonMinSize[widget.sizeStyle] ?? kMinInteractiveDimensionCupertino,
: BoxConstraints( minHeight: widget.minSize ?? kCupertinoButtonMinSize[widget.sizeStyle] ?? kMinInteractiveDimensionCupertino,
minWidth: widget.minSize!, ),
minHeight: widget.minSize!,
),
child: FadeTransition( child: FadeTransition(
opacity: _opacityAnimation, opacity: _opacityAnimation,
child: DecoratedBox( child: DecoratedBox(
@ -330,15 +399,13 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
), ),
) )
: null, : null,
borderRadius: widget.borderRadius, borderRadius: widget.borderRadius ?? kCupertinoButtonSizeBorderRadius[widget.sizeStyle],
color: backgroundColor != null && !enabled color: backgroundColor != null && !enabled
? CupertinoDynamicColor.resolve(widget.disabledColor, context) ? CupertinoDynamicColor.resolve(widget.disabledColor, context)
: backgroundColor, : backgroundColor,
), ),
child: Padding( child: Padding(
padding: widget.padding ?? (backgroundColor != null padding: widget.padding ?? kCupertinoButtonPadding[widget.sizeStyle]!,
? _kBackgroundButtonPadding
: _kButtonPadding),
child: Align( child: Align(
alignment: widget.alignment, alignment: widget.alignment,
widthFactor: 1.0, widthFactor: 1.0,

View File

@ -5,6 +5,10 @@
/// @docImport 'package:flutter/material.dart'; /// @docImport 'package:flutter/material.dart';
library; library;
import 'package:flutter/widgets.dart';
import 'button.dart';
/// The minimum dimension of any interactive region according to the iOS Human /// The minimum dimension of any interactive region according to the iOS Human
/// Interface Guidelines. /// Interface Guidelines.
/// ///
@ -31,3 +35,55 @@ const double kMinInteractiveDimensionCupertino = 44.0;
const double kCupertinoFocusColorOpacity = 0.80, const double kCupertinoFocusColorOpacity = 0.80,
kCupertinoFocusColorBrightness = 0.69, kCupertinoFocusColorBrightness = 0.69,
kCupertinoFocusColorSaturation = 0.835; kCupertinoFocusColorSaturation = 0.835;
/// Opacity values for the background of a [CupertinoButton.tinted].
///
/// See also:
///
/// * <https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS>
const double kCupertinoButtonTintedOpacityLight = 0.12,
kCupertinoButtonTintedOpacityDark = 0.26;
/// The default value for [IconThemeData.size] of [CupertinoButton.child].
///
/// Set to match the most-frequent size of icons in iOS (matches md/lg).
///
/// Used only when the [CupertinoTextThemeData.actionTextStyle] or [CupertinoTextThemeData.actionSmallTextStyle]
/// has a null [TextStyle.fontSize].
const double kCupertinoButtonDefaultIconSize = 20.0;
/// The padding values for the different [CupertinoButtonSize]s.
///
/// Based on the iOS (17) [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS).
const Map<CupertinoButtonSize, EdgeInsetsGeometry> kCupertinoButtonPadding = <CupertinoButtonSize, EdgeInsetsGeometry>{
CupertinoButtonSize.small: EdgeInsets.symmetric(
vertical: 6,
horizontal: 12,
),
CupertinoButtonSize.medium: EdgeInsets.symmetric(
vertical: 10,
horizontal: 15,
),
CupertinoButtonSize.large: EdgeInsets.symmetric(
vertical: 16,
horizontal: 20,
),
};
/// The border radius values for the different [CupertinoButtonSize]s.
///
/// Based on the iOS (17) [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS).
final Map<CupertinoButtonSize, BorderRadius> kCupertinoButtonSizeBorderRadius = <CupertinoButtonSize, BorderRadius>{
CupertinoButtonSize.small: BorderRadius.circular(40),
CupertinoButtonSize.medium: BorderRadius.circular(40),
CupertinoButtonSize.large: BorderRadius.circular(12),
};
/// The minimum size of a [CupertinoButton] based on the [CupertinoButtonSize].
///
/// Based on the iOS (17) [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS).
const Map<CupertinoButtonSize, double> kCupertinoButtonMinSize = <CupertinoButtonSize, double>{
CupertinoButtonSize.small: 28,
CupertinoButtonSize.medium: 32,
CupertinoButtonSize.large: 44,
};

View File

@ -133,7 +133,6 @@ class _CupertinoTextSelectionToolbarButtonState extends State<CupertinoTextSelec
color: isPressed color: isPressed
? _kToolbarPressedColor.resolveFrom(context) ? _kToolbarPressedColor.resolveFrom(context)
: CupertinoColors.transparent, : CupertinoColors.transparent,
borderRadius: null,
disabledColor: CupertinoColors.transparent, disabledColor: CupertinoColors.transparent,
// This CupertinoButton does not actually handle the onPressed callback, // This CupertinoButton does not actually handle the onPressed callback,
// this is only here to correctly enable/disable the button (see // this is only here to correctly enable/disable the button (see

View File

@ -30,6 +30,7 @@ const TextStyle _kDefaultTextStyle = TextStyle(
// field. // field.
// //
// Values derived from https://developer.apple.com/design/resources/. // Values derived from https://developer.apple.com/design/resources/.
// See [iOS 17 + iPadOS 17 UI Kit](https://www.figma.com/community/file/1248375255495415511) for details.
const TextStyle _kDefaultActionTextStyle = TextStyle( const TextStyle _kDefaultActionTextStyle = TextStyle(
inherit: false, inherit: false,
fontFamily: 'CupertinoSystemText', fontFamily: 'CupertinoSystemText',
@ -39,6 +40,21 @@ const TextStyle _kDefaultActionTextStyle = TextStyle(
decoration: TextDecoration.none, decoration: TextDecoration.none,
); );
// Please update _TextThemeDefaultsBuilder accordingly after changing the default
// color here, as their implementation depends on the default value of the color
// field.
//
// Values derived from https://developer.apple.com/design/resources/.
// See [iOS 17 + iPadOS 17 UI Kit](https://www.figma.com/community/file/1248375255495415511) for details.
const TextStyle _kDefaultActionSmallTextStyle = TextStyle(
inherit: false,
fontFamily: 'CupertinoSystemText',
fontSize: 15.0,
letterSpacing: -0.23,
color: CupertinoColors.activeBlue,
decoration: TextDecoration.none,
);
// Please update _TextThemeDefaultsBuilder accordingly after changing the default // Please update _TextThemeDefaultsBuilder accordingly after changing the default
// color here, as their implementation depends on the default value of the color // color here, as their implementation depends on the default value of the color
// field. // field.
@ -131,6 +147,7 @@ class CupertinoTextThemeData with Diagnosticable {
Color primaryColor = CupertinoColors.systemBlue, Color primaryColor = CupertinoColors.systemBlue,
TextStyle? textStyle, TextStyle? textStyle,
TextStyle? actionTextStyle, TextStyle? actionTextStyle,
TextStyle? actionSmallTextStyle,
TextStyle? tabLabelTextStyle, TextStyle? tabLabelTextStyle,
TextStyle? navTitleTextStyle, TextStyle? navTitleTextStyle,
TextStyle? navLargeTitleTextStyle, TextStyle? navLargeTitleTextStyle,
@ -142,6 +159,7 @@ class CupertinoTextThemeData with Diagnosticable {
primaryColor, primaryColor,
textStyle, textStyle,
actionTextStyle, actionTextStyle,
actionSmallTextStyle,
tabLabelTextStyle, tabLabelTextStyle,
navTitleTextStyle, navTitleTextStyle,
navLargeTitleTextStyle, navLargeTitleTextStyle,
@ -155,6 +173,7 @@ class CupertinoTextThemeData with Diagnosticable {
this._primaryColor, this._primaryColor,
this._textStyle, this._textStyle,
this._actionTextStyle, this._actionTextStyle,
this._actionSmallTextStyle,
this._tabLabelTextStyle, this._tabLabelTextStyle,
this._navTitleTextStyle, this._navTitleTextStyle,
this._navLargeTitleTextStyle, this._navLargeTitleTextStyle,
@ -176,6 +195,12 @@ class CupertinoTextThemeData with Diagnosticable {
return _actionTextStyle ?? _defaults.actionTextStyle(primaryColor: _primaryColor); return _actionTextStyle ?? _defaults.actionTextStyle(primaryColor: _primaryColor);
} }
final TextStyle? _actionSmallTextStyle;
/// The [TextStyle] of interactive text content such as text in a small button.
TextStyle get actionSmallTextStyle {
return _actionSmallTextStyle ?? _defaults.actionSmallTextStyle(primaryColor: _primaryColor);
}
final TextStyle? _tabLabelTextStyle; final TextStyle? _tabLabelTextStyle;
/// The [TextStyle] of unselected tabs. /// The [TextStyle] of unselected tabs.
TextStyle get tabLabelTextStyle => _tabLabelTextStyle ?? _defaults.tabLabelTextStyle; TextStyle get tabLabelTextStyle => _tabLabelTextStyle ?? _defaults.tabLabelTextStyle;
@ -216,6 +241,7 @@ class CupertinoTextThemeData with Diagnosticable {
CupertinoDynamicColor.maybeResolve(_primaryColor, context), CupertinoDynamicColor.maybeResolve(_primaryColor, context),
_resolveTextStyle(_textStyle, context), _resolveTextStyle(_textStyle, context),
_resolveTextStyle(_actionTextStyle, context), _resolveTextStyle(_actionTextStyle, context),
_resolveTextStyle(_actionSmallTextStyle, context),
_resolveTextStyle(_tabLabelTextStyle, context), _resolveTextStyle(_tabLabelTextStyle, context),
_resolveTextStyle(_navTitleTextStyle, context), _resolveTextStyle(_navTitleTextStyle, context),
_resolveTextStyle(_navLargeTitleTextStyle, context), _resolveTextStyle(_navLargeTitleTextStyle, context),
@ -231,6 +257,7 @@ class CupertinoTextThemeData with Diagnosticable {
Color? primaryColor, Color? primaryColor,
TextStyle? textStyle, TextStyle? textStyle,
TextStyle? actionTextStyle, TextStyle? actionTextStyle,
TextStyle? actionSmallTextStyle,
TextStyle? tabLabelTextStyle, TextStyle? tabLabelTextStyle,
TextStyle? navTitleTextStyle, TextStyle? navTitleTextStyle,
TextStyle? navLargeTitleTextStyle, TextStyle? navLargeTitleTextStyle,
@ -243,6 +270,7 @@ class CupertinoTextThemeData with Diagnosticable {
primaryColor ?? _primaryColor, primaryColor ?? _primaryColor,
textStyle ?? _textStyle, textStyle ?? _textStyle,
actionTextStyle ?? _actionTextStyle, actionTextStyle ?? _actionTextStyle,
actionSmallTextStyle ?? _actionSmallTextStyle,
tabLabelTextStyle ?? _tabLabelTextStyle, tabLabelTextStyle ?? _tabLabelTextStyle,
navTitleTextStyle ?? _navTitleTextStyle, navTitleTextStyle ?? _navTitleTextStyle,
navLargeTitleTextStyle ?? _navLargeTitleTextStyle, navLargeTitleTextStyle ?? _navLargeTitleTextStyle,
@ -258,6 +286,7 @@ class CupertinoTextThemeData with Diagnosticable {
const CupertinoTextThemeData defaultData = CupertinoTextThemeData(); const CupertinoTextThemeData defaultData = CupertinoTextThemeData();
properties.add(DiagnosticsProperty<TextStyle>('textStyle', textStyle, defaultValue: defaultData.textStyle)); properties.add(DiagnosticsProperty<TextStyle>('textStyle', textStyle, defaultValue: defaultData.textStyle));
properties.add(DiagnosticsProperty<TextStyle>('actionTextStyle', actionTextStyle, defaultValue: defaultData.actionTextStyle)); properties.add(DiagnosticsProperty<TextStyle>('actionTextStyle', actionTextStyle, defaultValue: defaultData.actionTextStyle));
properties.add(DiagnosticsProperty<TextStyle>('actionSmallTextStyle', actionSmallTextStyle, defaultValue: defaultData.actionSmallTextStyle));
properties.add(DiagnosticsProperty<TextStyle>('tabLabelTextStyle', tabLabelTextStyle, defaultValue: defaultData.tabLabelTextStyle)); properties.add(DiagnosticsProperty<TextStyle>('tabLabelTextStyle', tabLabelTextStyle, defaultValue: defaultData.tabLabelTextStyle));
properties.add(DiagnosticsProperty<TextStyle>('navTitleTextStyle', navTitleTextStyle, defaultValue: defaultData.navTitleTextStyle)); properties.add(DiagnosticsProperty<TextStyle>('navTitleTextStyle', navTitleTextStyle, defaultValue: defaultData.navTitleTextStyle));
properties.add(DiagnosticsProperty<TextStyle>('navLargeTitleTextStyle', navLargeTitleTextStyle, defaultValue: defaultData.navLargeTitleTextStyle)); properties.add(DiagnosticsProperty<TextStyle>('navLargeTitleTextStyle', navLargeTitleTextStyle, defaultValue: defaultData.navLargeTitleTextStyle));
@ -279,6 +308,7 @@ class CupertinoTextThemeData with Diagnosticable {
&& other._primaryColor == _primaryColor && other._primaryColor == _primaryColor
&& other._textStyle == _textStyle && other._textStyle == _textStyle
&& other._actionTextStyle == _actionTextStyle && other._actionTextStyle == _actionTextStyle
&& other._actionSmallTextStyle == _actionSmallTextStyle
&& other._tabLabelTextStyle == _tabLabelTextStyle && other._tabLabelTextStyle == _tabLabelTextStyle
&& other._navTitleTextStyle == _navTitleTextStyle && other._navTitleTextStyle == _navTitleTextStyle
&& other._navLargeTitleTextStyle == _navLargeTitleTextStyle && other._navLargeTitleTextStyle == _navLargeTitleTextStyle
@ -293,6 +323,7 @@ class CupertinoTextThemeData with Diagnosticable {
_primaryColor, _primaryColor,
_textStyle, _textStyle,
_actionTextStyle, _actionTextStyle,
_actionSmallTextStyle,
_tabLabelTextStyle, _tabLabelTextStyle,
_navTitleTextStyle, _navTitleTextStyle,
_navLargeTitleTextStyle, _navLargeTitleTextStyle,
@ -327,6 +358,7 @@ class _TextThemeDefaultsBuilder {
TextStyle get dateTimePickerTextStyle => _applyLabelColor(_kDefaultDateTimePickerTextStyle, labelColor); TextStyle get dateTimePickerTextStyle => _applyLabelColor(_kDefaultDateTimePickerTextStyle, labelColor);
TextStyle actionTextStyle({ Color? primaryColor }) => _kDefaultActionTextStyle.copyWith(color: primaryColor); TextStyle actionTextStyle({ Color? primaryColor }) => _kDefaultActionTextStyle.copyWith(color: primaryColor);
TextStyle actionSmallTextStyle({ Color? primaryColor }) => _kDefaultActionSmallTextStyle.copyWith(color: primaryColor);
TextStyle navActionTextStyle({ Color? primaryColor }) => actionTextStyle(primaryColor: primaryColor); TextStyle navActionTextStyle({ Color? primaryColor }) => actionTextStyle(primaryColor: primaryColor);
_TextThemeDefaultsBuilder resolveFrom(BuildContext context) { _TextThemeDefaultsBuilder resolveFrom(BuildContext context) {

View File

@ -27,8 +27,8 @@ void main() {
final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton));
expect( expect(
buttonBox.size, buttonBox.size,
// 1 10px character + 16px * 2 is smaller than the default 44px minimum. // 1 10px character + 20px * 2 = 50.0
const Size.square(44.0), const Size(50.0, 44.0),
); );
}); });
@ -44,7 +44,7 @@ void main() {
final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton));
expect( expect(
buttonBox.size, buttonBox.size,
// 1 10px character + 16px * 2 is smaller than defined 60.0px minimum // 1 10px character + 20px * 2 = 50.0 (is smaller than minSize: 60.0)
const Size.square(minSize), const Size.square(minSize),
); );
}); });
@ -59,8 +59,8 @@ void main() {
final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton));
expect( expect(
buttonBox.size.width, buttonBox.size.width,
// 4 10px character + 16px * 2 = 72. // 4 10px character + 20px * 2 = 80.0
72.0, 80.0,
); );
}); });
@ -129,17 +129,37 @@ void main() {
expect(align.alignment, Alignment.centerLeft); expect(align.alignment, Alignment.centerLeft);
}); });
testWidgets('Button with background is wider', (WidgetTester tester) async { testWidgets('Button size changes depending on size property', (WidgetTester tester) async {
const Widget child = Text('X', style: testStyle);
await tester.pumpWidget(boilerplate(child: const CupertinoButton( await tester.pumpWidget(boilerplate(child: const CupertinoButton(
onPressed: null, onPressed: null,
color: Color(0xFFFFFFFF), sizeStyle: CupertinoButtonSize.small,
child: Text('X', style: testStyle), child: child,
))); )));
final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton));
expect( expect(
buttonBox.size.width, buttonBox.size,
// 1 10px character + 64 * 2 = 138 for buttons with background. const Size(34.0, 28.0)
138.0, );
await tester.pumpWidget(boilerplate(child: const CupertinoButton(
onPressed: null,
sizeStyle: CupertinoButtonSize.medium,
child: child,
)));
expect(
buttonBox.size,
const Size(40.0, 32.0),
);
await tester.pumpWidget(boilerplate(child: const CupertinoButton(
onPressed: null,
child: child,
)));
expect(
buttonBox.size,
const Size(50.0, 44.0),
); );
}); });
@ -404,8 +424,27 @@ void main() {
), ),
), ),
); );
expect(textStyle.color, isSameColorAs(CupertinoColors.activeBlue));
await tester.pumpWidget(
CupertinoApp(
home: CupertinoButton.tinted(
onPressed: () { },
child: Builder(builder: (BuildContext context) {
textStyle = DefaultTextStyle.of(context).style;
return const Placeholder();
}),
),
),
);
expect(textStyle.color, CupertinoColors.activeBlue); expect(textStyle.color, CupertinoColors.activeBlue);
BoxDecoration decoration = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoButton),
matching: find.byType(DecoratedBox),
),
).decoration as BoxDecoration;
expect(decoration.color, isSameColorAs(CupertinoColors.activeBlue.withOpacity(0.12)));
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
@ -418,15 +457,14 @@ void main() {
), ),
), ),
); );
expect(textStyle.color, isSameColorAs(CupertinoColors.white)); expect(textStyle.color, isSameColorAs(CupertinoColors.white));
BoxDecoration decoration = tester.widget<DecoratedBox>( decoration = tester.widget<DecoratedBox>(
find.descendant( find.descendant(
of: find.byType(CupertinoButton), of: find.byType(CupertinoButton),
matching: find.byType(DecoratedBox), matching: find.byType(DecoratedBox),
), ),
).decoration as BoxDecoration; ).decoration as BoxDecoration;
expect(decoration.color, CupertinoColors.activeBlue); expect(decoration.color, isSameColorAs(CupertinoColors.activeBlue));
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
@ -442,6 +480,27 @@ void main() {
); );
expect(textStyle.color, isSameColorAs(CupertinoColors.systemBlue.darkColor)); expect(textStyle.color, isSameColorAs(CupertinoColors.systemBlue.darkColor));
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: CupertinoButton.tinted(
onPressed: () { },
child: Builder(builder: (BuildContext context) {
textStyle = DefaultTextStyle.of(context).style;
return const Placeholder();
}),
),
),
);
expect(textStyle.color, isSameColorAs(CupertinoColors.systemBlue.darkColor));
decoration = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoButton),
matching: find.byType(DecoratedBox),
),
).decoration as BoxDecoration;
expect(decoration.color, isSameColorAs(CupertinoColors.activeBlue.darkColor.withOpacity(0.26)));
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark), theme: const CupertinoThemeData(brightness: Brightness.dark),
@ -464,6 +523,14 @@ void main() {
expect(decoration.color, isSameColorAs(CupertinoColors.systemBlue.darkColor)); expect(decoration.color, isSameColorAs(CupertinoColors.systemBlue.darkColor));
}); });
testWidgets("All CupertinoButton const maps keys' match the available style sizes", (WidgetTester tester) async {
for (final CupertinoButtonSize size in CupertinoButtonSize.values) {
expect(kCupertinoButtonPadding[size], isNotNull);
expect(kCupertinoButtonSizeBorderRadius[size], isNotNull);
expect(kCupertinoButtonMinSize[size], isNotNull);
}
});
testWidgets('Hovering over Cupertino button updates cursor to clickable on Web', (WidgetTester tester) async { testWidgets('Hovering over Cupertino button updates cursor to clickable on Web', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
@ -612,32 +679,57 @@ void main() {
expect(focusNode.hasFocus, isFalse); expect(focusNode.hasFocus, isFalse);
}); });
testWidgets('IconThemeData is not replaced by CupertinoButton', (WidgetTester tester) async { testWidgets('IconThemeData falls back to default value when the TextStyle has a null size', (WidgetTester tester) async {
const IconThemeData givenIconTheme = IconThemeData(size: 12.0); const IconThemeData defaultIconTheme = IconThemeData(size: kCupertinoButtonDefaultIconSize);
IconThemeData? actualIconTheme; IconThemeData? actualIconTheme;
// Large size.
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
theme: const CupertinoThemeData(
textTheme: CupertinoTextThemeData(
actionTextStyle: TextStyle(),
),
),
home: Center( home: Center(
child: IconTheme( child: CupertinoButton(
data: givenIconTheme, onPressed: () {},
child: CupertinoButton( child: Builder(
onPressed: () {}, builder: (BuildContext context) {
child: Builder( actualIconTheme = IconTheme.of(context);
builder: (BuildContext context) {
actualIconTheme = IconTheme.of(context);
return const Placeholder(); return const Placeholder();
} }
),
), ),
), ),
), ),
), ),
); );
expect(actualIconTheme?.size, defaultIconTheme.size);
expect(actualIconTheme?.size, givenIconTheme.size); // Small size.
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(
textTheme: CupertinoTextThemeData(
actionSmallTextStyle: TextStyle(),
),
),
home: Center(
child: CupertinoButton(
onPressed: () {},
child: Builder(
builder: (BuildContext context) {
actualIconTheme = IconTheme.of(context);
return const Placeholder();
}
),
),
),
),
);
}); });
} }

View File

@ -29,6 +29,12 @@ void main() {
expect(theme.actionTextStyle.letterSpacing, -0.41); expect(theme.actionTextStyle.letterSpacing, -0.41);
expect(theme.actionTextStyle.fontWeight, null); expect(theme.actionTextStyle.fontWeight, null);
// ActionSmallTextStyle 15 -0.23 (aka "Subheadline/Regular")
expect(theme.actionSmallTextStyle.fontSize, 15);
expect(theme.actionSmallTextStyle.fontFamily, 'CupertinoSystemText');
expect(theme.actionSmallTextStyle.letterSpacing, -0.23);
expect(theme.actionSmallTextStyle.fontWeight, null);
// TextStyle 17 -0.41 // TextStyle 17 -0.41
expect(theme.tabLabelTextStyle.fontSize, 10); expect(theme.tabLabelTextStyle.fontSize, 10);
expect(theme.tabLabelTextStyle.fontFamily, 'CupertinoSystemText'); expect(theme.tabLabelTextStyle.fontFamily, 'CupertinoSystemText');

View File

@ -189,6 +189,7 @@ void main() {
'applyThemeToAll', 'applyThemeToAll',
'textStyle', 'textStyle',
'actionTextStyle', 'actionTextStyle',
'actionSmallTextStyle',
'tabLabelTextStyle', 'tabLabelTextStyle',
'navTitleTextStyle', 'navTitleTextStyle',
'navLargeTitleTextStyle', 'navLargeTitleTextStyle',