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.
children: editableTextState.contextMenuButtonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoButton(
borderRadius: null,
color: const Color(0xffaaaa00),
disabledColor: const Color(0xffaaaaff),
onPressed: buttonItem.onPressed,

View File

@ -10,14 +10,33 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'text_theme.dart';
import 'theme.dart';
// Measured against iOS 12 in Xcode.
const EdgeInsets _kButtonPadding = EdgeInsets.all(16.0);
const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric(
vertical: 14.0,
horizontal: 64.0,
);
// Measured against iOS (17) [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS).
/// The size of a [CupertinoButton].
/// Based on the iOS (17) [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/buttons#iOS-iPadOS).
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.
///
@ -48,12 +67,13 @@ class CupertinoButton extends StatefulWidget {
const CupertinoButton({
super.key,
required this.child,
this.sizeStyle = CupertinoButtonSize.large,
this.padding,
this.color,
this.disabledColor = CupertinoColors.quaternarySystemFill,
this.minSize = kMinInteractiveDimensionCupertino,
this.minSize,
this.pressedOpacity = 0.4,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
this.borderRadius,
this.alignment = Alignment.center,
this.focusColor,
this.focusNode,
@ -61,7 +81,34 @@ class CupertinoButton extends StatefulWidget {
this.autofocus = false,
required this.onPressed,
}) : 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.
///
@ -72,11 +119,12 @@ class CupertinoButton extends StatefulWidget {
const CupertinoButton.filled({
super.key,
required this.child,
this.sizeStyle = CupertinoButtonSize.large,
this.padding,
this.disabledColor = CupertinoColors.quaternarySystemFill,
this.minSize = kMinInteractiveDimensionCupertino,
this.disabledColor = CupertinoColors.tertiarySystemFill,
this.minSize,
this.pressedOpacity = 0.4,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
this.borderRadius,
this.alignment = Alignment.center,
this.focusColor,
this.focusNode,
@ -85,7 +133,7 @@ class CupertinoButton extends StatefulWidget {
required this.onPressed,
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
color = null,
_filled = true;
_style = _CupertinoButtonStyle.filled;
/// 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.
///
/// Defaults to round corners of 8 logical pixels.
/// Defaults to [kCupertinoButtonSizeBorderRadius], based on [sizeStyle].
final BorderRadius? borderRadius;
/// The size of the button.
///
/// Defaults to [CupertinoButtonSize.large].
final CupertinoButtonSize sizeStyle;
/// The alignment of the button's [child].
///
/// 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}
final bool autofocus;
final bool _filled;
final _CupertinoButtonStyle _style;
/// 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.
@ -273,15 +326,24 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
final bool enabled = widget.enabled;
final CupertinoThemeData themeData = CupertinoTheme.of(context);
final Color primaryColor = themeData.primaryColor;
final Color? backgroundColor = widget.color == null
? (widget._filled ? primaryColor : null)
: CupertinoDynamicColor.maybeResolve(widget.color, context);
final Color foregroundColor = backgroundColor != null
final Color? backgroundColor = (
widget.color == null
? widget._style != _CupertinoButtonStyle.plain
? primaryColor
: 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
: enabled
? primaryColor
: CupertinoDynamicColor.resolve(CupertinoColors.placeholderText, context);
: CupertinoDynamicColor.resolve(CupertinoColors.tertiaryLabel, context);
final Color effectiveFocusOutlineColor = widget.focusColor ??
HSLColor
@ -291,8 +353,17 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
.withSaturation(kCupertinoFocusColorSaturation)
.toColor();
final TextStyle textStyle = themeData.textTheme.textStyle.copyWith(color: foregroundColor);
final IconThemeData iconTheme = IconTheme.of(context).copyWith(color: foregroundColor);
final TextStyle textStyle = (
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(
cursor: enabled && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
@ -311,12 +382,10 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
child: Semantics(
button: true,
child: ConstrainedBox(
constraints: widget.minSize == null
? const BoxConstraints()
: BoxConstraints(
minWidth: widget.minSize!,
minHeight: widget.minSize!,
),
constraints: BoxConstraints(
minWidth: widget.minSize ?? kCupertinoButtonMinSize[widget.sizeStyle] ?? kMinInteractiveDimensionCupertino,
minHeight: widget.minSize ?? kCupertinoButtonMinSize[widget.sizeStyle] ?? kMinInteractiveDimensionCupertino,
),
child: FadeTransition(
opacity: _opacityAnimation,
child: DecoratedBox(
@ -330,15 +399,13 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
),
)
: null,
borderRadius: widget.borderRadius,
borderRadius: widget.borderRadius ?? kCupertinoButtonSizeBorderRadius[widget.sizeStyle],
color: backgroundColor != null && !enabled
? CupertinoDynamicColor.resolve(widget.disabledColor, context)
: backgroundColor,
),
child: Padding(
padding: widget.padding ?? (backgroundColor != null
? _kBackgroundButtonPadding
: _kButtonPadding),
padding: widget.padding ?? kCupertinoButtonPadding[widget.sizeStyle]!,
child: Align(
alignment: widget.alignment,
widthFactor: 1.0,

View File

@ -5,6 +5,10 @@
/// @docImport 'package:flutter/material.dart';
library;
import 'package:flutter/widgets.dart';
import 'button.dart';
/// The minimum dimension of any interactive region according to the iOS Human
/// Interface Guidelines.
///
@ -31,3 +35,55 @@ const double kMinInteractiveDimensionCupertino = 44.0;
const double kCupertinoFocusColorOpacity = 0.80,
kCupertinoFocusColorBrightness = 0.69,
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
? _kToolbarPressedColor.resolveFrom(context)
: CupertinoColors.transparent,
borderRadius: null,
disabledColor: CupertinoColors.transparent,
// This CupertinoButton does not actually handle the onPressed callback,
// this is only here to correctly enable/disable the button (see

View File

@ -30,6 +30,7 @@ const TextStyle _kDefaultTextStyle = TextStyle(
// 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 _kDefaultActionTextStyle = TextStyle(
inherit: false,
fontFamily: 'CupertinoSystemText',
@ -39,6 +40,21 @@ const TextStyle _kDefaultActionTextStyle = TextStyle(
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
// color here, as their implementation depends on the default value of the color
// field.
@ -131,6 +147,7 @@ class CupertinoTextThemeData with Diagnosticable {
Color primaryColor = CupertinoColors.systemBlue,
TextStyle? textStyle,
TextStyle? actionTextStyle,
TextStyle? actionSmallTextStyle,
TextStyle? tabLabelTextStyle,
TextStyle? navTitleTextStyle,
TextStyle? navLargeTitleTextStyle,
@ -142,6 +159,7 @@ class CupertinoTextThemeData with Diagnosticable {
primaryColor,
textStyle,
actionTextStyle,
actionSmallTextStyle,
tabLabelTextStyle,
navTitleTextStyle,
navLargeTitleTextStyle,
@ -155,6 +173,7 @@ class CupertinoTextThemeData with Diagnosticable {
this._primaryColor,
this._textStyle,
this._actionTextStyle,
this._actionSmallTextStyle,
this._tabLabelTextStyle,
this._navTitleTextStyle,
this._navLargeTitleTextStyle,
@ -176,6 +195,12 @@ class CupertinoTextThemeData with Diagnosticable {
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;
/// The [TextStyle] of unselected tabs.
TextStyle get tabLabelTextStyle => _tabLabelTextStyle ?? _defaults.tabLabelTextStyle;
@ -216,6 +241,7 @@ class CupertinoTextThemeData with Diagnosticable {
CupertinoDynamicColor.maybeResolve(_primaryColor, context),
_resolveTextStyle(_textStyle, context),
_resolveTextStyle(_actionTextStyle, context),
_resolveTextStyle(_actionSmallTextStyle, context),
_resolveTextStyle(_tabLabelTextStyle, context),
_resolveTextStyle(_navTitleTextStyle, context),
_resolveTextStyle(_navLargeTitleTextStyle, context),
@ -231,6 +257,7 @@ class CupertinoTextThemeData with Diagnosticable {
Color? primaryColor,
TextStyle? textStyle,
TextStyle? actionTextStyle,
TextStyle? actionSmallTextStyle,
TextStyle? tabLabelTextStyle,
TextStyle? navTitleTextStyle,
TextStyle? navLargeTitleTextStyle,
@ -243,6 +270,7 @@ class CupertinoTextThemeData with Diagnosticable {
primaryColor ?? _primaryColor,
textStyle ?? _textStyle,
actionTextStyle ?? _actionTextStyle,
actionSmallTextStyle ?? _actionSmallTextStyle,
tabLabelTextStyle ?? _tabLabelTextStyle,
navTitleTextStyle ?? _navTitleTextStyle,
navLargeTitleTextStyle ?? _navLargeTitleTextStyle,
@ -258,6 +286,7 @@ class CupertinoTextThemeData with Diagnosticable {
const CupertinoTextThemeData defaultData = CupertinoTextThemeData();
properties.add(DiagnosticsProperty<TextStyle>('textStyle', textStyle, defaultValue: defaultData.textStyle));
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>('navTitleTextStyle', navTitleTextStyle, defaultValue: defaultData.navTitleTextStyle));
properties.add(DiagnosticsProperty<TextStyle>('navLargeTitleTextStyle', navLargeTitleTextStyle, defaultValue: defaultData.navLargeTitleTextStyle));
@ -279,6 +308,7 @@ class CupertinoTextThemeData with Diagnosticable {
&& other._primaryColor == _primaryColor
&& other._textStyle == _textStyle
&& other._actionTextStyle == _actionTextStyle
&& other._actionSmallTextStyle == _actionSmallTextStyle
&& other._tabLabelTextStyle == _tabLabelTextStyle
&& other._navTitleTextStyle == _navTitleTextStyle
&& other._navLargeTitleTextStyle == _navLargeTitleTextStyle
@ -293,6 +323,7 @@ class CupertinoTextThemeData with Diagnosticable {
_primaryColor,
_textStyle,
_actionTextStyle,
_actionSmallTextStyle,
_tabLabelTextStyle,
_navTitleTextStyle,
_navLargeTitleTextStyle,
@ -327,6 +358,7 @@ class _TextThemeDefaultsBuilder {
TextStyle get dateTimePickerTextStyle => _applyLabelColor(_kDefaultDateTimePickerTextStyle, labelColor);
TextStyle actionTextStyle({ Color? primaryColor }) => _kDefaultActionTextStyle.copyWith(color: primaryColor);
TextStyle actionSmallTextStyle({ Color? primaryColor }) => _kDefaultActionSmallTextStyle.copyWith(color: primaryColor);
TextStyle navActionTextStyle({ Color? primaryColor }) => actionTextStyle(primaryColor: primaryColor);
_TextThemeDefaultsBuilder resolveFrom(BuildContext context) {

View File

@ -27,8 +27,8 @@ void main() {
final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton));
expect(
buttonBox.size,
// 1 10px character + 16px * 2 is smaller than the default 44px minimum.
const Size.square(44.0),
// 1 10px character + 20px * 2 = 50.0
const Size(50.0, 44.0),
);
});
@ -44,7 +44,7 @@ void main() {
final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton));
expect(
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),
);
});
@ -59,8 +59,8 @@ void main() {
final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton));
expect(
buttonBox.size.width,
// 4 10px character + 16px * 2 = 72.
72.0,
// 4 10px character + 20px * 2 = 80.0
80.0,
);
});
@ -129,17 +129,37 @@ void main() {
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(
onPressed: null,
color: Color(0xFFFFFFFF),
child: Text('X', style: testStyle),
sizeStyle: CupertinoButtonSize.small,
child: child,
)));
final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton));
expect(
buttonBox.size.width,
// 1 10px character + 64 * 2 = 138 for buttons with background.
138.0,
buttonBox.size,
const Size(34.0, 28.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);
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(
CupertinoApp(
@ -418,15 +457,14 @@ void main() {
),
),
);
expect(textStyle.color, isSameColorAs(CupertinoColors.white));
BoxDecoration decoration = tester.widget<DecoratedBox>(
decoration = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoButton),
matching: find.byType(DecoratedBox),
),
).decoration as BoxDecoration;
expect(decoration.color, CupertinoColors.activeBlue);
expect(decoration.color, isSameColorAs(CupertinoColors.activeBlue));
await tester.pumpWidget(
CupertinoApp(
@ -442,6 +480,27 @@ void main() {
);
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(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
@ -464,6 +523,14 @@ void main() {
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 {
await tester.pumpWidget(
CupertinoApp(
@ -612,32 +679,57 @@ void main() {
expect(focusNode.hasFocus, isFalse);
});
testWidgets('IconThemeData is not replaced by CupertinoButton', (WidgetTester tester) async {
const IconThemeData givenIconTheme = IconThemeData(size: 12.0);
testWidgets('IconThemeData falls back to default value when the TextStyle has a null size', (WidgetTester tester) async {
const IconThemeData defaultIconTheme = IconThemeData(size: kCupertinoButtonDefaultIconSize);
IconThemeData? actualIconTheme;
// Large size.
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(
textTheme: CupertinoTextThemeData(
actionTextStyle: TextStyle(),
),
),
home: Center(
child: IconTheme(
data: givenIconTheme,
child: CupertinoButton(
onPressed: () {},
child: Builder(
builder: (BuildContext context) {
actualIconTheme = IconTheme.of(context);
child: CupertinoButton(
onPressed: () {},
child: Builder(
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.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
expect(theme.tabLabelTextStyle.fontSize, 10);
expect(theme.tabLabelTextStyle.fontFamily, 'CupertinoSystemText');

View File

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