Skip to content

Commit 88e6f62

Browse files
authored
[CupertinoActionSheet] Fix the layout (part 1) (#149636)
This PR fixes the general layout of `CupertinoActionSheet` to match the native behavior. This PR adjusts the height of buttons, the height of the content section, the gap between the cancel button and the main sheet, and most importantly, the maximum height of the action sheet. The maximum height is the trickiest part. I tried to figure out a rule, and found that the top padding only depends the type of the device - notch-less, notch, capsule - but there isn't a clear rule that can unify the 3 padding numbers. This PR uses linear interpolation as a heuristic algorithm. See the in-code comment for details. * What about iPad? Well, action sheets look completely different on iPad, more similar to a drop down menu. This might be fixed in the future. ### Tests Among all the test changes, there are a few tests that have been converted to using `AnimationSheetRecorder` to verify the animation changes. Before the PR they were checking the height at each from, which is hard to reason whether a change makes sense, and hard to modify if anything needs changing. ### Result demo The following images compares native(left) with Flutter after PR (right) by stacking them closely, and show that their layout really match almost pixel perfect. <img width="455" alt="image" src="https://github.com/flutter/flutter/assets/1596656/f8be35bd-0da5-4908-92f7-7a1f4e999229"> _No notch (iPhone 13)_ <img width="405" alt="image" src="https://github.com/flutter/flutter/assets/1596656/54a37c2f-cd99-4e3b-86f0-045b1dfdbbb8"> _Notch (iPhone 13)_ <img width="385" alt="image" src="https://github.com/flutter/flutter/assets/1596656/546ab529-0b62-4e3d-9019-ef900d3552e5"> _Capsule (iPhone 15 Plus)_ <img width="1142" alt="image" src="https://github.com/flutter/flutter/assets/1596656/e06b6dac-dbcd-48f7-9dee-83700ae680e0"> _iPhone 13 landscape_ <img width="999" alt="image" src="https://github.com/flutter/flutter/assets/1596656/698cf530-51fc-4906-90a5-7a3ab626f489"> _All "capsule" devices share the same top padding in logical pixels (iPhone 15 Pro Max, iPhone 15 Pro, iPhone 15 Plus)_
1 parent 4a84fb0 commit 88e6f62

File tree

2 files changed

+303
-126
lines changed

2 files changed

+303
-126
lines changed

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

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,12 @@ const double _kDialogMinButtonHeight = 45.0;
8686
const double _kDialogMinButtonFontSize = 10.0;
8787

8888
// ActionSheet specific constants.
89-
const double _kActionSheetEdgeHorizontalPadding = 8.0;
89+
const double _kActionSheetEdgePadding = 8.0;
9090
const double _kActionSheetCancelButtonPadding = 8.0;
91-
const double _kActionSheetEdgeVerticalPadding = 10.0;
9291
const double _kActionSheetContentHorizontalPadding = 16.0;
93-
const double _kActionSheetContentVerticalPadding = 12.0;
94-
const double _kActionSheetButtonHeight = 56.0;
95-
const double _kActionSheetActionsSectionMinHeight = 84.3;
92+
const double _kActionSheetContentVerticalPadding = 13.5;
93+
const double _kActionSheetButtonHeight = 57.0;
94+
const double _kActionSheetActionsSectionMinHeight = 84.0;
9695

9796
// A translucent color that is painted on top of the blurred backdrop as the
9897
// dialog's background color
@@ -915,10 +914,89 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
915914
);
916915
}
917916

