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]),
);
}),
),
......
......@@ -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,28 +332,32 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
@override
Widget build(BuildContext context) {
Widget result = Stack(
children: <Widget>[
Positioned.fill(
child: _CupertinoPickerSemantics(
scrollController: widget.scrollController ?? _controller,
child: ListWheelScrollView.useDelegate(
controller: widget.scrollController ?? _controller,
physics: const FixedExtentScrollPhysics(),
diameterRatio: widget.diameterRatio,
perspective: _kDefaultPerspective,
offAxisFraction: widget.offAxisFraction,
useMagnifier: widget.useMagnifier,
magnification: widget.magnification,
itemExtent: widget.itemExtent,
onSelectedItemChanged: _handleSelectedItemChanged,
childDelegate: widget.childDelegate,
Widget result = DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.pickerTextStyle,
child: Stack(
children: <Widget>[
Positioned.fill(
child: _CupertinoPickerSemantics(
scrollController: widget.scrollController ?? _controller,
child: ListWheelScrollView.useDelegate(
controller: widget.scrollController ?? _controller,
physics: const FixedExtentScrollPhysics(),
diameterRatio: widget.diameterRatio,
perspective: _kDefaultPerspective,
offAxisFraction: widget.offAxisFraction,
useMagnifier: widget.useMagnifier,
magnification: widget.magnification,
itemExtent: widget.itemExtent,
squeeze: widget.squeeze,
onSelectedItemChanged: _handleSelectedItemChanged,
childDelegate: widget.childDelegate,
),
),
),
),
_buildGradientScreen(),
_buildMagnifierScreen(),
],
_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;
}
......
......@@ -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