Skip to content

Commit

Permalink
[CupertinoActionSheet] Match colors to native (flutter#149568)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dkwingsmt authored Jun 8, 2024
1 parent f380842 commit 32081aa
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 21 deletions.
42 changes: 21 additions & 21 deletions packages/flutter/lib/src/cupertino/dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,35 +104,35 @@ const Color _kDialogColor = CupertinoDynamicColor.withBrightness(
// Translucent light gray that is painted on top of the blurred backdrop as the
// background color of a pressed button.
// Eyeballed from iOS 13 beta simulator.
const Color _kPressedColor = CupertinoDynamicColor.withBrightness(
const Color _kDialogPressedColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFFE1E1E1),
darkColor: Color(0xFF2E2E2E),
);

const Color _kActionSheetCancelPressedColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFFECECEC),
darkColor: Color(0xFF49494B),
);
// Translucent light gray that is painted on top of the blurred backdrop as the
// background color of a pressed button.
// Eyeballed from iOS 17 simulator.
const Color _kActionSheetPressedColor = Color(0xCAE0E0E0);

const Color _kActionSheetCancelColor = Color(0xFFFFFFFF);
const Color _kActionSheetCancelPressedColor = Color(0xFFECECEC);

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

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

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

// The alert dialog layout policy changes depending on whether the user is using
// a "regular" font size vs a "large" font size. This is a spectrum. There are
Expand Down Expand Up @@ -1115,19 +1115,19 @@ class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackgrou
BorderRadius? borderRadius;
if (!widget.isCancel) {
backgroundColor = isBeingPressed
? _kPressedColor
: CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context);
? _kActionSheetPressedColor
: _kActionSheetBackgroundColor;
} else {
backgroundColor = isBeingPressed
? _kActionSheetCancelPressedColor
: CupertinoColors.secondarySystemGroupedBackground;
? _kActionSheetCancelPressedColor
: _kActionSheetCancelColor;
borderRadius = const BorderRadius.all(Radius.circular(_kCornerRadius));
}
return MetaData(
metaData: this,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
color: CupertinoDynamicColor.resolve(backgroundColor, context),
borderRadius: borderRadius,
),
child: widget.child,
Expand Down Expand Up @@ -2269,7 +2269,7 @@ class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget {
: _kCupertinoDialogWidth,
dividerThickness: _dividerThickness,
dialogColor: CupertinoDynamicColor.resolve(_kDialogColor, context),
dialogPressedColor: CupertinoDynamicColor.resolve(_kPressedColor, context),
dialogPressedColor: CupertinoDynamicColor.resolve(_kDialogPressedColor, context),
dividerColor: CupertinoDynamicColor.resolve(CupertinoColors.separator, context),
hasCancelButton: _hasCancelButton,
);
Expand All @@ -2283,7 +2283,7 @@ class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget {
: _kCupertinoDialogWidth
..dividerThickness = _dividerThickness
..dialogColor = CupertinoDynamicColor.resolve(_kDialogColor, context)
..dialogPressedColor = CupertinoDynamicColor.resolve(_kPressedColor, context)
..dialogPressedColor = CupertinoDynamicColor.resolve(_kDialogPressedColor, context)
..dividerColor = CupertinoDynamicColor.resolve(CupertinoColors.separator, context)
..hasCancelButton = _hasCancelButton;
}
Expand Down
107 changes: 107 additions & 0 deletions packages/flutter/test/cupertino/action_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,69 @@ import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';

void main() {
testWidgets('Overall looks correctly under light theme', (WidgetTester tester) async {
await tester.pumpWidget(
TestScaffoldApp(
theme: const CupertinoThemeData(brightness: Brightness.light),
actionSheet: CupertinoActionSheet(
message: const Text('The title'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);

await tester.tap(find.text('Go'));
await tester.pumpAndSettle();

final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('One')));
// This golden file also verifies the structure of an action sheet that
// has a message, no title, and no overscroll for any sections (in contrast
// to cupertinoActionSheet.dark-theme.png).
await expectLater(
find.byType(CupertinoApp),
matchesGoldenFile('cupertinoActionSheet.overall-light-theme.png'),
);

await gesture.up();
});

testWidgets('Overall looks correctly under dark theme', (WidgetTester tester) async {
await tester.pumpWidget(
TestScaffoldApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
actionSheet: CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: List<Widget>.generate(20, (int i) =>
CupertinoActionSheetAction(
onPressed: () {},
child: Text('Button $i'),
),
),
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);

await tester.tap(find.text('Go'));
await tester.pumpAndSettle();

final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button 0')));
// This golden file also verifies the structure of an action sheet that
// has both a message and a title, and an overscrolled action section (in
// contrast to cupertinoActionSheet.light-theme.png).
await expectLater(
find.byType(CupertinoApp),
matchesGoldenFile('cupertinoActionSheet.overall-dark-theme.png'),
);

await gesture.up();
});

testWidgets('Verify that a tap on modal barrier dismisses an action sheet', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Expand Down Expand Up @@ -1675,6 +1738,50 @@ Widget createAppWithButtonThatLaunchesActionSheet(Widget actionSheet) {
);
}

// Shows an app that has a button with text "Go", and clicking this button
// displays the `actionSheet` and hides the button.
//
// The `theme` will be applied to the app and determines the background.
class TestScaffoldApp extends StatefulWidget {
const TestScaffoldApp({super.key, required this.theme, required this.actionSheet});
final CupertinoThemeData theme;
final Widget actionSheet;

@override
TestScaffoldAppState createState() => TestScaffoldAppState();
}

class TestScaffoldAppState extends State<TestScaffoldApp> {
bool _pressedButton = false;

@override
Widget build(BuildContext context) {
return CupertinoApp(
theme: widget.theme,
home: Builder(builder: (BuildContext context) =>
CupertinoPageScaffold(
child: Center(
child: _pressedButton ? Container() : CupertinoButton(
onPressed: () {
setState(() {
_pressedButton = true;
});
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return widget.actionSheet;
},
);
},
child: const Text('Go'),
),
),
),
),
);
}
}

Widget boilerplate(Widget child) {
return Directionality(
textDirection: TextDirection.ltr,
Expand Down

0 comments on commit 32081aa

Please sign in to comment.