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';
import 'dialog.dart';
import 'feedback.dart';
import 'flat_button.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'text_theme.dart';
import 'theme.dart';
......@@ -44,15 +46,7 @@ const double _kTimePickerHeightLandscape = 316.0;
const double _kTimePickerHeightPortraitCollapsed = 484.0;
const double _kTimePickerHeightLandscapeCollapsed = 304.0;
/// The horizontal gap between the day period fragment and the fragment
/// 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;
const BoxConstraints _kMinTappableRegion = BoxConstraints(minWidth: 48, minHeight: 48);
enum _TimePickerHeaderId {
hour,
......@@ -194,9 +188,11 @@ class _TimePickerHeaderFormat {
class _DayPeriodControl extends StatelessWidget {
const _DayPeriodControl({
@required this.fragmentContext,
@required this.orientation,
});
final _TimePickerFragmentContext fragmentContext;
final Orientation orientation;
void _togglePeriod() {
final int newHour = (fragmentContext.selectedTime.hour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
......@@ -238,41 +234,73 @@ class _DayPeriodControl extends StatelessWidget {
final TextStyle pmStyle = headerTextTheme.subhead.copyWith(
color: !amSelected ? activeColor: inactiveColor
);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
GestureDetector(
excludeFromSemantics: true,
onTap: Feedback.wrapForTap(() {
_setAm(context);
}, context),
behavior: HitTestBehavior.opaque,
child: Semantics(
selected: amSelected,
onTap: () {
_setAm(context);
},
child: Text(materialLocalizations.anteMeridiemAbbreviation, style: amStyle),
final bool layoutPortrait = orientation == Orientation.portrait;
final Widget amButton = ConstrainedBox(
constraints: _kMinTappableRegion,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: Feedback.wrapForTap(() => _setAm(context), context),
child: Padding(
padding: layoutPortrait ? const EdgeInsets.only(bottom: 2.0) : const EdgeInsets.only(right: 4.0),
child: Align(
alignment: layoutPortrait ? Alignment.bottomCenter : Alignment.centerRight,
widthFactor: 1,
heightFactor: 1,
child: Semantics(
selected: amSelected,
child: Text(materialLocalizations.anteMeridiemAbbreviation, style: amStyle)
),
),
),
),
const SizedBox(width: 0.0, height: 4.0), // Vertical spacer
GestureDetector(
excludeFromSemantics: true,
onTap: Feedback.wrapForTap(() {
_setPm(context);
}, context),
behavior: HitTestBehavior.opaque,
child: Semantics(
selected: !amSelected,
onTap: () {
_setPm(context);
},
child: Text(materialLocalizations.postMeridiemAbbreviation, style: pmStyle),
),
);
final Widget pmButton = ConstrainedBox(
constraints: _kMinTappableRegion,
child: Material(
type: MaterialType.transparency,
textStyle: pmStyle,
child: InkWell(
onTap: Feedback.wrapForTap(() => _setPm(context), context),
child: Padding(
padding: layoutPortrait ? const EdgeInsets.only(top: 2.0) : const EdgeInsets.only(left: 4.0),
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 {
alwaysUse24HourFormat: alwaysUse24HourFormat,
);
return GestureDetector(
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context),
child: Semantics(
hint: localizations.timePickerHourModeAnnouncement,
value: formattedHour,
excludeSemantics: true,
increasedValue: formattedNextHour,
onIncrease: () {
fragmentContext.onTimeChange(nextHour);
},
decreasedValue: formattedPreviousHour,
onDecrease: () {
fragmentContext.onTimeChange(previousHour);
},
child: Text(formattedHour, style: hourStyle),
return Semantics(
hint: localizations.timePickerHourModeAnnouncement,
value: formattedHour,
excludeSemantics: true,
increasedValue: formattedNextHour,
onIncrease: () {
fragmentContext.onTimeChange(nextHour);
},
decreasedValue: formattedPreviousHour,
onDecrease: () {
fragmentContext.onTimeChange(previousHour);
},
child: ConstrainedBox(
constraints: _kMinTappableRegion,
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 {
);
final String formattedPreviousMinute = localizations.formatMinute(previousMinute);
return GestureDetector(
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context),
child: Semantics(
excludeSemantics: true,
hint: localizations.timePickerMinuteModeAnnouncement,
value: formattedMinute,
increasedValue: formattedNextMinute,
onIncrease: () {
fragmentContext.onTimeChange(nextMinute);
},
decreasedValue: formattedPreviousMinute,
onDecrease: () {
fragmentContext.onTimeChange(previousMinute);
},
child: Text(formattedMinute, style: minuteStyle),
return Semantics(
excludeSemantics: true,
hint: localizations.timePickerMinuteModeAnnouncement,
value: formattedMinute,
increasedValue: formattedNextMinute,
onIncrease: () {
fragmentContext.onTimeChange(nextMinute);
},
decreasedValue: formattedPreviousMinute,
onDecrease: () {
fragmentContext.onTimeChange(previousMinute);
},
child: ConstrainedBox(
constraints: _kMinTappableRegion,
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 {
/// configuration.
///
/// 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.
_TimePickerHeaderFragment hour() {
return _TimePickerHeaderFragment(
layoutId: _TimePickerHeaderId.hour,
widget: _HourControl(fragmentContext: context),
startMargin: _kPeriodGap,
);
}
......@@ -448,8 +492,7 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
_TimePickerHeaderFragment dayPeriod() {
return _TimePickerHeaderFragment(
layoutId: _TimePickerHeaderId.period,
widget: _DayPeriodControl(fragmentContext: context),
startMargin: _kPeriodGap,
widget: _DayPeriodControl(fragmentContext: context, orientation: orientation),
);
}
......@@ -508,7 +551,6 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
fragment3: minute(),
),
piece(
bottomMargin: _kVerticalGap,
fragment1: dayPeriod(),
),
);
......@@ -529,7 +571,6 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
case TimeOfDayFormat.a_space_h_colon_mm:
return format(
piece(
bottomMargin: _kVerticalGap,
fragment1: dayPeriod(),
),
piece(
......@@ -621,10 +662,11 @@ class _TimePickerHeaderLayout extends MultiChildLayoutDelegate {
final _TimePickerHeaderPiece centrepiece = format.pieces[format.centrepieceIndex];
double y = (size.height - height) / 2.0;
for (int pieceIndex = 0; pieceIndex < format.pieces.length; pieceIndex += 1) {
final double pieceVerticalCenter = y + pieceHeights[pieceIndex] / 2.0;
if (pieceIndex != format.centrepieceIndex)
_positionPiece(size.width, y, childSizes, format.pieces[pieceIndex].fragments);
_positionPiece(size.width, pieceVerticalCenter, childSizes, format.pieces[pieceIndex].fragments);
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;
}
......@@ -774,7 +816,7 @@ class _TimePickerHeader extends StatelessWidget {
use24HourDials: use24HourDials,
);
final _TimePickerHeaderFormat format = _buildHeaderFormat(timeOfDayFormat, fragmentContext);
final _TimePickerHeaderFormat format = _buildHeaderFormat(timeOfDayFormat, fragmentContext, orientation);
return Container(
width: width,
......
......@@ -542,6 +542,38 @@ void _tests() {
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 {
Widget buildFrame(TextDirection textDirection) {
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