Skip to content

Commit

Permalink
Update the cupertino picker visuals (#65501)
Browse files Browse the repository at this point in the history
  • Loading branch information
YeungKC committed Oct 7, 2020
1 parent 0fbc95d commit db25441
Show file tree
Hide file tree
Showing 8 changed files with 534 additions and 187 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -66,3 +66,4 @@ Alex Li <google@alexv525.com>
Ram Navan <hiramprasad@gmail.com>
meritozh <ah841814092@gmail.com>
Terrence Addison Tandijono(flotilla) <terrenceaddison32@gmail.com>
YeungKC <flutter@yeungkc.com>
429 changes: 305 additions & 124 deletions packages/flutter/lib/src/cupertino/date_picker.dart

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions packages/flutter/lib/src/cupertino/localizations.dart
Expand Up @@ -192,18 +192,30 @@ abstract class CupertinoLocalizations {
// The global version uses the translated string from the arb file.
String timerPickerHourLabel(int hour);

/// All possible hour labels that appears next to the hour picker in
/// [CupertinoTimerPicker]
List<String> get timerPickerHourLabels;

/// Label that appears next to the minute picker in
/// [CupertinoTimerPicker] when selected minute value is `minute`.
/// This function will deal with pluralization based on the `minute` parameter.
// The global version uses the translated string from the arb file.
String timerPickerMinuteLabel(int minute);

/// All possible minute labels that appears next to the minute picker in
/// [CupertinoTimerPicker]
List<String> get timerPickerMinuteLabels;

/// Label that appears next to the minute picker in
/// [CupertinoTimerPicker] when selected minute value is `second`.
/// This function will deal with pluralization based on the `second` parameter.
// The global version uses the translated string from the arb file.
String timerPickerSecondLabel(int second);

/// All possible second labels that appears next to the second picker in
/// [CupertinoTimerPicker]
List<String> get timerPickerSecondLabels;

/// The term used for cutting.
// The global version uses the translated string from the arb file.
String get cutButtonLabel;
Expand Down Expand Up @@ -380,12 +392,21 @@ class DefaultCupertinoLocalizations implements CupertinoLocalizations {
@override
String timerPickerHourLabel(int hour) => hour == 1 ? 'hour' : 'hours';

@override
List<String> get timerPickerHourLabels => const <String>['hour', 'hours'];

@override
String timerPickerMinuteLabel(int minute) => 'min.';

@override
List<String> get timerPickerMinuteLabels => const <String>['min.'];

@override
String timerPickerSecondLabel(int second) => 'sec.';

@override
List<String> get timerPickerSecondLabels => const <String>['sec.'];

@override
String get cutButtonLabel => 'Cut';

Expand Down
118 changes: 101 additions & 17 deletions packages/flutter/lib/src/cupertino/picker.dart
Expand Up @@ -10,11 +10,6 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';

/// Color of the 'magnifier' lens border.
const Color _kHighlighterBorder = CupertinoDynamicColor.withBrightness(
color: Color(0x33000000),
darkColor: Color(0x33FFFFFF),
);
// Eyeballed values comparing with a native picker to produce the right
// curvatures and densities.
const double _kDefaultDiameterRatio = 1.07;
Expand Down Expand Up @@ -79,6 +74,7 @@ class CupertinoPicker extends StatefulWidget {
required this.itemExtent,
required this.onSelectedItemChanged,
required List<Widget> children,
this.selectionOverlay = const CupertinoPickerDefaultSelectionOverlay(),
bool looping = false,
}) : assert(children != null),
assert(diameterRatio != null),
Expand Down Expand Up @@ -123,6 +119,7 @@ class CupertinoPicker extends StatefulWidget {
required this.onSelectedItemChanged,
required NullableIndexedWidgetBuilder itemBuilder,
int? childCount,
this.selectionOverlay = const CupertinoPickerDefaultSelectionOverlay(),
}) : assert(itemBuilder != null),
assert(diameterRatio != null),
assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
Expand Down Expand Up @@ -191,6 +188,18 @@ class CupertinoPicker extends StatefulWidget {
/// A delegate that lazily instantiates children.
final ListWheelChildDelegate childDelegate;

/// A widget overlaid on the picker to highlight the currently selected entry.
///
/// The [selectionOverlay] widget drawn above the [CupertinoPicker]'s picker
/// wheel.
/// It is vertically centered in the picker and is constrained to have the
/// same height as the center row.
///
/// If unspecified, it defaults to a [CupertinoPickerDefaultSelectionOverlay]
/// which is a gray rounded rectangle overlay in iOS 14 style.
/// This property can be set to null to remove the overlay.
final Widget selectionOverlay;

@override
State<StatefulWidget> createState() => _CupertinoPickerState();
}
Expand Down Expand Up @@ -251,22 +260,17 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
}
}

/// Draws the magnifier borders.
Widget _buildMagnifierScreen() {
final Color resolvedBorderColor = CupertinoDynamicColor.resolve(_kHighlighterBorder, context)!;
/// Draws the selectionOverlay.
Widget _buildSelectionOverlay(Widget selectionOverlay) {
final double height = widget.itemExtent * widget.magnification;

return IgnorePointer(
child: Center(
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(width: 0.0, color: resolvedBorderColor),
bottom: BorderSide(width: 0.0, color: resolvedBorderColor),
),
),
child: ConstrainedBox(
constraints: BoxConstraints.expand(
height: widget.itemExtent * widget.magnification,
height: height,
),
child: selectionOverlay,
),
),
);
Expand Down Expand Up @@ -299,7 +303,7 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
),
),
),
_buildMagnifierScreen(),
_buildSelectionOverlay(widget.selectionOverlay),
],
),
);
Expand All @@ -311,6 +315,86 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
}
}

