[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)_
This commit is contained in:
Tong Mu 2024-06-22 10:24:39 -07:00 committed by GitHub
parent 4a84fb0fea
commit 88e6f62974
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 303 additions and 126 deletions

View file

@ -86,13 +86,12 @@ const double _kDialogMinButtonHeight = 45.0;
const double _kDialogMinButtonFontSize = 10.0;
// ActionSheet specific constants.
const double _kActionSheetEdgeHorizontalPadding = 8.0;
const double _kActionSheetEdgePadding = 8.0;
const double _kActionSheetCancelButtonPadding = 8.0;
const double _kActionSheetEdgeVerticalPadding = 10.0;
const double _kActionSheetContentHorizontalPadding = 16.0;
const double _kActionSheetContentVerticalPadding = 12.0;
const double _kActionSheetButtonHeight = 56.0;
const double _kActionSheetActionsSectionMinHeight = 84.3;
const double _kActionSheetContentVerticalPadding = 13.5;
const double _kActionSheetButtonHeight = 57.0;
const double _kActionSheetActionsSectionMinHeight = 84.0;
// A translucent color that is painted on top of the blurred backdrop as the
// dialog's background color
@ -915,10 +914,89 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
);
}
// Given data point (x1, y1) and (x2, y2), derive the y corresponding to x
// using linear interpolation between the two data points, and extrapolates
// flatly beyond these points.
//
// (x2, y2)
// _____________
// /
// /
// _________/
// (x1, y1)
static double _lerp(double x, double x1, double y1, double x2, double y2) {
if (x <= x1) {
return y1;
} else if (x >= x2) {
return y2;
} else {
return Tween<double>(begin: y1, end: y2).transform(
(x - x1) / (x2 - x1)
);
}
}
// Derive the top padding, which is the distance between the top of a
// full-height action sheet and the top of the safe area.
//
// The algorithm and its values are derived from measuring on the simulator.
double _topPadding(BuildContext context) {
if (MediaQuery.orientationOf(context) == Orientation.landscape) {
return _kActionSheetEdgePadding;
}
// The top padding in portrait mode is in general close to the top view
// padding, but not always equal:
//
// | view padding | action sheet padding | ratio
// No notch (eg. iPhone SE) | 20.0 | 20.0 | 1.0
// Notch (eg. iPhone 13) | 47.0 | 47.0 | 1.0
// Capsule (eg. iPhone 15) | 59.0 | 54.0 | 0.915
//
// Currently, we cannot determine why the result changes on "capsules."
// Therefore, we'll hard code this rule, given the limited types of actual
// devices. To provide an algorithm that accepts arbitrary view padding, this
// function calculates the ratio as a continuous curve with linear
// interpolation.
// The x for lerp is the top view padding, while the y is ratio of
// action sheet padding versus top view padding.
const double viewPaddingData1 = 47.0;
const double paddingRatioData1 = 1.0;
const double viewPaddingData2 = 59.0;
const double paddingRatioData2 = 54.0 / 59.0;
final double currentViewPadding = MediaQuery.viewPaddingOf(context).top;
final double currentPaddingRatio = _lerp(
/* x= */currentViewPadding,
/* x1, y1= */viewPaddingData1, paddingRatioData1,
/* x2, y2= */viewPaddingData2, paddingRatioData2,
);
final double padding = (currentPaddingRatio * currentViewPadding).roundToDouble();
// In case there is no view padding, there should still be some space
// between the action sheet and the edge.
return math.max(padding, _kDialogEdgePadding);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
/*
*
* The title Content section |
* The message |
* Main sheet
* Action 1 | |
* Actions section |
* Action 2 | |
*
*
* Cancel
*
*/
final List<Widget> children = <Widget>[
Flexible(
child: ClipRRect(
@ -943,6 +1021,7 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
};
return SafeArea(
minimum: const EdgeInsets.only(bottom: _kActionSheetEdgePadding),
child: ScrollConfiguration(
// A CupertinoScrollbar is built-in below
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
@ -954,12 +1033,15 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
child: CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.elevated,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: _kActionSheetEdgeHorizontalPadding,
vertical: _kActionSheetEdgeVerticalPadding,
padding: EdgeInsets.only(
left: _kActionSheetEdgePadding,
right: _kActionSheetEdgePadding,
top: _topPadding(context),
// The bottom padding is set on SafeArea.minimum, allowing it to
// be consumed by bottom view padding.
),
child: SizedBox(
width: actionSheetWidth - _kActionSheetEdgeHorizontalPadding * 2,
width: actionSheetWidth - _kActionSheetEdgePadding * 2,
child: _ActionSheetGestureDetector(
child: Semantics(
explicitChildNodes: true,

View file

@ -292,14 +292,14 @@ void main() {
// (minus padding).
expect(
tester.getBottomLeft(find.byType(ClipRRect)),
tester.getBottomLeft(find.byType(CupertinoActionSheet)) - const Offset(-8.0, 10.0),
tester.getBottomLeft(find.byType(CupertinoActionSheet)) - const Offset(-8.0, 8.0),
);
// Check that the dialog size is the same as the content section size
// (minus padding).
expect(
tester.getSize(find.byType(ClipRRect)).height,
tester.getSize(find.byType(CupertinoActionSheet)).height - 20.0,
tester.getSize(find.byType(CupertinoActionSheet)).height - 16.0,
);
expect(
@ -344,15 +344,15 @@ void main() {
// at the top of the action sheet + padding).
expect(
tester.getTopLeft(finder),
tester.getTopLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, 10.0),
tester.getTopLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, 8.0),
);
expect(
tester.getTopLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, 10.0),
tester.getTopLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, 8.0),
tester.getTopLeft(find.widgetWithText(CupertinoActionSheetAction, 'One')),
);
expect(
tester.getBottomLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, -10.0),
tester.getBottomLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, -8.0),
tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Two')),
);
});
@ -1085,7 +1085,7 @@ void main() {
await tester.tap(find.text('Go'));
await tester.pump();
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, moreOrLessEquals(132.3));
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, moreOrLessEquals(130.3));
});
testWidgets('1 action button with cancel button', (WidgetTester tester) async {
@ -1112,7 +1112,7 @@ void main() {
await tester.pump();
// Action section is size of one action button.
expect(findScrollableActionsSectionRenderBox(tester).size.height, 56.0);
expect(findScrollableActionsSectionRenderBox(tester).size.height, 57.0);
});
testWidgets('2 action buttons with cancel button', (WidgetTester tester) async {
@ -1142,7 +1142,7 @@ void main() {
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.3));
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.0));
});
testWidgets('3 action buttons with cancel button', (WidgetTester tester) async {
@ -1176,7 +1176,7 @@ void main() {
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.3));
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.0));
});
testWidgets('4+ action buttons with cancel button', (WidgetTester tester) async {
@ -1214,7 +1214,7 @@ void main() {
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.3));
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.0));
});
testWidgets('1 action button without cancel button', (WidgetTester tester) async {
@ -1236,7 +1236,7 @@ void main() {
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, 56.0);
expect(findScrollableActionsSectionRenderBox(tester).size.height, 57.0);
});
testWidgets('2+ action buttons without cancel button', (WidgetTester tester) async {
@ -1262,7 +1262,7 @@ void main() {
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.3));
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.0));
});
testWidgets('Action sheet with just cancel button is correct', (WidgetTester tester) async {
@ -1280,8 +1280,12 @@ void main() {
await tester.tap(find.text('Go'));
await tester.pump();
// Height should be cancel button height + padding
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, 76.0);
// The action sheet consists of only a cancel button, so the height should
// be cancel button height + padding.
const double expectedHeight = 57 // button height
+ 8 // bottom edge padding
+ 8; // top edge padding, since the screen has no top view padding
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, expectedHeight);
expect(tester.getSize(find.byType(CupertinoActionSheet)).width, 600.0);
});
@ -1350,12 +1354,115 @@ void main() {
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Cancel')).dy, 590.0);
expect(tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Cancel')).dy, 592.0);
expect(
tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'One')).dy,
moreOrLessEquals(469.7),
);
expect(tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Two')).dy, 526.0);
expect(tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Two')).dy, 527.0);
});
// Verify that on a phone with the given `viewSize` and `viewPadding`, the the
// main sheet of a full-height action sheet will have a size of
// `expectedSize`.
//
// The `viewSize` and `viewPadding` can be captured on simulator. Changing
// `expectedSize` should be accompanied by screenshot comparison.
Future<void> verifyMaximumSize(
WidgetTester tester, {
required Size viewSize,
required EdgeInsets viewPadding,
required Size expectedSize,
}) async {
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
await binding.setSurfaceSize(viewSize);
addTearDown(() => binding.setSurfaceSize(null));
await tester.pumpWidget(
OverrideMediaQuery(
transformer: (MediaQueryData data) {
return data.copyWith(
size: viewSize,
viewPadding: viewPadding,
padding: viewPadding,
);
},
child:
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
actions: List<Widget>.generate(20, (int i) =>
CupertinoActionSheetAction(
onPressed: () {},
child: Text('Button $i'),
),
),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
final Finder mainSheet = find.byElementPredicate(
(Element element) {
return element.widget.runtimeType.toString() == '_ActionSheetMainSheet';
},
);
expect(tester.getSize(mainSheet), expectedSize);
}
testWidgets('The maximum size is correct on iPhone SE gen 3', (WidgetTester tester) async {
const double expectedHeight = 667 // View height
- 20 // Top view padding
- 20 // Top widget padding
- 8; // Bottom edge padding
await verifyMaximumSize(
tester,
viewSize: const Size(375, 667),
viewPadding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
expectedSize: const Size(359, expectedHeight),
);
});
testWidgets('The maximum size is correct on iPhone 13 Pro', (WidgetTester tester) async {
const double expectedHeight = 844 // View height
- 47 // Top view padding
- 47 // Top widget padding
- 34; // Bottom view padding
await verifyMaximumSize(
tester,
viewSize: const Size(390, 844),
viewPadding: const EdgeInsets.fromLTRB(0, 47, 0, 34),
expectedSize: const Size(374, expectedHeight),
);
});
testWidgets('The maximum size is correct on iPhone 15 Plus', (WidgetTester tester) async {
const double expectedHeight = 932 // View height
- 59 // Top view padding
- 54 // Top widget padding
- 34; // Bottom view padding
await verifyMaximumSize(
tester,
viewSize: const Size(430, 932),
viewPadding: const EdgeInsets.fromLTRB(0, 59, 0, 34),
expectedSize: const Size(414, expectedHeight),
);
});
testWidgets('The maximum size is correct on iPhone 13 Pro landscape', (WidgetTester tester) async {
const double expectedWidth = 390 // View height
- 8 * 2; // Edge padding
const double expectedHeight = 390 // View height
- 8 // Top edge padding
- 21; // Bottom view padding
await verifyMaximumSize(
tester,
viewSize: const Size(844, 390),
viewPadding: const EdgeInsets.fromLTRB(47, 0, 47, 21),
expectedSize: const Size(expectedWidth, expectedHeight),
);
});
testWidgets('Action buttons shows pressed color as soon as the pointer is down', (WidgetTester tester) async {
@ -1393,139 +1500,105 @@ void main() {
});
testWidgets('Enter/exit animation is correct', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () { },
),
CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () { },
),
],
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
final AnimationSheetBuilder enterRecorder = AnimationSheetBuilder(
frameSize: const Size(600, 600)
);
addTearDown(enterRecorder.dispose);
final Widget target = createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () { },
),
CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () { },
),
],
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () { },
),
),
);
await tester.pumpWidget(enterRecorder.record(target));
// Enter animation
await tester.tap(find.text('Go'));
await tester.pumpFrames(enterRecorder.record(target), const Duration(milliseconds: 400));
await tester.pump();
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0);
await expectLater(
enterRecorder.collate(5),
matchesGoldenFile('cupertinoActionSheet.enter.png'),
);
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(483.9, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(398.6, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(365.3, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(354.8, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(350.7, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(349.4, epsilon: 0.1));
// Action sheet has reached final height
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(349.4, epsilon: 0.1));
final AnimationSheetBuilder exitRecorder = AnimationSheetBuilder(
frameSize: const Size(600, 600)
);
addTearDown(exitRecorder.dispose);
await tester.pumpWidget(exitRecorder.record(target));
// Exit animation
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump();
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(349.4, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(465.5, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(550.8, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(584.1, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(594.6, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(598.7, epsilon: 0.1));
await tester.pumpFrames(exitRecorder.record(target), const Duration(milliseconds: 400));
// Action sheet has disappeared
await tester.pump(const Duration(milliseconds: 60));
expect(find.byType(CupertinoActionSheet), findsNothing);
});
testWidgets('Modal barrier is pressed during transition', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () { },
),
CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () { },
),
],
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
await expectLater(
exitRecorder.collate(5),
matchesGoldenFile('cupertinoActionSheet.exit.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
testWidgets('Animation is correct if entering is canceled halfway', (WidgetTester tester) async {
final AnimationSheetBuilder recorder = AnimationSheetBuilder(
frameSize: const Size(600, 600)
);
addTearDown(recorder.dispose);
final Widget target = createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () { },
),
CupertinoActionSheetAction(
child: const Text('Two'),
onPressed: () { },
),
],
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
onPressed: () { },
),
),
);
await tester.pumpWidget(recorder.record(target));
// Enter animation
await tester.tap(find.text('Go'));
await tester.pump();
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0);
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(483.92863239836686, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(398.5571539306641, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(365.3034101784229, epsilon: 0.1));
await tester.pumpFrames(recorder.record(target), const Duration(milliseconds: 200));
// Exit animation
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump(const Duration(milliseconds: 60));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(398.5571539306641, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(483.92863239836686, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0);
await tester.pumpFrames(recorder.record(target), const Duration(milliseconds: 400));
// Action sheet has disappeared
await tester.pump(const Duration(milliseconds: 60));
expect(find.byType(CupertinoActionSheet), findsNothing);
});
await expectLater(
recorder.collate(5),
matchesGoldenFile('cupertinoActionSheet.interrupted-enter.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
testWidgets('Action sheet semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
@ -1795,3 +1868,25 @@ Widget boilerplate(Widget child) {
child: child,
);
}
typedef MediaQueryTransformer = MediaQueryData Function(MediaQueryData);
class OverrideMediaQuery extends StatelessWidget {
const OverrideMediaQuery({
super.key,
required this.transformer,
required this.child,
});
final MediaQueryTransformer transformer;
final Widget child;
@override
Widget build(BuildContext context) {
final MediaQueryData currentData = MediaQuery.of(context);
return MediaQuery(
data: transformer(currentData),
child: child,
);
}
}