Skip to content

Commit 32081aa

Browse files
authored
[CupertinoActionSheet] Match colors to native (flutter#149568)
This PR matches the various colors of `CupertinoActionSheet` more closely with the native one. The following colors are changed. * Sheet background color * Pressed button color * Cancel button color * Pressed cancel button color * Divider color * Content text color The resulting colors match with native one with deviation of at most 1 (in terms of 0~255 RGB). The following are comparison (left to right: Native, Flutter after PR, Flutter current) <img width="1295" alt="image" src="https://github.com/flutter/flutter/assets/1596656/3703a4a8-a856-42b1-9395-a6e14b1881ca"> <img width="1268" alt="image" src="https://github.com/flutter/flutter/assets/1596656/1eb9964e-41f1-414a-99ae-0a2e7da8d3fd"> _Note: The divider thickness is adjusted to `1/dpr` instead of 0.3 in both Flutter version to make them look more native, as will be proposed in flutter#149636 ### Derivation All the colors are derived through color picker and calculation. The algorithm is as followed: * Assume all colors are translucent grey colors, i.e. having the same value `x` for R, G, and B, with an alpha `a`. * Given the barrier color is `x_B1=0` when the background is black, and `x_B2=204` when the background is white. * Pick the target color `x_t1` when the background is black, and `x_t2` when the background is white * Solve the following equations for `x` and `a` ``` a * x + (1-a) * x_B1 = x_t1 a * x + (1-a) * x_B2 = x_t2 a = 1 - (x_t1 - x_t2) / (x_B1 - x_B2) x = (x_t1 - (1-a) * x_B1) / a ``` These equations use a linear model for color composition, which might not be exact, but is close enough for an accuracy of (1/255). The full table is as follows: <img width="1091" alt="image" src="https://github.com/flutter/flutter/assets/1596656/0fb76291-c3cc-4bb5-aefa-03ac6ac9bf1f"> * The first two columns are colors picked from XCode. * The 3~4 columns are the colors picked from the current Flutter. Notice the deviation, which is sometimes drastic. * The 5~6 columns are the colors picked from Flutter after this PR. The deviation is at most 1. * The last few columns are calculation. * There are two rows whose calculation is based on adjusted numbers, since the original results are not accurate enough, possibly due to the linear composition. During the calculation, I assumed these colors vary between light and dark modes, but it turns out that both modes use the same set of colors. ### Screenshots
1 parent f380842 commit 32081aa

File tree

2 files changed

+128
-21
lines changed

2 files changed

+128
-21
lines changed

packages/flutter/lib/src/cupertino/dialog.dart

+21-21
Original file line numberDiff line numberDiff line change
@@ -104,35 +104,35 @@ const Color _kDialogColor = CupertinoDynamicColor.withBrightness(
104104
// Translucent light gray that is painted on top of the blurred backdrop as the
105105
// background color of a pressed button.
106106
// Eyeballed from iOS 13 beta simulator.
107-
const Color _kPressedColor = CupertinoDynamicColor.withBrightness(
107+
const Color _kDialogPressedColor = CupertinoDynamicColor.withBrightness(
108108
color: Color(0xFFE1E1E1),
109109
darkColor: Color(0xFF2E2E2E),
110110
);
111111

112-
const Color _kActionSheetCancelPressedColor = CupertinoDynamicColor.withBrightness(
113-
color: Color(0xFFECECEC),
114-
darkColor: Color(0xFF49494B),
115-
);
112+
// Translucent light gray that is painted on top of the blurred backdrop as the
113+
// background color of a pressed button.
114+
// Eyeballed from iOS 17 simulator.
115+
const Color _kActionSheetPressedColor = Color(0xCAE0E0E0);
116+
117+
const Color _kActionSheetCancelColor = Color(0xFFFFFFFF);
118+
const Color _kActionSheetCancelPressedColor = Color(0xFFECECEC);
116119

117120
// Translucent, very light gray that is painted on top of the blurred backdrop
118121
// as the action sheet's background color.
119122
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/39272. Use
120123
// System Materials once we have them.
121-
// Extracted from https://developer.apple.com/design/resources/.
122-
const Color _kActionSheetBackgroundColor = CupertinoDynamicColor.withBrightness(
123-
color: Color(0xC7F9F9F9),
124-
darkColor: Color(0xC7252525),
125-
);
124+
// Eyeballed from iOS 17 simulator.
125+
const Color _kActionSheetBackgroundColor = Color(0xC8FCFCFC);
126126

127127
// The gray color used for text that appears in the title area.
128-
// Extracted from https://developer.apple.com/design/resources/.
129-
const Color _kActionSheetContentTextColor = Color(0xFF8F8F8F);
128+
// Eyeballed from iOS 17 simulator.
129+
const Color _kActionSheetContentTextColor = Color(0x851D1D1D);
130130

131131
// Translucent gray that is painted on top of the blurred backdrop in the gap
132132
// areas between the content section and actions section, as well as between
133133
// buttons.
134-
// Eye-balled from iOS 13 beta simulator.
135-
const Color _kActionSheetButtonDividerColor = _kActionSheetContentTextColor;
134+
// Eyeballed from iOS 17 simulator.
135+
const Color _kActionSheetButtonDividerColor = Color(0xD4C9C9C9);
136136

137137
// The alert dialog layout policy changes depending on whether the user is using
138138
// a "regular" font size vs a "large" font size. This is a spectrum. There are
@@ -1115,19 +1115,19 @@ class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackgrou
11151115
BorderRadius? borderRadius;
11161116
if (!widget.isCancel) {
11171117
backgroundColor = isBeingPressed
1118-
? _kPressedColor
1119-
: CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context);
1118+
? _kActionSheetPressedColor
1119+
: _kActionSheetBackgroundColor;
11201120
} else {
11211121
backgroundColor = isBeingPressed
1122-
? _kActionSheetCancelPressedColor
1123-
: CupertinoColors.secondarySystemGroupedBackground;
1122+
? _kActionSheetCancelPressedColor
1123+
: _kActionSheetCancelColor;
11241124
borderRadius = const BorderRadius.all(Radius.circular(_kCornerRadius));
11251125
}
11261126
return MetaData(
11271127
metaData: this,
11281128
child: Container(
11291129
decoration: BoxDecoration(
1130-
color: backgroundColor,
1130+
color: CupertinoDynamicColor.resolve(backgroundColor, context),
11311131
borderRadius: borderRadius,
11321132
),
11331133
child: widget.child,
@@ -2269,7 +2269,7 @@ class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget {
22692269
: _kCupertinoDialogWidth,
22702270
dividerThickness: _dividerThickness,
22712271
dialogColor: CupertinoDynamicColor.resolve(_kDialogColor, context),
2272-
dialogPressedColor: CupertinoDynamicColor.resolve(_kPressedColor, context),
2272+
dialogPressedColor: CupertinoDynamicColor.resolve(_kDialogPressedColor, context),
22732273
dividerColor: CupertinoDynamicColor.resolve(CupertinoColors.separator, context),
22742274
hasCancelButton: _hasCancelButton,
22752275
);
@@ -2283,7 +2283,7 @@ class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget {
22832283
: _kCupertinoDialogWidth
22842284
..dividerThickness = _dividerThickness
22852285
..dialogColor = CupertinoDynamicColor.resolve(_kDialogColor, context)
2286-
..dialogPressedColor = CupertinoDynamicColor.resolve(_kPressedColor, context)
2286+
..dialogPressedColor = CupertinoDynamicColor.resolve(_kDialogPressedColor, context)
22872287
..dividerColor = CupertinoDynamicColor.resolve(CupertinoColors.separator, context)
22882288
..hasCancelButton = _hasCancelButton;
22892289
}

packages/flutter/test/cupertino/action_sheet_test.dart

+107
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,69 @@ import 'package:flutter_test/flutter_test.dart';
1818
import '../widgets/semantics_tester.dart';
1919

2020
void main() {
21+
testWidgets('Overall looks correctly under light theme', (WidgetTester tester) async {
22+
await tester.pumpWidget(
23+
TestScaffoldApp(
24+
theme: const CupertinoThemeData(brightness: Brightness.light),
25+
actionSheet: CupertinoActionSheet(
26+
message: const Text('The title'),
27+
actions: <Widget>[
28+
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
29+
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
30+
],
31+
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
32+
),
33+
),
34+
);
35+
36+
await tester.tap(find.text('Go'));
37+
await tester.pumpAndSettle();
38+
39+
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('One')));
40+
// This golden file also verifies the structure of an action sheet that
41+
// has a message, no title, and no overscroll for any sections (in contrast
42+
// to cupertinoActionSheet.dark-theme.png).
43+
await expectLater(
44+
find.byType(CupertinoApp),
45+
matchesGoldenFile('cupertinoActionSheet.overall-light-theme.png'),
46+
);
47+
48+
await gesture.up();
49+
});
50+
51+
testWidgets('Overall looks correctly under dark theme', (WidgetTester tester) async {
52+
await tester.pumpWidget(
53+
TestScaffoldApp(
54+
theme: const CupertinoThemeData(brightness: Brightness.dark),
55+
actionSheet: CupertinoActionSheet(
56+
title: const Text('The title'),
57+
message: const Text('The message'),
58+
actions: List<Widget>.generate(20, (int i) =>
59+
CupertinoActionSheetAction(
60+
onPressed: () {},
61+
child: Text('Button $i'),
62+
),
63+
),
64+
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
65+
),
66+
),
67+
);
68+
69+
await tester.tap(find.text('Go'));
70+
await tester.pumpAndSettle();
71+
72+
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button 0')));
73+
// This golden file also verifies the structure of an action sheet that
74+
// has both a message and a title, and an overscrolled action section (in
75+
// contrast to cupertinoActionSheet.light-theme.png).
76+
await expectLater(
77+
find.byType(CupertinoApp),
78+
matchesGoldenFile('cupertinoActionSheet.overall-dark-theme.png'),
79+
);
80+
81+
await gesture.up();
82+
});
83+
2184
testWidgets('Verify that a tap on modal barrier dismisses an action sheet', (WidgetTester tester) async {
2285
await tester.pumpWidget(
2386
createAppWithButtonThatLaunchesActionSheet(
@@ -1675,6 +1738,50 @@ Widget createAppWithButtonThatLaunchesActionSheet(Widget actionSheet) {
16751738
);
16761739
}
16771740

1741+
// Shows an app that has a button with text "Go", and clicking this button
1742+
// displays the `actionSheet` and hides the button.
1743+
//
1744+
// The `theme` will be applied to the app and determines the background.
1745+
class TestScaffoldApp extends StatefulWidget {
1746+
const TestScaffoldApp({super.key, required this.theme, required this.actionSheet});
1747+
final CupertinoThemeData theme;
1748+
final Widget actionSheet;
1749+
1750+
@override
1751+
TestScaffoldAppState createState() => TestScaffoldAppState();
1752+
}
1753+
1754+
class TestScaffoldAppState extends State<TestScaffoldApp> {
1755+
bool _pressedButton = false;
1756+
1757+
@override
1758+
Widget build(BuildContext context) {
1759+
return CupertinoApp(
1760+
theme: widget.theme,
1761+
home: Builder(builder: (BuildContext context) =>
1762+
CupertinoPageScaffold(
1763+
child: Center(
1764+
child: _pressedButton ? Container() : CupertinoButton(
1765+
onPressed: () {
1766+
setState(() {
1767+
_pressedButton = true;
1768+
});
1769+
showCupertinoModalPopup<void>(
1770+
context: context,
1771+
builder: (BuildContext context) {
1772+
return widget.actionSheet;
1773+
},
1774+
);
1775+
},
1776+
child: const Text('Go'),
1777+
),
1778+
),
1779+
),
1780+
),
1781+
);
1782+
}
1783+
}
1784+
16781785
Widget boilerplate(Widget child) {
16791786
return Directionality(
16801787
textDirection: TextDirection.ltr,

0 commit comments

Comments
 (0)