/// A default selection overlay for [CupertinoPicker]s.
///
/// It draws a gray rounded rectangle to match the picker visuals introduced in
/// iOS 14.
///
/// This widget is typically only used in [CupertinoPicker.selectionOverlay].
/// In an iOS 14 multi-column picker, the selection overlay is a single rounded
/// rectangle that spans the entire multi-column picker.
/// To achieve the same effect using [CupertinoPickerDefaultSelectionOverlay],
/// the additional margin and corner radii on the left or the right side can be
/// disabled by turning off [capLeftEdge] and [capRightEdge], so this selection
/// overlay visually connects with selection overlays of adjoining
/// [CupertinoPicker]s (i.e., other "column"s).
///
/// See also:
///
/// * [CupertinoPicker], which uses this widget as its default [CupertinoPicker.selectionOverlay].
class CupertinoPickerDefaultSelectionOverlay extends StatelessWidget {

/// Creates an iOS 14 style selection overlay that highlights the magnified
/// area (or the currently selected item, depending on how you described it
/// elsewhere) of a [CupertinoPicker].
///
/// The [background] argument default value is [CupertinoColors.tertiarySystemFill].
/// It must be non-null.
///
/// The [capLeftEdge] and [capRightEdge] arguments decide whether to add a
/// default margin and use rounded corners on the left and right side of the
/// rectangular overlay.
/// Default to true and must not be null.
const CupertinoPickerDefaultSelectionOverlay({
Key? key,
this.background = CupertinoColors.tertiarySystemFill,
this.capLeftEdge = true,
this.capRightEdge = true,
}) : assert(background != null),
assert(capLeftEdge != null),
assert(capRightEdge != null),
super(key: key);

/// Whether to use the default use rounded corners and margin on the left side.
final bool capLeftEdge;

/// Whether to use the default use rounded corners and margin on the right side.
final bool capRightEdge;

/// The color to fill in the background of the [CupertinoPickerDefaultSelectionOverlay].
/// It Support for use [CupertinoDynamicColor].
///
/// Typically this should not be set to a fully opaque color, as the currently
/// selected item of the underlying [CupertinoPicker] should remain visible.
/// Defaults to [CupertinoColors.tertiarySystemFill].
final Color background;

/// Default margin of the 'SelectionOverlay'.
static const double _defaultSelectionOverlayHorizontalMargin = 9;

/// Default radius of the 'SelectionOverlay'.
static const double _defaultSelectionOverlayRadius = 8;

@override
Widget build(BuildContext context) {
const Radius radius = Radius.circular(_defaultSelectionOverlayRadius);

return Container(
margin: EdgeInsets.only(
left: capLeftEdge ? _defaultSelectionOverlayHorizontalMargin : 0,
right: capRightEdge ? _defaultSelectionOverlayHorizontalMargin : 0,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.horizontal(
left: capLeftEdge ? radius : Radius.zero,
right: capRightEdge ? radius : Radius.zero,
),
color: CupertinoDynamicColor.resolve(background, context),
),
);
}
}

// Turns the scroll semantics of the ListView into a single adjustable semantics
// node. This is done by removing all of the child semantics of the scroll
// wheel and using the scroll indexes to look up the current, previous, and
Expand Down
7 changes: 6 additions & 1 deletion packages/flutter/lib/src/cupertino/text_theme.dart
Expand Up @@ -73,12 +73,17 @@ const TextStyle _kDefaultLargeTitleTextStyle = TextStyle(
//
// Inspected on iOS 13 simulator with "Debug View Hierarchy".
// Value extracted from off-center labels. Centered labels have a font size of 25pt.
//
// The letterSpacing sourced from iOS 14 simulator screenshots for comparison.
// See also:
//
// * https://github.com/flutter/flutter/pull/65501#discussion_r486557093
const TextStyle _kDefaultPickerTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Display',
fontSize: 21.0,
fontWeight: FontWeight.w400,
letterSpacing: -0.41,
letterSpacing: -0.6,
color: CupertinoColors.label,
);

Expand Down
85 changes: 43 additions & 42 deletions packages/flutter/test/cupertino/date_picker_test.dart
Expand Up @@ -1206,47 +1206,48 @@ void main() {
});
});

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'),
);

// 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'),
);
});
// 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),
// fontSize: 21,
// ),
// ),
// ),
// 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'),
// );
//
// // 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'),
// );
// });

testWidgets('TimerPicker only changes hour label after scrolling stops', (WidgetTester tester) async {
Duration? duration;
Expand Down Expand Up @@ -1327,7 +1328,7 @@ void main() {
),
);

expect(tester.getSize(find.descendant(of: find.byKey(key), matching: find.byType(Row))), const Size(330, 216));
expect(tester.getSize(find.descendant(of: find.byKey(key), matching: find.byType(Row))), const Size(342, 216));
});

testWidgets('scrollController can be removed or added', (WidgetTester tester) async {
Expand Down

0 comments on commit db25441

Please sign in to comment.