Unverified Commit 017997b9 authored by Darren Austin's avatar Darren Austin Committed by GitHub

Increase size of touch regions in the Time Picker header (#32053)

- Increased the AM/PM, minute and hour buttons to at least 48x48
- Added InkWells to all of them
- Adjusted the landscape layout for the AM/PM buttons to be horizontal
- Added a test to ensure the regions are at least 48x48
parent 2d2edbf7
...@@ -16,6 +16,8 @@ import 'debug.dart'; ...@@ -16,6 +16,8 @@ import 'debug.dart';
import 'dialog.dart'; import 'dialog.dart';
import 'feedback.dart'; import 'feedback.dart';
import 'flat_button.dart'; import 'flat_button.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'text_theme.dart'; import 'text_theme.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -44,15 +46,7 @@ const double _kTimePickerHeightLandscape = 316.0; ...@@ -44,15 +46,7 @@ const double _kTimePickerHeightLandscape = 316.0;
const double _kTimePickerHeightPortraitCollapsed = 484.0; const double _kTimePickerHeightPortraitCollapsed = 484.0;
const double _kTimePickerHeightLandscapeCollapsed = 304.0; const double _kTimePickerHeightLandscapeCollapsed = 304.0;
/// The horizontal gap between the day period fragment and the fragment const BoxConstraints _kMinTappableRegion = BoxConstraints(minWidth: 48, minHeight: 48);
/// positioned next to it horizontally.
///
/// Normally there's only one horizontal sibling, and it may appear on the left
/// or right depending on the current [TextDirection].
const double _kPeriodGap = 8.0;
/// The vertical gap between pieces when laid out vertically (in portrait mode).
const double _kVerticalGap = 8.0;
enum _TimePickerHeaderId { enum _TimePickerHeaderId {
hour, hour,
...@@ -194,9 +188,11 @@ class _TimePickerHeaderFormat { ...@@ -194,9 +188,11 @@ class _TimePickerHeaderFormat {
class _DayPeriodControl extends StatelessWidget { class _DayPeriodControl extends StatelessWidget {
const _DayPeriodControl({ const _DayPeriodControl({
@required this.fragmentContext, @required this.fragmentContext,
@required this.orientation,
}); });
final _TimePickerFragmentContext fragmentContext; final _TimePickerFragmentContext fragmentContext;
final Orientation orientation;
void _togglePeriod() { void _togglePeriod() {
final int newHour = (fragmentContext.selectedTime.hour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay; final int newHour = (fragmentContext.selectedTime.hour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
...@@ -238,41 +234,73 @@ class _DayPeriodControl extends StatelessWidget { ...@@ -238,41 +234,73 @@ class _DayPeriodControl extends StatelessWidget {
final TextStyle pmStyle = headerTextTheme.subhead.copyWith( final TextStyle pmStyle = headerTextTheme.subhead.copyWith(
color: !amSelected ? activeColor: inactiveColor color: !amSelected ? activeColor: inactiveColor
); );
final bool layoutPortrait = orientation == Orientation.portrait;
return Column(
mainAxisSize: MainAxisSize.min, final Widget amButton = ConstrainedBox(
children: <Widget>[ constraints: _kMinTappableRegion,
GestureDetector( child: Material(
excludeFromSemantics: true, type: MaterialType.transparency,
onTap: Feedback.wrapForTap(() { child: InkWell(
_setAm(context); onTap: Feedback.wrapForTap(() => _setAm(context), context),
}, context), child: Padding(
behavior: HitTestBehavior.opaque, padding: layoutPortrait ? const EdgeInsets.only(bottom: 2.0) : const EdgeInsets.only(right: 4.0),
child: Semantics( child: Align(
selected: amSelected, alignment: layoutPortrait ? Alignment.bottomCenter : Alignment.centerRight,
onTap: () { widthFactor: 1,
_setAm(context); heightFactor: 1,
}, child: Semantics(
child: Text(materialLocalizations.anteMeridiemAbbreviation, style: amStyle), selected: amSelected,
child: Text(materialLocalizations.anteMeridiemAbbreviation, style: amStyle)
),
),
), ),
), ),
const SizedBox(width: 0.0, height: 4.0), // Vertical spacer ),
GestureDetector( );
excludeFromSemantics: true,
onTap: Feedback.wrapForTap(() { final Widget pmButton = ConstrainedBox(
_setPm(context); constraints: _kMinTappableRegion,
}, context), child: Material(
behavior: HitTestBehavior.opaque, type: MaterialType.transparency,
child: Semantics( textStyle: pmStyle,
selected: !amSelected, child: InkWell(
onTap: () { onTap: Feedback.wrapForTap(() => _setPm(context), context),
_setPm(context); child: Padding(
}, padding: layoutPortrait ? const EdgeInsets.only(top: 2.0) : const EdgeInsets.only(left: 4.0),
child: Text(materialLocalizations.postMeridiemAbbreviation, style: pmStyle), child: Align(
alignment: orientation == Orientation.portrait ? Alignment.topCenter : Alignment.centerLeft,
widthFactor: 1,
heightFactor: 1,
child: Semantics(
selected: !amSelected,
child: Text(materialLocalizations.postMeridiemAbbreviation, style: pmStyle),
),
),
), ),
), ),
], ),
); );
switch (orientation) {
case Orientation.portrait:
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
amButton,
pmButton,
],
);
case Orientation.landscape:
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
amButton,
pmButton,
],
);
}
return null;
} }
} }
...@@ -326,22 +354,28 @@ class _HourControl extends StatelessWidget { ...@@ -326,22 +354,28 @@ class _HourControl extends StatelessWidget {
alwaysUse24HourFormat: alwaysUse24HourFormat, alwaysUse24HourFormat: alwaysUse24HourFormat,
); );
return GestureDetector( return Semantics(
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context), hint: localizations.timePickerHourModeAnnouncement,
child: Semantics( value: formattedHour,
hint: localizations.timePickerHourModeAnnouncement, excludeSemantics: true,
value: formattedHour, increasedValue: formattedNextHour,
excludeSemantics: true, onIncrease: () {
increasedValue: formattedNextHour, fragmentContext.onTimeChange(nextHour);
onIncrease: () { },
fragmentContext.onTimeChange(nextHour); decreasedValue: formattedPreviousHour,
}, onDecrease: () {
decreasedValue: formattedPreviousHour, fragmentContext.onTimeChange(previousHour);
onDecrease: () { },
fragmentContext.onTimeChange(previousHour); child: ConstrainedBox(
}, constraints: _kMinTappableRegion,
child: Text(formattedHour, style: hourStyle), child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context),
child: Text(formattedHour, style: hourStyle, textAlign: TextAlign.end),
),
), ),
),
); );
} }
} }
...@@ -390,22 +424,28 @@ class _MinuteControl extends StatelessWidget { ...@@ -390,22 +424,28 @@ class _MinuteControl extends StatelessWidget {
); );
final String formattedPreviousMinute = localizations.formatMinute(previousMinute); final String formattedPreviousMinute = localizations.formatMinute(previousMinute);
return GestureDetector( return Semantics(
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context), excludeSemantics: true,
child: Semantics( hint: localizations.timePickerMinuteModeAnnouncement,
excludeSemantics: true, value: formattedMinute,
hint: localizations.timePickerMinuteModeAnnouncement, increasedValue: formattedNextMinute,
value: formattedMinute, onIncrease: () {
increasedValue: formattedNextMinute, fragmentContext.onTimeChange(nextMinute);
onIncrease: () { },
fragmentContext.onTimeChange(nextMinute); decreasedValue: formattedPreviousMinute,
}, onDecrease: () {
decreasedValue: formattedPreviousMinute, fragmentContext.onTimeChange(previousMinute);
onDecrease: () { },
fragmentContext.onTimeChange(previousMinute); child: ConstrainedBox(
}, constraints: _kMinTappableRegion,
child: Text(formattedMinute, style: minuteStyle), child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context),
child: Text(formattedMinute, style: minuteStyle, textAlign: TextAlign.start),
),
), ),
),
); );
} }
} }
...@@ -415,13 +455,17 @@ class _MinuteControl extends StatelessWidget { ...@@ -415,13 +455,17 @@ class _MinuteControl extends StatelessWidget {
/// configuration. /// configuration.
/// ///
/// The [timeOfDayFormat] and [context] arguments must not be null. /// The [timeOfDayFormat] and [context] arguments must not be null.
_TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _TimePickerFragmentContext context) { _TimePickerHeaderFormat _buildHeaderFormat(
TimeOfDayFormat timeOfDayFormat,
_TimePickerFragmentContext context,
Orientation orientation
) {
// Creates an hour fragment. // Creates an hour fragment.
_TimePickerHeaderFragment hour() { _TimePickerHeaderFragment hour() {
return _TimePickerHeaderFragment( return _TimePickerHeaderFragment(
layoutId: _TimePickerHeaderId.hour, layoutId: _TimePickerHeaderId.hour,
widget: _HourControl(fragmentContext: context), widget: _HourControl(fragmentContext: context),
startMargin: _kPeriodGap,
); );
} }
...@@ -448,8 +492,7 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim ...@@ -448,8 +492,7 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
_TimePickerHeaderFragment dayPeriod() { _TimePickerHeaderFragment dayPeriod() {
return _TimePickerHeaderFragment( return _TimePickerHeaderFragment(
layoutId: _TimePickerHeaderId.period, layoutId: _TimePickerHeaderId.period,
widget: _DayPeriodControl(fragmentContext: context), widget: _DayPeriodControl(fragmentContext: context, orientation: orientation),
startMargin: _kPeriodGap,
); );
} }
...@@ -508,7 +551,6 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim ...@@ -508,7 +551,6 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
fragment3: minute(), fragment3: minute(),
), ),
piece( piece(
bottomMargin: _kVerticalGap,
fragment1: dayPeriod(), fragment1: dayPeriod(),
), ),
); );
...@@ -529,7 +571,6 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim ...@@ -529,7 +571,6 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
case TimeOfDayFormat.a_space_h_colon_mm: case TimeOfDayFormat.a_space_h_colon_mm:
return format( return format(
piece( piece(
bottomMargin: _kVerticalGap,
fragment1: dayPeriod(), fragment1: dayPeriod(),
), ),
piece( piece(
...@@ -621,10 +662,11 @@ class _TimePickerHeaderLayout extends MultiChildLayoutDelegate { ...@@ -621,10 +662,11 @@ class _TimePickerHeaderLayout extends MultiChildLayoutDelegate {
final _TimePickerHeaderPiece centrepiece = format.pieces[format.centrepieceIndex]; final _TimePickerHeaderPiece centrepiece = format.pieces[format.centrepieceIndex];
double y = (size.height - height) / 2.0; double y = (size.height - height) / 2.0;
for (int pieceIndex = 0; pieceIndex < format.pieces.length; pieceIndex += 1) { for (int pieceIndex = 0; pieceIndex < format.pieces.length; pieceIndex += 1) {
final double pieceVerticalCenter = y + pieceHeights[pieceIndex] / 2.0;
if (pieceIndex != format.centrepieceIndex) if (pieceIndex != format.centrepieceIndex)
_positionPiece(size.width, y, childSizes, format.pieces[pieceIndex].fragments); _positionPiece(size.width, pieceVerticalCenter, childSizes, format.pieces[pieceIndex].fragments);
else else
_positionPivoted(size.width, y, childSizes, centrepiece.fragments, centrepiece.pivotIndex); _positionPivoted(size.width, pieceVerticalCenter, childSizes, centrepiece.fragments, centrepiece.pivotIndex);
y += pieceHeights[pieceIndex] + format.pieces[pieceIndex].bottomMargin; y += pieceHeights[pieceIndex] + format.pieces[pieceIndex].bottomMargin;
} }
...@@ -774,7 +816,7 @@ class _TimePickerHeader extends StatelessWidget { ...@@ -774,7 +816,7 @@ class _TimePickerHeader extends StatelessWidget {
use24HourDials: use24HourDials, use24HourDials: use24HourDials,
); );
final _TimePickerHeaderFormat format = _buildHeaderFormat(timeOfDayFormat, fragmentContext); final _TimePickerHeaderFormat format = _buildHeaderFormat(timeOfDayFormat, fragmentContext, orientation);
return Container( return Container(
width: width, width: width,
......
...@@ -542,6 +542,38 @@ void _tests() { ...@@ -542,6 +542,38 @@ void _tests() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('header touch regions are large enough', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, false);
final Size amSize = tester.getSize(find.ancestor(
of: find.text('AM'),
matching: find.byType(InkWell),
));
expect(amSize.width, greaterThanOrEqualTo(48.0));
expect(amSize.height, greaterThanOrEqualTo(48.0));
final Size pmSize = tester.getSize(find.ancestor(
of: find.text('PM'),
matching: find.byType(InkWell),
));
expect(pmSize.width, greaterThanOrEqualTo(48.0));
expect(pmSize.height, greaterThanOrEqualTo(48.0));
final Size hourSize = tester.getSize(find.ancestor(
of: find.text('7'),
matching: find.byType(InkWell),
));
expect(hourSize.width, greaterThanOrEqualTo(48.0));
expect(hourSize.height, greaterThanOrEqualTo(48.0));
final Size minuteSize = tester.getSize(find.ancestor(
of: find.text('00'),
matching: find.byType(InkWell),
));
expect(minuteSize.width, greaterThanOrEqualTo(48.0));
expect(minuteSize.height, greaterThanOrEqualTo(48.0));
});
testWidgets('builder parameter', (WidgetTester tester) async { testWidgets('builder parameter', (WidgetTester tester) async {
Widget buildFrame(TextDirection textDirection) { Widget buildFrame(TextDirection textDirection) {
return MaterialApp( return MaterialApp(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment