Skip to content

Commit 1227961

Browse files
authored
Make Cupertino alert dialog honor text scale factor (flutter#14346)
This updates the CupertinoAlertDialog to respect text scale factor more properly. Before this, it would scale, but would clip the action buttons at large scales, and would draw in the safe area. It also didn't match the iOS alert because the content didn't scroll. Now it does those properly. I didn't address the fact that buttons should lay out properly (Issue flutter#14345), but that's probably pretty low priority. Fixes flutter#12484
1 parent 4a1eeed commit 1227961

File tree

3 files changed

+138
-39
lines changed

3 files changed

+138
-39
lines changed

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

+74-33
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import 'package:flutter/foundation.dart';
77
import 'package:flutter/widgets.dart';
88

99
import 'colors.dart';
10+
import 'scrollbar.dart';
1011

1112
// TODO(abarth): These constants probably belong somewhere more general.
1213

1314
const TextStyle _kCupertinoDialogTitleStyle = const TextStyle(
1415
fontFamily: '.SF UI Display',
1516
inherit: false,
16-
fontSize: 17.5,
17+
fontSize: 17.5,
1718
fontWeight: FontWeight.w600,
1819
color: CupertinoColors.black,
1920
height: 1.25,
@@ -23,7 +24,7 @@ const TextStyle _kCupertinoDialogTitleStyle = const TextStyle(
2324
const TextStyle _kCupertinoDialogContentStyle = const TextStyle(
2425
fontFamily: '.SF UI Text',
2526
inherit: false,
26-
fontSize: 12.4,
27+
fontSize: 12.4,
2728
fontWeight: FontWeight.w500,
2829
color: CupertinoColors.black,
2930
height: 1.35,
@@ -33,7 +34,7 @@ const TextStyle _kCupertinoDialogContentStyle = const TextStyle(
3334
const TextStyle _kCupertinoDialogActionStyle = const TextStyle(
3435
fontFamily: '.SF UI Text',
3536
inherit: false,
36-
fontSize: 16.8,
37+
fontSize: 16.8,
3738
fontWeight: FontWeight.w400,
3839
color: CupertinoColors.activeBlue,
3940
textBaseline: TextBaseline.alphabetic,
@@ -78,7 +79,7 @@ class CupertinoDialog extends StatelessWidget {
7879
borderRadius: const BorderRadius.all(const Radius.circular(12.0)),
7980
child: new DecoratedBox(
8081
// To get the effect, 2 white fills are needed. One blended with the
81-
// background before applying the blur and one overlayed on top of
82+
// background before applying the blur and one overlaid on top of
8283
// the blur.
8384
decoration: _kCupertinoDialogBackFill,
8485
child: new BackdropFilter(
@@ -116,6 +117,7 @@ class CupertinoAlertDialog extends StatelessWidget {
116117
this.title,
117118
this.content,
118119
this.actions,
120+
this.scrollController,
119121
}) : super(key: key);
120122

121123
/// The (optional) title of the dialog is displayed in a large font at the top
@@ -136,15 +138,24 @@ class CupertinoAlertDialog extends StatelessWidget {
136138
/// Typically this is a list of [CupertinoDialogAction] widgets.
137139
final List<Widget> actions;
138140

141+
/// A scroll controller that can be used to control the scrolling of the message
142+
/// in the dialog.
143+
///
144+
/// Defaults to null, and is typically not needed, since most alert messages are short.
145+
final ScrollController scrollController;
146+
139147
@override
140148
Widget build(BuildContext context) {
141-
final List<Widget> children = <Widget>[];
142-
143-
children.add(const SizedBox(height: 18.0));
144-
149+
const double edgePadding = 20.0;
150+
final List<Widget> titleContentGroup = <Widget>[];
145151
if (title != null) {
146-
children.add(new Padding(
147-
padding: const EdgeInsets.only(left: 20.0, right: 20.0, bottom: 2.0),
152+
titleContentGroup.add(new Padding(
153+
padding: new EdgeInsets.only(
154+
left: edgePadding,
155+
right: edgePadding,
156+
bottom: content == null ? edgePadding : 1.0,
157+
top: edgePadding,
158+
),
148159
child: new DefaultTextStyle(
149160
style: _kCupertinoDialogTitleStyle,
150161
textAlign: TextAlign.center,
@@ -154,38 +165,57 @@ class CupertinoAlertDialog extends StatelessWidget {
154165
}
155166

156167
if (content != null) {
157-
children.add(new Flexible(
158-
fit: FlexFit.loose,
159-
child: new Padding(
160-
padding: const EdgeInsets.symmetric(horizontal: 20.0),
168+
titleContentGroup.add(
169+
new Padding(
170+
padding: new EdgeInsets.only(
171+
left: edgePadding,
172+
right: edgePadding,
173+
bottom: edgePadding,
174+
top: title == null ? edgePadding : 1.0,
175+
),
161176
child: new DefaultTextStyle(
162177
style: _kCupertinoDialogContentStyle,
163178
textAlign: TextAlign.center,
164179
child: content,
165180
),
166181
),
167-
));
182+
);
168183
}
169184

170-
children.add(const SizedBox(height: 22.0));
185+
final List<Widget> children = <Widget>[];
186+
if (titleContentGroup.isNotEmpty) {
187+
children.add(
188+
new Flexible(
189+
child: new CupertinoScrollbar(
190+
child: new ListView(
191+
controller: scrollController,
192+
shrinkWrap: true,
193+
children: titleContentGroup,
194+
),
195+
),
196+
),
197+
);
198+
}
171199

172200
if (actions != null) {
173201
children.add(new _CupertinoButtonBar(
174202
children: actions,
175203
));
176204
}
177205

178-
return new CupertinoDialog(
179-
child: new Column(
180-
mainAxisSize: MainAxisSize.min,
181-
crossAxisAlignment: CrossAxisAlignment.stretch,
182-
children: children,
206+
return new Padding(
207+
padding: const EdgeInsets.symmetric(vertical: edgePadding),
208+
child: new CupertinoDialog(
209+
child: new Column(
210+
mainAxisSize: MainAxisSize.min,
211+
crossAxisAlignment: CrossAxisAlignment.stretch,
212+
children: children,
213+
),
183214
),
184215
);
185216
}
186217
}
187218

188-
189219
/// A button typically used in a [CupertinoAlertDialog].
190220
///
191221
/// See also:
@@ -208,7 +238,7 @@ class CupertinoDialogAction extends StatelessWidget {
208238

209239
/// Set to true if button is the default choice in the dialog.
210240
///
211-
/// Default buttons are bolded.
241+
/// Default buttons are bold.
212242
final bool isDefaultAction;
213243

214244
/// Whether this action destroys an object.
@@ -229,22 +259,29 @@ class CupertinoDialogAction extends StatelessWidget {
229259
Widget build(BuildContext context) {
230260
TextStyle style = _kCupertinoDialogActionStyle;
231261

232-
if (isDefaultAction)
262+
if (isDefaultAction) {
233263
style = style.copyWith(fontWeight: FontWeight.w600);
264+
}
234265

235-
if (isDestructiveAction)
266+
if (isDestructiveAction) {
236267
style = style.copyWith(color: CupertinoColors.destructiveRed);
268+
}
237269

238-
if (!enabled)
270+
if (!enabled) {
239271
style = style.copyWith(color: style.color.withOpacity(0.5));
272+
}
240273

274+
final double textScaleFactor = MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0;
241275
return new GestureDetector(
242276
onTap: onPressed,
243277
behavior: HitTestBehavior.opaque,
244-
child: new Center(
278+
child: new Container(
279+
alignment: Alignment.center,
280+
padding: new EdgeInsets.all(10.0 * textScaleFactor),
245281
child: new DefaultTextStyle(
246282
style: style,
247283
child: child,
284+
textAlign: TextAlign.center,
248285
),
249286
),
250287
);
@@ -271,18 +308,22 @@ class _CupertinoButtonBar extends StatelessWidget {
271308

272309
for (Widget child in children) {
273310
// TODO(abarth): Listen for the buttons being highlighted.
311+
// TODO(gspencer): These buttons don't lay out in the same way as iOS.
312+
// When they get wide, they should stack vertically instead of in a row.
313+
// When they get really big, the vertically-stacked buttons should scroll
314+
// (separately from the top message).
274315
buttons.add(new Expanded(child: child));
275316
}
276317

277318
return new CustomPaint(
278319
painter: new _CupertinoButtonBarPainter(children.length),
279-
child: new SizedBox(
280-
height: _kButtonBarHeight,
281-
child: new Row(
282-
crossAxisAlignment: CrossAxisAlignment.stretch,
283-
children: buttons
320+
child: new UnconstrainedBox(
321+
constrainedAxis: Axis.horizontal,
322+
child: new ConstrainedBox(
323+
constraints: const BoxConstraints(minHeight: _kButtonBarHeight),
324+
child: new Row(children: buttons),
284325
),
285-
)
326+
),
286327
);
287328
}
288329
}

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

+1-6
Original file line numberDiff line numberDiff line change
@@ -434,12 +434,7 @@ class _DialogRoute<T> extends PopupRoute<T> {
434434

435435
@override
436436
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
437-
return new MediaQuery.removePadding(
438-
context: context,
439-
removeTop: true,
440-
removeBottom: true,
441-
removeLeft: true,
442-
removeRight: true,
437+
return new SafeArea(
443438
child: new Builder(
444439
builder: (BuildContext context) {
445440
return theme != null ? new Theme(data: theme, child: child) : child;

packages/flutter/test/cupertino/dialog_test.dart

+63
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,69 @@ void main() {
9898
expect(widget.style.fontWeight, equals(FontWeight.w600));
9999
expect(widget.style.color.red, greaterThan(widget.style.color.blue));
100100
});
101+
102+
testWidgets('Message is scrollable, has correct padding with large text sizes',
103+
(WidgetTester tester) async {
104+
final ScrollController scrollController = new ScrollController(keepScrollOffset: true);
105+
await tester.pumpWidget(
106+
new MaterialApp(home: new Material(
107+
child: new Center(
108+
child: new Builder(builder: (BuildContext context) {
109+
return new RaisedButton(
110+
onPressed: () {
111+
showDialog<Null>(
112+
context: context,
113+
child: new Builder(builder: (BuildContext context) {
114+
return new MediaQuery(
115+
data: MediaQuery.of(context).copyWith(textScaleFactor: 3.0),
116+
child: new CupertinoAlertDialog(
117+
title: const Text('The Title'),
118+
content: new Text('Very long content ' * 20),
119+
actions: <Widget>[
120+
const CupertinoDialogAction(
121+
child: const Text('Cancel'),
122+
),
123+
const CupertinoDialogAction(
124+
isDestructiveAction: true,
125+
child: const Text('OK'),
126+
),
127+
],
128+
scrollController: scrollController,
129+
),
130+
);
131+
}),
132+
);
133+
},
134+
child: const Text('Go'),
135+
);
136+
}),
137+
),
138+
)),
139+
);
140+
141+
await tester.tap(find.text('Go'));
142+
143+
await tester.pump();
144+
await tester.pump(const Duration(seconds: 1));
145+
146+
expect(scrollController.offset, 0.0);
147+
scrollController.jumpTo(100.0);
148+
expect(scrollController.offset, 100.0);
149+
150+
// Find the actual dialog box. The first decorated box is the popup barrier.
151+
expect(tester.getSize(find.byType(DecoratedBox).at(1)), equals(const Size(270.0, 560.0)));
152+
153+
// Check sizes/locations of the text.
154+
expect(tester.getSize(find.text('The Title')), equals(const Size(230.0, 198.0)));
155+
expect(tester.getSize(find.text('Cancel')), equals(const Size(75.0, 300.0)));
156+
expect(tester.getSize(find.text('OK')), equals(const Size(75.0, 100.0)));
157+
expect(tester.getTopLeft(find.text('The Title')), equals(const Offset(285.0, 40.0)));
158+
159+
// The Cancel and OK buttons have different Y values because "Cancel" is
160+
// wrapping (as it should with large text sizes like this).
161+
expect(tester.getTopLeft(find.text('Cancel')), equals(const Offset(295.0, 250.0)));
162+
expect(tester.getTopLeft(find.text('OK')), equals(const Offset(430.0, 350.0)));
163+
});
101164
}
102165

103166
Widget boilerplate(Widget child) {

0 commit comments

Comments
 (0)