Unverified Commit 8cfc9246 authored by xster's avatar xster Committed by GitHub

CupertinoPicker fidelity revision (#31464)

parent 9e51e13e
09ebc5361187e9cc20ddc350dc047f95812c61a4
8057c8e1e0276a2ae7c26a0e04d54f339f3c51ca
......@@ -95,8 +95,8 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
setState(() => _selectedColorIndex = index);
},
children: List<Widget>.generate(coolColorNames.length, (int index) {
return Center(child:
Text(coolColorNames[index]),
return Center(
child: Text(coolColorNames[index]),
);
}),
),
......
......@@ -8,13 +8,17 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'localizations.dart';
import 'picker.dart';
import 'theme.dart';
// Default aesthetic values obtained by comparing with iOS pickers.
const double _kItemExtent = 32.0;
const double _kPickerWidth = 330.0;
const bool _kUseMagnifier = true;
const double _kMagnification = 1.05;
const double _kMagnification = 1.08;
const double _kDatePickerPadSize = 12.0;
// The density of a date picker is different from a generic picker.
// Eyeballed from iOS.
const double _kSqueeze = 1.25;
// Considers setting the default background color from the theme, in the future.
const Color _kBackgroundColor = CupertinoColors.white;
......@@ -22,6 +26,10 @@ const TextStyle _kDefaultPickerTextStyle = TextStyle(
letterSpacing: -0.83,
);
TextStyle _themeTextStyle(BuildContext context) {
return CupertinoTheme.of(context).textTheme.dateTimePickerTextStyle;
}
// Lays out the date picker based on how much space each single column needs.
//
// Each column is a child of this delegate, indexed from 0 to number of columns - 1.
......@@ -58,6 +66,13 @@ class _DatePickerLayoutDelegate extends MultiChildLayoutDelegate {
if (index == 0 || index == columnWidths.length - 1)
childWidth += remainingWidth / 2;
assert(
childWidth >= 0,
'Insufficient horizontal space to render the CupertinoDatePicker '
'because the parent is too narrow at ${size.width}px.\n'
'An additional ${-remainingWidth}px is needed to avoid overlapping '
'columns.',
);
layoutChild(index, BoxConstraints.tight(Size(childWidth, size.height)));
positionChild(index, Offset(currentHorizontalOffset, 0.0));
......@@ -131,6 +146,13 @@ enum _PickerColumnType {
/// * US-English: [July | 13 | 2012]
/// * Vietnamese: [13 | Tháng 7 | 2012]
///
/// Can be used with [showCupertinoModalPopup] to display the picker modally at
/// the bottom of the screen.
///
/// Sizes itself to its parent and may not render correctly if not given the
/// full screen width. Content texts are shown with
/// [CupertinoTextThemeData.dateTimePickerTextStyle].
///
/// See also:
///
/// * [CupertinoTimerPicker], the class that implements the iOS-style timer picker.
......@@ -321,7 +343,7 @@ class CupertinoDatePicker extends StatefulWidget {
final TextPainter painter = TextPainter(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
style: _themeTextStyle(context),
text: longestText,
),
textDirection: Directionality.of(context),
......@@ -339,6 +361,10 @@ class CupertinoDatePicker extends StatefulWidget {
typedef _ColumnBuilder = Widget Function(double offAxisFraction, TransitionBuilder itemPositioningBuilder);
class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
// Fraction of the farthest column's vanishing point vs its width. Eyeballed
// vs iOS.
static const double _kMaximumOffAxisFraction = 0.45;
int textDirectionFactor;
CupertinoLocalizations localizations;
......@@ -462,6 +488,7 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
useMagnifier: _kUseMagnifier,
magnification: _kMagnification,
backgroundColor: _kBackgroundColor,
squeeze: _kSqueeze,
onSelectedItemChanged: (int index) {
selectedDayFromInitial = index;
widget.onDateTimeChanged(_getDateTime());
......@@ -478,9 +505,21 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
if (widget.maximumDate != null && dateTime.isAfter(widget.maximumDate))
return null;
final DateTime now = DateTime.now();
String dateText;
if (dateTime == DateTime(now.year, now.month, now.day)) {
dateText = localizations.todayLabel;
} else {
dateText = localizations.datePickerMediumDate(dateTime);
}
return itemPositioningBuilder(
context,
Text(localizations.datePickerMediumDate(dateTime)),
Text(
dateText,
style: _themeTextStyle(context),
),
);
},
);
......@@ -494,6 +533,7 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
useMagnifier: _kUseMagnifier,
magnification: _kMagnification,
backgroundColor: _kBackgroundColor,
squeeze: _kSqueeze,
onSelectedItemChanged: (int index) {
if (widget.use24hFormat) {
selectedHour = index;
......@@ -531,6 +571,7 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
Text(
localizations.datePickerHour(hour),
semanticsLabel: localizations.datePickerHourSemanticsLabel(hour),
style: _themeTextStyle(context),
),
);
}),
......@@ -546,6 +587,7 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
useMagnifier: _kUseMagnifier,
magnification: _kMagnification,
backgroundColor: _kBackgroundColor,
squeeze: _kSqueeze,
onSelectedItemChanged: (int index) {
selectedMinute = index * widget.minuteInterval;
widget.onDateTimeChanged(_getDateTime());
......@@ -557,6 +599,7 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
Text(
localizations.datePickerMinute(minute),
semanticsLabel: localizations.datePickerMinuteSemanticsLabel(minute),
style: _themeTextStyle(context),
),
);
}),
......@@ -572,6 +615,7 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
useMagnifier: _kUseMagnifier,
magnification: _kMagnification,
backgroundColor: _kBackgroundColor,
squeeze: _kSqueeze,
onSelectedItemChanged: (int index) {
selectedAmPm = index;
widget.onDateTimeChanged(_getDateTime());
......@@ -582,7 +626,8 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
Text(
index == 0
? localizations.anteMeridiemAbbreviation
: localizations.postMeridiemAbbreviation
: localizations.postMeridiemAbbreviation,
style: _themeTextStyle(context),
),
);
}),
......@@ -630,9 +675,9 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
for (int i = 0; i < columnWidths.length; i++) {
double offAxisFraction = 0.0;
if (i == 0)
offAxisFraction = -0.5 * textDirectionFactor;
offAxisFraction = -_kMaximumOffAxisFraction * textDirectionFactor;
else if (i >= 2 || columnWidths.length == 2)
offAxisFraction = 0.5 * textDirectionFactor;
offAxisFraction = _kMaximumOffAxisFraction * textDirectionFactor;
EdgeInsets padding = const EdgeInsets.only(right: _kDatePickerPadSize);
if (i == columnWidths.length - 1)
......@@ -735,21 +780,22 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
useMagnifier: _kUseMagnifier,
magnification: _kMagnification,
backgroundColor: _kBackgroundColor,
squeeze: _kSqueeze,
onSelectedItemChanged: (int index) {
selectedDay = index + 1;
if (DateTime(selectedYear, selectedMonth, selectedDay).day == selectedDay)
widget.onDateTimeChanged(DateTime(selectedYear, selectedMonth, selectedDay));
},
children: List<Widget>.generate(31, (int index) {
TextStyle disableTextStyle; // Null if not out of range.
TextStyle textStyle = _themeTextStyle(context);
if (index >= daysInCurrentMonth) {
disableTextStyle = const TextStyle(color: CupertinoColors.inactiveGray);
textStyle = textStyle.copyWith(color: CupertinoColors.inactiveGray);
}
return itemPositioningBuilder(
context,
Text(
localizations.datePickerDayOfMonth(index + 1),
style: disableTextStyle,
style: textStyle,
),
);
}),
......@@ -765,6 +811,7 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
useMagnifier: _kUseMagnifier,
magnification: _kMagnification,
backgroundColor: _kBackgroundColor,
squeeze: _kSqueeze,
onSelectedItemChanged: (int index) {
selectedMonth = index + 1;
if (DateTime(selectedYear, selectedMonth, selectedDay).day == selectedDay)
......@@ -773,7 +820,10 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
children: List<Widget>.generate(12, (int index) {
return itemPositioningBuilder(
context,
Text(localizations.datePickerMonth(index + 1)),
Text(
localizations.datePickerMonth(index + 1),
style: _themeTextStyle(context),
),
);
}),
looping: true,
......@@ -802,7 +852,10 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
return itemPositioningBuilder(
context,
Text(localizations.datePickerYear(index)),
Text(
localizations.datePickerYear(index),
style: _themeTextStyle(context),
),
);
},
);
......@@ -956,6 +1009,8 @@ enum CupertinoTimerPickerMode {
///
/// There are several modes of the timer picker listed in [CupertinoTimerPickerMode].
///
/// Sizes itself to its parent.
///
/// See also:
///
/// * [CupertinoDatePicker], the class that implements different display modes
......@@ -1045,7 +1100,7 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
Widget _buildLabel(String text) {
return Text(
text,
textScaleFactor: 0.8,
textScaleFactor: 0.9,
style: const TextStyle(fontWeight: FontWeight.w600),
);
}
......@@ -1067,6 +1122,7 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
offAxisFraction: -0.5 * textDirectionFactor,
itemExtent: _kItemExtent,
backgroundColor: _kBackgroundColor,
squeeze: _kSqueeze,
onSelectedItemChanged: (int index) {
setState(() {
selectedHour = index;
......@@ -1145,6 +1201,7 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
offAxisFraction: offAxisFraction,
itemExtent: _kItemExtent,
backgroundColor: _kBackgroundColor,
squeeze: _kSqueeze,
onSelectedItemChanged: (int index) {
setState(() {
selectedMinute = index * widget.minuteInterval;
......@@ -1257,6 +1314,7 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
offAxisFraction: offAxisFraction,
itemExtent: _kItemExtent,
backgroundColor: _kBackgroundColor,
squeeze: _kSqueeze,
onSelectedItemChanged: (int index) {
setState(() {
selectedSecond = index * widget.secondInterval;
......
......@@ -142,6 +142,10 @@ abstract class CupertinoLocalizations {
// The global version uses the translated string from the arb file.
String get postMeridiemAbbreviation;
/// Label shown in date pickers when the date is today.
// The global version uses the translated string from the arb file.
String get todayLabel;
/// The term used by the system to announce dialog alerts.
// The global version uses the translated string from the arb file.
String get alertDialogLabel;
......@@ -338,6 +342,9 @@ class DefaultCupertinoLocalizations implements CupertinoLocalizations {
@override
String get postMeridiemAbbreviation => 'PM';
@override
String get todayLabel => 'Today';
@override
String get alertDialogLabel => 'Alert';
......@@ -354,10 +361,10 @@ class DefaultCupertinoLocalizations implements CupertinoLocalizations {
String timerPickerHourLabel(int hour) => hour == 1 ? 'hour' : 'hours';
@override
String timerPickerMinuteLabel(int minute) => 'min';
String timerPickerMinuteLabel(int minute) => 'min.';
@override
String timerPickerSecondLabel(int second) => 'sec';
String timerPickerSecondLabel(int second) => 'sec.';
@override
String get cutButtonLabel => 'Cut';
......
......@@ -7,13 +7,16 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'theme.dart';
/// Color of the 'magnifier' lens border.
const Color _kHighlighterBorder = Color(0xFF7F7F7F);
const Color _kDefaultBackground = Color(0xFFD2D4DB);
// Eyeballed values comparing with a native picker.
// Values closer to PI produces denser flatter lists.
const double _kDefaultDiameterRatio = 1.35;
const double _kDefaultPerspective = 0.004;
// Eyeballed values comparing with a native picker to produce the right
// curvatures and densities.
const double _kDefaultDiameterRatio = 1.07;
const double _kDefaultPerspective = 0.003;
const double _kSqueeze = 1.45;
/// Opacity fraction value that hides the wheel above and below the 'magnifier'
/// lens with the same color as the background.
const double _kForegroundScreenOpacityFraction = 0.7;
......@@ -26,6 +29,11 @@ const double _kForegroundScreenOpacityFraction = 0.7;
/// Can be used with [showCupertinoModalPopup] to display the picker modally at the
/// bottom of the screen.
///
/// Sizes itself to its parent. All children are sized to the same size based
/// on [itemExtent].
///
/// By default, descendent texts are shown with [CupertinoTextThemeData.pickerTextStyle].
///
/// See also:
///
/// * [ListWheelScrollView], the generic widget backing this picker without
......@@ -58,6 +66,7 @@ class CupertinoPicker extends StatefulWidget {
this.useMagnifier = false,
this.magnification = 1.0,
this.scrollController,
this.squeeze = _kSqueeze,
@required this.itemExtent,
@required this.onSelectedItemChanged,
@required List<Widget> children,
......@@ -68,6 +77,8 @@ class CupertinoPicker extends StatefulWidget {
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
assert(squeeze != null),
assert(squeeze > 0),
childDelegate = looping
? ListWheelChildLoopingListDelegate(children: children)
: ListWheelChildListDelegate(children: children),
......@@ -98,6 +109,7 @@ class CupertinoPicker extends StatefulWidget {
this.useMagnifier = false,
this.magnification = 1.0,
this.scrollController,
this.squeeze = _kSqueeze,
@required this.itemExtent,
@required this.onSelectedItemChanged,
@required IndexedWidgetBuilder itemBuilder,
......@@ -108,6 +120,8 @@ class CupertinoPicker extends StatefulWidget {
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
assert(squeeze != null),
assert(squeeze > 0),
childDelegate = ListWheelChildBuilderDelegate(builder: itemBuilder, childCount: childCount),
super(key: key);
......@@ -151,6 +165,11 @@ class CupertinoPicker extends StatefulWidget {
/// height. Must not be null and must be positive.
final double itemExtent;
/// {@macro flutter.rendering.wheelList.squeeze}
///
/// Defaults to `1.45` fo visually mimic iOS.
final double squeeze;
/// An option callback when the currently centered item changes.
///
/// Value changes when the item closest to the center changes.
......@@ -313,7 +332,9 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
@override
Widget build(BuildContext context) {
Widget result = Stack(
Widget result = DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.pickerTextStyle,
child: Stack(
children: <Widget>[
Positioned.fill(
child: _CupertinoPickerSemantics(
......@@ -327,6 +348,7 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
useMagnifier: widget.useMagnifier,
magnification: widget.magnification,
itemExtent: widget.itemExtent,
squeeze: widget.squeeze,
onSelectedItemChanged: _handleSelectedItemChanged,
childDelegate: widget.childDelegate,
),
......@@ -335,6 +357,7 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
_buildGradientScreen(),
_buildMagnifierScreen(),
],
),
);
// Adds the appropriate opacity under the magnifier if the background
// color is transparent.
......
......@@ -83,6 +83,46 @@ const TextStyle _kDefaultLargeTitleDarkTextStyle = TextStyle(
color: CupertinoColors.white,
);
// Eyeballed value since it's not documented in https://developer.apple.com/design/resources/.
const TextStyle _kDefaultPickerLightTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Display',
fontSize: 25.0,
fontWeight: FontWeight.w400,
letterSpacing: -0.41,
color: CupertinoColors.black,
);
// Eyeballed value since it's not documented in https://developer.apple.com/design/resources/.
const TextStyle _kDefaultPickerDarkTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Display',
fontSize: 25.0,
fontWeight: FontWeight.w400,
letterSpacing: -0.41,
color: CupertinoColors.white,
);
// Eyeballed value since it's not documented in https://developer.apple.com/design/resources/.
const TextStyle _kDefaultDateTimePickerLightTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Display',
fontSize: 21,
fontWeight: FontWeight.w300,
letterSpacing: -1.05,
color: CupertinoColors.black,
);
// Eyeballed value since it's not documented in https://developer.apple.com/design/resources/.
const TextStyle _kDefaultDateTimePickerDarkTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Display',
fontSize: 21,
fontWeight: FontWeight.w300,
letterSpacing: -1.05,
color: CupertinoColors.white,
);
/// Cupertino typography theme in a [CupertinoThemeData].
@immutable
class CupertinoTextThemeData extends Diagnosticable {
......@@ -104,6 +144,8 @@ class CupertinoTextThemeData extends Diagnosticable {
TextStyle navTitleTextStyle,
TextStyle navLargeTitleTextStyle,
TextStyle navActionTextStyle,
TextStyle pickerTextStyle,
TextStyle dateTimePickerTextStyle,
}) : _primaryColor = primaryColor ?? CupertinoColors.activeBlue,
_brightness = brightness,
_textStyle = textStyle,
......@@ -111,7 +153,9 @@ class CupertinoTextThemeData extends Diagnosticable {
_tabLabelTextStyle = tabLabelTextStyle,
_navTitleTextStyle = navTitleTextStyle,
_navLargeTitleTextStyle = navLargeTitleTextStyle,
_navActionTextStyle = navActionTextStyle;
_navActionTextStyle = navActionTextStyle,
_pickerTextStyle = pickerTextStyle,
_dateTimePickerTextStyle = dateTimePickerTextStyle;
final Color _primaryColor;
final Brightness _brightness;
......@@ -155,6 +199,20 @@ class CupertinoTextThemeData extends Diagnosticable {
);
}
final TextStyle _pickerTextStyle;
/// Typography of pickers.
TextStyle get pickerTextStyle {
return _pickerTextStyle ??
(_isLight ? _kDefaultPickerLightTextStyle : _kDefaultPickerDarkTextStyle);
}
final TextStyle _dateTimePickerTextStyle;
/// Typography of date time pickers.
TextStyle get dateTimePickerTextStyle {
return _dateTimePickerTextStyle ??
(_isLight ? _kDefaultDateTimePickerLightTextStyle : _kDefaultDateTimePickerDarkTextStyle);
}
/// Returns a copy of the current [CupertinoTextThemeData] instance with
/// specified overrides.
CupertinoTextThemeData copyWith({
......@@ -166,6 +224,8 @@ class CupertinoTextThemeData extends Diagnosticable {
TextStyle navTitleTextStyle,
TextStyle navLargeTitleTextStyle,
TextStyle navActionTextStyle,
TextStyle pickerTextStyle,
TextStyle dateTimePickerTextStyle,
}) {
return CupertinoTextThemeData(
primaryColor: primaryColor ?? _primaryColor,
......@@ -176,6 +236,8 @@ class CupertinoTextThemeData extends Diagnosticable {
navTitleTextStyle: navTitleTextStyle ?? _navTitleTextStyle,
navLargeTitleTextStyle: navLargeTitleTextStyle ?? _navLargeTitleTextStyle,
navActionTextStyle: navActionTextStyle ?? _navActionTextStyle,
pickerTextStyle: pickerTextStyle ?? _pickerTextStyle,
dateTimePickerTextStyle: dateTimePickerTextStyle ?? _dateTimePickerTextStyle,
);
}
}
......@@ -137,10 +137,11 @@ class RenderListWheelViewport
@required ViewportOffset offset,
double diameterRatio = defaultDiameterRatio,
double perspective = defaultPerspective,
double offAxisFraction = 0.0,
double offAxisFraction = 0,
bool useMagnifier = false,
double magnification = 1.0,
double magnification = 1,
@required double itemExtent,
double squeeze = 1,
bool clipToSize = true,
bool renderChildrenOutsideViewport = false,
List<RenderBox> children,
......@@ -156,6 +157,8 @@ class RenderListWheelViewport
assert(magnification != null),
assert(magnification > 0),
assert(itemExtent != null),
assert(squeeze != null),
assert(squeeze > 0),
assert(itemExtent > 0),
assert(clipToSize != null),
assert(renderChildrenOutsideViewport != null),
......@@ -170,6 +173,7 @@ class RenderListWheelViewport
_useMagnifier = useMagnifier,
_magnification = magnification,
_itemExtent = itemExtent,
_squeeze = squeeze,
_clipToSize = clipToSize,
_renderChildrenOutsideViewport = renderChildrenOutsideViewport {
addAll(children);
......@@ -381,6 +385,39 @@ class RenderListWheelViewport
markNeedsLayout();
}
/// {@template flutter.rendering.wheelList.squeeze}
/// The angular compactness of the children on the wheel.
///
/// This denotes a ratio of the number of children on the wheel vs the number
/// of children that would fit on a flat list of equivalent size, assuming
/// [diameterRatio] of 1.
///
/// For instance, if this RenderListWheelViewport has a height of 100px and
/// [itemExtent] is 20px, 5 items would fit on an equivalent flat list.
/// With a [squeeze] of 1, 5 items would also be shown in the
/// RenderListWheelViewport. With a [squeeze] of 2, 10 items would be shown
/// in the RenderListWheelViewport.
///
/// Changing this value will change the number of children built and shown
/// inside the wheel.
///
/// Must not be null and must be positive.
/// {@endtemplate}
///
/// Defaults to 1.
double get squeeze => _squeeze;
double _squeeze;
set squeeze(double value) {
assert(value != null);
assert(value > 0);
if (value == _squeeze)
return;
_squeeze = value;
markNeedsLayout();
markNeedsSemanticsUpdate();
}
/// {@template flutter.rendering.wheelList.clipToSize}
/// Whether to clip painted children to the inside of this viewport.
///
......@@ -614,7 +651,7 @@ class RenderListWheelViewport
// The height, in pixel, that children will be visible and might be laid out
// and painted.
double visibleHeight = size.height;
double visibleHeight = size.height * _squeeze;
// If renderChildrenOutsideViewport is true, we spawn extra children by
// doubling the visibility range, those that are in the backside of the
// cylinder won't be painted anyway.
......@@ -769,7 +806,7 @@ class RenderListWheelViewport
// Get child's center as a fraction of the viewport's height.
final double fractionalY =
(untransformedPaintingCoordinates.dy + _itemExtent / 2.0) / size.height;
final double angle = -(fractionalY - 0.5) * 2.0 * _maxVisibleRadian;
final double angle = -(fractionalY - 0.5) * 2.0 * _maxVisibleRadian / squeeze;
// Don't paint the backside of the cylinder when
// renderChildrenOutsideViewport is true. Otherwise, only children within
// suitable angles (via _first/lastVisibleLayoutOffset) reach the paint
......
......@@ -576,6 +576,7 @@ class ListWheelScrollView extends StatefulWidget {
this.useMagnifier = false,
this.magnification = 1.0,
@required this.itemExtent,
this.squeeze = 1.0,
this.onSelectedItemChanged,
this.clipToSize = true,
this.renderChildrenOutsideViewport = false,
......@@ -589,6 +590,8 @@ class ListWheelScrollView extends StatefulWidget {
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
assert(squeeze != null),
assert(squeeze > 0),
assert(clipToSize != null),
assert(renderChildrenOutsideViewport != null),
assert(
......@@ -610,6 +613,7 @@ class ListWheelScrollView extends StatefulWidget {
this.useMagnifier = false,
this.magnification = 1.0,
@required this.itemExtent,
this.squeeze = 1.0,
this.onSelectedItemChanged,
this.clipToSize = true,
this.renderChildrenOutsideViewport = false,
......@@ -623,6 +627,8 @@ class ListWheelScrollView extends StatefulWidget {
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
assert(squeeze != null),
assert(squeeze > 0),
assert(clipToSize != null),
assert(renderChildrenOutsideViewport != null),
assert(
......@@ -675,6 +681,11 @@ class ListWheelScrollView extends StatefulWidget {
/// positive.
final double itemExtent;
/// {@macro flutter.rendering.wheelList.squeeze}
///
/// Defaults to 1.
final double squeeze;
/// On optional listener that's called when the centered item changes.
final ValueChanged<int> onSelectedItemChanged;
......@@ -747,6 +758,7 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> {
useMagnifier: widget.useMagnifier,
magnification: widget.magnification,
itemExtent: widget.itemExtent,
squeeze: widget.squeeze,
clipToSize: widget.clipToSize,
renderChildrenOutsideViewport: widget.renderChildrenOutsideViewport,
offset: offset,
......@@ -941,6 +953,7 @@ class ListWheelViewport extends RenderObjectWidget {
this.useMagnifier = false,
this.magnification = 1.0,
@required this.itemExtent,
this.squeeze = 1.0,
this.clipToSize = true,
this.renderChildrenOutsideViewport = false,
@required this.offset,
......@@ -954,6 +967,8 @@ class ListWheelViewport extends RenderObjectWidget {
assert(perspective <= 0.01, RenderListWheelViewport.perspectiveTooHighMessage),
assert(itemExtent != null),
assert(itemExtent > 0),
assert(squeeze != null),
assert(squeeze > 0),
assert(clipToSize != null),
assert(renderChildrenOutsideViewport != null),
assert(
......@@ -980,6 +995,11 @@ class ListWheelViewport extends RenderObjectWidget {
/// {@macro flutter.rendering.wheelList.itemExtent}
final double itemExtent;
/// {@macro flutter.rendering.wheelList.squeeze}
///
/// Defaults to 1.
final double squeeze;
/// {@macro flutter.rendering.wheelList.clipToSize}
final bool clipToSize;
......@@ -1008,6 +1028,7 @@ class ListWheelViewport extends RenderObjectWidget {
useMagnifier: useMagnifier,
magnification: magnification,
itemExtent: itemExtent,
squeeze: squeeze,
clipToSize: clipToSize,
renderChildrenOutsideViewport: renderChildrenOutsideViewport,
);
......@@ -1023,6 +1044,7 @@ class ListWheelViewport extends RenderObjectWidget {
..useMagnifier = useMagnifier
..magnification = magnification
..itemExtent = itemExtent
..squeeze = squeeze
..clipToSize = clipToSize
..renderChildrenOutsideViewport = renderChildrenOutsideViewport;
}
......
......@@ -123,13 +123,13 @@ void main() {
expect(tester.getTopLeft(find.text('30')).dx > lastOffset.dx, true);
lastOffset = tester.getTopLeft(find.text('30'));
expect(tester.getTopLeft(find.text('min')).dx > lastOffset.dx, true);
lastOffset = tester.getTopLeft(find.text('min'));
expect(tester.getTopLeft(find.text('min.')).dx > lastOffset.dx, true);
lastOffset = tester.getTopLeft(find.text('min.'));
expect(tester.getTopLeft(find.text('59')).dx > lastOffset.dx, true);
lastOffset = tester.getTopLeft(find.text('59'));
expect(tester.getTopLeft(find.text('sec')).dx > lastOffset.dx, true);
expect(tester.getTopLeft(find.text('sec.')).dx > lastOffset.dx, true);
});
testWidgets('columns are ordered correctly when text direction is rtl', (WidgetTester tester) async {
......@@ -153,13 +153,13 @@ void main() {
expect(tester.getTopLeft(find.text('30')).dx > lastOffset.dx, false);
lastOffset = tester.getTopLeft(find.text('30'));
expect(tester.getTopLeft(find.text('min')).dx > lastOffset.dx, false);
lastOffset = tester.getTopLeft(find.text('min'));
expect(tester.getTopLeft(find.text('min.')).dx > lastOffset.dx, false);
lastOffset = tester.getTopLeft(find.text('min.'));
expect(tester.getTopLeft(find.text('59')).dx > lastOffset.dx, false);
lastOffset = tester.getTopLeft(find.text('59'));
expect(tester.getTopLeft(find.text('sec')).dx > lastOffset.dx, false);
expect(tester.getTopLeft(find.text('sec.')).dx > lastOffset.dx, false);
});
testWidgets('width of picker is consistent', (WidgetTester tester) async {
......@@ -178,7 +178,7 @@ void main() {
// Distance between the first column and the last column.
final double distance =
tester.getCenter(find.text('sec')).dx - tester.getCenter(find.text('12')).dx;
tester.getCenter(find.text('sec.')).dx - tester.getCenter(find.text('12')).dx;
await tester.pumpWidget(
CupertinoApp(
......@@ -195,7 +195,7 @@ void main() {
// Distance between the first and the last column should be the same.
expect(
tester.getCenter(find.text('sec')).dx - tester.getCenter(find.text('12')).dx,
tester.getCenter(find.text('sec.')).dx - tester.getCenter(find.text('12')).dx,
distance,
);
});
......@@ -270,7 +270,8 @@ void main() {
DateTime newDateTime;
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
width: 400,
height: 400,
child: CupertinoDatePicker(
......@@ -278,6 +279,7 @@ void main() {
initialDateTime: DateTime(2018, 10, 10, 10, 3),
minuteInterval: 3,
)
),
)
)
);
......@@ -295,7 +297,8 @@ void main() {
DateTime selectedDateTime;
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -305,6 +308,7 @@ void main() {
),
),
),
),
);
await tester.drag(find.text('10'), const Offset(0.0, 32.0), touchSlopY: 0);
......@@ -315,7 +319,8 @@ void main() {
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -326,6 +331,7 @@ void main() {
),
),
),
),
);
await tester.drag(find.text('9'), const Offset(0.0, 32.0), touchSlopY: 0);
......@@ -339,7 +345,8 @@ void main() {
testWidgets('date picker has expected string', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -349,6 +356,7 @@ void main() {
),
),
),
),
);
expect(find.text('September'), findsOneWidget);
......@@ -359,7 +367,8 @@ void main() {
testWidgets('datetime picker has expected string', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -369,6 +378,7 @@ void main() {
),
),
),
),
);
expect(find.text('Sat Sep 15'), findsOneWidget);
......@@ -397,7 +407,8 @@ void main() {
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 800.0,
child: CupertinoDatePicker(
......@@ -407,6 +418,7 @@ void main() {
),
),
),
),
);
// Distance between the first and the last column should be the same.
......@@ -419,7 +431,8 @@ void main() {
testWidgets('width of picker in date mode is consistent', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -429,6 +442,7 @@ void main() {
),
),
),
),
);
// Distance between the first column and the last column.
......@@ -437,7 +451,8 @@ void main() {
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 800.0,
child: CupertinoDatePicker(
......@@ -447,6 +462,7 @@ void main() {
),
),
),
),
);
// Distance between the first and the last column should be the same.
......@@ -459,7 +475,8 @@ void main() {
testWidgets('width of picker in time mode is consistent', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -469,6 +486,7 @@ void main() {
),
),
),
),
);
// Distance between the first column and the last column.
......@@ -477,7 +495,8 @@ void main() {
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 800.0,
child: CupertinoDatePicker(
......@@ -487,6 +506,7 @@ void main() {
),
),
),
),
);
// Distance between the first and the last column should be the same.
......@@ -500,7 +520,8 @@ void main() {
DateTime date;
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -512,6 +533,7 @@ void main() {
),
),
),
),
);
await tester.drag(find.text('March'), const Offset(0, 32.0), touchSlopY: 0.0);
......@@ -539,7 +561,8 @@ void main() {
DateTime date;
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -551,6 +574,7 @@ void main() {
),
),
),
),
);
await tester.drag(find.text('27'), const Offset(0.0, -32.0), touchSlopY: 0.0);
......@@ -592,7 +616,8 @@ void main() {
DateTime date;
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -604,6 +629,7 @@ void main() {
),
),
),
),
);
// 0:15 -> 0:16
......@@ -618,7 +644,8 @@ void main() {
DateTime date;
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -630,6 +657,7 @@ void main() {
),
),
),
),
);
// 12:15 -> 12:16
......@@ -644,7 +672,8 @@ void main() {
DateTime date;
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -657,6 +686,7 @@ void main() {
),
),
),
),
);
// 12:25 -> 12:26
......@@ -672,7 +702,8 @@ void main() {
DateTime date;
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -684,6 +715,7 @@ void main() {
),
),
),
),
);
// 3:00 -> 15:00
......@@ -719,7 +751,8 @@ void main() {
DateTime date;
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
......@@ -731,6 +764,7 @@ void main() {
),
),
),
),
);
const Offset deltaOffset = Offset(0.0, -18.0);
......@@ -763,6 +797,67 @@ void main() {
expect(date, DateTime(2018, 1, 1, 15, 59));
});
testWidgets('date picker given too narrow space horizontally shows message', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: SizedBox(
// This is too small to draw the picker out fully.
width: 100,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
initialDateTime: DateTime(2019, 1, 1, 4),
onDateTimeChanged: (_) {},
)
),
)
)
);
final dynamic exception = tester.takeException();
expect(exception, isAssertionError);
expect(
exception.toString(),
contains('Insufficient horizontal space to render the CupertinoDatePicker'),
);
});
testWidgets('DatePicker golden tests', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: SizedBox(
width: 400,
height: 400,
child: RepaintBoundary(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
initialDateTime: DateTime(2019, 1, 1, 4),
onDateTimeChanged: (_) {},
),
)
),
)
)
);
await expectLater(
find.byType(CupertinoDatePicker),
matchesGoldenFile('date_picker_test.datetime.initial.1.png'),
skip: !Platform.isLinux
);
// Slightly drag the hour component to make the current hour off-center.
await tester.drag(find.text('4'), Offset(0, _kRowOffset.dy / 2));
await tester.pump();
await expectLater(
find.byType(CupertinoDatePicker),
matchesGoldenFile('date_picker_test.datetime.drag.1.png'),
skip: !Platform.isLinux
);
});
});
testWidgets('scrollController can be removed or added', (WidgetTester tester) async {
......@@ -838,37 +933,6 @@ void main() {
expect(lastSelectedItem, 1);
handle.dispose();
});
testWidgets('DatePicker golden tests', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: SizedBox(
width: 200,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
initialDateTime: DateTime(2019, 1, 1, 4),
onDateTimeChanged: (_) {},
)
)
)
);
await expectLater(
find.byType(CupertinoDatePicker),
matchesGoldenFile('date_picker_test.datetime.initial.png'),
skip: !Platform.isLinux
);
// Slightly drag the hour component to make the current hour off-center.
await tester.drag(find.text('4'), Offset(0, _kRowOffset.dy / 2));
await tester.pump();
await expectLater(
find.byType(CupertinoDatePicker),
matchesGoldenFile('date_picker_test.datetime.drag.png'),
skip: !Platform.isLinux
);
});
}
Widget _buildPicker({ FixedExtentScrollController controller, ValueChanged<int> onSelectedItemChanged }) {
......
......@@ -3,10 +3,47 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Picker respects theme styling', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: 300.0,
width: 300.0,
child: CupertinoPicker(
itemExtent: 50.0,
onSelectedItemChanged: (_) { },
children: List<Widget>.generate(3, (int index) {
return Container(
height: 50.0,
width: 300.0,
child: Text(index.toString()),
);
}),
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject(find.text('1'));
expect(paragraph.text.style, const TextStyle(
inherit: false,
fontFamily: '.SF Pro Display',
fontSize: 25.0,
fontWeight: FontWeight.w400,
letterSpacing: -0.41,
color: CupertinoColors.black,
));
});
group('layout', () {
testWidgets('selected item is in the middle', (WidgetTester tester) async {
final FixedExtentScrollController controller =
......
......@@ -349,6 +349,51 @@ void main() {
// value of childCount should be 4.
expect(viewport.childCount, 4);
});
testWidgets('a tighter squeeze lays out more children', (WidgetTester tester) async {
final FixedExtentScrollController controller =
FixedExtentScrollController(initialItem: 10);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) { },
children: List<Widget>.generate(20, (int index) {
return Text(index.toString());
}),
),
)
);
final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Text)).parent.parent;
// The screen is vertically 600px. Since the middle item is centered,
// half of the first and last items are visible, making 7 children visible.
expect(viewport.childCount, 7);
// Pump the same widget again but with double the squeeze.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
squeeze: 2,
onSelectedItemChanged: (_) { },
children: List<Widget>.generate(20, (int index) {
return Text(index.toString());
}),
),
)
);
// 12 instead of 6 children are laid out + 1 because the middle item is
// centered.
expect(viewport.childCount, 13);
});
});
group('pre-transform viewport', () {
......
......@@ -33,6 +33,11 @@
"description": "The abbreviation for post meridiem (after noon) shown in the time picker when it's not using the 24h format. Reference the text iOS uses such as in the iOS clock app."
},
"todayLabel": "Today",
"@todayLabel": {
"description": "A label shown in the date picker when the date is today."
},
"alertDialogLabel": "Alert",
"@alertDialogLabel": {
"description": "The accessibility audio announcement made when an iOS style alert dialog is opened."
......@@ -45,15 +50,15 @@
"plural": "hour"
},
"timerPickerMinuteLabelOne": "min",
"timerPickerMinuteLabelOther": "min",
"timerPickerMinuteLabelOne": "min.",
"timerPickerMinuteLabelOther": "min.",
"@timerPickerMinuteLabel": {
"description": "The label adjacent to a minute integer number in a countdown timer. The reference abbreviation is what iOS does in the stock clock app's countdown timer.",
"plural": "minute"
},
"timerPickerSecondLabelOne": "sec",
"timerPickerSecondLabelOther": "sec",
"timerPickerSecondLabelOne": "sec.",
"timerPickerSecondLabelOther": "sec.",
"@timerPickerSecondLabel": {
"description": "The label adjacent to a second integer number in a countdown timer. The reference abbreviation is what iOS does in the stock clock app's countdown timer.",
"plural": "second"
......
......@@ -7,6 +7,7 @@
"datePickerDateTimeOrder": "date_time_dayPeriod",
"anteMeridiemAbbreviation": "AM",
"postMeridiemAbbreviation": "PM",
"todayLabel": "aujourd'hui",
"alertDialogLabel": "Alerte",
"timerPickerHourLabelOne": "heure",
"timerPickerHourLabelOther": "heures",
......
......@@ -94,16 +94,19 @@ class CupertinoLocalizationEn extends GlobalCupertinoLocalizations {
String get timerPickerHourLabelOther => r'hours';
@override
String get timerPickerMinuteLabelOne => r'min';
String get timerPickerMinuteLabelOne => r'min.';
@override
String get timerPickerMinuteLabelOther => r'min';
String get timerPickerMinuteLabelOther => r'min.';
@override
String get timerPickerSecondLabelOne => r'sec';
String get timerPickerSecondLabelOne => r'sec.';
@override
String get timerPickerSecondLabelOther => r'sec';
String get timerPickerSecondLabelOther => r'sec.';
@override
String get todayLabel => r'Today';
}
/// The translations for French (`fr`).
......@@ -189,6 +192,9 @@ class CupertinoLocalizationFr extends GlobalCupertinoLocalizations {
@override
String get timerPickerSecondLabelOther => r's';
@override
String get todayLabel => r'aujourd' "'" r'hui';
}
/// The set of supported languages, as language code strings.
......
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