Cupertino Date Picker (#21251)

diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_picker_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_picker_demo.dart
index 5d1e3ff..3309061 100644
--- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_picker_demo.dart
+++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_picker_demo.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
 
 import '../../gallery/demo.dart';
 import 'cupertino_navigation_demo.dart' show coolColorNames;
@@ -22,6 +23,15 @@
 
   Duration timer = Duration();
 
+  // Value that is shown in the date picker in date mode.
+  DateTime date = DateTime.now();
+
+  // Value that is shown in the date picker in time mode.
+  DateTime time = DateTime.now();
+
+  // Value that is shown in the date picker in dateAndTime mode.
+  DateTime dateTime = DateTime.now();
+
   Widget _buildMenu(List<Widget> children) {
     return Container(
       decoration: const BoxDecoration(
@@ -53,30 +63,10 @@
     );
   }
 
-  Widget _buildColorPicker() {
-    final FixedExtentScrollController scrollController =
-      FixedExtentScrollController(initialItem: _selectedColorIndex);
-    return CupertinoPicker(
-      scrollController: scrollController,
-      itemExtent: _kPickerItemHeight,
-      backgroundColor: CupertinoColors.white,
-      onSelectedItemChanged: (int index) {
-        setState(() {
-          _selectedColorIndex = index;
-        });
-      },
-      children: List<Widget>.generate(coolColorNames.length, (int index) {
-        return Center(child:
-        Text(coolColorNames[index]),
-        );
-      }),
-    );
-  }
-
   Widget _buildBottomPicker(Widget picker) {
     return Container(
       height: _kPickerSheetHeight,
-      padding: const EdgeInsets.only(top: 8.0),
+      padding: const EdgeInsets.only(top: 6.0),
       color: CupertinoColors.white,
       child: DefaultTextStyle(
         style: const TextStyle(
@@ -95,6 +85,47 @@
     );
   }
 
+  Widget _buildColorPicker(BuildContext context) {
+    final FixedExtentScrollController scrollController =
+        FixedExtentScrollController(initialItem: _selectedColorIndex);
+
+    return GestureDetector(
+      onTap: () async {
+        await showCupertinoModalPopup<void>(
+          context: context,
+          builder: (BuildContext context) {
+            return _buildBottomPicker(
+              CupertinoPicker(
+                scrollController: scrollController,
+                itemExtent: _kPickerItemHeight,
+                backgroundColor: CupertinoColors.white,
+                onSelectedItemChanged: (int index) {
+                  setState(() => _selectedColorIndex = index);
+                },
+                children: List<Widget>.generate(coolColorNames.length, (int index) {
+                  return Center(child:
+                  Text(coolColorNames[index]),
+                  );
+                }),
+              ),
+            );
+          },
+        );
+      },
+      child: _buildMenu(
+        <Widget>[
+          const Text('Favorite Color'),
+          Text(
+            coolColorNames[_selectedColorIndex],
+            style: const TextStyle(
+                color: CupertinoColors.inactiveGray
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
   Widget _buildCountdownTimerPicker(BuildContext context) {
     return GestureDetector(
       onTap: () {
@@ -105,9 +136,7 @@
               CupertinoTimerPicker(
                 initialTimerDuration: timer,
                 onTimerDurationChanged: (Duration newTimer) {
-                  setState(() {
-                    timer = newTimer;
-                  });
+                  setState(() => timer = newTimer);
                 },
               ),
             );
@@ -115,15 +144,105 @@
         );
       },
       child: _buildMenu(
-          <Widget>[
-            const Text('Countdown Timer'),
-            Text(
-              '${timer.inHours}:'
+        <Widget>[
+          const Text('Countdown Timer'),
+          Text(
+            '${timer.inHours}:'
                 '${(timer.inMinutes % 60).toString().padLeft(2,'0')}:'
                 '${(timer.inSeconds % 60).toString().padLeft(2,'0')}',
-              style: const TextStyle(color: CupertinoColors.inactiveGray),
-            ),
-          ]
+            style: const TextStyle(color: CupertinoColors.inactiveGray),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildDatePicker(BuildContext context) {
+    return GestureDetector(
+      onTap: () {
+        showCupertinoModalPopup<void>(
+          context: context,
+          builder: (BuildContext context) {
+            return _buildBottomPicker(
+              CupertinoDatePicker(
+                mode: CupertinoDatePickerMode.date,
+                initialDateTime: date,
+                onDateTimeChanged: (DateTime newDateTime) {
+                  setState(() => date = newDateTime);
+                },
+              ),
+            );
+          },
+        );
+      },
+      child: _buildMenu(
+        <Widget>[
+          const Text('Date'),
+          Text(
+            DateFormat.yMMMMd().format(date),
+            style: const TextStyle(color: CupertinoColors.inactiveGray),
+          ),
+        ]
+      ),
+    );
+  }
+
+  Widget _buildTimePicker(BuildContext context) {
+    return GestureDetector(
+      onTap: () {
+        showCupertinoModalPopup<void>(
+          context: context,
+          builder: (BuildContext context) {
+            return _buildBottomPicker(
+              CupertinoDatePicker(
+                mode: CupertinoDatePickerMode.time,
+                initialDateTime: time,
+                onDateTimeChanged: (DateTime newDateTime) {
+                  setState(() => time = newDateTime);
+                },
+              ),
+            );
+          },
+        );
+      },
+      child: _buildMenu(
+        <Widget>[
+          const Text('Time'),
+          Text(
+            DateFormat.jm().format(time),
+            style: const TextStyle(color: CupertinoColors.inactiveGray),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildDateAndTimePicker(BuildContext context) {
+    return GestureDetector(
+      onTap: () {
+        showCupertinoModalPopup<void>(
+          context: context,
+          builder: (BuildContext context) {
+            return _buildBottomPicker(
+              CupertinoDatePicker(
+                mode: CupertinoDatePickerMode.dateAndTime,
+                initialDateTime: dateTime,
+                onDateTimeChanged: (DateTime newDateTime) {
+                  setState(() => dateTime = newDateTime);
+                },
+              ),
+            );
+          },
+        );
+      },
+      child: _buildMenu(
+        <Widget>[
+          const Text('Date and Time'),
+          Text(
+            DateFormat.yMMMd().add_jm().format(dateTime),
+            style: const TextStyle(color: CupertinoColors.inactiveGray),
+          ),
+        ],
       ),
     );
   }
@@ -146,28 +265,11 @@
           child: ListView(
             children: <Widget>[
               const Padding(padding: EdgeInsets.only(top: 32.0)),
-              GestureDetector(
-                onTap: () async {
-                  await showCupertinoModalPopup<void>(
-                    context: context,
-                    builder: (BuildContext context) {
-                      return _buildBottomPicker(_buildColorPicker());
-                    },
-                  );
-                },
-                child: _buildMenu(
-                    <Widget>[
-                      const Text('Favorite Color'),
-                      Text(
-                        coolColorNames[_selectedColorIndex],
-                        style: const TextStyle(
-                            color: CupertinoColors.inactiveGray
-                        ),
-                      ),
-                    ]
-                ),
-              ),
+              _buildColorPicker(context),
               _buildCountdownTimerPicker(context),
+              _buildDatePicker(context),
+              _buildTimePicker(context),
+              _buildDateAndTimePicker(context),
             ],
           ),
         ),
diff --git a/packages/flutter/lib/src/cupertino/date_picker.dart b/packages/flutter/lib/src/cupertino/date_picker.dart
index f7c3be1..6d687b3 100644
--- a/packages/flutter/lib/src/cupertino/date_picker.dart
+++ b/packages/flutter/lib/src/cupertino/date_picker.dart
@@ -2,30 +2,906 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/scheduler.dart';
 import 'package:flutter/widgets.dart';
 
 import 'colors.dart';
 import 'localizations.dart';
 import 'picker.dart';
 
-/// Default aesthetic values obtained by comparing with iOS pickers.
+// Default aesthetic values obtained by comparing with iOS pickers.
 const double _kItemExtent = 32.0;
 const double _kPickerWidth = 330.0;
-/// Considers setting the default background color from the theme, in the future.
+const bool _kUseMagnifier = true;
+const double _kMagnification = 1.1;
+const double _kDatePickerPadSize = 12.0;
+// Considers setting the default background color from the theme, in the future.
 const Color _kBackgroundColor = CupertinoColors.white;
 
+// 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.
+// Each column will be padded horizontally by 12.0 both left and right.
+//
+// The picker will be placed in the center, and the leftmost and rightmost
+// column will be extended equally to the remaining width.
+class _DatePickerLayoutDelegate extends MultiChildLayoutDelegate {
+  _DatePickerLayoutDelegate({
+    @required this.columnWidths,
+    @required this.textDirectionFactor,
+  }) : assert(columnWidths != null),
+       assert(textDirectionFactor != null);
 
-// The iOS timer picker has its width fixed to 330.0 in all modes.
+  // The list containing widths of all columns.
+  final List<double> columnWidths;
+
+  // textDirectionFactor is 1 if text is written left to right, and -1 if right to left.
+  final int textDirectionFactor;
+
+  @override
+  void performLayout(Size size) {
+    double remainingWidth = size.width;
+
+    for (int i = 0; i < columnWidths.length; i++)
+      remainingWidth -= columnWidths[i] + _kDatePickerPadSize * 2;
+
+    double currentHorizontalOffset = 0.0;
+
+    for (int i = 0; i < columnWidths.length; i++) {
+      final int index = textDirectionFactor == 1 ? i : columnWidths.length - i - 1;
+
+      double childWidth = columnWidths[index] + _kDatePickerPadSize * 2;
+      if (index == 0 || index == columnWidths.length - 1)
+        childWidth += remainingWidth / 2;
+
+      layoutChild(index, BoxConstraints.tight(Size(childWidth, size.height)));
+      positionChild(index, Offset(currentHorizontalOffset, 0.0));
+
+      currentHorizontalOffset += childWidth;
+    }
+  }
+
+  @override
+  bool shouldRelayout(_DatePickerLayoutDelegate oldDelegate) {
+    return columnWidths != oldDelegate.columnWidths
+      || textDirectionFactor != oldDelegate.textDirectionFactor;
+  }
+}
+
+/// Different display modes of [CupertinoDatePicker].
+///
+/// See also:
+///
+///  * [CupertinoDatePicker], the class that implements different display modes
+///  of the iOS-style date picker.
+///  * [CupertinoPicker], the class that implements a content agnostic spinner UI.
+enum CupertinoDatePickerMode {
+  /// Mode that shows the date in hour, minute, and (optional) an AM/PM designation.
+  /// The AM/PM designation is shown only if [CupertinoDatePicker] does not use 24h format.
+  /// Column order is subject to internationalization.
+  ///
+  /// Example: [4 | 14 | PM].
+  time,
+  /// Mode that shows the date in month, day of month, and year.
+  /// Name of month is spelled in full.
+  /// Column order is subject to internationalization.
+  ///
+  /// Example: [July | 13 | 2012].
+  date,
+  /// Mode that shows the date as day of the week, month, day of month and
+  /// the time in hour, minute, and (optional) an AM/PM designation.
+  /// The AM/PM designation is shown only if [CupertinoDatePicker] does not use 24h format.
+  /// Column order is subject to internationalization.
+  ///
+  /// Example: [Fri Jul 13 | 4 | 14 | PM]
+  dateAndTime,
+}
+
+// Different types of column in CupertinoDatePicker.
+enum _PickerColumnType {
+  // Day of month column in date mode.
+  dayOfMonth,
+  // Month column in date mode.
+  month,
+  // Year column in date mode.
+  year,
+  // Medium date column in dateAndTime mode.
+  date,
+  // Hour column in time and dateAndTime mode.
+  hour,
+  // minute column in time and dateAndTime mode.
+  minute,
+  // AM/PM column in time and dateAndTime mode.
+  dayPeriod,
+}
+
+/// A date picker widget in iOS style.
+///
+/// There are several modes of the date picker listed in [CupertinoDatePickerMode].
+///
+/// The class will display its children as consecutive columns. Its children
+/// order is based on internationalization.
+///
+/// Example of the picker in date mode:
+///
+///  * US-English: [July | 13 | 2012]
+///  * Vietnamese: [13 | Tháng 7 | 2012]
+///
+/// See also:
+///
+///  * [CupertinoTimerPicker], the class that implements the iOS-style timer picker.
+///  * [CupertinoPicker], the class that implements a content agnostic spinner UI.
+class CupertinoDatePicker extends StatefulWidget {
+  /// Constructs an iOS style date picker.
+  ///
+  /// [mode] is one of the mode listed in [CupertinoDatePickerMode] and defaults
+  /// to [CupertinoDatePickerMode.dateAndTime].
+  ///
+  /// [onDateTimeChanged] is the callback called when the selected date or time
+  /// changes and must not be null.
+  ///
+  /// [initialDateTime] is the initial date time of the picker. Defaults to the
+  /// present date and time and must not be null. The present must conform to
+  /// the intervals set in [minimumDate], [maximumDate], [minimumYear], and
+  /// [maximumYear].
+  ///
+  /// [minimumDate] is the minimum date that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.dateAndTime] mode. Null if there's no limit.
+  ///
+  /// [maximumDate] is the maximum date that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.dateAndTime] mode. Null if there's no limit.
+  ///
+  /// [minimumYear] is the minimum year that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null.
+  ///
+  /// [maximumYear] is the maximum year that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.date] mode. Null if there's no limit.
+  ///
+  /// [minuteInterval] is the granularity of the minute spinner. Must be a
+  /// positive integer factor of 60.
+  ///
+  /// [use24hFormat] decides whether 24 hour format is used. Defaults to false.
+  CupertinoDatePicker({
+    this.mode = CupertinoDatePickerMode.dateAndTime,
+    @required this.onDateTimeChanged,
+    // ignore: always_require_non_null_named_parameters
+    DateTime initialDateTime,
+    this.minimumDate,
+    this.maximumDate,
+    this.minimumYear = 1,
+    this.maximumYear,
+    this.minuteInterval = 1,
+    this.use24hFormat = false,
+  }) : this.initialDateTime = initialDateTime ?? DateTime.now(),
+       assert(mode != null),
+       assert(onDateTimeChanged != null),
+       assert(initialDateTime != null),
+       assert(
+         mode != CupertinoDatePickerMode.dateAndTime || minimumDate == null || !initialDateTime.isBefore(minimumDate),
+         'initial date is before minimum date',
+       ),
+       assert(
+         mode != CupertinoDatePickerMode.dateAndTime || maximumDate == null || !initialDateTime.isAfter(maximumDate),
+         'initial date is after maximum date',
+       ),
+       assert(minimumYear != null),
+       assert(
+         mode != CupertinoDatePickerMode.date || (minimumYear >= 1 && initialDateTime.year >= minimumYear),
+         'initial year is not greater than minimum year, or mininum year is not positive',
+       ),
+       assert(
+         mode != CupertinoDatePickerMode.date || maximumYear == null || initialDateTime.year <= maximumYear,
+         'initial year is not smaller than maximum year',
+       ),
+       assert(
+         minuteInterval > 0 && 60 % minuteInterval == 0,
+         'minute interval is not a positive integer factor of 60',
+       ),
+       assert(
+         initialDateTime.minute % minuteInterval == 0,
+         'initial minute is not divisible by minute interval',
+       );
+
+  /// The mode of the date picker as one of [CupertinoDatePickerMode].
+  /// Defaults to [CupertinoDatePickerMode.dateAndTime]. Cannot be null and
+  /// value cannot change after initial build.
+  final CupertinoDatePickerMode mode;
+
+  /// The initial date and/or time of the picker. Defaults to the present date
+  /// and time and must not be null. The present must conform to the intervals
+  /// set in [minimumDate], [maximumDate], [minimumYear], and [maximumYear].
+  ///
+  /// Changing this value after the initial build will not affect the currently
+  /// selected date time.
+  final DateTime initialDateTime;
+
+  /// Minimum date that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.dateAndTime] mode. Null if there's no limit.
+  final DateTime minimumDate;
+
+  /// Maximum date that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.dateAndTime] mode. Null if there's no limit.
+  final DateTime maximumDate;
+
+  /// Minimum year that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null.
+  final int minimumYear;
+
+  /// Maximum year that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.date] mode. Null if there's no limit.
+  final int maximumYear;
+
+  /// The granularity of the minutes spinner, if it is shown in the current mode.
+  /// Must be an integer factor of 60.
+  final int minuteInterval;
+
+  /// Whether to use 24 hour format. Defaults to false.
+  final bool use24hFormat;
+
+  /// Callback called when the selected date and/or time changes. Must not be
+  /// null.
+  final ValueChanged<DateTime> onDateTimeChanged;
+
+  @override
+  State<StatefulWidget> createState() {
+    // The `time` mode and `dateAndTime` mode of the picker share the time
+    // columns, so they are placed together to one state.
+    // The `date` mode has different children and is implemented in a different
+    // state.
+    if (mode == CupertinoDatePickerMode.time || mode == CupertinoDatePickerMode.dateAndTime)
+      return _CupertinoDatePickerDateTimeState();
+    else
+      return _CupertinoDatePickerDateState();
+  }
+
+  // Estimate the minimum width that each column needs to layout its content.
+  static double _getColumnWidth(
+    _PickerColumnType columnType,
+    CupertinoLocalizations localizations,
+    BuildContext context,
+  ) {
+    String longestText = '';
+
+    switch (columnType) {
+      case _PickerColumnType.date:
+        // Measuring the length of all possible date is impossible, so here
+        // just some dates are measured.
+        for (int i = 1; i <= 12; i++) {
+          // An arbitrary date.
+          final String date =
+              localizations.datePickerMediumDate(DateTime(2018, i, 25));
+          if (longestText.length < date.length)
+            longestText = date;
+        }
+        break;
+      case _PickerColumnType.hour:
+        for (int i = 0 ; i < 24; i++) {
+          final String hour = localizations.datePickerHour(i);
+          if (longestText.length < hour.length)
+            longestText = hour;
+        }
+        break;
+      case _PickerColumnType.minute:
+        for (int i = 0 ; i < 60; i++) {
+          final String minute = localizations.datePickerMinute(i);
+          if (longestText.length < minute.length)
+            longestText = minute;
+        }
+        break;
+      case _PickerColumnType.dayPeriod:
+        longestText =
+          localizations.anteMeridiemAbbreviation.length > localizations.postMeridiemAbbreviation.length
+            ? localizations.anteMeridiemAbbreviation
+            : localizations.postMeridiemAbbreviation;
+        break;
+      case _PickerColumnType.dayOfMonth:
+        for (int i = 1 ; i <=31; i++) {
+          final String dayOfMonth = localizations.datePickerDayOfMonth(i);
+          if (longestText.length < dayOfMonth.length)
+            longestText = dayOfMonth;
+        }
+        break;
+      case _PickerColumnType.month:
+        for (int i = 1 ; i <=12; i++) {
+          final String month = localizations.datePickerMonth(i);
+          if (longestText.length < month.length)
+            longestText = month;
+        }
+        break;
+      case _PickerColumnType.year:
+        longestText = localizations.datePickerYear(2018);
+        break;
+    }
+
+    assert(longestText != '', 'column type is not appropriate');
+
+    final TextPainter painter = TextPainter(
+      text: TextSpan(
+        style: DefaultTextStyle.of(context).style,
+        text: longestText,
+      ),
+      textDirection: Directionality.of(context),
+    );
+
+    // This operation is expensive and should be avoided. It is called here only
+    // because there's no other way to get the information we want without
+    // laying out the text.
+    painter.layout();
+
+    return painter.maxIntrinsicWidth;
+  }
+}
+
+typedef _ColumnBuilder = Widget Function(double offAxisFraction, TransitionBuilder itemPositioningBuilder);
+
+class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
+  int textDirectionFactor;
+  CupertinoLocalizations localizations;
+
+  // Alignment based on text direction. The variable name is self descriptive,
+  // however, when text direction is rtl, alignment is reversed.
+  Alignment alignCenterLeft;
+  Alignment alignCenterRight;
+
+  // Read this out when the state is initially created. Changes in initialDateTime
+  // in the widget after first build is ignored.
+  DateTime initialDateTime;
+
+  // The currently selected values of the date picker.
+  int selectedDayFromInitial; // The difference in days between the initial date and the currently selected date.
+  int selectedHour;
+  int selectedMinute;
+  int selectedAmPm; // 0 means AM, 1 means PM.
+
+  // The controller of the AM/PM column.
+  FixedExtentScrollController amPmController;
+
+  // Estimated width of columns.
+  final Map<int, double> estimatedColumnWidths = <int, double>{};
+
+  @override
+  void initState() {
+    super.initState();
+    initialDateTime = widget.initialDateTime;
+    selectedDayFromInitial = 0;
+    selectedHour = widget.initialDateTime.hour;
+    selectedMinute = widget.initialDateTime.minute;
+    selectedAmPm = 0;
+
+    if (!widget.use24hFormat) {
+      selectedAmPm = selectedHour ~/ 12;
+      selectedHour = selectedHour % 12;
+      if (selectedHour == 0)
+        selectedHour = 12;
+
+      amPmController = FixedExtentScrollController(initialItem: selectedAmPm);
+    }
+  }
+
+  @override
+  void didUpdateWidget(CupertinoDatePicker oldWidget) {
+    super.didUpdateWidget(oldWidget);
+
+    assert(
+      oldWidget.mode == widget.mode,
+      "The CupertinoDatePicker's mode cannot change once it's built",
+    );
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+
+    textDirectionFactor = Directionality.of(context) == TextDirection.ltr ? 1 : -1;
+    localizations = CupertinoLocalizations.of(context) ?? const DefaultCupertinoLocalizations();
+
+    alignCenterLeft = textDirectionFactor == 1 ? Alignment.centerLeft : Alignment.centerRight;
+    alignCenterRight = textDirectionFactor == 1 ? Alignment.centerRight : Alignment.centerLeft;
+
+    estimatedColumnWidths.clear();
+  }
+
+  // Lazily calculate the column width of the column being displayed only.
+  double _getEstimatedColumnWidth(_PickerColumnType columnType) {
+    if (estimatedColumnWidths[columnType.index] == null) {
+      estimatedColumnWidths[columnType.index] =
+          CupertinoDatePicker._getColumnWidth(columnType, localizations, context);
+    }
+
+    return estimatedColumnWidths[columnType.index];
+  }
+
+  // Gets the current date time of the picker.
+  DateTime _getDateTime() {
+    final DateTime date = DateTime(
+      initialDateTime.year,
+      initialDateTime.month,
+      initialDateTime.day,
+    ).add(Duration(days: selectedDayFromInitial));
+
+    return DateTime(
+      date.year,
+      date.month,
+      date.day,
+      selectedHour + selectedAmPm * 12,
+      selectedMinute,
+    );
+  }
+
+  // Builds the date column. The date is displayed in medium date format (e.g. Fri Aug 31).
+  Widget _buildMediumDatePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
+    return CupertinoPicker.builder(
+      scrollController: FixedExtentScrollController(initialItem: selectedDayFromInitial),
+      offAxisFraction: offAxisFraction,
+      itemExtent: _kItemExtent,
+      useMagnifier: _kUseMagnifier,
+      magnification: _kMagnification,
+      backgroundColor: _kBackgroundColor,
+      onSelectedItemChanged: (int index) {
+        selectedDayFromInitial = index;
+        widget.onDateTimeChanged(_getDateTime());
+      },
+      itemBuilder: (BuildContext context, int index) {
+        final DateTime dateTime = DateTime(
+          initialDateTime.year,
+          initialDateTime.month,
+          initialDateTime.day,
+        ).add(Duration(days: index));
+
+        if (widget.minimumDate != null && dateTime.isBefore(widget.minimumDate))
+          return null;
+        if (widget.maximumDate != null && dateTime.isAfter(widget.maximumDate))
+          return null;
+
+        return itemPositioningBuilder(
+          context,
+          Text(localizations.datePickerMediumDate(dateTime)),
+        );
+      },
+    );
+  }
+
+  Widget _buildHourPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
+    return CupertinoPicker(
+      scrollController: FixedExtentScrollController(initialItem: selectedHour),
+      offAxisFraction: offAxisFraction,
+      itemExtent: _kItemExtent,
+      useMagnifier: _kUseMagnifier,
+      magnification: _kMagnification,
+      backgroundColor: _kBackgroundColor,
+      onSelectedItemChanged: (int index) {
+        if (widget.use24hFormat) {
+          selectedHour = index;
+          widget.onDateTimeChanged(_getDateTime());
+        }
+        else {
+          final int currentHourIn24h = selectedHour + selectedAmPm * 12;
+          // Automatically scrolls the am/pm column when the hour column value
+          // goes far enough. This behavior is similar to
+          // iOS picker version.
+          if (currentHourIn24h ~/ 12 != index ~/ 12) {
+            selectedHour = index % 12;
+            amPmController.animateToItem(
+              1 - amPmController.selectedItem,
+              duration: const Duration(milliseconds: 300), // Set by comparing with iOS version.
+              curve: Curves.easeOut,
+            ); // Set by comparing with iOS version.
+          }
+          else {
+            selectedHour = index % 12;
+            widget.onDateTimeChanged(_getDateTime());
+          }
+        }
+      },
+      children: List<Widget>.generate(24, (int index) {
+        int hour = index;
+        if (!widget.use24hFormat)
+          hour = hour % 12 == 0 ? 12 : hour % 12;
+
+        return itemPositioningBuilder(
+          context,
+          Text(
+            localizations.datePickerHour(hour),
+            semanticsLabel: localizations.datePickerHourSemanticsLabel(hour),
+          ),
+        );
+      }),
+      looping: true,
+    );
+  }
+
+  Widget _buildMinutePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
+    return CupertinoPicker(
+      scrollController: FixedExtentScrollController(initialItem: selectedMinute),
+      offAxisFraction: offAxisFraction,
+      itemExtent: _kItemExtent,
+      useMagnifier: _kUseMagnifier,
+      magnification: _kMagnification,
+      backgroundColor: _kBackgroundColor,
+      onSelectedItemChanged: (int index) {
+        selectedMinute = index * widget.minuteInterval;
+        widget.onDateTimeChanged(_getDateTime());
+      },
+      children: List<Widget>.generate(60 ~/ widget.minuteInterval, (int index) {
+        final int minute = index * widget.minuteInterval;
+        return itemPositioningBuilder(
+          context,
+          Text(
+            localizations.datePickerMinute(minute),
+            semanticsLabel: localizations.datePickerMinuteSemanticsLabel(minute),
+          ),
+        );
+      }),
+      looping: true,
+    );
+  }
+
+  Widget _buildAmPmPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
+    return CupertinoPicker(
+      scrollController: amPmController,
+      offAxisFraction: offAxisFraction,
+      itemExtent: _kItemExtent,
+      useMagnifier: _kUseMagnifier,
+      magnification: _kMagnification,
+      backgroundColor: _kBackgroundColor,
+      onSelectedItemChanged: (int index) {
+        selectedAmPm = index;
+        widget.onDateTimeChanged(_getDateTime());
+      },
+      children: List<Widget>.generate(2, (int index) {
+        return itemPositioningBuilder(
+          context,
+          Text(
+            index == 0
+              ? localizations.anteMeridiemAbbreviation
+              : localizations.postMeridiemAbbreviation
+          ),
+        );
+      }),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // Widths of the columns in this picker, ordered from left to right.
+    final List<double> columnWidths = <double>[
+      _getEstimatedColumnWidth(_PickerColumnType.hour),
+      _getEstimatedColumnWidth(_PickerColumnType.minute),
+    ];
+    final List<_ColumnBuilder> pickerBuilders = <_ColumnBuilder>[
+      _buildHourPicker,
+      _buildMinutePicker,
+    ];
+
+    // Adds am/pm column if the picker is not using 24h format.
+    if (!widget.use24hFormat) {
+      if (localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.date_time_dayPeriod
+        || localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.time_dayPeriod_date) {
+        pickerBuilders.add(_buildAmPmPicker);
+        columnWidths.add(_getEstimatedColumnWidth(_PickerColumnType.dayPeriod));
+      }
+      else {
+        pickerBuilders.insert(0, _buildAmPmPicker);
+        columnWidths.insert(0, _getEstimatedColumnWidth(_PickerColumnType.dayPeriod));
+      }
+    }
+
+    // Adds medium date column if the picker's mode is date and time.
+    if (widget.mode == CupertinoDatePickerMode.dateAndTime) {
+      if (localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.time_dayPeriod_date
+          || localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.dayPeriod_time_date) {
+        pickerBuilders.add(_buildMediumDatePicker);
+        columnWidths.add(_getEstimatedColumnWidth(_PickerColumnType.date));
+      }
+      else {
+        pickerBuilders.insert(0, _buildMediumDatePicker);
+        columnWidths.insert(0, _getEstimatedColumnWidth(_PickerColumnType.date));
+      }
+    }
+
+    final List<Widget> pickers = <Widget>[];
+
+    for (int i = 0; i < columnWidths.length; i++) {
+      double offAxisFraction = 0.0;
+      if (i == 0)
+        offAxisFraction = -0.5 * textDirectionFactor;
+      else if (i >= 2 || columnWidths.length == 2)
+        offAxisFraction = 0.5 * textDirectionFactor;
+
+      EdgeInsets padding = const EdgeInsets.only(right: _kDatePickerPadSize);
+      if (i == columnWidths.length - 1)
+        padding = padding.flipped;
+      if (textDirectionFactor == -1)
+        padding = padding.flipped;
+
+      pickers.add(LayoutId(
+        id: i,
+        child: pickerBuilders[i](
+          offAxisFraction,
+          (BuildContext context, Widget child) {
+            return Container(
+              alignment: i == columnWidths.length - 1
+                ? alignCenterLeft
+                : alignCenterRight,
+              padding: padding,
+              child: Container(
+                alignment: i == columnWidths.length - 1 ? alignCenterLeft : alignCenterRight,
+                width: i == 0 || i == columnWidths.length - 1
+                  ? null
+                  : columnWidths[i] + _kDatePickerPadSize,
+                child: child,
+              ),
+            );
+          },
+        ),
+      ));
+    }
+
+    return MediaQuery(
+      data: const MediaQueryData(textScaleFactor: 1.0),
+      child: CustomMultiChildLayout(
+        delegate: _DatePickerLayoutDelegate(
+          columnWidths: columnWidths,
+          textDirectionFactor: textDirectionFactor,
+        ),
+        children: pickers,
+      ),
+    );
+  }
+}
+
+class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
+  int textDirectionFactor;
+  CupertinoLocalizations localizations;
+
+  // Alignment based on text direction. The variable name is self descriptive,
+  // however, when text direction is rtl, alignment is reversed.
+  Alignment alignCenterLeft;
+  Alignment alignCenterRight;
+
+  // The currently selected values of the picker.
+  int selectedDay;
+  int selectedMonth;
+  int selectedYear;
+
+  // The controller of the day picker. There are cases where the selected value
+  // of the picker is invalid (e.g. February 30th 2018), and this dayController
+  // is responsible for jumping to a valid value.
+  FixedExtentScrollController dayController;
+
+  // Estimated width of columns.
+  Map<int, double> estimatedColumnWidths = <int, double>{};
+
+  @override
+  void initState() {
+    super.initState();
+    selectedDay = widget.initialDateTime.day;
+    selectedMonth = widget.initialDateTime.month;
+    selectedYear = widget.initialDateTime.year;
+
+    dayController = FixedExtentScrollController(initialItem: selectedDay - 1);
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+
+    textDirectionFactor = Directionality.of(context) == TextDirection.ltr ? 1 : -1;
+    localizations = CupertinoLocalizations.of(context) ?? const DefaultCupertinoLocalizations();
+
+    alignCenterLeft = textDirectionFactor == 1 ? Alignment.centerLeft : Alignment.centerRight;
+    alignCenterRight = textDirectionFactor == 1 ? Alignment.centerRight : Alignment.centerLeft;
+
+    estimatedColumnWidths[_PickerColumnType.dayOfMonth.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.dayOfMonth, localizations, context);
+    estimatedColumnWidths[_PickerColumnType.month.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context);
+    estimatedColumnWidths[_PickerColumnType.year.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.year, localizations, context);
+  }
+
+  Widget _buildDayPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
+    final int daysInCurrentMonth = DateTime(selectedYear, (selectedMonth + 1) % 12, 0).day;
+    return CupertinoPicker(
+      scrollController: dayController,
+      offAxisFraction: offAxisFraction,
+      itemExtent: _kItemExtent,
+      useMagnifier: _kUseMagnifier,
+      magnification: _kMagnification,
+      backgroundColor: _kBackgroundColor,
+      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.
+        if (index >= daysInCurrentMonth) {
+          disableTextStyle = const TextStyle(color: CupertinoColors.inactiveGray);
+        }
+        return itemPositioningBuilder(
+          context,
+          Text(
+            localizations.datePickerDayOfMonth(index + 1),
+            style: disableTextStyle,
+          ),
+        );
+      }),
+      looping: true,
+    );
+  }
+
+  Widget _buildMonthPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
+    return CupertinoPicker(
+      scrollController: FixedExtentScrollController(initialItem: selectedMonth - 1),
+      offAxisFraction: offAxisFraction,
+      itemExtent: _kItemExtent,
+      useMagnifier: _kUseMagnifier,
+      magnification: _kMagnification,
+      backgroundColor: _kBackgroundColor,
+      onSelectedItemChanged: (int index) {
+        selectedMonth = index + 1;
+        if (DateTime(selectedYear, selectedMonth, selectedDay).day == selectedDay)
+          widget.onDateTimeChanged(DateTime(selectedYear, selectedMonth, selectedDay));
+      },
+      children: List<Widget>.generate(12, (int index) {
+        return itemPositioningBuilder(
+          context,
+          Text(localizations.datePickerMonth(index + 1)),
+        );
+      }),
+      looping: true,
+    );
+  }
+
+  Widget _buildYearPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
+    return CupertinoPicker.builder(
+      scrollController: FixedExtentScrollController(initialItem: selectedYear),
+      itemExtent: _kItemExtent,
+      offAxisFraction: offAxisFraction,
+      useMagnifier: _kUseMagnifier,
+      magnification: _kMagnification,
+      backgroundColor: _kBackgroundColor,
+      onSelectedItemChanged: (int index) {
+        selectedYear = index;
+        if (DateTime(selectedYear, selectedMonth, selectedDay).day == selectedDay)
+          widget.onDateTimeChanged(DateTime(selectedYear, selectedMonth, selectedDay));
+      },
+      itemBuilder: (BuildContext context, int index) {
+        if (index < widget.minimumYear)
+          return null;
+
+        if (widget.maximumYear != null && index > widget.maximumYear)
+          return null;
+
+        return itemPositioningBuilder(
+          context,
+          Text(localizations.datePickerYear(index)),
+        );
+      },
+    );
+  }
+
+  bool _keepInValidRange(ScrollEndNotification notification) {
+    // Whenever scrolling lands on an invalid entry, the picker
+    // automatically scrolls to a valid one.
+    final int desiredDay = DateTime(selectedYear, selectedMonth, selectedDay).day;
+    if (desiredDay != selectedDay) {
+      SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
+        dayController.animateToItem(
+          // The next valid date is also the amount of days overflown.
+          dayController.selectedItem - desiredDay,
+          duration: const Duration(milliseconds: 200),
+          curve: Curves.easeOut,
+        );
+      });
+    }
+    setState(() {
+      // Rebuild because the number of valid days per month are different
+      // depending on the month and year.
+    });
+    return false;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    List<_ColumnBuilder> pickerBuilders = <_ColumnBuilder>[];
+    List<double> columnWidths = <double>[];
+
+    switch (localizations.datePickerDateOrder) {
+      case DatePickerDateOrder.mdy:
+        pickerBuilders = <_ColumnBuilder>[_buildMonthPicker, _buildDayPicker, _buildYearPicker];
+        columnWidths = <double>[
+          estimatedColumnWidths[_PickerColumnType.month.index],
+          estimatedColumnWidths[_PickerColumnType.dayOfMonth.index],
+          estimatedColumnWidths[_PickerColumnType.year.index]];
+        break;
+      case DatePickerDateOrder.dmy:
+        pickerBuilders = <_ColumnBuilder>[_buildDayPicker, _buildMonthPicker, _buildYearPicker];
+        columnWidths = <double>[
+          estimatedColumnWidths[_PickerColumnType.dayOfMonth.index],
+          estimatedColumnWidths[_PickerColumnType.month.index],
+          estimatedColumnWidths[_PickerColumnType.year.index]];
+        break;
+      case DatePickerDateOrder.ymd:
+        pickerBuilders = <_ColumnBuilder>[_buildYearPicker, _buildMonthPicker, _buildDayPicker];
+        columnWidths = <double>[
+          estimatedColumnWidths[_PickerColumnType.year.index],
+          estimatedColumnWidths[_PickerColumnType.month.index],
+          estimatedColumnWidths[_PickerColumnType.dayOfMonth.index]];
+        break;
+      case DatePickerDateOrder.ydm:
+        pickerBuilders = <_ColumnBuilder>[_buildYearPicker, _buildDayPicker, _buildMonthPicker];
+        columnWidths = <double>[
+          estimatedColumnWidths[_PickerColumnType.year.index],
+          estimatedColumnWidths[_PickerColumnType.dayOfMonth.index],
+          estimatedColumnWidths[_PickerColumnType.month.index]];
+        break;
+      default:
+        assert(false, 'date order is not specified');
+    }
+
+    final List<Widget> pickers = <Widget>[];
+
+    for (int i = 0; i < columnWidths.length; i++) {
+      final double offAxisFraction = (i - 1) * 0.3 * textDirectionFactor;
+
+      EdgeInsets padding = const EdgeInsets.only(right: _kDatePickerPadSize);
+      if (textDirectionFactor == -1)
+        padding = const EdgeInsets.only(left: _kDatePickerPadSize);
+
+      pickers.add(LayoutId(
+        id: i,
+        child: pickerBuilders[i](
+          offAxisFraction,
+          (BuildContext context, Widget child) {
+            return Container(
+              alignment: i == columnWidths.length - 1
+                  ? alignCenterLeft
+                  : alignCenterRight,
+              padding: i == 0 ? null : padding,
+              child: Container(
+                alignment: i == 0 ? alignCenterLeft : alignCenterRight,
+                width: columnWidths[i] + _kDatePickerPadSize,
+                child: child,
+              ),
+            );
+          },
+        ),
+      ));
+    }
+
+    return MediaQuery(
+      data: const MediaQueryData(textScaleFactor: 1.0),
+      child: NotificationListener<ScrollEndNotification>(
+        onNotification: _keepInValidRange,
+        child: CustomMultiChildLayout(
+          delegate: _DatePickerLayoutDelegate(
+            columnWidths: columnWidths,
+            textDirectionFactor: textDirectionFactor,
+          ),
+          children: pickers,
+        ),
+      ),
+    );
+  }
+}
+
+
+// The iOS date picker and timer picker has their width fixed to 330.0 in all
+// modes.
 //
 // If the maximum width given to the picker is greater than 330.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 smaller than 330.0, the picker is
-// placed in the center and both left side and right side are clipped.
+// If the maximum width given to the picker is smaller than 330.0, the picker's
+// layout will be broken.
 
 
 /// Different modes of [CupertinoTimerPicker].
+///
+/// See also:
+///
+///  * [CupertinoTimerPicker], the class that implements the iOS-style timer picker.
+///  * [CupertinoPicker], the class that implements a content agnostic spinner UI.
 enum CupertinoTimerPickerMode {
   /// Mode that shows the timer duration in hour and minute.
   ///
@@ -47,14 +923,20 @@
 /// The duration is bound between 0 and 23 hours 59 minutes 59 seconds.
 ///
 /// There are several modes of the timer picker listed in [CupertinoTimerPickerMode].
+///
+/// See also:
+///
+///  * [CupertinoDatePicker], the class that implements different display modes
+///  of the iOS-style date picker.
+///  * [CupertinoPicker], the class that implements a content agnostic spinner UI.
 class CupertinoTimerPicker extends StatefulWidget {
   /// Constructs an iOS style countdown timer picker.
   ///
   /// [mode] is one of the modes listed in [CupertinoTimerPickerMode] and
   /// defaults to [CupertinoTimerPickerMode.hms].
   ///
-  /// [onTimerDurationChanged] is the callback when the selected duration changes
-  /// and must not be null.
+  /// [onTimerDurationChanged] is the callback called when the selected duration
+  /// changes and must not be null.
   ///
   /// [initialTimerDuration] defaults to 0 second and is limited from 0 second
   /// to 23 hours 59 minutes 59 seconds.
@@ -93,7 +975,7 @@
   /// of 60.
   final int secondInterval;
 
-  /// Callback when the timer duration changes.
+  /// Callback called when the timer duration changes.
   final ValueChanged<Duration> onTimerDurationChanged;
 
   @override
@@ -304,8 +1186,7 @@
           ),
         ),
       );
-    }
-    else {
+    } else {
       minuteLabel = IgnorePointer(
         child: Container(
           alignment: alignCenterRight,
@@ -420,16 +1301,14 @@
           Expanded(child: _buildMinuteColumn()),
         ],
       );
-    }
-    else if (widget.mode == CupertinoTimerPickerMode.ms) {
+    } else if (widget.mode == CupertinoTimerPickerMode.ms) {
       picker = Row(
         children: <Widget>[
           Expanded(child: _buildMinuteColumn()),
           Expanded(child: _buildSecondColumn()),
         ],
       );
-    }
-    else {
+    } else {
       picker = Row(
         children: <Widget>[
           Expanded(child: _buildHourColumn()),
diff --git a/packages/flutter/lib/src/cupertino/localizations.dart b/packages/flutter/lib/src/cupertino/localizations.dart
index 92317be..bf6c4a5 100644
--- a/packages/flutter/lib/src/cupertino/localizations.dart
+++ b/packages/flutter/lib/src/cupertino/localizations.dart
@@ -7,6 +7,46 @@
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 
+/// Determines the order of the columns inside [CupertinoDatePicker] in
+/// time and date time mode.
+enum DatePickerDateTimeOrder {
+  /// Order of the columns, from left to right: date, hour, minute, am/pm.
+  ///
+  /// Example: [Fri Aug 31 | 02 | 08 | PM].
+  date_time_dayPeriod,
+  /// Order of the columns, from left to right: date, am/pm, hour, minute.
+  ///
+  /// Example: [Fri Aug 31 | PM | 02 | 08].
+  date_dayPeriod_time,
+  /// Order of the columns, from left to right: hour, minute, am/pm, date.
+  ///
+  /// Example: [02 | 08 | PM | Fri Aug 31].
+  time_dayPeriod_date,
+  /// Order of the columns, from left to right: am/pm, hour, minute, date.
+  ///
+  /// Example: [PM | 02 | 08 | Fri Aug 31].
+  dayPeriod_time_date,
+}
+
+/// Determines the order of the columns inside [CupertinoDatePicker] in date mode.
+enum DatePickerDateOrder {
+  /// Order of the columns, from left to right: day, month, year.
+  ///
+  /// Example: [12 | March | 1996]
+  dmy,
+  /// Order of the columns, from left to right: month, day, year.
+  ///
+  /// Example: [March | 12 | 1996]
+  mdy,
+  /// Order of the columns, from left to right: year, month, day.
+  ///
+  /// Example: [1996 | March | 12]
+  ymd,
+  /// Order of the columns, from left to right: year, day, month.
+  ///
+  /// Example: [1996 | 12 | March]
+  ydm,
+}
 
 /// Defines the localized resource values used by the Cupertino widgets.
 ///
@@ -61,6 +101,9 @@
   ///  - Arabic: ٠١
   String datePickerHour(int hour);
 
+  /// Semantics label for the given hour value in [CupertinoDatePicker].
+  String datePickerHourSemanticsLabel(int hour);
+
   /// Minute that is shown in [CupertinoDatePicker] spinner corresponding
   /// to the given minute value.
   ///
@@ -70,9 +113,14 @@
   ///  - Arabic: ٠١
   String datePickerMinute(int minute);
 
+  /// Semantics label for the given minute value in [CupertinoDatePicker].
+  String datePickerMinuteSemanticsLabel(int minute);
+
   /// The order of the date elements that will be shown in [CupertinoDatePicker].
-  /// Can be any permutation of 'DMY' ('D': day, 'M': month, 'Y': year).
-  String get datePickerDateOrder;
+  DatePickerDateOrder get datePickerDateOrder;
+
+  /// The order of the time elements that will be shown in [CupertinoDatePicker].
+  DatePickerDateTimeOrder get datePickerDateTimeOrder;
 
   /// The abbreviation for ante meridiem (before noon) shown in the time picker.
   String get anteMeridiemAbbreviation;
@@ -217,9 +265,19 @@
   String datePickerHour(int hour) => hour.toString().padLeft(2, '0');
 
   @override
+  String datePickerHourSemanticsLabel(int hour) => hour.toString() + " o'clock";
+
+  @override
   String datePickerMinute(int minute) => minute.toString().padLeft(2, '0');
 
   @override
+  String datePickerMinuteSemanticsLabel(int minute) {
+    if (minute == 1)
+       return '1 minute';
+    return minute.toString() + ' minutes';
+  }
+
+  @override
   String datePickerMediumDate(DateTime date) {
     return '${_shortWeekdays[date.weekday - DateTime.monday]} '
       '${_shortMonths[date.month - DateTime.january]} '
@@ -227,7 +285,10 @@
   }
 
   @override
-  String get datePickerDateOrder => 'MDY';
+  DatePickerDateOrder get datePickerDateOrder => DatePickerDateOrder.mdy;
+
+  @override
+  DatePickerDateTimeOrder get datePickerDateTimeOrder => DatePickerDateTimeOrder.date_time_dayPeriod;
 
   @override
   String get anteMeridiemAbbreviation => 'AM';
diff --git a/packages/flutter/test/cupertino/date_picker_test.dart b/packages/flutter/test/cupertino/date_picker_test.dart
index adc09e3..2282d26 100644
--- a/packages/flutter/test/cupertino/date_picker_test.dart
+++ b/packages/flutter/test/cupertino/date_picker_test.dart
@@ -97,36 +97,6 @@
       );
     });
 
-    testWidgets('secondInterval is positive and is a factor of 60', (WidgetTester tester) async {
-      expect(
-        () {
-          CupertinoTimerPicker(
-            onTimerDurationChanged: (_) {},
-            secondInterval: 0,
-          );
-        },
-        throwsAssertionError,
-      );
-      expect(
-        () {
-          CupertinoTimerPicker(
-            onTimerDurationChanged: (_) {},
-            secondInterval: -1,
-          );
-        },
-        throwsAssertionError,
-      );
-      expect(
-        () {
-          CupertinoTimerPicker(
-            onTimerDurationChanged: (_) {},
-            secondInterval: 7,
-          );
-        },
-        throwsAssertionError,
-      );
-    });
-
     testWidgets('columns are ordered correctly when text direction is ltr', (WidgetTester tester) async {
       await tester.pumpWidget(
         Directionality(
@@ -223,4 +193,364 @@
       );
     });
   });
+  group('Date picker', () {
+    testWidgets('mode is not null', (WidgetTester tester) async {
+      expect(
+        () {
+          CupertinoDatePicker(
+            mode: null,
+            onDateTimeChanged: (_) {},
+            initialDateTime: DateTime.now(),
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    testWidgets('onDateTimeChanged is not null', (WidgetTester tester) async {
+      expect(
+        () {
+          CupertinoDatePicker(
+            onDateTimeChanged: null,
+            initialDateTime: DateTime.now(),
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    testWidgets('initial date time is not null', (WidgetTester tester) async {
+      expect(
+        () {
+          CupertinoDatePicker(
+            onDateTimeChanged: (_) {},
+            initialDateTime: null,
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    testWidgets('initial date time is not null', (WidgetTester tester) async {
+      expect(
+            () {
+          CupertinoDatePicker(
+            onDateTimeChanged: (_) {},
+            initialDateTime: null,
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    testWidgets('changing initialDateTime after first build does not do anything', (WidgetTester tester) async {
+      DateTime selectedDateTime;
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 400.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.dateAndTime,
+              onDateTimeChanged: (DateTime dateTime) => selectedDateTime = dateTime,
+              initialDateTime: DateTime(2018, 1, 1, 10, 30),
+            ),
+          ),
+        ),
+      );
+
+      await tester.drag(find.text('10'), const Offset(0.0, 32.0));
+      await tester.pump();
+      await tester.pump(const Duration(milliseconds: 500));
+
+      expect(selectedDateTime, DateTime(2018, 1, 1, 9, 30));
+
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 400.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.dateAndTime,
+              onDateTimeChanged: (DateTime dateTime) => selectedDateTime = dateTime,
+              // Change the initial date, but it shouldn't affect the present state.
+              initialDateTime: DateTime(2016, 4, 5, 15, 00),
+            ),
+          ),
+        ),
+      );
+
+      await tester.drag(find.text('09'), const Offset(0.0, 32.0));
+      await tester.pump();
+      await tester.pump(const Duration(milliseconds: 500));
+
+      // Moving up an hour is still based on the original initial date time.
+      expect(selectedDateTime, DateTime(2018, 1, 1, 8, 30));
+    });
+
+    testWidgets('width of picker in date and time mode is consistent', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 400.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.dateAndTime,
+              onDateTimeChanged: (_) {},
+              initialDateTime: DateTime(2018, 1, 1, 10, 30),
+            ),
+          ),
+        ),
+      );
+
+      // Distance between the first column and the last column.
+      final double distance =
+          tester.getCenter(find.text('Mon Jan 1')).dx - tester.getCenter(find.text('AM')).dx;
+
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 800.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.dateAndTime,
+              onDateTimeChanged: (_) {},
+              initialDateTime: DateTime(2018, 1, 1, 10, 30),
+            ),
+          ),
+        ),
+      );
+
+      // Distance between the first and the last column should be the same.
+      expect(
+        tester.getCenter(find.text('Mon Jan 1')).dx - tester.getCenter(find.text('AM')).dx,
+        distance,
+      );
+    });
+
+    testWidgets('width of picker in date mode is consistent', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 400.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.date,
+              onDateTimeChanged: (_) {},
+              initialDateTime: DateTime(2018, 1, 1, 10, 30),
+            ),
+          ),
+        ),
+      );
+
+      // Distance between the first column and the last column.
+      final double distance =
+          tester.getCenter(find.text('January')).dx - tester.getCenter(find.text('2018')).dx;
+
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 800.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.date,
+              onDateTimeChanged: (_) {},
+              initialDateTime: DateTime(2018, 1, 1, 10, 30),
+            ),
+          ),
+        ),
+      );
+
+      // Distance between the first and the last column should be the same.
+      expect(
+        tester.getCenter(find.text('January')).dx - tester.getCenter(find.text('2018')).dx,
+        distance,
+      );
+    });
+
+    testWidgets('width of picker in time mode is consistent', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 400.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.time,
+              onDateTimeChanged: (_) {},
+              initialDateTime: DateTime(2018, 1, 1, 10, 30),
+            ),
+          ),
+        ),
+      );
+
+      // Distance between the first column and the last column.
+      final double distance =
+          tester.getCenter(find.text('10')).dx - tester.getCenter(find.text('AM')).dx;
+
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 800.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.time,
+              onDateTimeChanged: (_) {},
+              initialDateTime: DateTime(2018, 1, 1, 10, 30),
+            ),
+          ),
+        ),
+      );
+
+      // Distance between the first and the last column should be the same.
+      expect(
+        tester.getCenter(find.text('10')).dx - tester.getCenter(find.text('AM')).dx,
+        distance,
+      );
+    });
+
+    testWidgets('picker automatically scrolls away from invalid date on month change', (WidgetTester tester) async {
+      DateTime date;
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 400.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.date,
+              onDateTimeChanged: (DateTime newDate) {
+                date = newDate;
+              },
+              initialDateTime: DateTime(2018, 3, 30),
+            ),
+          ),
+        ),
+      );
+
+      await tester.drag(find.text('March'), const Offset(0.0, 32.0));
+      // Momentarily, the 2018 and the incorrect 30 of February is aligned.
+      expect(
+        tester.getTopLeft(find.text('2018')).dy,
+        tester.getTopLeft(find.text('30')).dy,
+      );
+      await tester.pump(); // Once to trigger the post frame animate call.
+      await tester.pump(); // Once to start the DrivenScrollActivity.
+      await tester.pump(const Duration(milliseconds: 500));
+
+      expect(
+        date,
+        DateTime(2018, 2, 28),
+      );
+      expect(
+        tester.getTopLeft(find.text('2018')).dy,
+        tester.getTopLeft(find.text('28')).dy,
+      );
+    });
+
+    testWidgets('picker automatically scrolls away from invalid date on day change', (WidgetTester tester) async {
+      DateTime date;
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 400.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.date,
+              onDateTimeChanged: (DateTime newDate) {
+                date = newDate;
+              },
+              initialDateTime: DateTime(2018, 2, 27), // 2018 has 28 days in Feb.
+            ),
+          ),
+        ),
+      );
+
+      await tester.drag(find.text('27'), const Offset(0.0, -32.0));
+      await tester.pump();
+      expect(
+        date,
+        DateTime(2018, 2, 28),
+      );
+
+
+      await tester.drag(find.text('28'), const Offset(0.0, -32.0));
+      await tester.pump(); // Once to trigger the post frame animate call.
+
+      // Callback doesn't transiently go into invalid dates.
+      expect(
+        date,
+        DateTime(2018, 2, 28),
+      );
+      // Momentarily, the invalid 29th of Feb is dragged into the middle.
+      expect(
+        tester.getTopLeft(find.text('2018')).dy,
+        tester.getTopLeft(find.text('29')).dy,
+      );
+
+      await tester.pump(); // Once to start the DrivenScrollActivity.
+      await tester.pump(const Duration(milliseconds: 500));
+
+      expect(
+        date,
+        DateTime(2018, 2, 28),
+      );
+      expect(
+        tester.getTopLeft(find.text('2018')).dy,
+        tester.getTopLeft(find.text('28')).dy,
+      );
+    });
+
+    testWidgets('picker automatically scrolls the am/pm column when the hour column changes enough', (WidgetTester tester) async {
+      DateTime date;
+      await tester.pumpWidget(
+        SizedBox(
+          height: 400.0,
+          width: 400.0,
+          child: Directionality(
+            textDirection: TextDirection.ltr,
+            child: CupertinoDatePicker(
+              mode: CupertinoDatePickerMode.time,
+              onDateTimeChanged: (DateTime newDate) {
+                date = newDate;
+              },
+              initialDateTime: DateTime(2018, 1, 1, 11, 59),
+            ),
+          ),
+        ),
+      );
+
+      await tester.drag(find.text('11'), const Offset(0.0, -32.0));
+      await tester.pump();
+      await tester.pump(const Duration(milliseconds: 500));
+
+      expect(date, DateTime(2018, 1, 1, 12, 59));
+
+      await tester.drag(find.text('12'), const Offset(0.0, 32.0));
+      await tester.pump();
+      await tester.pump(const Duration(milliseconds: 500));
+
+      expect(date, DateTime(2018, 1, 1, 11, 59));
+
+      await tester.drag(find.text('11'), const Offset(0.0, 64.0));
+      await tester.pump();
+      await tester.pump(const Duration(milliseconds: 500));
+
+      expect(date, DateTime(2018, 1, 1, 9, 59));
+
+      await tester.drag(find.text('09'), const Offset(0.0, -192.0));
+      await tester.pump();
+      await tester.pump(const Duration(milliseconds: 500));
+
+      expect(date, DateTime(2018, 1, 1, 15, 59));
+    });
+  });
 }
\ No newline at end of file
diff --git a/packages/flutter/test/cupertino/localizations_test.dart b/packages/flutter/test/cupertino/localizations_test.dart
index b8be8b6..cc23c25 100644
--- a/packages/flutter/test/cupertino/localizations_test.dart
+++ b/packages/flutter/test/cupertino/localizations_test.dart
@@ -13,9 +13,12 @@
     expect(localizations.datePickerMonth(1), isNotNull);
     expect(localizations.datePickerDayOfMonth(1), isNotNull);
     expect(localizations.datePickerHour(0), isNotNull);
+    expect(localizations.datePickerHourSemanticsLabel(0), isNotNull);
     expect(localizations.datePickerMinute(0), isNotNull);
+    expect(localizations.datePickerMinuteSemanticsLabel(0), isNotNull);
     expect(localizations.datePickerMediumDate(DateTime.now()), isNotNull);
     expect(localizations.datePickerDateOrder, isNotNull);
+    expect(localizations.datePickerDateTimeOrder, isNotNull);
 
     expect(localizations.anteMeridiemAbbreviation, isNotNull);
     expect(localizations.postMeridiemAbbreviation, isNotNull);