Unverified Commit 63c3de10 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Timer picker fidelity revision (#38481)

* WIP

* trying out different numbers

* apply intrinsic width and height

* update

* update behavior

* documentation

* wip

* fix tests

* constants

* respect theme

* respect theme

* add new test

* add new test

* update

* review

* update golden commit hash
parent 476a4de1
ead5d5df3236f6d9e619e640029f9811e4eb0716 49f8198b72f6e12c65fe1db2e46162de0204e671
...@@ -12,11 +12,14 @@ import 'localizations.dart'; ...@@ -12,11 +12,14 @@ import 'localizations.dart';
import 'picker.dart'; import 'picker.dart';
import 'theme.dart'; import 'theme.dart';
// Default aesthetic values obtained by comparing with iOS pickers. // Values derived from https://developer.apple.com/design/resources/ and on iOS
// simulators with "Debug View Hierarchy".
const double _kItemExtent = 32.0; const double _kItemExtent = 32.0;
const double _kPickerWidth = 330.0; // From the picker's intrinsic content size constraint.
const double _kPickerWidth = 320.0;
const double _kPickerHeight = 216.0;
const bool _kUseMagnifier = true; const bool _kUseMagnifier = true;
const double _kMagnification = 1.08; const double _kMagnification = 2.35/2.1;
const double _kDatePickerPadSize = 12.0; const double _kDatePickerPadSize = 12.0;
// The density of a date picker is different from a generic picker. // The density of a date picker is different from a generic picker.
// Eyeballed from iOS. // Eyeballed from iOS.
...@@ -28,6 +31,20 @@ const TextStyle _kDefaultPickerTextStyle = TextStyle( ...@@ -28,6 +31,20 @@ const TextStyle _kDefaultPickerTextStyle = TextStyle(
letterSpacing: -0.83, letterSpacing: -0.83,
); );
// Half of the horizontal padding value between the timer picker's columns.
const double _kTimerPickerHalfColumnPadding = 2;
// The horizontal padding between the timer picker's number label and its
// corresponding unit label.
const double _kTimerPickerLabelPadSize = 4.5;
const double _kTimerPickerLabelFontSize = 17.0;
// The width of each colmn of the countdown time picker.
const double _kTimerPickerColumnIntrinsicWidth = 106;
// Unfortunately turning on magnification for the timer picker messes up the label
// alignment. So we'll have to hard code the font size and turn magnification off
// for now.
const double _kTimerPickerNumberLabelFontSize = 23;
TextStyle _themeTextStyle(BuildContext context) { TextStyle _themeTextStyle(BuildContext context) {
return CupertinoTheme.of(context).textTheme.dateTimePickerTextStyle; return CupertinoTheme.of(context).textTheme.dateTimePickerTextStyle;
} }
...@@ -990,14 +1007,19 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> { ...@@ -990,14 +1007,19 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
} }
// The iOS date picker and timer picker has their width fixed to 330.0 in all // The iOS date picker and timer picker has their width fixed to 320.0 in all
// modes. // modes. The only exception is the hms mode (which doesn't have a native counterpart),
// with a fixed width of 330.0 px.
//
// For date pickers, if the maximum width given to the picker is greater than
// 320.0, the leftmost and rightmost column will be extended equally so that the
// widths match, and the picker is in the center.
// //
// If the maximum width given to the picker is greater than 330.0, the leftmost // For timer pickers, if the maximum width given to the picker is greater than
// and rightmost column will be extended equally so that the widths match, and // its intrinsic width, it will keep its intrinsic size and position itself in the
// the picker is in the center. // parent using its alignment parameter.
// //
// If the maximum width given to the picker is smaller than 330.0, the picker's // If the maximum width given to the picker is smaller than 320.0, the picker's
// layout will be broken. // layout will be broken.
...@@ -1029,7 +1051,10 @@ enum CupertinoTimerPickerMode { ...@@ -1029,7 +1051,10 @@ enum CupertinoTimerPickerMode {
/// ///
/// There are several modes of the timer picker listed in [CupertinoTimerPickerMode]. /// There are several modes of the timer picker listed in [CupertinoTimerPickerMode].
/// ///
/// Sizes itself to its parent. /// The picker has a fixed size of 320 x 216, in logical pixels, with the exception
/// of [CupertinoTimerPickerMode.hms], which is 330 x 216. If the parent widget
/// provides more space than it needs, the picker will position itself according
/// to its [alignment] property.
/// ///
/// See also: /// See also:
/// ///
...@@ -1054,10 +1079,12 @@ class CupertinoTimerPicker extends StatefulWidget { ...@@ -1054,10 +1079,12 @@ class CupertinoTimerPicker extends StatefulWidget {
/// [secondInterval] is the granularity of the second spinner. Must be a /// [secondInterval] is the granularity of the second spinner. Must be a
/// positive integer factor of 60. /// positive integer factor of 60.
CupertinoTimerPicker({ CupertinoTimerPicker({
Key key,
this.mode = CupertinoTimerPickerMode.hms, this.mode = CupertinoTimerPickerMode.hms,
this.initialTimerDuration = Duration.zero, this.initialTimerDuration = Duration.zero,
this.minuteInterval = 1, this.minuteInterval = 1,
this.secondInterval = 1, this.secondInterval = 1,
this.alignment = Alignment.center,
this.backgroundColor = _kBackgroundColor, this.backgroundColor = _kBackgroundColor,
@required this.onTimerDurationChanged, @required this.onTimerDurationChanged,
}) : assert(mode != null), }) : assert(mode != null),
...@@ -1068,7 +1095,9 @@ class CupertinoTimerPicker extends StatefulWidget { ...@@ -1068,7 +1095,9 @@ class CupertinoTimerPicker extends StatefulWidget {
assert(secondInterval > 0 && 60 % secondInterval == 0), assert(secondInterval > 0 && 60 % secondInterval == 0),
assert(initialTimerDuration.inMinutes % minuteInterval == 0), assert(initialTimerDuration.inMinutes % minuteInterval == 0),
assert(initialTimerDuration.inSeconds % secondInterval == 0), assert(initialTimerDuration.inSeconds % secondInterval == 0),
assert(backgroundColor != null); assert(backgroundColor != null),
assert(alignment != null),
super(key: key);
/// The mode of the timer picker. /// The mode of the timer picker.
final CupertinoTimerPickerMode mode; final CupertinoTimerPickerMode mode;
...@@ -1087,6 +1116,11 @@ class CupertinoTimerPicker extends StatefulWidget { ...@@ -1087,6 +1116,11 @@ class CupertinoTimerPicker extends StatefulWidget {
/// Callback called when the timer duration changes. /// Callback called when the timer duration changes.
final ValueChanged<Duration> onTimerDurationChanged; final ValueChanged<Duration> onTimerDurationChanged;
/// Defines how the timper picker should be positioned within its parent.
///
/// This property must not be null. It defaults to [Alignment.center].
final AlignmentGeometry alignment;
/// Background color of timer picker. /// Background color of timer picker.
/// ///
/// Defaults to [CupertinoColors.white] when null. /// Defaults to [CupertinoColors.white] when null.
...@@ -1097,19 +1131,35 @@ class CupertinoTimerPicker extends StatefulWidget { ...@@ -1097,19 +1131,35 @@ class CupertinoTimerPicker extends StatefulWidget {
} }
class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> { class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
int textDirectionFactor; TextDirection textDirection;
CupertinoLocalizations localizations; CupertinoLocalizations localizations;
int get textDirectionFactor {
// Alignment based on text direction. The variable name is self descriptive, switch (textDirection) {
// however, when text direction is rtl, alignment is reversed. case TextDirection.ltr:
Alignment alignCenterLeft; return 1;
Alignment alignCenterRight; case TextDirection.rtl:
return -1;
}
return 1;
}
// The currently selected values of the picker. // The currently selected values of the picker.
int selectedHour; int selectedHour;
int selectedMinute; int selectedMinute;
int selectedSecond; int selectedSecond;
// On iOS the selected values won't be reported until the scrolling fully stops.
// The values below are the latest selected values when the picker comes to a full stop.
int lastSelectedHour;
int lastSelectedMinute;
int lastSelectedSecond;
final TextPainter textPainter = TextPainter();
final List<String> numbers = List<String>.generate(10, (int i) => '${9 - i}');
double numberLabelWidth;
double numberLabelHeight;
double numberLabelBaseline;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -1123,12 +1173,13 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> { ...@@ -1123,12 +1173,13 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
selectedSecond = widget.initialTimerDuration.inSeconds % 60; selectedSecond = widget.initialTimerDuration.inSeconds % 60;
} }
// Builds a text label with customized scale factor and font weight. @override
Widget _buildLabel(String text) { void didUpdateWidget(CupertinoTimerPicker oldWidget) {
return Text( super.didUpdateWidget(oldWidget);
text,
textScaleFactor: 0.9, assert(
style: const TextStyle(fontWeight: FontWeight.w600), oldWidget.mode == widget.mode,
"The CupertinoTimerPicker's mode cannot change once it's built",
); );
} }
...@@ -1136,14 +1187,97 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> { ...@@ -1136,14 +1187,97 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
textDirectionFactor = Directionality.of(context) == TextDirection.ltr ? 1 : -1; textDirection = Directionality.of(context);
localizations = CupertinoLocalizations.of(context); localizations = CupertinoLocalizations.of(context);
alignCenterLeft = textDirectionFactor == 1 ? Alignment.centerLeft : Alignment.centerRight; textPainter.textDirection = textDirection;
alignCenterRight = textDirectionFactor == 1 ? Alignment.centerRight : Alignment.centerLeft; final TextStyle textStyle = _textStyleFrom(context);
double maxWidth = double.negativeInfinity;
String widestNumber;
// Assumes that:
// - 2-digit numbers are always wider than 1-digit numbers.
// - There's at least one number in 1-9 that's wider than or equal to 0.
// - The widest 2-digit number is composed of 2 same 1-digit numbers
// that has the biggest width.
// - If two different 1-digit numbers are of the same width, their corresponding
// 2 digit numbers are of the same width.
for (String input in numbers) {
textPainter.text = TextSpan(
text: input,
style: textStyle
);
textPainter.layout();
if (textPainter.maxIntrinsicWidth > maxWidth) {
maxWidth = textPainter.maxIntrinsicWidth;
widestNumber = input;
}
}
textPainter.text = TextSpan(
text: '$widestNumber$widestNumber',
style: textStyle
);
textPainter.layout();
numberLabelWidth = textPainter.maxIntrinsicWidth;
numberLabelHeight = textPainter.height;
numberLabelBaseline = textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
}
// Builds a text label with scale factor 1.0 and font weight semi-bold.
// `pickerPadding ` is the additional padding the corresponding picker has to apply
// around the `Text`, in order to extend its separators towards the closest
// horizontal edge of the encompassing widget.
Widget _buildLabel(String text, EdgeInsetsDirectional pickerPadding) {
final EdgeInsetsDirectional padding = EdgeInsetsDirectional.only(
start: numberLabelWidth
+ _kTimerPickerLabelPadSize
+ pickerPadding.start,
);
return IgnorePointer(
child: Container(
alignment: AlignmentDirectional.centerStart.resolve(textDirection),
padding: padding.resolve(textDirection),
child: SizedBox(
height: numberLabelHeight,
child: Baseline(
baseline: numberLabelBaseline,
baselineType: TextBaseline.alphabetic,
child: Text(
text,
style: const TextStyle(
fontSize: _kTimerPickerLabelFontSize,
fontWeight: FontWeight.w600
),
maxLines: 1,
softWrap: false,
),
),
),
),
);
} }
Widget _buildHourPicker() { // The picker has to be wider than its content, since the separators
// are part of the picker.
Widget _buildPickerNumberLabel(String text, EdgeInsetsDirectional padding) {
return Container(
width: _kTimerPickerColumnIntrinsicWidth + padding.horizontal,
padding: padding.resolve(textDirection),
alignment: AlignmentDirectional.centerStart.resolve(textDirection),
child: Container(
width: numberLabelWidth,
alignment: AlignmentDirectional.centerEnd.resolve(textDirection),
child: Text(text, softWrap: false, maxLines: 1, overflow: TextOverflow.visible),
),
);
}
Widget _buildHourPicker(EdgeInsetsDirectional additionalPadding) {
return CupertinoPicker( return CupertinoPicker(
scrollController: FixedExtentScrollController(initialItem: selectedHour), scrollController: FixedExtentScrollController(initialItem: selectedHour),
offAxisFraction: -0.5 * textDirectionFactor, offAxisFraction: -0.5 * textDirectionFactor,
...@@ -1161,9 +1295,6 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> { ...@@ -1161,9 +1295,6 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
}); });
}, },
children: List<Widget>.generate(24, (int index) { children: List<Widget>.generate(24, (int index) {
final double hourLabelWidth =
widget.mode == CupertinoTimerPickerMode.hm ? _kPickerWidth / 4 : _kPickerWidth / 6;
final String semanticsLabel = textDirectionFactor == 1 final String semanticsLabel = textDirectionFactor == 1
? localizations.timerPickerHour(index) + localizations.timerPickerHourLabel(index) ? localizations.timerPickerHour(index) + localizations.timerPickerHourLabel(index)
: localizations.timerPickerHourLabel(index) + localizations.timerPickerHour(index); : localizations.timerPickerHourLabel(index) + localizations.timerPickerHour(index);
...@@ -1171,55 +1302,42 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> { ...@@ -1171,55 +1302,42 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
return Semantics( return Semantics(
label: semanticsLabel, label: semanticsLabel,
excludeSemantics: true, excludeSemantics: true,
child: Container( child: _buildPickerNumberLabel(localizations.timerPickerHour(index), additionalPadding),
alignment: alignCenterRight,
padding: textDirectionFactor == 1
? EdgeInsets.only(right: hourLabelWidth)
: EdgeInsets.only(left: hourLabelWidth),
child: Container(
alignment: alignCenterRight,
// Adds some spaces between words.
padding: const EdgeInsets.symmetric(horizontal: 2.0),
child: Text(localizations.timerPickerHour(index)),
),
),
); );
}), }),
); );
} }
Widget _buildHourColumn() { Widget _buildHourColumn(EdgeInsetsDirectional additionalPadding) {
final Widget hourLabel = IgnorePointer(
child: Container(
alignment: alignCenterRight,
child: Container(
alignment: alignCenterLeft,
// Adds some spaces between words.
padding: const EdgeInsets.symmetric(horizontal: 2.0),
width: widget.mode == CupertinoTimerPickerMode.hm
? _kPickerWidth / 4
: _kPickerWidth / 6,
child: _buildLabel(localizations.timerPickerHourLabel(selectedHour)),
),
),
);
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
_buildHourPicker(), NotificationListener<ScrollEndNotification>(
hourLabel, onNotification: (ScrollEndNotification notification) {
setState(() { lastSelectedHour = selectedHour; });
return false;
},
child: _buildHourPicker(additionalPadding),
),
_buildLabel(
localizations.timerPickerHourLabel(lastSelectedHour ?? selectedHour),
additionalPadding
),
], ],
); );
} }
Widget _buildMinutePicker() { Widget _buildMinutePicker(EdgeInsetsDirectional additionalPadding) {
double offAxisFraction; double offAxisFraction;
if (widget.mode == CupertinoTimerPickerMode.hm) switch (widget.mode) {
case CupertinoTimerPickerMode.hm:
offAxisFraction = 0.5 * textDirectionFactor; offAxisFraction = 0.5 * textDirectionFactor;
else if (widget.mode == CupertinoTimerPickerMode.hms) break;
case CupertinoTimerPickerMode.hms:
offAxisFraction = 0.0; offAxisFraction = 0.0;
else break;
case CupertinoTimerPickerMode.ms:
offAxisFraction = -0.5 * textDirectionFactor; offAxisFraction = -0.5 * textDirectionFactor;
}
return CupertinoPicker( return CupertinoPicker(
scrollController: FixedExtentScrollController( scrollController: FixedExtentScrollController(
...@@ -1229,6 +1347,7 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> { ...@@ -1229,6 +1347,7 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
itemExtent: _kItemExtent, itemExtent: _kItemExtent,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
squeeze: _kSqueeze, squeeze: _kSqueeze,
looping: true,
onSelectedItemChanged: (int index) { onSelectedItemChanged: (int index) {
setState(() { setState(() {
selectedMinute = index * widget.minuteInterval; selectedMinute = index * widget.minuteInterval;
...@@ -1246,94 +1365,36 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> { ...@@ -1246,94 +1365,36 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
? localizations.timerPickerMinute(minute) + localizations.timerPickerMinuteLabel(minute) ? localizations.timerPickerMinute(minute) + localizations.timerPickerMinuteLabel(minute)
: localizations.timerPickerMinuteLabel(minute) + localizations.timerPickerMinute(minute); : localizations.timerPickerMinuteLabel(minute) + localizations.timerPickerMinute(minute);
if (widget.mode == CupertinoTimerPickerMode.ms) {
return Semantics( return Semantics(
label: semanticsLabel, label: semanticsLabel,
excludeSemantics: true, excludeSemantics: true,
child: Container( child: _buildPickerNumberLabel(localizations.timerPickerMinute(minute), additionalPadding),
alignment: alignCenterRight,
padding: textDirectionFactor == 1
? const EdgeInsets.only(right: _kPickerWidth / 4)
: const EdgeInsets.only(left: _kPickerWidth / 4),
child: Container(
alignment: alignCenterRight,
padding: const EdgeInsets.symmetric(horizontal: 2.0),
child: Text(localizations.timerPickerMinute(minute)),
),
),
); );
} else {
return Semantics(
label: semanticsLabel,
excludeSemantics: true,
child: Container(
alignment: alignCenterLeft,
child: Container(
alignment: alignCenterRight,
width: widget.mode == CupertinoTimerPickerMode.hm
? _kPickerWidth / 10
: _kPickerWidth / 6,
// Adds some spaces between words.
padding: const EdgeInsets.symmetric(horizontal: 2.0),
child: Text(localizations.timerPickerMinute(minute)),
),
),
);
}
}), }),
); );
} }
Widget _buildMinuteColumn() { Widget _buildMinuteColumn(EdgeInsetsDirectional additionalPadding) {
Widget minuteLabel;
if (widget.mode == CupertinoTimerPickerMode.hm) {
minuteLabel = IgnorePointer(
child: Container(
alignment: alignCenterLeft,
padding: textDirectionFactor == 1
? const EdgeInsets.only(left: _kPickerWidth / 10)
: const EdgeInsets.only(right: _kPickerWidth / 10),
child: Container(
alignment: alignCenterLeft,
// Adds some spaces between words.
padding: const EdgeInsets.symmetric(horizontal: 2.0),
child: _buildLabel(localizations.timerPickerMinuteLabel(selectedMinute)),
),
),
);
} else {
minuteLabel = IgnorePointer(
child: Container(
alignment: alignCenterRight,
child: Container(
alignment: alignCenterLeft,
width: widget.mode == CupertinoTimerPickerMode.ms
? _kPickerWidth / 4
: _kPickerWidth / 6,
// Adds some spaces between words.
padding: const EdgeInsets.symmetric(horizontal: 2.0),
child: _buildLabel(localizations.timerPickerMinuteLabel(selectedMinute)),
),
),
);
}
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
_buildMinutePicker(), NotificationListener<ScrollEndNotification>(
minuteLabel, onNotification: (ScrollEndNotification notification) {
setState(() { lastSelectedMinute = selectedMinute; });
return false;
},
child: _buildMinutePicker(additionalPadding),
),
_buildLabel(
localizations.timerPickerMinuteLabel(lastSelectedMinute ?? selectedMinute),
additionalPadding,
),
], ],
); );
} }
Widget _buildSecondPicker(EdgeInsetsDirectional additionalPadding) {
Widget _buildSecondPicker() {
final double offAxisFraction = 0.5 * textDirectionFactor; final double offAxisFraction = 0.5 * textDirectionFactor;
final double secondPickerWidth =
widget.mode == CupertinoTimerPickerMode.ms ? _kPickerWidth / 10 : _kPickerWidth / 6;
return CupertinoPicker( return CupertinoPicker(
scrollController: FixedExtentScrollController( scrollController: FixedExtentScrollController(
initialItem: selectedSecond ~/ widget.secondInterval, initialItem: selectedSecond ~/ widget.secondInterval,
...@@ -1342,6 +1403,7 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> { ...@@ -1342,6 +1403,7 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
itemExtent: _kItemExtent, itemExtent: _kItemExtent,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
squeeze: _kSqueeze, squeeze: _kSqueeze,
looping: true,
onSelectedItemChanged: (int index) { onSelectedItemChanged: (int index) {
setState(() { setState(() {
selectedSecond = index * widget.secondInterval; selectedSecond = index * widget.secondInterval;
...@@ -1362,89 +1424,102 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> { ...@@ -1362,89 +1424,102 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
return Semantics( return Semantics(
label: semanticsLabel, label: semanticsLabel,
excludeSemantics: true, excludeSemantics: true,
child: Container( child: _buildPickerNumberLabel(localizations.timerPickerSecond(second), additionalPadding),
alignment: alignCenterLeft,
child: Container(
alignment: alignCenterRight,
// Adds some spaces between words.
padding: const EdgeInsets.symmetric(horizontal: 2.0),
width: secondPickerWidth,
child: Text(localizations.timerPickerSecond(second)),
),
),
); );
}), }),
); );
} }
Widget _buildSecondColumn() { Widget _buildSecondColumn(EdgeInsetsDirectional additionalPadding) {
final double secondPickerWidth =
widget.mode == CupertinoTimerPickerMode.ms ? _kPickerWidth / 10 : _kPickerWidth / 6;
final Widget secondLabel = IgnorePointer(
child: Container(
alignment: alignCenterLeft,
padding: textDirectionFactor == 1
? EdgeInsets.only(left: secondPickerWidth)
: EdgeInsets.only(right: secondPickerWidth),
child: Container(
alignment: alignCenterLeft,
// Adds some spaces between words.
padding: const EdgeInsets.symmetric(horizontal: 2.0),
child: _buildLabel(localizations.timerPickerSecondLabel(selectedSecond)),
),
),
);
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
_buildSecondPicker(), NotificationListener<ScrollEndNotification>(
secondLabel, onNotification: (ScrollEndNotification notification) {
setState(() { lastSelectedSecond = selectedSecond; });
return false;
},
child: _buildSecondPicker(additionalPadding),
),
_buildLabel(
localizations.timerPickerSecondLabel(lastSelectedSecond ?? selectedSecond),
additionalPadding,
),
], ],
); );
} }
TextStyle _textStyleFrom(BuildContext context) {
return CupertinoTheme.of(context).textTheme
.pickerTextStyle.merge(
const TextStyle(
fontSize: _kTimerPickerNumberLabelFontSize,
)
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// The timer picker can be divided into columns corresponding to hour, // The timer picker can be divided into columns corresponding to hour,
// minute, and second. Each column consists of a scrollable and a fixed // minute, and second. Each column consists of a scrollable and a fixed
// label on top of it. // label on top of it.
Widget picker; List<Widget> columns;
const double paddingValue = _kPickerWidth - 2 * _kTimerPickerColumnIntrinsicWidth - 2 * _kTimerPickerHalfColumnPadding;
if (widget.mode == CupertinoTimerPickerMode.hm) { // The default totalWidth for 2-column modes.
picker = Row( double totalWidth = _kPickerWidth;
children: <Widget>[ assert(paddingValue >= 0);
Expanded(child: _buildHourColumn()),
Expanded(child: _buildMinuteColumn()), switch (widget.mode) {
], case CupertinoTimerPickerMode.hm:
); // Pad the widget to make it as wide as `_kPickerWidth`.
} else if (widget.mode == CupertinoTimerPickerMode.ms) { columns = <Widget>[
picker = Row( _buildHourColumn(const EdgeInsetsDirectional.only(start: paddingValue / 2, end: _kTimerPickerHalfColumnPadding)),
children: <Widget>[ _buildMinuteColumn(const EdgeInsetsDirectional.only(start: _kTimerPickerHalfColumnPadding, end: paddingValue / 2))
Expanded(child: _buildMinuteColumn()), ];
Expanded(child: _buildSecondColumn()), break;
], case CupertinoTimerPickerMode.ms:
); // Pad the widget to make it as wide as `_kPickerWidth`.
} else { columns = <Widget>[
picker = Row( _buildMinuteColumn(const EdgeInsetsDirectional.only(start: paddingValue / 2, end: _kTimerPickerHalfColumnPadding)),
children: <Widget>[ _buildSecondColumn(const EdgeInsetsDirectional.only(start: _kTimerPickerHalfColumnPadding, end: paddingValue / 2))
Expanded(child: _buildHourColumn()), ];
Container( break;
width: _kPickerWidth / 3, case CupertinoTimerPickerMode.hms:
child: _buildMinuteColumn(), const double paddingValue = _kTimerPickerHalfColumnPadding * 2;
), totalWidth = _kTimerPickerColumnIntrinsicWidth * 3 + 4 * _kTimerPickerHalfColumnPadding + paddingValue;
Expanded(child: _buildSecondColumn()), columns = <Widget>[
], _buildHourColumn(const EdgeInsetsDirectional.only(start: paddingValue / 2, end: _kTimerPickerHalfColumnPadding)),
); _buildMinuteColumn(const EdgeInsetsDirectional.only(start: _kTimerPickerHalfColumnPadding, end: _kTimerPickerHalfColumnPadding)),
_buildSecondColumn(const EdgeInsetsDirectional.only(start: _kTimerPickerHalfColumnPadding, end: paddingValue / 2))
];
break;
} }
final CupertinoThemeData themeData = CupertinoTheme.of(context);
return MediaQuery( return MediaQuery(
data: const MediaQueryData( data: const MediaQueryData(
// The native iOS picker's text scaling is fixed, so we will also fix it // The native iOS picker's text scaling is fixed, so we will also fix it
// as well in our picker. // as well in our picker.
textScaleFactor: 1.0, textScaleFactor: 1.0,
), ),
child: picker, child: CupertinoTheme(
data: themeData.copyWith(
textTheme: themeData.textTheme.copyWith(
pickerTextStyle: _textStyleFrom(context),
)
),
child: Align(
alignment: widget.alignment,
child: Container(
color: _kBackgroundColor,
width: totalWidth,
height: _kPickerHeight,
child: DefaultTextStyle(
style: _textStyleFrom(context),
child: Row(children: columns.map((Widget child) => Expanded(child: child)).toList(growable: false)),
),
),
),
),
); );
} }
} }
...@@ -104,22 +104,22 @@ const TextStyle _kDefaultPickerDarkTextStyle = TextStyle( ...@@ -104,22 +104,22 @@ const TextStyle _kDefaultPickerDarkTextStyle = TextStyle(
); );
// Eyeballed value since it's not documented in https://developer.apple.com/design/resources/. // Eyeballed value since it's not documented in https://developer.apple.com/design/resources/.
// Inspected on iOS 13 simulator with "Debug View Hierarchy".
const TextStyle _kDefaultDateTimePickerLightTextStyle = TextStyle( const TextStyle _kDefaultDateTimePickerLightTextStyle = TextStyle(
inherit: false, inherit: false,
fontFamily: '.SF Pro Display', fontFamily: '.SF Pro Display',
fontSize: 21, fontSize: 21,
fontWeight: FontWeight.w300, fontWeight: FontWeight.normal,
letterSpacing: -1.05,
color: CupertinoColors.black, color: CupertinoColors.black,
); );
// Eyeballed value since it's not documented in https://developer.apple.com/design/resources/. // Eyeballed value since it's not documented in https://developer.apple.com/design/resources/.
// Inspected on iOS 13 simulator with "Debug View Hierarchy".
const TextStyle _kDefaultDateTimePickerDarkTextStyle = TextStyle( const TextStyle _kDefaultDateTimePickerDarkTextStyle = TextStyle(
inherit: false, inherit: false,
fontFamily: '.SF Pro Display', fontFamily: '.SF Pro Display',
fontSize: 21, fontSize: 21,
fontWeight: FontWeight.w300, fontWeight: FontWeight.normal,
letterSpacing: -1.05,
color: CupertinoColors.white, color: CupertinoColors.white,
); );
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -248,8 +249,8 @@ void main() { ...@@ -248,8 +249,8 @@ void main() {
width: 400.0, width: 400.0,
child: CupertinoTimerPicker( child: CupertinoTimerPicker(
minuteInterval: 10, minuteInterval: 10,
secondInterval: 15, secondInterval: 12,
initialTimerDuration: const Duration(hours: 10, minutes: 40, seconds: 45), initialTimerDuration: const Duration(hours: 10, minutes: 40, seconds: 48),
mode: CupertinoTimerPickerMode.hms, mode: CupertinoTimerPickerMode.hms,
onTimerDurationChanged: (Duration d) { onTimerDurationChanged: (Duration d) {
duration = d; duration = d;
...@@ -261,13 +262,13 @@ void main() { ...@@ -261,13 +262,13 @@ void main() {
await tester.drag(find.text('40'), _kRowOffset); await tester.drag(find.text('40'), _kRowOffset);
await tester.pump(); await tester.pump();
await tester.drag(find.text('45'), -_kRowOffset); await tester.drag(find.text('48'), -_kRowOffset);
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect( expect(
duration, duration,
const Duration(hours: 10, minutes: 50, seconds: 30), const Duration(hours: 10, minutes: 50, seconds: 36),
); );
}); });
...@@ -905,7 +906,7 @@ void main() { ...@@ -905,7 +906,7 @@ void main() {
CupertinoApp( CupertinoApp(
home: Center( home: Center(
child: SizedBox( child: SizedBox(
width: 400, width: 500,
height: 400, height: 400,
child: RepaintBoundary( child: RepaintBoundary(
child: CupertinoDatePicker( child: CupertinoDatePicker(
...@@ -923,7 +924,7 @@ void main() { ...@@ -923,7 +924,7 @@ void main() {
find.byType(CupertinoDatePicker), find.byType(CupertinoDatePicker),
matchesGoldenFile( matchesGoldenFile(
'date_picker_test.datetime.initial.png', 'date_picker_test.datetime.initial.png',
version: 1, version: 2,
), ),
); );
...@@ -935,10 +936,140 @@ void main() { ...@@ -935,10 +936,140 @@ void main() {
find.byType(CupertinoDatePicker), find.byType(CupertinoDatePicker),
matchesGoldenFile( matchesGoldenFile(
'date_picker_test.datetime.drag.png', 'date_picker_test.datetime.drag.png',
version: 2,
),
);
});
});
testWidgets('TimerPicker golden tests', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
// Also check if the picker respects the theme.
theme: const CupertinoThemeData(
textTheme: CupertinoTextThemeData(
pickerTextStyle: TextStyle(
color: Color(0xFF663311),
),
),
),
home: Center(
child: SizedBox(
width: 320,
height: 216,
child: RepaintBoundary(
child: CupertinoTimerPicker(
mode: CupertinoTimerPickerMode.hm,
initialTimerDuration: const Duration(hours: 23, minutes: 59),
onTimerDurationChanged: (_) {},
),
)
),
),
),
);
await expectLater(
find.byType(CupertinoTimerPicker),
matchesGoldenFile(
'timer_picker_test.datetime.initial.png',
version: 1, version: 1,
), ),
); );
// Slightly drag the minute component to make the current minute off-center.
await tester.drag(find.text('59'), Offset(0, _kRowOffset.dy / 2));
await tester.pump();
await expectLater(
find.byType(CupertinoTimerPicker),
matchesGoldenFile(
'timer_picker_test.datetime.drag.png',
version: 1,
),
);
});
testWidgets('TimerPicker only changes hour label after scrolling stops', (WidgetTester tester) async {
Duration duration;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: SizedBox(
width: 320,
height: 216,
child: CupertinoTimerPicker(
mode: CupertinoTimerPickerMode.hm,
initialTimerDuration: const Duration(hours: 2, minutes: 30),
onTimerDurationChanged: (Duration d) { duration = d; },
),
),
),
),
);
expect(duration, isNull);
expect(find.text('hour'), findsNothing);
expect(find.text('hours'), findsOneWidget);
await tester.drag(find.text('2'), Offset(0, -_kRowOffset.dy));
// Duration should change but not the label.
expect(duration?.inHours, 1);
expect(find.text('hour'), findsNothing);
expect(find.text('hours'), findsOneWidget);
await tester.pumpAndSettle();
// Now the label should change.
expect(duration?.inHours, 1);
expect(find.text('hours'), findsNothing);
expect(find.text('hour'), findsOneWidget);
}); });
testWidgets('TimerPicker has intrinsic width and height', (WidgetTester tester) async {
const Key key = Key('key');
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTimerPicker(
key: key,
mode: CupertinoTimerPickerMode.hm,
initialTimerDuration: const Duration(hours: 2, minutes: 30),
onTimerDurationChanged: (Duration d) {},
),
),
);
expect(tester.getSize(find.descendant(of: find.byKey(key), matching: find.byType(Row))), const Size(320, 216));
// Different modes shouldn't share state.
await tester.pumpWidget(const Placeholder());
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTimerPicker(
key: key,
mode: CupertinoTimerPickerMode.ms,
initialTimerDuration: const Duration(minutes: 30, seconds: 3),
onTimerDurationChanged: (Duration d) {},
),
),
);
expect(tester.getSize(find.descendant(of: find.byKey(key), matching: find.byType(Row))), const Size(320, 216));
// Different modes shouldn't share state.
await tester.pumpWidget(const Placeholder());
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTimerPicker(
key: key,
mode: CupertinoTimerPickerMode.hms,
initialTimerDuration: const Duration(hours: 5, minutes: 17, seconds: 19),
onTimerDurationChanged: (Duration d) {},
),
),
);
expect(tester.getSize(find.descendant(of: find.byKey(key), matching: find.byType(Row))), const Size(330, 216));
}); });
testWidgets('scrollController can be removed or added', (WidgetTester tester) async { testWidgets('scrollController can be removed or added', (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment