mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
[Time Picker] Double tapping hours/minutes will switch time picker to input mode (#67076)
This commit is contained in:
parent
a2eef79fe4
commit
9e715205b7
|
@ -82,6 +82,8 @@ class _TimePickerFragmentContext {
|
||||||
@required this.mode,
|
@required this.mode,
|
||||||
@required this.onTimeChange,
|
@required this.onTimeChange,
|
||||||
@required this.onModeChange,
|
@required this.onModeChange,
|
||||||
|
@required this.onHourDoubleTapped,
|
||||||
|
@required this.onMinuteDoubleTapped,
|
||||||
@required this.use24HourDials,
|
@required this.use24HourDials,
|
||||||
}) : assert(selectedTime != null),
|
}) : assert(selectedTime != null),
|
||||||
assert(mode != null),
|
assert(mode != null),
|
||||||
|
@ -93,6 +95,8 @@ class _TimePickerFragmentContext {
|
||||||
final _TimePickerMode mode;
|
final _TimePickerMode mode;
|
||||||
final ValueChanged<TimeOfDay> onTimeChange;
|
final ValueChanged<TimeOfDay> onTimeChange;
|
||||||
final ValueChanged<_TimePickerMode> onModeChange;
|
final ValueChanged<_TimePickerMode> onModeChange;
|
||||||
|
final GestureTapCallback onHourDoubleTapped;
|
||||||
|
final GestureTapCallback onMinuteDoubleTapped;
|
||||||
final bool use24HourDials;
|
final bool use24HourDials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +107,8 @@ class _TimePickerHeader extends StatelessWidget {
|
||||||
@required this.orientation,
|
@required this.orientation,
|
||||||
@required this.onModeChanged,
|
@required this.onModeChanged,
|
||||||
@required this.onChanged,
|
@required this.onChanged,
|
||||||
|
@required this.onHourDoubleTapped,
|
||||||
|
@required this.onMinuteDoubleTapped,
|
||||||
@required this.use24HourDials,
|
@required this.use24HourDials,
|
||||||
@required this.helpText,
|
@required this.helpText,
|
||||||
}) : assert(selectedTime != null),
|
}) : assert(selectedTime != null),
|
||||||
|
@ -115,6 +121,8 @@ class _TimePickerHeader extends StatelessWidget {
|
||||||
final Orientation orientation;
|
final Orientation orientation;
|
||||||
final ValueChanged<_TimePickerMode> onModeChanged;
|
final ValueChanged<_TimePickerMode> onModeChanged;
|
||||||
final ValueChanged<TimeOfDay> onChanged;
|
final ValueChanged<TimeOfDay> onChanged;
|
||||||
|
final GestureTapCallback onHourDoubleTapped;
|
||||||
|
final GestureTapCallback onMinuteDoubleTapped;
|
||||||
final bool use24HourDials;
|
final bool use24HourDials;
|
||||||
final String helpText;
|
final String helpText;
|
||||||
|
|
||||||
|
@ -136,6 +144,8 @@ class _TimePickerHeader extends StatelessWidget {
|
||||||
mode: mode,
|
mode: mode,
|
||||||
onTimeChange: onChanged,
|
onTimeChange: onChanged,
|
||||||
onModeChange: _handleChangeMode,
|
onModeChange: _handleChangeMode,
|
||||||
|
onHourDoubleTapped: onHourDoubleTapped,
|
||||||
|
onMinuteDoubleTapped: onMinuteDoubleTapped,
|
||||||
use24HourDials: use24HourDials,
|
use24HourDials: use24HourDials,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -246,6 +256,7 @@ class _HourMinuteControl extends StatelessWidget {
|
||||||
const _HourMinuteControl({
|
const _HourMinuteControl({
|
||||||
@required this.text,
|
@required this.text,
|
||||||
@required this.onTap,
|
@required this.onTap,
|
||||||
|
@required this.onDoubleTap,
|
||||||
@required this.isSelected,
|
@required this.isSelected,
|
||||||
}) : assert(text != null),
|
}) : assert(text != null),
|
||||||
assert(onTap != null),
|
assert(onTap != null),
|
||||||
|
@ -253,6 +264,7 @@ class _HourMinuteControl extends StatelessWidget {
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
final GestureTapCallback onTap;
|
final GestureTapCallback onTap;
|
||||||
|
final GestureTapCallback onDoubleTap;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -284,6 +296,7 @@ class _HourMinuteControl extends StatelessWidget {
|
||||||
shape: shape,
|
shape: shape,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
onDoubleTap: isSelected ? onDoubleTap : null,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
|
@ -359,6 +372,7 @@ class _HourControl extends StatelessWidget {
|
||||||
isSelected: fragmentContext.mode == _TimePickerMode.hour,
|
isSelected: fragmentContext.mode == _TimePickerMode.hour,
|
||||||
text: formattedHour,
|
text: formattedHour,
|
||||||
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context),
|
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context),
|
||||||
|
onDoubleTap: fragmentContext.onHourDoubleTapped,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -448,6 +462,7 @@ class _MinuteControl extends StatelessWidget {
|
||||||
isSelected: fragmentContext.mode == _TimePickerMode.minute,
|
isSelected: fragmentContext.mode == _TimePickerMode.minute,
|
||||||
text: formattedMinute,
|
text: formattedMinute,
|
||||||
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context),
|
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context),
|
||||||
|
onDoubleTap: fragmentContext.onMinuteDoubleTapped,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1265,6 +1280,8 @@ class _TimePickerInput extends StatefulWidget {
|
||||||
Key key,
|
Key key,
|
||||||
@required this.initialSelectedTime,
|
@required this.initialSelectedTime,
|
||||||
@required this.helpText,
|
@required this.helpText,
|
||||||
|
@required this.autofocusHour,
|
||||||
|
@required this.autofocusMinute,
|
||||||
@required this.onChanged,
|
@required this.onChanged,
|
||||||
}) : assert(initialSelectedTime != null),
|
}) : assert(initialSelectedTime != null),
|
||||||
assert(onChanged != null),
|
assert(onChanged != null),
|
||||||
|
@ -1276,6 +1293,10 @@ class _TimePickerInput extends StatefulWidget {
|
||||||
/// Optionally provide your own help text to the time picker.
|
/// Optionally provide your own help text to the time picker.
|
||||||
final String helpText;
|
final String helpText;
|
||||||
|
|
||||||
|
final bool autofocusHour;
|
||||||
|
|
||||||
|
final bool autofocusMinute;
|
||||||
|
|
||||||
final ValueChanged<TimeOfDay> onChanged;
|
final ValueChanged<TimeOfDay> onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1430,6 +1451,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||||
_HourTextField(
|
_HourTextField(
|
||||||
selectedTime: _selectedTime,
|
selectedTime: _selectedTime,
|
||||||
style: hourMinuteStyle,
|
style: hourMinuteStyle,
|
||||||
|
autofocus: widget.autofocusHour,
|
||||||
validator: _validateHour,
|
validator: _validateHour,
|
||||||
onSavedSubmitted: _handleHourSavedSubmitted,
|
onSavedSubmitted: _handleHourSavedSubmitted,
|
||||||
onChanged: _handleHourChanged,
|
onChanged: _handleHourChanged,
|
||||||
|
@ -1460,6 +1482,7 @@ class _TimePickerInputState extends State<_TimePickerInput> {
|
||||||
_MinuteTextField(
|
_MinuteTextField(
|
||||||
selectedTime: _selectedTime,
|
selectedTime: _selectedTime,
|
||||||
style: hourMinuteStyle,
|
style: hourMinuteStyle,
|
||||||
|
autofocus: widget.autofocusMinute,
|
||||||
validator: _validateMinute,
|
validator: _validateMinute,
|
||||||
onSavedSubmitted: _handleMinuteSavedSubmitted,
|
onSavedSubmitted: _handleMinuteSavedSubmitted,
|
||||||
),
|
),
|
||||||
|
@ -1507,6 +1530,7 @@ class _HourTextField extends StatelessWidget {
|
||||||
Key key,
|
Key key,
|
||||||
@required this.selectedTime,
|
@required this.selectedTime,
|
||||||
@required this.style,
|
@required this.style,
|
||||||
|
@required this.autofocus,
|
||||||
@required this.validator,
|
@required this.validator,
|
||||||
@required this.onSavedSubmitted,
|
@required this.onSavedSubmitted,
|
||||||
@required this.onChanged,
|
@required this.onChanged,
|
||||||
|
@ -1514,6 +1538,7 @@ class _HourTextField extends StatelessWidget {
|
||||||
|
|
||||||
final TimeOfDay selectedTime;
|
final TimeOfDay selectedTime;
|
||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
|
final bool autofocus;
|
||||||
final FormFieldValidator<String> validator;
|
final FormFieldValidator<String> validator;
|
||||||
final ValueChanged<String> onSavedSubmitted;
|
final ValueChanged<String> onSavedSubmitted;
|
||||||
final ValueChanged<String> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
|
@ -1523,6 +1548,7 @@ class _HourTextField extends StatelessWidget {
|
||||||
return _HourMinuteTextField(
|
return _HourMinuteTextField(
|
||||||
selectedTime: selectedTime,
|
selectedTime: selectedTime,
|
||||||
isHour: true,
|
isHour: true,
|
||||||
|
autofocus: autofocus,
|
||||||
style: style,
|
style: style,
|
||||||
semanticHintText: MaterialLocalizations.of(context).timePickerHourLabel,
|
semanticHintText: MaterialLocalizations.of(context).timePickerHourLabel,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
|
@ -1537,12 +1563,14 @@ class _MinuteTextField extends StatelessWidget {
|
||||||
Key key,
|
Key key,
|
||||||
@required this.selectedTime,
|
@required this.selectedTime,
|
||||||
@required this.style,
|
@required this.style,
|
||||||
|
@required this.autofocus,
|
||||||
@required this.validator,
|
@required this.validator,
|
||||||
@required this.onSavedSubmitted,
|
@required this.onSavedSubmitted,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final TimeOfDay selectedTime;
|
final TimeOfDay selectedTime;
|
||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
|
final bool autofocus;
|
||||||
final FormFieldValidator<String> validator;
|
final FormFieldValidator<String> validator;
|
||||||
final ValueChanged<String> onSavedSubmitted;
|
final ValueChanged<String> onSavedSubmitted;
|
||||||
|
|
||||||
|
@ -1551,6 +1579,7 @@ class _MinuteTextField extends StatelessWidget {
|
||||||
return _HourMinuteTextField(
|
return _HourMinuteTextField(
|
||||||
selectedTime: selectedTime,
|
selectedTime: selectedTime,
|
||||||
isHour: false,
|
isHour: false,
|
||||||
|
autofocus: autofocus,
|
||||||
style: style,
|
style: style,
|
||||||
semanticHintText: MaterialLocalizations.of(context).timePickerMinuteLabel,
|
semanticHintText: MaterialLocalizations.of(context).timePickerMinuteLabel,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
|
@ -1564,6 +1593,7 @@ class _HourMinuteTextField extends StatefulWidget {
|
||||||
Key key,
|
Key key,
|
||||||
@required this.selectedTime,
|
@required this.selectedTime,
|
||||||
@required this.isHour,
|
@required this.isHour,
|
||||||
|
@required this.autofocus,
|
||||||
@required this.style,
|
@required this.style,
|
||||||
@required this.semanticHintText,
|
@required this.semanticHintText,
|
||||||
@required this.validator,
|
@required this.validator,
|
||||||
|
@ -1573,6 +1603,7 @@ class _HourMinuteTextField extends StatefulWidget {
|
||||||
|
|
||||||
final TimeOfDay selectedTime;
|
final TimeOfDay selectedTime;
|
||||||
final bool isHour;
|
final bool isHour;
|
||||||
|
final bool autofocus;
|
||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
final String semanticHintText;
|
final String semanticHintText;
|
||||||
final FormFieldValidator<String> validator;
|
final FormFieldValidator<String> validator;
|
||||||
|
@ -1658,6 +1689,7 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> {
|
||||||
child: MediaQuery(
|
child: MediaQuery(
|
||||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
|
autofocus: widget.autofocus ?? false,
|
||||||
expands: true,
|
expands: true,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
inputFormatters: <TextInputFormatter>[
|
inputFormatters: <TextInputFormatter>[
|
||||||
|
@ -1746,6 +1778,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||||
_TimePickerMode _mode = _TimePickerMode.hour;
|
_TimePickerMode _mode = _TimePickerMode.hour;
|
||||||
_TimePickerMode _lastModeAnnounced;
|
_TimePickerMode _lastModeAnnounced;
|
||||||
bool _autoValidate;
|
bool _autoValidate;
|
||||||
|
bool _autofocusHour;
|
||||||
|
bool _autofocusMinute;
|
||||||
|
|
||||||
TimeOfDay get selectedTime => _selectedTime;
|
TimeOfDay get selectedTime => _selectedTime;
|
||||||
TimeOfDay _selectedTime;
|
TimeOfDay _selectedTime;
|
||||||
|
@ -1788,6 +1822,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||||
break;
|
break;
|
||||||
case TimePickerEntryMode.input:
|
case TimePickerEntryMode.input:
|
||||||
_formKey.currentState.save();
|
_formKey.currentState.save();
|
||||||
|
_autofocusHour = false;
|
||||||
|
_autofocusMinute = false;
|
||||||
_entryMode = TimePickerEntryMode.dial;
|
_entryMode = TimePickerEntryMode.dial;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1833,6 +1869,16 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleHourDoubleTapped() {
|
||||||
|
_autofocusHour = true;
|
||||||
|
_handleEntryModeToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleMinuteDoubleTapped() {
|
||||||
|
_autofocusMinute = true;
|
||||||
|
_handleEntryModeToggle();
|
||||||
|
}
|
||||||
|
|
||||||
void _handleHourSelected() {
|
void _handleHourSelected() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_mode = _TimePickerMode.minute;
|
_mode = _TimePickerMode.minute;
|
||||||
|
@ -1962,6 +2008,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
onModeChanged: _handleModeChanged,
|
onModeChanged: _handleModeChanged,
|
||||||
onChanged: _handleTimeChanged,
|
onChanged: _handleTimeChanged,
|
||||||
|
onHourDoubleTapped: _handleHourDoubleTapped,
|
||||||
|
onMinuteDoubleTapped: _handleMinuteDoubleTapped,
|
||||||
use24HourDials: use24HourDials,
|
use24HourDials: use24HourDials,
|
||||||
helpText: widget.helpText,
|
helpText: widget.helpText,
|
||||||
);
|
);
|
||||||
|
@ -2014,6 +2062,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||||
_TimePickerInput(
|
_TimePickerInput(
|
||||||
initialSelectedTime: _selectedTime,
|
initialSelectedTime: _selectedTime,
|
||||||
helpText: widget.helpText,
|
helpText: widget.helpText,
|
||||||
|
autofocusHour: _autofocusHour,
|
||||||
|
autofocusMinute: _autofocusMinute,
|
||||||
onChanged: _handleTimeChanged,
|
onChanged: _handleTimeChanged,
|
||||||
),
|
),
|
||||||
actions,
|
actions,
|
||||||
|
|
|
@ -803,6 +803,89 @@ void _testsInput() {
|
||||||
expect(find.byType(TextField), findsNothing);
|
expect(find.byType(TextField), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Can double tap hours (when selected) to enter input mode', (WidgetTester tester) async {
|
||||||
|
await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
|
||||||
|
final Finder hourFinder = find.ancestor(
|
||||||
|
of: find.text('7'),
|
||||||
|
matching: find.byType(InkWell),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(TextField), findsNothing);
|
||||||
|
|
||||||
|
// Double tap the hour.
|
||||||
|
await tester.tap(hourFinder);
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await tester.tap(hourFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(TextField), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Can not double tap hours (when not selected) to enter input mode', (WidgetTester tester) async {
|
||||||
|
await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
|
||||||
|
final Finder hourFinder = find.ancestor(
|
||||||
|
of: find.text('7'),
|
||||||
|
matching: find.byType(InkWell),
|
||||||
|
);
|
||||||
|
final Finder minuteFinder = find.ancestor(
|
||||||
|
of: find.text('00'),
|
||||||
|
matching: find.byType(InkWell),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(TextField), findsNothing);
|
||||||
|
|
||||||
|
// Switch to minutes mode.
|
||||||
|
await tester.tap(minuteFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Double tap the hour.
|
||||||
|
await tester.tap(hourFinder);
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await tester.tap(hourFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(TextField), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Can double tap minutes (when selected) to enter input mode', (WidgetTester tester) async {
|
||||||
|
await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
|
||||||
|
final Finder minuteFinder = find.ancestor(
|
||||||
|
of: find.text('00'),
|
||||||
|
matching: find.byType(InkWell),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(TextField), findsNothing);
|
||||||
|
|
||||||
|
// Switch to minutes mode.
|
||||||
|
await tester.tap(minuteFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Double tap the minutes.
|
||||||
|
await tester.tap(minuteFinder);
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await tester.tap(minuteFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(TextField), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Can not double tap minutes (when not selected) to enter input mode', (WidgetTester tester) async {
|
||||||
|
await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
|
||||||
|
final Finder minuteFinder = find.ancestor(
|
||||||
|
of: find.text('00'),
|
||||||
|
matching: find.byType(InkWell),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(TextField), findsNothing);
|
||||||
|
|
||||||
|
// Double tap the minutes.
|
||||||
|
await tester.tap(minuteFinder);
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await tester.tap(minuteFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(TextField), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Entered text returns time', (WidgetTester tester) async {
|
testWidgets('Entered text returns time', (WidgetTester tester) async {
|
||||||
TimeOfDay result;
|
TimeOfDay result;
|
||||||
|
|
Loading…
Reference in a new issue