917+
// Given data point (x1, y1) and (x2, y2), derive the y corresponding to x
918+
// using linear interpolation between the two data points, and extrapolates
919+
// flatly beyond these points.
920+
//
921+
// (x2, y2)
922+
// _____________
923+
// /
924+
// /
925+
// _________/
926+
// (x1, y1)
927+
static double _lerp(double x, double x1, double y1, double x2, double y2) {
928+
if (x <= x1) {
929+
return y1;
930+
} else if (x >= x2) {
931+
return y2;
932+
} else {
933+
return Tween<double>(begin: y1, end: y2).transform(
934+
(x - x1) / (x2 - x1)
935+
);
936+
}
937+
}
938+
939+
// Derive the top padding, which is the distance between the top of a
940+
// full-height action sheet and the top of the safe area.
941+
//
942+
// The algorithm and its values are derived from measuring on the simulator.
943+
double _topPadding(BuildContext context) {
944+
if (MediaQuery.orientationOf(context) == Orientation.landscape) {
945+
return _kActionSheetEdgePadding;
946+
}
947+
948+
// The top padding in portrait mode is in general close to the top view
949+
// padding, but not always equal:
950+
//
951+
// | view padding | action sheet padding | ratio
952+
// No notch (eg. iPhone SE) | 20.0 | 20.0 | 1.0
953+
// Notch (eg. iPhone 13) | 47.0 | 47.0 | 1.0
954+
// Capsule (eg. iPhone 15) | 59.0 | 54.0 | 0.915
955+
//
956+
// Currently, we cannot determine why the result changes on "capsules."
957+
// Therefore, we'll hard code this rule, given the limited types of actual
958+
// devices. To provide an algorithm that accepts arbitrary view padding, this
959+
// function calculates the ratio as a continuous curve with linear
960+
// interpolation.
961+
962+
// The x for lerp is the top view padding, while the y is ratio of
963+
// action sheet padding versus top view padding.
964+
const double viewPaddingData1 = 47.0;
965+
const double paddingRatioData1 = 1.0;
966+
const double viewPaddingData2 = 59.0;
967+
const double paddingRatioData2 = 54.0 / 59.0;
968+
969+
final double currentViewPadding = MediaQuery.viewPaddingOf(context).top;
970+
971+
final double currentPaddingRatio = _lerp(
972+
/* x= */currentViewPadding,
973+
/* x1, y1= */viewPaddingData1, paddingRatioData1,
974+
/* x2, y2= */viewPaddingData2, paddingRatioData2,
975+
);
976+
final double padding = (currentPaddingRatio * currentViewPadding).roundToDouble();
977+
// In case there is no view padding, there should still be some space
978+
// between the action sheet and the edge.
979+
return math.max(padding, _kDialogEdgePadding);
980+
}
981+
918982
@override
919983
Widget build(BuildContext context) {
920984
assert(debugCheckHasMediaQuery(context));
921985

986+
/*
987+
* ╭─────────────────╮ ↑ ↑
988+
* │ The title │ Content section |
989+
* │ The message │ ↓ |
990+
* ├─────────────────┤ ↑ Main sheet
991+
* │ Action 1 │ | |
992+
* ├─────────────────┤ Actions section |
993+
* │ Action 2 │ | |
994+
* ╰─────────────────╯ ↓ ↓
995+
* ╭─────────────────╮
996+
* │ Cancel │
997+
* ╰─────────────────╯
998+
*/
999+
9221000
final List<Widget> children = <Widget>[
9231001
Flexible(
9241002
child: ClipRRect(
@@ -943,6 +1021,7 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
9431021
};
9441022

9451023
return SafeArea(
1024+
minimum: const EdgeInsets.only(bottom: _kActionSheetEdgePadding),
9461025
child: ScrollConfiguration(
9471026
// A CupertinoScrollbar is built-in below
9481027
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
@@ -954,12 +1033,15 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
9541033
child: CupertinoUserInterfaceLevel(
9551034
data: CupertinoUserInterfaceLevelData.elevated,
9561035
child: Padding(
957-
padding: const EdgeInsets.symmetric(
958-
horizontal: _kActionSheetEdgeHorizontalPadding,
959-
vertical: _kActionSheetEdgeVerticalPadding,
1036+
padding: EdgeInsets.only(
1037+
left: _kActionSheetEdgePadding,
1038+
right: _kActionSheetEdgePadding,
1039+
top: _topPadding(context),
1040+
// The bottom padding is set on SafeArea.minimum, allowing it to
1041+
// be consumed by bottom view padding.
9601042
),
9611043
child: SizedBox(
962-
width: actionSheetWidth - _kActionSheetEdgeHorizontalPadding * 2,
1044+
width: actionSheetWidth - _kActionSheetEdgePadding * 2,
9631045
child: _ActionSheetGestureDetector(
9641046
child: Semantics(
9651047
explicitChildNodes: true,

0 commit comments

Comments
 (0)