TimePicker moves to minute mode after hour selection (#31566)

Adds a feature of the native Android Time Picker to our Material Time Picker. When the user selects an hour, it automatically switches to minute mode.

This is a merging of two pull requests:

Code changes from @sdolski #24677
Tests from @lucaslcode #29876

Thanks to both of you for your contributions!
diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart
index d012519..05d9c7d 100644
--- a/packages/flutter/lib/src/material/time_picker.dart
+++ b/packages/flutter/lib/src/material/time_picker.dart
@@ -1000,12 +1000,16 @@
     @required this.mode,
     @required this.use24HourDials,
     @required this.onChanged,
-  }) : assert(selectedTime != null);
+    @required this.onHourSelected,
+  }) : assert(selectedTime != null),
+       assert(mode != null),
+       assert(use24HourDials != null);
 
   final TimeOfDay selectedTime;
   final _TimePickerMode mode;
   final bool use24HourDials;
   final ValueChanged<TimeOfDay> onChanged;
+  final VoidCallback onHourSelected;
 
   @override
   _DialState createState() => _DialState();
@@ -1169,6 +1173,11 @@
     _position = null;
     _center = null;
     _animateTo(_getThetaForTime(widget.selectedTime));
+    if (widget.mode == _TimePickerMode.hour) {
+      if (widget.onHourSelected != null) {
+        widget.onHourSelected();
+      }
+    }
   }
 
   void _handleTapUp(TapUpDetails details) {
@@ -1183,6 +1192,9 @@
       } else {
         _announceToAccessibility(context, localizations.formatDecimal(newTime.hourOfPeriod));
       }
+      if (widget.onHourSelected != null) {
+        widget.onHourSelected();
+      }
     } else {
       _announceToAccessibility(context, localizations.formatDecimal(newTime.minute));
     }
@@ -1522,6 +1534,12 @@
     });
   }
 
+  void _handleHourSelected() {
+    setState(() {
+      _mode = _TimePickerMode.minute;
+    });
+  }
+
   void _handleCancel() {
     Navigator.pop(context);
   }
@@ -1547,6 +1565,7 @@
           use24HourDials: use24HourDials,
           selectedTime: _selectedTime,
           onChanged: _handleTimeChanged,
+          onHourSelected: _handleHourSelected,
         ),
       ),
     );
diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart
index 93f4ab2..198a892 100644
--- a/packages/flutter/test/material/time_picker_test.dart
+++ b/packages/flutter/test/material/time_picker_test.dart
@@ -134,6 +134,39 @@
     expect(result.hour, equals(9));
   });
 
+  testWidgets('tap-select switches from hour to minute', (WidgetTester tester) async {
+    TimeOfDay result;
+
+    final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
+    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
+    final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)
+
+    await tester.tapAt(hour6);
+    await tester.pump(const Duration(milliseconds: 50));
+    await tester.tapAt(min45);
+    await finishPicker(tester);
+    expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
+  });
+
+  testWidgets('drag-select switches from hour to minute', (WidgetTester tester) async {
+    TimeOfDay result;
+
+    final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
+    final Offset hour3 = Offset(center.dx + 50.0, center.dy);
+    final Offset hour6 = Offset(center.dx, center.dy + 50.0);
+    final Offset hour9 = Offset(center.dx - 50.0, center.dy);
+
+    TestGesture gesture = await tester.startGesture(hour6);
+    await gesture.moveBy(hour9 - hour6);
+    await gesture.up();
+    await tester.pump(const Duration(milliseconds: 50));
+    gesture = await tester.startGesture(hour6);
+    await gesture.moveBy(hour3 - hour6);
+    await gesture.up();
+    await finishPicker(tester);
+    expect(result, equals(const TimeOfDay(hour: 9, minute: 15)));
+  });
+
   group('haptic feedback', () {
     const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
     const Duration kSlowFeedbackInterval = Duration(milliseconds: 200);