Make time picker accessible (#13152)
* make time picker accessible * use new CustomPaint a11y API * flutter_localizations tests; use bigger distance delta * fix am/pm control; selected values * fix translations; remove @mustCallSuper in describeSemanticsConfiguration * exclude AM/PM announcement from iOS as on iOS the label is read back automatically
This commit is contained in:
parent
927a143d79
commit
b80751cdc3
@ -119,6 +119,14 @@ abstract class MaterialLocalizations {
|
|||||||
/// The abbreviation for post meridiem (after noon) shown in the time picker.
|
/// The abbreviation for post meridiem (after noon) shown in the time picker.
|
||||||
String get postMeridiemAbbreviation;
|
String get postMeridiemAbbreviation;
|
||||||
|
|
||||||
|
/// The text-to-speech announcement made when a time picker invoked using
|
||||||
|
/// [showTimePicker] is set to the hour picker mode.
|
||||||
|
String get timePickerHourModeAnnouncement;
|
||||||
|
|
||||||
|
/// The text-to-speech announcement made when a time picker invoked using
|
||||||
|
/// [showTimePicker] is set to the minute picker mode.
|
||||||
|
String get timePickerMinuteModeAnnouncement;
|
||||||
|
|
||||||
/// The format used to lay out the time picker.
|
/// The format used to lay out the time picker.
|
||||||
///
|
///
|
||||||
/// The documentation for [TimeOfDayFormat] enum values provides details on
|
/// The documentation for [TimeOfDayFormat] enum values provides details on
|
||||||
@ -505,6 +513,12 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get postMeridiemAbbreviation => 'PM';
|
String get postMeridiemAbbreviation => 'PM';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get timePickerHourModeAnnouncement => 'Select hours';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get timePickerMinuteModeAnnouncement => 'Select minutes';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat: false }) {
|
TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat: false }) {
|
||||||
return alwaysUse24HourFormat
|
return alwaysUse24HourFormat
|
||||||
|
@ -147,7 +147,7 @@ class TextField extends StatefulWidget {
|
|||||||
///
|
///
|
||||||
/// This text style is also used as the base style for the [decoration].
|
/// This text style is also used as the base style for the [decoration].
|
||||||
///
|
///
|
||||||
/// If null, defaults to a text style from the current [Theme].
|
/// If null, defaults to the `subhead` text style from the current [Theme].
|
||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
|
|
||||||
/// How the text being edited should be aligned horizontally.
|
/// How the text being edited should be aligned horizontally.
|
||||||
|
@ -6,6 +6,7 @@ import 'dart:async';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ class _TimePickerFragmentContext {
|
|||||||
@required this.inactiveStyle,
|
@required this.inactiveStyle,
|
||||||
@required this.onTimeChange,
|
@required this.onTimeChange,
|
||||||
@required this.onModeChange,
|
@required this.onModeChange,
|
||||||
|
@required this.targetPlatform,
|
||||||
}) : assert(headerTextTheme != null),
|
}) : assert(headerTextTheme != null),
|
||||||
assert(textDirection != null),
|
assert(textDirection != null),
|
||||||
assert(selectedTime != null),
|
assert(selectedTime != null),
|
||||||
@ -77,7 +79,8 @@ class _TimePickerFragmentContext {
|
|||||||
assert(inactiveColor != null),
|
assert(inactiveColor != null),
|
||||||
assert(inactiveStyle != null),
|
assert(inactiveStyle != null),
|
||||||
assert(onTimeChange != null),
|
assert(onTimeChange != null),
|
||||||
assert(onModeChange != null);
|
assert(onModeChange != null),
|
||||||
|
assert(targetPlatform != null);
|
||||||
|
|
||||||
final TextTheme headerTextTheme;
|
final TextTheme headerTextTheme;
|
||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
@ -89,6 +92,7 @@ class _TimePickerFragmentContext {
|
|||||||
final TextStyle inactiveStyle;
|
final TextStyle inactiveStyle;
|
||||||
final ValueChanged<TimeOfDay> onTimeChange;
|
final ValueChanged<TimeOfDay> onTimeChange;
|
||||||
final ValueChanged<_TimePickerMode> onModeChange;
|
final ValueChanged<_TimePickerMode> onModeChange;
|
||||||
|
final TargetPlatform targetPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains the [widget] and layout properties of an atom of time information,
|
/// Contains the [widget] and layout properties of an atom of time information,
|
||||||
@ -183,9 +187,30 @@ class _DayPeriodControl extends StatelessWidget {
|
|||||||
|
|
||||||
final _TimePickerFragmentContext fragmentContext;
|
final _TimePickerFragmentContext fragmentContext;
|
||||||
|
|
||||||
void _handleChangeDayPeriod() {
|
void _togglePeriod() {
|
||||||
final int newHour = (fragmentContext.selectedTime.hour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
|
final int newHour = (fragmentContext.selectedTime.hour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
|
||||||
fragmentContext.onTimeChange(fragmentContext.selectedTime.replacing(hour: newHour));
|
final TimeOfDay newTime = fragmentContext.selectedTime.replacing(hour: newHour);
|
||||||
|
fragmentContext.onTimeChange(newTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setAm(BuildContext context) {
|
||||||
|
if (fragmentContext.selectedTime.period == DayPeriod.am) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fragmentContext.targetPlatform == TargetPlatform.android) {
|
||||||
|
_announceToAccessibility(context, MaterialLocalizations.of(context).anteMeridiemAbbreviation);
|
||||||
|
}
|
||||||
|
_togglePeriod();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setPm(BuildContext context) {
|
||||||
|
if (fragmentContext.selectedTime.period == DayPeriod.pm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fragmentContext.targetPlatform == TargetPlatform.android) {
|
||||||
|
_announceToAccessibility(context, MaterialLocalizations.of(context).postMeridiemAbbreviation);
|
||||||
|
}
|
||||||
|
_togglePeriod();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -195,25 +220,47 @@ class _DayPeriodControl extends StatelessWidget {
|
|||||||
final TimeOfDay selectedTime = fragmentContext.selectedTime;
|
final TimeOfDay selectedTime = fragmentContext.selectedTime;
|
||||||
final Color activeColor = fragmentContext.activeColor;
|
final Color activeColor = fragmentContext.activeColor;
|
||||||
final Color inactiveColor = fragmentContext.inactiveColor;
|
final Color inactiveColor = fragmentContext.inactiveColor;
|
||||||
|
final bool amSelected = selectedTime.period == DayPeriod.am;
|
||||||
final TextStyle amStyle = headerTextTheme.subhead.copyWith(
|
final TextStyle amStyle = headerTextTheme.subhead.copyWith(
|
||||||
color: selectedTime.period == DayPeriod.am ? activeColor: inactiveColor
|
color: amSelected ? activeColor: inactiveColor
|
||||||
);
|
);
|
||||||
final TextStyle pmStyle = headerTextTheme.subhead.copyWith(
|
final TextStyle pmStyle = headerTextTheme.subhead.copyWith(
|
||||||
color: selectedTime.period == DayPeriod.pm ? activeColor: inactiveColor
|
color: !amSelected ? activeColor: inactiveColor
|
||||||
);
|
);
|
||||||
|
|
||||||
return new GestureDetector(
|
return new Column(
|
||||||
onTap: Feedback.wrapForTap(_handleChangeDayPeriod, context),
|
mainAxisSize: MainAxisSize.min,
|
||||||
behavior: HitTestBehavior.opaque,
|
children: <Widget>[
|
||||||
child: new Column(
|
new GestureDetector(
|
||||||
mainAxisSize: MainAxisSize.min,
|
excludeFromSemantics: true,
|
||||||
children: <Widget>[
|
onTap: Feedback.wrapForTap(() {
|
||||||
new Text(materialLocalizations.anteMeridiemAbbreviation, style: amStyle),
|
_setAm(context);
|
||||||
const SizedBox(width: 0.0, height: 4.0), // Vertical spacer
|
}, context),
|
||||||
new Text(materialLocalizations.postMeridiemAbbreviation, style: pmStyle),
|
behavior: HitTestBehavior.opaque,
|
||||||
],
|
child: new Semantics(
|
||||||
),
|
selected: amSelected,
|
||||||
|
onTap: () {
|
||||||
|
_setAm(context);
|
||||||
|
},
|
||||||
|
child: new Text(materialLocalizations.anteMeridiemAbbreviation, style: amStyle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 0.0, height: 4.0), // Vertical spacer
|
||||||
|
new GestureDetector(
|
||||||
|
excludeFromSemantics: true,
|
||||||
|
onTap: Feedback.wrapForTap(() {
|
||||||
|
_setPm(context);
|
||||||
|
}, context),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: new Semantics(
|
||||||
|
selected: !amSelected,
|
||||||
|
onTap: () {
|
||||||
|
_setPm(context);
|
||||||
|
},
|
||||||
|
child: new Text(materialLocalizations.postMeridiemAbbreviation, style: pmStyle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,13 +282,18 @@ class _HourControl extends StatelessWidget {
|
|||||||
final TextStyle hourStyle = fragmentContext.mode == _TimePickerMode.hour
|
final TextStyle hourStyle = fragmentContext.mode == _TimePickerMode.hour
|
||||||
? fragmentContext.activeStyle
|
? fragmentContext.activeStyle
|
||||||
: fragmentContext.inactiveStyle;
|
: fragmentContext.inactiveStyle;
|
||||||
|
final String formattedHour = localizations.formatHour(
|
||||||
|
fragmentContext.selectedTime,
|
||||||
|
alwaysUse24HourFormat: MediaQuery.of(context).alwaysUse24HourFormat,
|
||||||
|
);
|
||||||
|
|
||||||
return new GestureDetector(
|
return new GestureDetector(
|
||||||
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context),
|
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context),
|
||||||
child: new Text(localizations.formatHour(
|
child: new Semantics(
|
||||||
fragmentContext.selectedTime,
|
selected: fragmentContext.mode == _TimePickerMode.hour,
|
||||||
alwaysUse24HourFormat: MediaQuery.of(context).alwaysUse24HourFormat,
|
hint: localizations.timePickerHourModeAnnouncement,
|
||||||
), style: hourStyle),
|
child: new Text(formattedHour, style: hourStyle),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,7 +310,9 @@ class _StringFragment extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new Text(value, style: fragmentContext.inactiveStyle);
|
return new ExcludeSemantics(
|
||||||
|
child: new Text(value, style: fragmentContext.inactiveStyle),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +335,11 @@ class _MinuteControl extends StatelessWidget {
|
|||||||
|
|
||||||
return new GestureDetector(
|
return new GestureDetector(
|
||||||
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context),
|
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context),
|
||||||
child: new Text(localizations.formatMinute(fragmentContext.selectedTime), style: minuteStyle),
|
child: new Semantics(
|
||||||
|
selected: fragmentContext.mode == _TimePickerMode.minute,
|
||||||
|
hint: localizations.timePickerMinuteModeAnnouncement,
|
||||||
|
child: new Text(localizations.formatMinute(fragmentContext.selectedTime), style: minuteStyle),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -636,6 +694,7 @@ class _TimePickerHeader extends StatelessWidget {
|
|||||||
inactiveStyle: baseHeaderStyle.copyWith(color: inactiveColor),
|
inactiveStyle: baseHeaderStyle.copyWith(color: inactiveColor),
|
||||||
onTimeChange: onChanged,
|
onTimeChange: onChanged,
|
||||||
onModeChange: _handleChangeMode,
|
onModeChange: _handleChangeMode,
|
||||||
|
targetPlatform: themeData.platform,
|
||||||
);
|
);
|
||||||
|
|
||||||
final _TimePickerHeaderFormat format = _buildHeaderFormat(timeOfDayFormat, fragmentContext);
|
final _TimePickerHeaderFormat format = _buildHeaderFormat(timeOfDayFormat, fragmentContext);
|
||||||
@ -661,26 +720,28 @@ class _TimePickerHeader extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TextPainter> _buildPainters(TextTheme textTheme, List<String> labels) {
|
|
||||||
final TextStyle style = textTheme.subhead;
|
|
||||||
final List<TextPainter> painters = new List<TextPainter>(labels.length);
|
|
||||||
for (int i = 0; i < painters.length; ++i) {
|
|
||||||
final String label = labels[i];
|
|
||||||
// TODO(abarth): Handle textScaleFactor.
|
|
||||||
// https://github.com/flutter/flutter/issues/5939
|
|
||||||
painters[i] = new TextPainter(
|
|
||||||
text: new TextSpan(style: style, text: label),
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
)..layout();
|
|
||||||
}
|
|
||||||
return painters;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum _DialRing {
|
enum _DialRing {
|
||||||
outer,
|
outer,
|
||||||
inner,
|
inner,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _TappableLabel {
|
||||||
|
_TappableLabel({
|
||||||
|
@required this.value,
|
||||||
|
@required this.painter,
|
||||||
|
@required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The value this label is displaying.
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
/// Paints the text of the label.
|
||||||
|
final TextPainter painter;
|
||||||
|
|
||||||
|
/// Called when a tap gesture is detected on the label.
|
||||||
|
final VoidCallback onTap;
|
||||||
|
}
|
||||||
|
|
||||||
class _DialPainter extends CustomPainter {
|
class _DialPainter extends CustomPainter {
|
||||||
const _DialPainter({
|
const _DialPainter({
|
||||||
@required this.primaryOuterLabels,
|
@required this.primaryOuterLabels,
|
||||||
@ -691,16 +752,20 @@ class _DialPainter extends CustomPainter {
|
|||||||
@required this.accentColor,
|
@required this.accentColor,
|
||||||
@required this.theta,
|
@required this.theta,
|
||||||
@required this.activeRing,
|
@required this.activeRing,
|
||||||
|
@required this.textDirection,
|
||||||
|
@required this.selectedValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<TextPainter> primaryOuterLabels;
|
final List<_TappableLabel> primaryOuterLabels;
|
||||||
final List<TextPainter> primaryInnerLabels;
|
final List<_TappableLabel> primaryInnerLabels;
|
||||||
final List<TextPainter> secondaryOuterLabels;
|
final List<_TappableLabel> secondaryOuterLabels;
|
||||||
final List<TextPainter> secondaryInnerLabels;
|
final List<_TappableLabel> secondaryInnerLabels;
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
final Color accentColor;
|
final Color accentColor;
|
||||||
final double theta;
|
final double theta;
|
||||||
final _DialRing activeRing;
|
final _DialRing activeRing;
|
||||||
|
final TextDirection textDirection;
|
||||||
|
final int selectedValue;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
@ -726,15 +791,16 @@ class _DialPainter extends CustomPainter {
|
|||||||
-labelRadius * math.sin(theta));
|
-labelRadius * math.sin(theta));
|
||||||
}
|
}
|
||||||
|
|
||||||
void paintLabels(List<TextPainter> labels, _DialRing ring) {
|
void paintLabels(List<_TappableLabel> labels, _DialRing ring) {
|
||||||
if (labels == null)
|
if (labels == null)
|
||||||
return;
|
return;
|
||||||
final double labelThetaIncrement = -_kTwoPi / labels.length;
|
final double labelThetaIncrement = -_kTwoPi / labels.length;
|
||||||
double labelTheta = math.PI / 2.0;
|
double labelTheta = math.PI / 2.0;
|
||||||
|
|
||||||
for (TextPainter label in labels) {
|
for (_TappableLabel label in labels) {
|
||||||
final Offset labelOffset = new Offset(-label.width / 2.0, -label.height / 2.0);
|
final TextPainter labelPainter = label.painter;
|
||||||
label.paint(canvas, getOffsetForTheta(labelTheta, ring) + labelOffset);
|
final Offset labelOffset = new Offset(-labelPainter.width / 2.0, -labelPainter.height / 2.0);
|
||||||
|
labelPainter.paint(canvas, getOffsetForTheta(labelTheta, ring) + labelOffset);
|
||||||
labelTheta += labelThetaIncrement;
|
labelTheta += labelThetaIncrement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -762,6 +828,80 @@ class _DialPainter extends CustomPainter {
|
|||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const double _kSemanticNodeSizeScale = 1.5;
|
||||||
|
|
||||||
|
@override
|
||||||
|
SemanticsBuilderCallback get semanticsBuilder => _buildSemantics;
|
||||||
|
|
||||||
|
/// Creates semantics nodes for the hour/minute labels painted on the dial.
|
||||||
|
///
|
||||||
|
/// The nodes are positioned on top of the text and their size is
|
||||||
|
/// [_kSemanticNodeSizeScale] bigger than those of the text boxes to provide
|
||||||
|
/// bigger tap area.
|
||||||
|
List<CustomPainterSemantics> _buildSemantics(Size size) {
|
||||||
|
final double radius = size.shortestSide / 2.0;
|
||||||
|
final Offset center = new Offset(size.width / 2.0, size.height / 2.0);
|
||||||
|
const double labelPadding = 24.0;
|
||||||
|
final double outerLabelRadius = radius - labelPadding;
|
||||||
|
final double innerLabelRadius = radius - labelPadding * 2.5;
|
||||||
|
|
||||||
|
Offset getOffsetForTheta(double theta, _DialRing ring) {
|
||||||
|
double labelRadius;
|
||||||
|
switch (ring) {
|
||||||
|
case _DialRing.outer:
|
||||||
|
labelRadius = outerLabelRadius;
|
||||||
|
break;
|
||||||
|
case _DialRing.inner:
|
||||||
|
labelRadius = innerLabelRadius;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return center + new Offset(labelRadius * math.cos(theta),
|
||||||
|
-labelRadius * math.sin(theta));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<CustomPainterSemantics> nodes = <CustomPainterSemantics>[];
|
||||||
|
|
||||||
|
void paintLabels(List<_TappableLabel> labels, _DialRing ring) {
|
||||||
|
if (labels == null)
|
||||||
|
return;
|
||||||
|
final double labelThetaIncrement = -_kTwoPi / labels.length;
|
||||||
|
double labelTheta = math.PI / 2.0;
|
||||||
|
|
||||||
|
for (_TappableLabel label in labels) {
|
||||||
|
final TextPainter labelPainter = label.painter;
|
||||||
|
final double width = labelPainter.width * _kSemanticNodeSizeScale;
|
||||||
|
final double height = labelPainter.height * _kSemanticNodeSizeScale;
|
||||||
|
final Offset nodeOffset = getOffsetForTheta(labelTheta, ring) + new Offset(-width / 2.0, -height / 2.0);
|
||||||
|
final CustomPainterSemantics node = new CustomPainterSemantics(
|
||||||
|
rect: new Rect.fromLTRB(
|
||||||
|
nodeOffset.dx,
|
||||||
|
nodeOffset.dy,
|
||||||
|
nodeOffset.dx + width,
|
||||||
|
nodeOffset.dy + height
|
||||||
|
),
|
||||||
|
properties: new SemanticsProperties(
|
||||||
|
selected: label.value == selectedValue,
|
||||||
|
label: labelPainter.text.text,
|
||||||
|
textDirection: textDirection,
|
||||||
|
onTap: label.onTap,
|
||||||
|
),
|
||||||
|
tags: new Set<SemanticsTag>.from(const <SemanticsTag>[
|
||||||
|
// Used by tests to find this node.
|
||||||
|
const SemanticsTag('dial-label'),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
nodes.add(node);
|
||||||
|
labelTheta += labelThetaIncrement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paintLabels(primaryOuterLabels, _DialRing.outer);
|
||||||
|
paintLabels(primaryInnerLabels, _DialRing.inner);
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(_DialPainter oldPainter) {
|
bool shouldRepaint(_DialPainter oldPainter) {
|
||||||
return oldPainter.primaryOuterLabels != primaryOuterLabels
|
return oldPainter.primaryOuterLabels != primaryOuterLabels
|
||||||
@ -796,6 +936,7 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_updateDialRingFromWidget();
|
||||||
_thetaController = new AnimationController(
|
_thetaController = new AnimationController(
|
||||||
duration: _kDialAnimateDuration,
|
duration: _kDialAnimateDuration,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
@ -827,8 +968,14 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
if (!_dragging)
|
if (!_dragging)
|
||||||
_animateTo(_getThetaForTime(widget.selectedTime));
|
_animateTo(_getThetaForTime(widget.selectedTime));
|
||||||
}
|
}
|
||||||
if (widget.mode == _TimePickerMode.hour && widget.use24HourDials && widget.selectedTime.period == DayPeriod.am) {
|
_updateDialRingFromWidget();
|
||||||
_activeRing = _DialRing.inner;
|
}
|
||||||
|
|
||||||
|
void _updateDialRingFromWidget() {
|
||||||
|
if (widget.mode == _TimePickerMode.hour && widget.use24HourDials) {
|
||||||
|
_activeRing = widget.selectedTime.hour >= 1 && widget.selectedTime.hour <= 12
|
||||||
|
? _DialRing.inner
|
||||||
|
: _DialRing.outer;
|
||||||
} else {
|
} else {
|
||||||
_activeRing = _DialRing.outer;
|
_activeRing = _DialRing.outer;
|
||||||
}
|
}
|
||||||
@ -862,9 +1009,9 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double _getThetaForTime(TimeOfDay time) {
|
double _getThetaForTime(TimeOfDay time) {
|
||||||
final double fraction = (widget.mode == _TimePickerMode.hour) ?
|
final double fraction = widget.mode == _TimePickerMode.hour
|
||||||
(time.hour / TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerPeriod :
|
? (time.hour / TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerPeriod
|
||||||
(time.minute / TimeOfDay.minutesPerHour) % TimeOfDay.minutesPerHour;
|
: (time.minute / TimeOfDay.minutesPerHour) % TimeOfDay.minutesPerHour;
|
||||||
return (math.PI / 2.0 - fraction * _kTwoPi) % _kTwoPi;
|
return (math.PI / 2.0 - fraction * _kTwoPi) % _kTwoPi;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -890,12 +1037,13 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _notifyOnChangedIfNeeded() {
|
TimeOfDay _notifyOnChangedIfNeeded() {
|
||||||
if (widget.onChanged == null)
|
|
||||||
return;
|
|
||||||
final TimeOfDay current = _getTimeForTheta(_theta.value);
|
final TimeOfDay current = _getTimeForTheta(_theta.value);
|
||||||
|
if (widget.onChanged == null)
|
||||||
|
return current;
|
||||||
if (current != widget.selectedTime)
|
if (current != widget.selectedTime)
|
||||||
widget.onChanged(current);
|
widget.onChanged(current);
|
||||||
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateThetaForPan() {
|
void _updateThetaForPan() {
|
||||||
@ -944,6 +1092,63 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
_animateTo(_getThetaForTime(widget.selectedTime));
|
_animateTo(_getThetaForTime(widget.selectedTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleTapUp(TapUpDetails details) {
|
||||||
|
final RenderBox box = context.findRenderObject();
|
||||||
|
_position = box.globalToLocal(details.globalPosition);
|
||||||
|
_center = box.size.center(Offset.zero);
|
||||||
|
_updateThetaForPan();
|
||||||
|
final TimeOfDay newTime = _notifyOnChangedIfNeeded();
|
||||||
|
if (widget.mode == _TimePickerMode.hour) {
|
||||||
|
if (widget.use24HourDials) {
|
||||||
|
_announceToAccessibility(context, localizations.formatDecimal(newTime.hour));
|
||||||
|
} else {
|
||||||
|
_announceToAccessibility(context, localizations.formatDecimal(newTime.hourOfPeriod));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_announceToAccessibility(context, localizations.formatDecimal(newTime.minute));
|
||||||
|
}
|
||||||
|
_animateTo(_getThetaForTime(_getTimeForTheta(_theta.value)));
|
||||||
|
_dragging = false;
|
||||||
|
_position = null;
|
||||||
|
_center = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectHour(int hour) {
|
||||||
|
_announceToAccessibility(context, localizations.formatDecimal(hour));
|
||||||
|
TimeOfDay time;
|
||||||
|
if (widget.mode == _TimePickerMode.hour && widget.use24HourDials) {
|
||||||
|
_activeRing = hour >= 1 && hour <= 12
|
||||||
|
? _DialRing.inner
|
||||||
|
: _DialRing.outer;
|
||||||
|
time = new TimeOfDay(hour: hour, minute: widget.selectedTime.minute);
|
||||||
|
} else {
|
||||||
|
_activeRing = _DialRing.outer;
|
||||||
|
if (widget.selectedTime.period == DayPeriod.am) {
|
||||||
|
time = new TimeOfDay(hour: hour, minute: widget.selectedTime.minute);
|
||||||
|
} else {
|
||||||
|
time = new TimeOfDay(hour: hour + TimeOfDay.hoursPerPeriod, minute: widget.selectedTime.minute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final double angle = _getThetaForTime(time);
|
||||||
|
_thetaTween
|
||||||
|
..begin = angle
|
||||||
|
..end = angle;
|
||||||
|
_notifyOnChangedIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectMinute(int minute) {
|
||||||
|
_announceToAccessibility(context, localizations.formatDecimal(minute));
|
||||||
|
final TimeOfDay time = new TimeOfDay(
|
||||||
|
hour: widget.selectedTime.hour,
|
||||||
|
minute: minute,
|
||||||
|
);
|
||||||
|
final double angle = _getThetaForTime(time);
|
||||||
|
_thetaTween
|
||||||
|
..begin = angle
|
||||||
|
..end = angle;
|
||||||
|
_notifyOnChangedIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
static const List<TimeOfDay> _amHours = const <TimeOfDay>[
|
static const List<TimeOfDay> _amHours = const <TimeOfDay>[
|
||||||
const TimeOfDay(hour: 12, minute: 0),
|
const TimeOfDay(hour: 12, minute: 0),
|
||||||
const TimeOfDay(hour: 1, minute: 0),
|
const TimeOfDay(hour: 1, minute: 0),
|
||||||
@ -974,31 +1179,66 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
const TimeOfDay(hour: 23, minute: 0),
|
const TimeOfDay(hour: 23, minute: 0),
|
||||||
];
|
];
|
||||||
|
|
||||||
List<TextPainter> _build24HourInnerRing(TextTheme textTheme) {
|
_TappableLabel _buildTappableLabel(TextTheme textTheme, int value, String label, VoidCallback onTap) {
|
||||||
return _buildPainters(textTheme, _amHours
|
final TextStyle style = textTheme.subhead;
|
||||||
.map((TimeOfDay timeOfDay) {
|
// TODO(abarth): Handle textScaleFactor.
|
||||||
return localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat);
|
// https://github.com/flutter/flutter/issues/5939
|
||||||
})
|
return new _TappableLabel(
|
||||||
.toList());
|
value: value,
|
||||||
|
painter: new TextPainter(
|
||||||
|
text: new TextSpan(style: style, text: label),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout(),
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TextPainter> _build24HourOuterRing(TextTheme textTheme) {
|
List<_TappableLabel> _build24HourInnerRing(TextTheme textTheme) {
|
||||||
return _buildPainters(textTheme, _pmHours
|
final List<_TappableLabel> labels = <_TappableLabel>[];
|
||||||
.map((TimeOfDay timeOfDay) {
|
for (TimeOfDay timeOfDay in _amHours) {
|
||||||
return localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat);
|
labels.add(_buildTappableLabel(
|
||||||
})
|
textTheme,
|
||||||
.toList());
|
timeOfDay.hour,
|
||||||
|
localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
|
||||||
|
() {
|
||||||
|
_selectHour(timeOfDay.hour);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TextPainter> _build12HourOuterRing(TextTheme textTheme) {
|
List<_TappableLabel> _build24HourOuterRing(TextTheme textTheme) {
|
||||||
return _buildPainters(textTheme, _amHours
|
final List<_TappableLabel> labels = <_TappableLabel>[];
|
||||||
.map((TimeOfDay timeOfDay) {
|
for (TimeOfDay timeOfDay in _pmHours) {
|
||||||
return localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat);
|
labels.add(_buildTappableLabel(
|
||||||
})
|
textTheme,
|
||||||
.toList());
|
timeOfDay.hour,
|
||||||
|
localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
|
||||||
|
() {
|
||||||
|
_selectHour(timeOfDay.hour);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TextPainter> _buildMinutes(TextTheme textTheme) {
|
List<_TappableLabel> _build12HourOuterRing(TextTheme textTheme) {
|
||||||
|
final List<_TappableLabel> labels = <_TappableLabel>[];
|
||||||
|
for (TimeOfDay timeOfDay in _amHours) {
|
||||||
|
labels.add(_buildTappableLabel(
|
||||||
|
textTheme,
|
||||||
|
timeOfDay.hour,
|
||||||
|
localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
|
||||||
|
() {
|
||||||
|
_selectHour(timeOfDay.hour);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_TappableLabel> _buildMinutes(TextTheme textTheme) {
|
||||||
const List<TimeOfDay> _minuteMarkerValues = const <TimeOfDay>[
|
const List<TimeOfDay> _minuteMarkerValues = const <TimeOfDay>[
|
||||||
const TimeOfDay(hour: 0, minute: 0),
|
const TimeOfDay(hour: 0, minute: 0),
|
||||||
const TimeOfDay(hour: 0, minute: 5),
|
const TimeOfDay(hour: 0, minute: 5),
|
||||||
@ -1014,7 +1254,18 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
const TimeOfDay(hour: 0, minute: 55),
|
const TimeOfDay(hour: 0, minute: 55),
|
||||||
];
|
];
|
||||||
|
|
||||||
return _buildPainters(textTheme, _minuteMarkerValues.map(localizations.formatMinute).toList());
|
final List<_TappableLabel> labels = <_TappableLabel>[];
|
||||||
|
for (TimeOfDay timeOfDay in _minuteMarkerValues) {
|
||||||
|
labels.add(_buildTappableLabel(
|
||||||
|
textTheme,
|
||||||
|
timeOfDay.minute,
|
||||||
|
localizations.formatMinute(timeOfDay),
|
||||||
|
() {
|
||||||
|
_selectMinute(timeOfDay.minute);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1030,23 +1281,27 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
List<TextPainter> primaryOuterLabels;
|
List<_TappableLabel> primaryOuterLabels;
|
||||||
List<TextPainter> primaryInnerLabels;
|
List<_TappableLabel> primaryInnerLabels;
|
||||||
List<TextPainter> secondaryOuterLabels;
|
List<_TappableLabel> secondaryOuterLabels;
|
||||||
List<TextPainter> secondaryInnerLabels;
|
List<_TappableLabel> secondaryInnerLabels;
|
||||||
|
int selectedDialValue;
|
||||||
switch (widget.mode) {
|
switch (widget.mode) {
|
||||||
case _TimePickerMode.hour:
|
case _TimePickerMode.hour:
|
||||||
if (widget.use24HourDials) {
|
if (widget.use24HourDials) {
|
||||||
|
selectedDialValue = widget.selectedTime.hour;
|
||||||
primaryOuterLabels = _build24HourOuterRing(theme.textTheme);
|
primaryOuterLabels = _build24HourOuterRing(theme.textTheme);
|
||||||
secondaryOuterLabels = _build24HourOuterRing(theme.accentTextTheme);
|
secondaryOuterLabels = _build24HourOuterRing(theme.accentTextTheme);
|
||||||
primaryInnerLabels = _build24HourInnerRing(theme.textTheme);
|
primaryInnerLabels = _build24HourInnerRing(theme.textTheme);
|
||||||
secondaryInnerLabels = _build24HourInnerRing(theme.accentTextTheme);
|
secondaryInnerLabels = _build24HourInnerRing(theme.accentTextTheme);
|
||||||
} else {
|
} else {
|
||||||
|
selectedDialValue = widget.selectedTime.hourOfPeriod;
|
||||||
primaryOuterLabels = _build12HourOuterRing(theme.textTheme);
|
primaryOuterLabels = _build12HourOuterRing(theme.textTheme);
|
||||||
secondaryOuterLabels = _build12HourOuterRing(theme.accentTextTheme);
|
secondaryOuterLabels = _build12HourOuterRing(theme.accentTextTheme);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case _TimePickerMode.minute:
|
case _TimePickerMode.minute:
|
||||||
|
selectedDialValue = widget.selectedTime.minute;
|
||||||
primaryOuterLabels = _buildMinutes(theme.textTheme);
|
primaryOuterLabels = _buildMinutes(theme.textTheme);
|
||||||
primaryInnerLabels = null;
|
primaryInnerLabels = null;
|
||||||
secondaryOuterLabels = _buildMinutes(theme.accentTextTheme);
|
secondaryOuterLabels = _buildMinutes(theme.accentTextTheme);
|
||||||
@ -1055,12 +1310,15 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new GestureDetector(
|
return new GestureDetector(
|
||||||
|
excludeFromSemantics: true,
|
||||||
onPanStart: _handlePanStart,
|
onPanStart: _handlePanStart,
|
||||||
onPanUpdate: _handlePanUpdate,
|
onPanUpdate: _handlePanUpdate,
|
||||||
onPanEnd: _handlePanEnd,
|
onPanEnd: _handlePanEnd,
|
||||||
|
onTapUp: _handleTapUp,
|
||||||
child: new CustomPaint(
|
child: new CustomPaint(
|
||||||
key: const ValueKey<String>('time-picker-dial'), // used for testing.
|
key: const ValueKey<String>('time-picker-dial'),
|
||||||
painter: new _DialPainter(
|
painter: new _DialPainter(
|
||||||
|
selectedValue: selectedDialValue,
|
||||||
primaryOuterLabels: primaryOuterLabels,
|
primaryOuterLabels: primaryOuterLabels,
|
||||||
primaryInnerLabels: primaryInnerLabels,
|
primaryInnerLabels: primaryInnerLabels,
|
||||||
secondaryOuterLabels: secondaryOuterLabels,
|
secondaryOuterLabels: secondaryOuterLabels,
|
||||||
@ -1069,7 +1327,8 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
|||||||
accentColor: themeData.accentColor,
|
accentColor: themeData.accentColor,
|
||||||
theta: _theta.value,
|
theta: _theta.value,
|
||||||
activeRing: _activeRing,
|
activeRing: _activeRing,
|
||||||
)
|
textDirection: Directionality.of(context),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1105,9 +1364,19 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
|||||||
_selectedTime = widget.initialTime;
|
_selectedTime = widget.initialTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
localizations = MaterialLocalizations.of(context);
|
||||||
|
_announceInitialTimeOnce();
|
||||||
|
_announceModeOnce();
|
||||||
|
}
|
||||||
|
|
||||||
_TimePickerMode _mode = _TimePickerMode.hour;
|
_TimePickerMode _mode = _TimePickerMode.hour;
|
||||||
|
_TimePickerMode _lastModeAnnounced;
|
||||||
TimeOfDay _selectedTime;
|
TimeOfDay _selectedTime;
|
||||||
Timer _vibrateTimer;
|
Timer _vibrateTimer;
|
||||||
|
MaterialLocalizations localizations;
|
||||||
|
|
||||||
void _vibrate() {
|
void _vibrate() {
|
||||||
switch (Theme.of(context).platform) {
|
switch (Theme.of(context).platform) {
|
||||||
@ -1128,9 +1397,42 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
|||||||
_vibrate();
|
_vibrate();
|
||||||
setState(() {
|
setState(() {
|
||||||
_mode = mode;
|
_mode = mode;
|
||||||
|
_announceModeOnce();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _announceModeOnce() {
|
||||||
|
if (_lastModeAnnounced == _mode) {
|
||||||
|
// Already announced it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_mode) {
|
||||||
|
case _TimePickerMode.hour:
|
||||||
|
_announceToAccessibility(context, localizations.timePickerHourModeAnnouncement);
|
||||||
|
break;
|
||||||
|
case _TimePickerMode.minute:
|
||||||
|
_announceToAccessibility(context, localizations.timePickerMinuteModeAnnouncement);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_lastModeAnnounced = _mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _announcedInitialTime = false;
|
||||||
|
|
||||||
|
void _announceInitialTimeOnce() {
|
||||||
|
if (_announcedInitialTime)
|
||||||
|
return;
|
||||||
|
|
||||||
|
final MediaQueryData media = MediaQuery.of(context);
|
||||||
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||||
|
_announceToAccessibility(
|
||||||
|
context,
|
||||||
|
localizations.formatTimeOfDay(widget.initialTime, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
|
||||||
|
);
|
||||||
|
_announcedInitialTime = true;
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTimeChanged(TimeOfDay value) {
|
void _handleTimeChanged(TimeOfDay value) {
|
||||||
_vibrate();
|
_vibrate();
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -1149,7 +1451,6 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(debugCheckHasMediaQuery(context));
|
assert(debugCheckHasMediaQuery(context));
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
final MediaQueryData media = MediaQuery.of(context);
|
final MediaQueryData media = MediaQuery.of(context);
|
||||||
final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat(alwaysUse24HourFormat: media.alwaysUse24HourFormat);
|
final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat(alwaysUse24HourFormat: media.alwaysUse24HourFormat);
|
||||||
|
|
||||||
@ -1270,8 +1571,13 @@ Future<TimeOfDay> showTimePicker({
|
|||||||
}) async {
|
}) async {
|
||||||
assert(context != null);
|
assert(context != null);
|
||||||
assert(initialTime != null);
|
assert(initialTime != null);
|
||||||
|
|
||||||
return await showDialog<TimeOfDay>(
|
return await showDialog<TimeOfDay>(
|
||||||
context: context,
|
context: context,
|
||||||
child: new _TimePickerDialog(initialTime: initialTime),
|
child: new _TimePickerDialog(initialTime: initialTime),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _announceToAccessibility(BuildContext context, String message) {
|
||||||
|
SemanticsService.announce(message, Directionality.of(context));
|
||||||
|
}
|
||||||
|
@ -432,7 +432,8 @@ class RenderCustomPaint extends RenderProxyBox {
|
|||||||
// Check if we need to rebuild semantics.
|
// Check if we need to rebuild semantics.
|
||||||
if (newPainter == null) {
|
if (newPainter == null) {
|
||||||
assert(oldPainter != null); // We should be called only for changes.
|
assert(oldPainter != null); // We should be called only for changes.
|
||||||
markNeedsSemanticsUpdate();
|
if (attached)
|
||||||
|
markNeedsSemanticsUpdate();
|
||||||
} else if (oldPainter == null ||
|
} else if (oldPainter == null ||
|
||||||
newPainter.runtimeType != oldPainter.runtimeType ||
|
newPainter.runtimeType != oldPainter.runtimeType ||
|
||||||
newPainter.shouldRebuildSemantics(oldPainter)) {
|
newPainter.shouldRebuildSemantics(oldPainter)) {
|
||||||
|
@ -822,7 +822,8 @@ class PipelineOwner {
|
|||||||
/// objects for a given [PipelineOwner] are closed, the [PipelineOwner] stops
|
/// objects for a given [PipelineOwner] are closed, the [PipelineOwner] stops
|
||||||
/// maintaining the semantics tree.
|
/// maintaining the semantics tree.
|
||||||
SemanticsHandle ensureSemantics({ VoidCallback listener }) {
|
SemanticsHandle ensureSemantics({ VoidCallback listener }) {
|
||||||
if (_outstandingSemanticsHandle++ == 0) {
|
_outstandingSemanticsHandle += 1;
|
||||||
|
if (_outstandingSemanticsHandle == 1) {
|
||||||
assert(_semanticsOwner == null);
|
assert(_semanticsOwner == null);
|
||||||
_semanticsOwner = new SemanticsOwner();
|
_semanticsOwner = new SemanticsOwner();
|
||||||
if (onSemanticsOwnerCreated != null)
|
if (onSemanticsOwnerCreated != null)
|
||||||
@ -833,7 +834,8 @@ class PipelineOwner {
|
|||||||
|
|
||||||
void _didDisposeSemanticsHandle() {
|
void _didDisposeSemanticsHandle() {
|
||||||
assert(_semanticsOwner != null);
|
assert(_semanticsOwner != null);
|
||||||
if (--_outstandingSemanticsHandle == 0) {
|
_outstandingSemanticsHandle -= 1;
|
||||||
|
if (_outstandingSemanticsHandle == 0) {
|
||||||
_semanticsOwner.dispose();
|
_semanticsOwner.dispose();
|
||||||
_semanticsOwner = null;
|
_semanticsOwner = null;
|
||||||
if (onSemanticsOwnerDisposed != null)
|
if (onSemanticsOwnerDisposed != null)
|
||||||
|
@ -2583,8 +2583,8 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
|
|||||||
/// purposes.
|
/// purposes.
|
||||||
///
|
///
|
||||||
/// If this tag is used, the first "outer" semantics node is the regular node
|
/// If this tag is used, the first "outer" semantics node is the regular node
|
||||||
/// of this object. The second "inner" node is introduces as a child to that
|
/// of this object. The second "inner" node is introduced as a child to that
|
||||||
/// node. All scrollable children are now a child of the inner node, which has
|
/// node. All scrollable children become children of the inner node, which has
|
||||||
/// the semantic scrolling logic enabled. All children that have been
|
/// the semantic scrolling logic enabled. All children that have been
|
||||||
/// excluded from scrolling with [excludeFromScrolling] are turned into
|
/// excluded from scrolling with [excludeFromScrolling] are turned into
|
||||||
/// children of the outer node.
|
/// children of the outer node.
|
||||||
@ -3204,6 +3204,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||||
|
super.describeSemanticsConfiguration(config);
|
||||||
config.isSemanticBoundary = container;
|
config.isSemanticBoundary = container;
|
||||||
config.explicitChildNodes = explicitChildNodes;
|
config.explicitChildNodes = explicitChildNodes;
|
||||||
|
|
||||||
|
@ -325,7 +325,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
|||||||
/// Provides a brief textual description of the result of an action performed
|
/// Provides a brief textual description of the result of an action performed
|
||||||
/// on the widget.
|
/// on the widget.
|
||||||
///
|
///
|
||||||
/// If a hint is provided, there must either by an ambient [Directionality]
|
/// If a hint is provided, there must either be an ambient [Directionality]
|
||||||
/// or an explicit [textDirection] should be provided.
|
/// or an explicit [textDirection] should be provided.
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
@ -889,7 +889,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration();
|
static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration();
|
||||||
|
|
||||||
/// Reconfigures the properties of this object to describe the configuration
|
/// Reconfigures the properties of this object to describe the configuration
|
||||||
/// provided in the `config` argument and the children listen in the
|
/// provided in the `config` argument and the children listed in the
|
||||||
/// `childrenInInversePaintOrder` argument.
|
/// `childrenInInversePaintOrder` argument.
|
||||||
///
|
///
|
||||||
/// The arguments may be null; this represents an empty configuration (all
|
/// The arguments may be null; this represents an empty configuration (all
|
||||||
@ -899,7 +899,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
|||||||
/// list is used as-is and should therefore not be changed after this call.
|
/// list is used as-is and should therefore not be changed after this call.
|
||||||
void updateWith({
|
void updateWith({
|
||||||
@required SemanticsConfiguration config,
|
@required SemanticsConfiguration config,
|
||||||
@required List<SemanticsNode> childrenInInversePaintOrder,
|
List<SemanticsNode> childrenInInversePaintOrder,
|
||||||
}) {
|
}) {
|
||||||
config ??= _kEmptyConfig;
|
config ??= _kEmptyConfig;
|
||||||
if (_isDifferentFromCurrentSemanticAnnotation(config))
|
if (_isDifferentFromCurrentSemanticAnnotation(config))
|
||||||
@ -1338,7 +1338,7 @@ class SemanticsConfiguration {
|
|||||||
/// create semantic boundaries that are either writable or not for children.
|
/// create semantic boundaries that are either writable or not for children.
|
||||||
bool explicitChildNodes = false;
|
bool explicitChildNodes = false;
|
||||||
|
|
||||||
/// Whether the owning [RenderObject] makes other [RenderObjects] previously
|
/// Whether the owning [RenderObject] makes other [RenderObject]s previously
|
||||||
/// painted within the same semantic boundary unreachable for accessibility
|
/// painted within the same semantic boundary unreachable for accessibility
|
||||||
/// purposes.
|
/// purposes.
|
||||||
///
|
///
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
/// An event that can be send by the application to notify interested listeners
|
/// An event sent by the application to notify interested listeners that
|
||||||
/// that something happened to the user interface (e.g. a view scrolled).
|
/// something happened to the user interface (e.g. a view scrolled).
|
||||||
///
|
///
|
||||||
/// These events are usually interpreted by assistive technologies to give the
|
/// These events are usually interpreted by assistive technologies to give the
|
||||||
/// user additional clues about the current state of the UI.
|
/// user additional clues about the current state of the UI.
|
||||||
|
@ -4922,7 +4922,7 @@ class BlockSemantics extends SingleChildRenderObjectWidget {
|
|||||||
/// When [excluding] is true, this widget (and its subtree) is excluded from
|
/// When [excluding] is true, this widget (and its subtree) is excluded from
|
||||||
/// the semantics tree.
|
/// the semantics tree.
|
||||||
///
|
///
|
||||||
/// This can be used to hide subwidgets that would otherwise be
|
/// This can be used to hide descendant widgets that would otherwise be
|
||||||
/// reported but that would only be confusing. For example, the
|
/// reported but that would only be confusing. For example, the
|
||||||
/// material library's [Chip] widget hides the avatar since it is
|
/// material library's [Chip] widget hides the avatar since it is
|
||||||
/// redundant with the chip label.
|
/// redundant with the chip label.
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -10,6 +11,9 @@ import 'package:flutter/rendering.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../rendering/mock_canvas.dart';
|
||||||
|
import '../rendering/recording_canvas.dart';
|
||||||
|
import '../widgets/semantics_tester.dart';
|
||||||
import 'feedback_tester.dart';
|
import 'feedback_tester.dart';
|
||||||
|
|
||||||
class _TimePickerLauncher extends StatelessWidget {
|
class _TimePickerLauncher extends StatelessWidget {
|
||||||
@ -47,7 +51,7 @@ Future<Offset> startPicker(WidgetTester tester, ValueChanged<TimeOfDay> onChange
|
|||||||
await tester.pumpWidget(new _TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US')));
|
await tester.pumpWidget(new _TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US')));
|
||||||
await tester.tap(find.text('X'));
|
await tester.tap(find.text('X'));
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
return tester.getCenter(find.byKey(const Key('time-picker-dial')));
|
return tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Null> finishPicker(WidgetTester tester) async {
|
Future<Null> finishPicker(WidgetTester tester) async {
|
||||||
@ -57,6 +61,12 @@ Future<Null> finishPicker(WidgetTester tester) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
group('Time picker', () {
|
||||||
|
_tests();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tests() {
|
||||||
testWidgets('tap-select an hour', (WidgetTester tester) async {
|
testWidgets('tap-select an hour', (WidgetTester tester) async {
|
||||||
TimeOfDay result;
|
TimeOfDay result;
|
||||||
|
|
||||||
@ -210,7 +220,8 @@ void main() {
|
|||||||
const List<String> labels12To11TwoDigit = const <String>['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11'];
|
const List<String> labels12To11TwoDigit = const <String>['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11'];
|
||||||
const List<String> labels00To23 = const <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
|
const List<String> labels00To23 = const <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
|
||||||
|
|
||||||
Future<Null> mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async {
|
Future<Null> mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat,
|
||||||
|
{ TimeOfDay initialTime: const TimeOfDay(hour: 7, minute: 0) }) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
new Localizations(
|
new Localizations(
|
||||||
locale: const Locale('en', 'US'),
|
locale: const Locale('en', 'US'),
|
||||||
@ -220,57 +231,235 @@ void main() {
|
|||||||
],
|
],
|
||||||
child: new MediaQuery(
|
child: new MediaQuery(
|
||||||
data: new MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat),
|
data: new MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat),
|
||||||
child: new Directionality(
|
child: new Material(
|
||||||
textDirection: TextDirection.ltr,
|
child: new Directionality(
|
||||||
child: new Navigator(
|
textDirection: TextDirection.ltr,
|
||||||
onGenerateRoute: (RouteSettings settings) {
|
child: new Navigator(
|
||||||
return new MaterialPageRoute<dynamic>(builder: (BuildContext context) {
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0));
|
return new MaterialPageRoute<dynamic>(builder: (BuildContext context) {
|
||||||
return new Container();
|
return new FlatButton(
|
||||||
});
|
onPressed: () {
|
||||||
},
|
showTimePicker(context: context, initialTime: initialTime);
|
||||||
|
},
|
||||||
|
child: const Text('X'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Pump once, because the dialog shows up asynchronously.
|
|
||||||
await tester.pump();
|
await tester.tap(find.text('X'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
|
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
|
||||||
await mediaQueryBoilerplate(tester, false);
|
await mediaQueryBoilerplate(tester, false);
|
||||||
|
|
||||||
final CustomPaint dialPaint = tester.widget(find.descendant(
|
final CustomPaint dialPaint = tester.widget(findDialPaint);
|
||||||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
|
|
||||||
matching: find.byType(CustomPaint),
|
|
||||||
));
|
|
||||||
final dynamic dialPainter = dialPaint.painter;
|
final dynamic dialPainter = dialPaint.painter;
|
||||||
final List<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
||||||
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11);
|
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11);
|
||||||
expect(dialPainter.primaryInnerLabels, null);
|
expect(dialPainter.primaryInnerLabels, null);
|
||||||
|
|
||||||
final List<TextPainter> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
||||||
expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11);
|
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11);
|
||||||
expect(dialPainter.secondaryInnerLabels, null);
|
expect(dialPainter.secondaryInnerLabels, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
|
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
|
||||||
await mediaQueryBoilerplate(tester, true);
|
await mediaQueryBoilerplate(tester, true);
|
||||||
|
|
||||||
final CustomPaint dialPaint = tester.widget(find.descendant(
|
final CustomPaint dialPaint = tester.widget(findDialPaint);
|
||||||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
|
|
||||||
matching: find.byType(CustomPaint),
|
|
||||||
));
|
|
||||||
final dynamic dialPainter = dialPaint.painter;
|
final dynamic dialPainter = dialPaint.painter;
|
||||||
final List<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
||||||
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
|
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels00To23);
|
||||||
final List<TextPainter> primaryInnerLabels = dialPainter.primaryInnerLabels;
|
final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels;
|
||||||
expect(primaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
|
expect(primaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11TwoDigit);
|
||||||
|
|
||||||
final List<TextPainter> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
||||||
expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
|
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels00To23);
|
||||||
final List<TextPainter> secondaryInnerLabels = dialPainter.secondaryInnerLabels;
|
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels;
|
||||||
expect(secondaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
|
expect(secondaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11TwoDigit);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
|
||||||
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||||
|
await mediaQueryBoilerplate(tester, false);
|
||||||
|
|
||||||
|
expect(semantics, includesNodeWith(label: 'AM', actions: <SemanticsAction>[SemanticsAction.tap]));
|
||||||
|
expect(semantics, includesNodeWith(label: 'PM', actions: <SemanticsAction>[SemanticsAction.tap]));
|
||||||
|
|
||||||
|
semantics.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('provides semantics information for header and footer', (WidgetTester tester) async {
|
||||||
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||||
|
await mediaQueryBoilerplate(tester, true);
|
||||||
|
|
||||||
|
expect(semantics, isNot(includesNodeWith(label: ':')));
|
||||||
|
expect(semantics.nodesWith(label: '00'), hasLength(2),
|
||||||
|
reason: '00 appears once in the header, then again in the dial');
|
||||||
|
expect(semantics.nodesWith(label: '07'), hasLength(2),
|
||||||
|
reason: '07 appears once in the header, then again in the dial');
|
||||||
|
expect(semantics, includesNodeWith(label: 'CANCEL'));
|
||||||
|
expect(semantics, includesNodeWith(label: 'OK'));
|
||||||
|
|
||||||
|
// In 24-hour mode we don't have AM/PM control.
|
||||||
|
expect(semantics, isNot(includesNodeWith(label: 'AM')));
|
||||||
|
expect(semantics, isNot(includesNodeWith(label: 'PM')));
|
||||||
|
|
||||||
|
semantics.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('provides semantics information for hours', (WidgetTester tester) async {
|
||||||
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||||
|
await mediaQueryBoilerplate(tester, true);
|
||||||
|
|
||||||
|
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||||
|
final CustomPainter dialPainter = dialPaint.painter;
|
||||||
|
final _CustomPainterSemanticsTester painterTester = new _CustomPainterSemanticsTester(tester, dialPainter, semantics);
|
||||||
|
|
||||||
|
painterTester.addLabel('00', 86.0, 12.0, 134.0, 36.0);
|
||||||
|
painterTester.addLabel('13', 129.0, 23.5, 177.0, 47.5);
|
||||||
|
painterTester.addLabel('14', 160.5, 55.0, 208.5, 79.0);
|
||||||
|
painterTester.addLabel('15', 172.0, 98.0, 220.0, 122.0);
|
||||||
|
painterTester.addLabel('16', 160.5, 141.0, 208.5, 165.0);
|
||||||
|
painterTester.addLabel('17', 129.0, 172.5, 177.0, 196.5);
|
||||||
|
painterTester.addLabel('18', 86.0, 184.0, 134.0, 208.0);
|
||||||
|
painterTester.addLabel('19', 43.0, 172.5, 91.0, 196.5);
|
||||||
|
painterTester.addLabel('20', 11.5, 141.0, 59.5, 165.0);
|
||||||
|
painterTester.addLabel('21', 0.0, 98.0, 48.0, 122.0);
|
||||||
|
painterTester.addLabel('22', 11.5, 55.0, 59.5, 79.0);
|
||||||
|
painterTester.addLabel('23', 43.0, 23.5, 91.0, 47.5);
|
||||||
|
painterTester.addLabel('12', 86.0, 48.0, 134.0, 72.0);
|
||||||
|
painterTester.addLabel('01', 111.0, 54.7, 159.0, 78.7);
|
||||||
|
painterTester.addLabel('02', 129.3, 73.0, 177.3, 97.0);
|
||||||
|
painterTester.addLabel('03', 136.0, 98.0, 184.0, 122.0);
|
||||||
|
painterTester.addLabel('04', 129.3, 123.0, 177.3, 147.0);
|
||||||
|
painterTester.addLabel('05', 111.0, 141.3, 159.0, 165.3);
|
||||||
|
painterTester.addLabel('06', 86.0, 148.0, 134.0, 172.0);
|
||||||
|
painterTester.addLabel('07', 61.0, 141.3, 109.0, 165.3);
|
||||||
|
painterTester.addLabel('08', 42.7, 123.0, 90.7, 147.0);
|
||||||
|
painterTester.addLabel('09', 36.0, 98.0, 84.0, 122.0);
|
||||||
|
painterTester.addLabel('10', 42.7, 73.0, 90.7, 97.0);
|
||||||
|
painterTester.addLabel('11', 61.0, 54.7, 109.0, 78.7);
|
||||||
|
|
||||||
|
painterTester.assertExpectations();
|
||||||
|
semantics.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('provides semantics information for minutes', (WidgetTester tester) async {
|
||||||
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||||
|
await mediaQueryBoilerplate(tester, true);
|
||||||
|
await tester.tap(find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_MinuteControl'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||||
|
final CustomPainter dialPainter = dialPaint.painter;
|
||||||
|
final _CustomPainterSemanticsTester painterTester = new _CustomPainterSemanticsTester(tester, dialPainter, semantics);
|
||||||
|
|
||||||
|
painterTester.addLabel('00', 86.0, 12.0, 134.0, 36.0);
|
||||||
|
painterTester.addLabel('05', 129.0, 23.5, 177.0, 47.5);
|
||||||
|
painterTester.addLabel('10', 160.5, 55.0, 208.5, 79.0);
|
||||||
|
painterTester.addLabel('15', 172.0, 98.0, 220.0, 122.0);
|
||||||
|
painterTester.addLabel('20', 160.5, 141.0, 208.5, 165.0);
|
||||||
|
painterTester.addLabel('25', 129.0, 172.5, 177.0, 196.5);
|
||||||
|
painterTester.addLabel('30', 86.0, 184.0, 134.0, 208.0);
|
||||||
|
painterTester.addLabel('35', 43.0, 172.5, 91.0, 196.5);
|
||||||
|
painterTester.addLabel('40', 11.5, 141.0, 59.5, 165.0);
|
||||||
|
painterTester.addLabel('45', 0.0, 98.0, 48.0, 122.0);
|
||||||
|
painterTester.addLabel('50', 11.5, 55.0, 59.5, 79.0);
|
||||||
|
painterTester.addLabel('55', 43.0, 23.5, 91.0, 47.5);
|
||||||
|
|
||||||
|
painterTester.assertExpectations();
|
||||||
|
semantics.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('picks the right dial ring from widget configuration', (WidgetTester tester) async {
|
||||||
|
await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 12, minute: 0));
|
||||||
|
dynamic dialPaint = tester.widget(findDialPaint);
|
||||||
|
expect('${dialPaint.painter.activeRing}', '_DialRing.inner');
|
||||||
|
|
||||||
|
await tester.pumpWidget(new Container()); // make sure previous state isn't reused
|
||||||
|
|
||||||
|
await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 0, minute: 0));
|
||||||
|
dialPaint = tester.widget(findDialPaint);
|
||||||
|
expect('${dialPaint.painter.activeRing}', '_DialRing.outer');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Finder findDialPaint = find.descendant(
|
||||||
|
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
|
||||||
|
matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
|
||||||
|
);
|
||||||
|
|
||||||
|
class _SemanticsNodeExpectation {
|
||||||
|
final String label;
|
||||||
|
final double left;
|
||||||
|
final double top;
|
||||||
|
final double right;
|
||||||
|
final double bottom;
|
||||||
|
|
||||||
|
_SemanticsNodeExpectation(this.label, this.left, this.top, this.right, this.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomPainterSemanticsTester {
|
||||||
|
_CustomPainterSemanticsTester(this.tester, this.painter, this.semantics);
|
||||||
|
|
||||||
|
final WidgetTester tester;
|
||||||
|
final CustomPainter painter;
|
||||||
|
final SemanticsTester semantics;
|
||||||
|
final PaintPattern expectedLabels = paints;
|
||||||
|
final List<_SemanticsNodeExpectation> expectedNodes = <_SemanticsNodeExpectation>[];
|
||||||
|
|
||||||
|
void addLabel(String label, double left, double top, double right, double bottom) {
|
||||||
|
expectedNodes.add(new _SemanticsNodeExpectation(label, left, top, right, bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertExpectations() {
|
||||||
|
final TestRecordingCanvas canvasRecording = new TestRecordingCanvas();
|
||||||
|
painter.paint(canvasRecording, const Size(220.0, 220.0));
|
||||||
|
final List<ui.Paragraph> paragraphs = canvasRecording.invocations
|
||||||
|
.where((RecordedInvocation recordedInvocation) {
|
||||||
|
return recordedInvocation.invocation.memberName == #drawParagraph;
|
||||||
|
})
|
||||||
|
.map<ui.Paragraph>((RecordedInvocation recordedInvocation) {
|
||||||
|
return recordedInvocation.invocation.positionalArguments.first;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final PaintPattern expectedLabels = paints;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for (_SemanticsNodeExpectation expectation in expectedNodes) {
|
||||||
|
expect(semantics, includesNodeWith(label: expectation.label));
|
||||||
|
final Iterable<SemanticsNode> dialLabelNodes = semantics
|
||||||
|
.nodesWith(label: expectation.label)
|
||||||
|
.where((SemanticsNode node) => node.tags?.contains(const SemanticsTag('dial-label')) ?? false);
|
||||||
|
expect(dialLabelNodes, hasLength(1), reason: 'Expected exactly one label ${expectation.label}');
|
||||||
|
final Rect rect = new Rect.fromLTRB(expectation.left, expectation.top, expectation.right, expectation.bottom);
|
||||||
|
expect(dialLabelNodes.single.rect, within(distance: 1.0, from: rect),
|
||||||
|
reason: 'This is checking the node rectangle for label ${expectation.label}');
|
||||||
|
|
||||||
|
final ui.Paragraph paragraph = paragraphs[i++];
|
||||||
|
|
||||||
|
// The label text paragraph and the semantics node share the same center,
|
||||||
|
// but have different sizes.
|
||||||
|
final Offset center = dialLabelNodes.single.rect.center;
|
||||||
|
final Offset topLeft = center.translate(
|
||||||
|
-paragraph.width / 2.0,
|
||||||
|
-paragraph.height / 2.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expectedLabels.paragraph(
|
||||||
|
paragraph: paragraph,
|
||||||
|
offset: within<Offset>(distance: 1.0, from: topLeft),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(tester.renderObject(findDialPaint), expectedLabels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -282,8 +282,15 @@ abstract class PaintPattern {
|
|||||||
/// arguments that are passed to this method are compared to the actual
|
/// arguments that are passed to this method are compared to the actual
|
||||||
/// [Canvas.drawParagraph] call's argument, and any mismatches result in failure.
|
/// [Canvas.drawParagraph] call's argument, and any mismatches result in failure.
|
||||||
///
|
///
|
||||||
|
/// The `offset` argument can be either an [Offset] or a [Matcher]. If it is
|
||||||
|
/// an [Offset] then the actual value must match the expected offset
|
||||||
|
/// precisely. If it is a [Matcher] then the comparison is made according to
|
||||||
|
/// the semantics of the [Matcher]. For example, [within] can be used to
|
||||||
|
/// assert that the actual offset is within a given distance from the expected
|
||||||
|
/// offset.
|
||||||
|
///
|
||||||
/// If no call to [Canvas.drawParagraph] was made, then this results in failure.
|
/// If no call to [Canvas.drawParagraph] was made, then this results in failure.
|
||||||
void paragraph({ ui.Paragraph paragraph, Offset offset });
|
void paragraph({ ui.Paragraph paragraph, dynamic offset });
|
||||||
|
|
||||||
/// Indicates that an image is expected next.
|
/// Indicates that an image is expected next.
|
||||||
///
|
///
|
||||||
@ -626,7 +633,7 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paragraph({ ui.Paragraph paragraph, Offset offset }) {
|
void paragraph({ ui.Paragraph paragraph, dynamic offset }) {
|
||||||
_predicates.add(new _FunctionPaintPredicate(#drawParagraph, <dynamic>[paragraph, offset]));
|
_predicates.add(new _FunctionPaintPredicate(#drawParagraph, <dynamic>[paragraph, offset]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1140,8 +1147,12 @@ class _FunctionPaintPredicate extends _PaintPredicate {
|
|||||||
for (int index = 0; index < arguments.length; index += 1) {
|
for (int index = 0; index < arguments.length; index += 1) {
|
||||||
final dynamic actualArgument = call.current.invocation.positionalArguments[index];
|
final dynamic actualArgument = call.current.invocation.positionalArguments[index];
|
||||||
final dynamic desiredArgument = arguments[index];
|
final dynamic desiredArgument = arguments[index];
|
||||||
if (desiredArgument != null && desiredArgument != actualArgument)
|
|
||||||
|
if (desiredArgument is Matcher) {
|
||||||
|
expect(actualArgument, desiredArgument);
|
||||||
|
} else if (desiredArgument != null && desiredArgument != actualArgument) {
|
||||||
throw 'It called ${_symbolName(symbol)} with argument $index having value ${_valueName(actualArgument)} when ${_valueName(desiredArgument)} was expected.';
|
throw 'It called ${_symbolName(symbol)} with argument $index having value ${_valueName(actualArgument)} when ${_valueName(desiredArgument)} was expected.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
call.moveNext();
|
call.moveNext();
|
||||||
}
|
}
|
||||||
|
@ -299,6 +299,47 @@ class SemanticsTester {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SemanticsTester for ${tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode}';
|
String toString() => 'SemanticsTester for ${tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode}';
|
||||||
|
|
||||||
|
Iterable<SemanticsNode> nodesWith({
|
||||||
|
String label,
|
||||||
|
String value,
|
||||||
|
TextDirection textDirection,
|
||||||
|
List<SemanticsAction> actions,
|
||||||
|
List<SemanticsFlags> flags,
|
||||||
|
}) {
|
||||||
|
bool checkNode(SemanticsNode node) {
|
||||||
|
if (label != null && node.label != label)
|
||||||
|
return false;
|
||||||
|
if (value != null && node.value != value)
|
||||||
|
return false;
|
||||||
|
if (textDirection != null && node.textDirection != textDirection)
|
||||||
|
return false;
|
||||||
|
if (actions != null) {
|
||||||
|
final int expectedActions = actions.fold(0, (int value, SemanticsAction action) => value | action.index);
|
||||||
|
final int actualActions = node.getSemanticsData().actions;
|
||||||
|
if (expectedActions != actualActions)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (flags != null) {
|
||||||
|
final int expectedFlags = flags.fold(0, (int value, SemanticsFlags flag) => value | flag.index);
|
||||||
|
final int actualFlags = node.getSemanticsData().flags;
|
||||||
|
if (expectedFlags != actualFlags)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<SemanticsNode> result = <SemanticsNode>[];
|
||||||
|
bool visit(SemanticsNode node) {
|
||||||
|
if (checkNode(node)) {
|
||||||
|
result.add(node);
|
||||||
|
}
|
||||||
|
node.visitChildren(visit);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
visit(tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HasSemantics extends Matcher {
|
class _HasSemantics extends Matcher {
|
||||||
@ -354,41 +395,13 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
|
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
|
||||||
bool result = false;
|
return item.nodesWith(
|
||||||
SemanticsNodeVisitor visitor;
|
label: label,
|
||||||
visitor = (SemanticsNode node) {
|
value: value,
|
||||||
if (checkNode(node)) {
|
textDirection: textDirection,
|
||||||
result = true;
|
actions: actions,
|
||||||
} else {
|
flags: flags,
|
||||||
node.visitChildren(visitor);
|
).isNotEmpty;
|
||||||
}
|
|
||||||
return !result;
|
|
||||||
};
|
|
||||||
final SemanticsNode root = item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
|
|
||||||
visitor(root);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool checkNode(SemanticsNode node) {
|
|
||||||
if (label != null && node.label != label)
|
|
||||||
return false;
|
|
||||||
if (value != null && node.value != value)
|
|
||||||
return false;
|
|
||||||
if (textDirection != null && node.textDirection != textDirection)
|
|
||||||
return false;
|
|
||||||
if (actions != null) {
|
|
||||||
final int expectedActions = actions.fold(0, (int value, SemanticsAction action) => value | action.index);
|
|
||||||
final int actualActions = node.getSemanticsData().actions;
|
|
||||||
if (expectedActions != actualActions)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (flags != null) {
|
|
||||||
final int expectedFlags = flags.fold(0, (int value, SemanticsFlags flag) => value | flag.index);
|
|
||||||
final int actualFlags = node.getSemanticsData().flags;
|
|
||||||
if (expectedFlags != actualFlags)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -45,6 +45,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'الاطّلاع على التراخيص',
|
'viewLicensesButtonLabel': r'الاطّلاع على التراخيص',
|
||||||
'anteMeridiemAbbreviation': r'ص',
|
'anteMeridiemAbbreviation': r'ص',
|
||||||
'postMeridiemAbbreviation': r'م',
|
'postMeridiemAbbreviation': r'م',
|
||||||
|
'timePickerHourModeAnnouncement': r'حدد ساعات',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'حدد دقائق',
|
||||||
},
|
},
|
||||||
'de': const <String, String>{
|
'de': const <String, String>{
|
||||||
'scriptCategory': r'English-like',
|
'scriptCategory': r'English-like',
|
||||||
@ -77,6 +79,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'LIZENZEN ANZEIGEN',
|
'viewLicensesButtonLabel': r'LIZENZEN ANZEIGEN',
|
||||||
'anteMeridiemAbbreviation': r'VORM.',
|
'anteMeridiemAbbreviation': r'VORM.',
|
||||||
'postMeridiemAbbreviation': r'NACHM.',
|
'postMeridiemAbbreviation': r'NACHM.',
|
||||||
|
'timePickerHourModeAnnouncement': r'Stunde auswählen',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'Minute auswählen',
|
||||||
},
|
},
|
||||||
'de_CH': const <String, String>{
|
'de_CH': const <String, String>{
|
||||||
'scriptCategory': r'English-like',
|
'scriptCategory': r'English-like',
|
||||||
@ -140,6 +144,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'VIEW LICENSES',
|
'viewLicensesButtonLabel': r'VIEW LICENSES',
|
||||||
'anteMeridiemAbbreviation': r'AM',
|
'anteMeridiemAbbreviation': r'AM',
|
||||||
'postMeridiemAbbreviation': r'PM',
|
'postMeridiemAbbreviation': r'PM',
|
||||||
|
'timePickerHourModeAnnouncement': r'Select hours',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'Select minutes',
|
||||||
},
|
},
|
||||||
'en_AU': const <String, String>{
|
'en_AU': const <String, String>{
|
||||||
'scriptCategory': r'English-like',
|
'scriptCategory': r'English-like',
|
||||||
@ -389,6 +395,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'VER LICENCIAS',
|
'viewLicensesButtonLabel': r'VER LICENCIAS',
|
||||||
'anteMeridiemAbbreviation': r'A.M.',
|
'anteMeridiemAbbreviation': r'A.M.',
|
||||||
'postMeridiemAbbreviation': r'P.M.',
|
'postMeridiemAbbreviation': r'P.M.',
|
||||||
|
'timePickerHourModeAnnouncement': r'Seleccione Horas',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'Seleccione Minutos',
|
||||||
},
|
},
|
||||||
'es_US': const <String, String>{
|
'es_US': const <String, String>{
|
||||||
'scriptCategory': r'English-like',
|
'scriptCategory': r'English-like',
|
||||||
@ -426,6 +434,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'مشاهده مجوزها',
|
'viewLicensesButtonLabel': r'مشاهده مجوزها',
|
||||||
'anteMeridiemAbbreviation': r'ق.ظ.',
|
'anteMeridiemAbbreviation': r'ق.ظ.',
|
||||||
'postMeridiemAbbreviation': r'ب.ظ.',
|
'postMeridiemAbbreviation': r'ب.ظ.',
|
||||||
|
'timePickerHourModeAnnouncement': r'ساعت ها را انتخاب کنید',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'دقیقه را انتخاب کنید',
|
||||||
},
|
},
|
||||||
'fr': const <String, String>{
|
'fr': const <String, String>{
|
||||||
'scriptCategory': r'English-like',
|
'scriptCategory': r'English-like',
|
||||||
@ -458,6 +468,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'AFFICHER LES LICENCES',
|
'viewLicensesButtonLabel': r'AFFICHER LES LICENCES',
|
||||||
'anteMeridiemAbbreviation': r'AM',
|
'anteMeridiemAbbreviation': r'AM',
|
||||||
'postMeridiemAbbreviation': r'PM',
|
'postMeridiemAbbreviation': r'PM',
|
||||||
|
'timePickerHourModeAnnouncement': r'Sélectionnez les heures',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'Sélectionnez les minutes',
|
||||||
},
|
},
|
||||||
'fr_CA': const <String, String>{
|
'fr_CA': const <String, String>{
|
||||||
'scriptCategory': r'English-like',
|
'scriptCategory': r'English-like',
|
||||||
@ -526,6 +538,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'הצגת הרישיונות',
|
'viewLicensesButtonLabel': r'הצגת הרישיונות',
|
||||||
'anteMeridiemAbbreviation': r'AM',
|
'anteMeridiemAbbreviation': r'AM',
|
||||||
'postMeridiemAbbreviation': r'PM',
|
'postMeridiemAbbreviation': r'PM',
|
||||||
|
'timePickerHourModeAnnouncement': r'בחר שעות',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'בחר דקות',
|
||||||
},
|
},
|
||||||
'it': const <String, String>{
|
'it': const <String, String>{
|
||||||
'scriptCategory': r'English-like',
|
'scriptCategory': r'English-like',
|
||||||
@ -557,6 +571,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'VISUALIZZA LICENZE',
|
'viewLicensesButtonLabel': r'VISUALIZZA LICENZE',
|
||||||
'anteMeridiemAbbreviation': r'AM',
|
'anteMeridiemAbbreviation': r'AM',
|
||||||
'postMeridiemAbbreviation': r'PM',
|
'postMeridiemAbbreviation': r'PM',
|
||||||
|
'timePickerHourModeAnnouncement': r'Seleziona ore',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'Seleziona minuti',
|
||||||
},
|
},
|
||||||
'ja': const <String, String>{
|
'ja': const <String, String>{
|
||||||
'scriptCategory': r'dense',
|
'scriptCategory': r'dense',
|
||||||
@ -588,6 +604,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'ライセンスを表示',
|
'viewLicensesButtonLabel': r'ライセンスを表示',
|
||||||
'anteMeridiemAbbreviation': r'AM',
|
'anteMeridiemAbbreviation': r'AM',
|
||||||
'postMeridiemAbbreviation': r'PM',
|
'postMeridiemAbbreviation': r'PM',
|
||||||
|
'timePickerHourModeAnnouncement': r'時を選択',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'分を選択',
|
||||||
},
|
},
|
||||||
'ps': const <String, String>{
|
'ps': const <String, String>{
|
||||||
'scriptCategory': r'tall',
|
'scriptCategory': r'tall',
|
||||||
@ -616,6 +634,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'pasteButtonLabel': r'پیټ کړئ',
|
'pasteButtonLabel': r'پیټ کړئ',
|
||||||
'selectAllButtonLabel': r'غوره کړئ',
|
'selectAllButtonLabel': r'غوره کړئ',
|
||||||
'viewLicensesButtonLabel': r'لیدلس وګورئ',
|
'viewLicensesButtonLabel': r'لیدلس وګورئ',
|
||||||
|
'timePickerHourModeAnnouncement': r'وختونه وټاکئ',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'منې غوره کړئ',
|
||||||
},
|
},
|
||||||
'pt': const <String, String>{
|
'pt': const <String, String>{
|
||||||
'scriptCategory': r'English-like',
|
'scriptCategory': r'English-like',
|
||||||
@ -644,6 +664,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'pasteButtonLabel': r'COLAR',
|
'pasteButtonLabel': r'COLAR',
|
||||||
'selectAllButtonLabel': r'SELECIONAR TUDO',
|
'selectAllButtonLabel': r'SELECIONAR TUDO',
|
||||||
'viewLicensesButtonLabel': r'VER LICENÇAS',
|
'viewLicensesButtonLabel': r'VER LICENÇAS',
|
||||||
|
'timePickerHourModeAnnouncement': r'Selecione horários',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'Selecione Minutos',
|
||||||
},
|
},
|
||||||
'pt_PT': const <String, String>{
|
'pt_PT': const <String, String>{
|
||||||
'scriptCategory': r'English-like',
|
'scriptCategory': r'English-like',
|
||||||
@ -709,6 +731,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'ЛИЦЕНЗИИ',
|
'viewLicensesButtonLabel': r'ЛИЦЕНЗИИ',
|
||||||
'anteMeridiemAbbreviation': r'АМ',
|
'anteMeridiemAbbreviation': r'АМ',
|
||||||
'postMeridiemAbbreviation': r'PM',
|
'postMeridiemAbbreviation': r'PM',
|
||||||
|
'timePickerHourModeAnnouncement': r'ВЫБРАТЬ ЧАСЫ',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'ВЫБРАТЬ МИНУТЫ',
|
||||||
},
|
},
|
||||||
'ur': const <String, String>{
|
'ur': const <String, String>{
|
||||||
'scriptCategory': r'tall',
|
'scriptCategory': r'tall',
|
||||||
@ -740,6 +764,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'viewLicensesButtonLabel': r'لائسنسز دیکھیں',
|
'viewLicensesButtonLabel': r'لائسنسز دیکھیں',
|
||||||
'anteMeridiemAbbreviation': r'AM',
|
'anteMeridiemAbbreviation': r'AM',
|
||||||
'postMeridiemAbbreviation': r'PM',
|
'postMeridiemAbbreviation': r'PM',
|
||||||
|
'timePickerHourModeAnnouncement': r'گھنٹے منتخب کریں',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'منٹ منتخب کریں',
|
||||||
},
|
},
|
||||||
'zh': const <String, String>{
|
'zh': const <String, String>{
|
||||||
'scriptCategory': r'dense',
|
'scriptCategory': r'dense',
|
||||||
@ -771,6 +797,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
'previousMonthTooltip': r'上个月',
|
'previousMonthTooltip': r'上个月',
|
||||||
'anteMeridiemAbbreviation': r'上午',
|
'anteMeridiemAbbreviation': r'上午',
|
||||||
'postMeridiemAbbreviation': r'下午',
|
'postMeridiemAbbreviation': r'下午',
|
||||||
|
'timePickerHourModeAnnouncement': r'选择小时',
|
||||||
|
'timePickerMinuteModeAnnouncement': r'选择分钟',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,5 +31,7 @@
|
|||||||
"selectAllButtonLabel": "اختيار الكل",
|
"selectAllButtonLabel": "اختيار الكل",
|
||||||
"viewLicensesButtonLabel": "الاطّلاع على التراخيص",
|
"viewLicensesButtonLabel": "الاطّلاع على التراخيص",
|
||||||
"anteMeridiemAbbreviation": "ص",
|
"anteMeridiemAbbreviation": "ص",
|
||||||
"postMeridiemAbbreviation": "م"
|
"postMeridiemAbbreviation": "م",
|
||||||
|
"timePickerHourModeAnnouncement": "حدد ساعات",
|
||||||
|
"timePickerMinuteModeAnnouncement": "حدد دقائق"
|
||||||
}
|
}
|
||||||
|
@ -28,5 +28,7 @@
|
|||||||
"selectAllButtonLabel": "ALLE AUSWÄHLEN",
|
"selectAllButtonLabel": "ALLE AUSWÄHLEN",
|
||||||
"viewLicensesButtonLabel": "LIZENZEN ANZEIGEN",
|
"viewLicensesButtonLabel": "LIZENZEN ANZEIGEN",
|
||||||
"anteMeridiemAbbreviation": "VORM.",
|
"anteMeridiemAbbreviation": "VORM.",
|
||||||
"postMeridiemAbbreviation": "NACHM."
|
"postMeridiemAbbreviation": "NACHM.",
|
||||||
|
"timePickerHourModeAnnouncement": "Stunde auswählen",
|
||||||
|
"timePickerMinuteModeAnnouncement": "Minute auswählen"
|
||||||
}
|
}
|
||||||
|
@ -143,5 +143,15 @@
|
|||||||
"postMeridiemAbbreviation": "PM",
|
"postMeridiemAbbreviation": "PM",
|
||||||
"@postMeridiemAbbreviation": {
|
"@postMeridiemAbbreviation": {
|
||||||
"description": "The abbreviation for post meridiem (after noon) shown in the time picker. Translations for this abbreviation will only be provided for locales that support it."
|
"description": "The abbreviation for post meridiem (after noon) shown in the time picker. Translations for this abbreviation will only be provided for locales that support it."
|
||||||
|
},
|
||||||
|
|
||||||
|
"timePickerHourModeAnnouncement": "Select hours",
|
||||||
|
"@timePickerHourModeAnnouncement": {
|
||||||
|
"description": "The audio announcement made when the time picker dialog is set to hour mode."
|
||||||
|
},
|
||||||
|
|
||||||
|
"timePickerMinuteModeAnnouncement": "Select minutes",
|
||||||
|
"@timePickerMinuteModeAnnouncement": {
|
||||||
|
"description": "The audio announcement made when the time picker dialog is set to minute mode."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,5 +28,7 @@
|
|||||||
"selectAllButtonLabel": "SELECCIONAR TODO",
|
"selectAllButtonLabel": "SELECCIONAR TODO",
|
||||||
"viewLicensesButtonLabel": "VER LICENCIAS",
|
"viewLicensesButtonLabel": "VER LICENCIAS",
|
||||||
"anteMeridiemAbbreviation": "A.M.",
|
"anteMeridiemAbbreviation": "A.M.",
|
||||||
"postMeridiemAbbreviation": "P.M."
|
"postMeridiemAbbreviation": "P.M.",
|
||||||
|
"timePickerHourModeAnnouncement": "Seleccione Horas",
|
||||||
|
"timePickerMinuteModeAnnouncement": "Seleccione Minutos"
|
||||||
}
|
}
|
||||||
|
@ -27,5 +27,7 @@
|
|||||||
"selectAllButtonLabel": "انتخاب همه",
|
"selectAllButtonLabel": "انتخاب همه",
|
||||||
"viewLicensesButtonLabel": "مشاهده مجوزها",
|
"viewLicensesButtonLabel": "مشاهده مجوزها",
|
||||||
"anteMeridiemAbbreviation": "ق.ظ.",
|
"anteMeridiemAbbreviation": "ق.ظ.",
|
||||||
"postMeridiemAbbreviation": "ب.ظ."
|
"postMeridiemAbbreviation": "ب.ظ.",
|
||||||
|
"timePickerHourModeAnnouncement": "ساعت ها را انتخاب کنید",
|
||||||
|
"timePickerMinuteModeAnnouncement": "دقیقه را انتخاب کنید"
|
||||||
}
|
}
|
||||||
|
@ -28,5 +28,7 @@
|
|||||||
"selectAllButtonLabel": "TOUT SÉLECTIONNER",
|
"selectAllButtonLabel": "TOUT SÉLECTIONNER",
|
||||||
"viewLicensesButtonLabel": "AFFICHER LES LICENCES",
|
"viewLicensesButtonLabel": "AFFICHER LES LICENCES",
|
||||||
"anteMeridiemAbbreviation": "AM",
|
"anteMeridiemAbbreviation": "AM",
|
||||||
"postMeridiemAbbreviation": "PM"
|
"postMeridiemAbbreviation": "PM",
|
||||||
|
"timePickerHourModeAnnouncement": "Sélectionnez les heures",
|
||||||
|
"timePickerMinuteModeAnnouncement": "Sélectionnez les minutes"
|
||||||
}
|
}
|
||||||
|
@ -29,5 +29,7 @@
|
|||||||
"selectAllButtonLabel": "בחירת הכול",
|
"selectAllButtonLabel": "בחירת הכול",
|
||||||
"viewLicensesButtonLabel": "הצגת הרישיונות",
|
"viewLicensesButtonLabel": "הצגת הרישיונות",
|
||||||
"anteMeridiemAbbreviation": "AM",
|
"anteMeridiemAbbreviation": "AM",
|
||||||
"postMeridiemAbbreviation": "PM"
|
"postMeridiemAbbreviation": "PM",
|
||||||
|
"timePickerHourModeAnnouncement": "בחר שעות",
|
||||||
|
"timePickerMinuteModeAnnouncement": "בחר דקות"
|
||||||
}
|
}
|
||||||
|
@ -27,5 +27,7 @@
|
|||||||
"selectAllButtonLabel": "SELEZIONA TUTTO",
|
"selectAllButtonLabel": "SELEZIONA TUTTO",
|
||||||
"viewLicensesButtonLabel": "VISUALIZZA LICENZE",
|
"viewLicensesButtonLabel": "VISUALIZZA LICENZE",
|
||||||
"anteMeridiemAbbreviation": "AM",
|
"anteMeridiemAbbreviation": "AM",
|
||||||
"postMeridiemAbbreviation": "PM"
|
"postMeridiemAbbreviation": "PM",
|
||||||
|
"timePickerHourModeAnnouncement": "Seleziona ore",
|
||||||
|
"timePickerMinuteModeAnnouncement": "Seleziona minuti"
|
||||||
}
|
}
|
||||||
|
@ -27,5 +27,7 @@
|
|||||||
"selectAllButtonLabel": "すべて選択",
|
"selectAllButtonLabel": "すべて選択",
|
||||||
"viewLicensesButtonLabel": "ライセンスを表示",
|
"viewLicensesButtonLabel": "ライセンスを表示",
|
||||||
"anteMeridiemAbbreviation": "AM",
|
"anteMeridiemAbbreviation": "AM",
|
||||||
"postMeridiemAbbreviation": "PM"
|
"postMeridiemAbbreviation": "PM",
|
||||||
|
"timePickerHourModeAnnouncement": "時を選択",
|
||||||
|
"timePickerMinuteModeAnnouncement": "分を選択"
|
||||||
}
|
}
|
||||||
|
@ -26,5 +26,7 @@
|
|||||||
"okButtonLabel": "سمه ده",
|
"okButtonLabel": "سمه ده",
|
||||||
"pasteButtonLabel": "پیټ کړئ",
|
"pasteButtonLabel": "پیټ کړئ",
|
||||||
"selectAllButtonLabel": "غوره کړئ",
|
"selectAllButtonLabel": "غوره کړئ",
|
||||||
"viewLicensesButtonLabel": "لیدلس وګورئ"
|
"viewLicensesButtonLabel": "لیدلس وګورئ",
|
||||||
|
"timePickerHourModeAnnouncement": "وختونه وټاکئ",
|
||||||
|
"timePickerMinuteModeAnnouncement": "منې غوره کړئ"
|
||||||
}
|
}
|
||||||
|
@ -26,5 +26,7 @@
|
|||||||
"okButtonLabel": "OK",
|
"okButtonLabel": "OK",
|
||||||
"pasteButtonLabel": "COLAR",
|
"pasteButtonLabel": "COLAR",
|
||||||
"selectAllButtonLabel": "SELECIONAR TUDO",
|
"selectAllButtonLabel": "SELECIONAR TUDO",
|
||||||
"viewLicensesButtonLabel": "VER LICENÇAS"
|
"viewLicensesButtonLabel": "VER LICENÇAS",
|
||||||
|
"timePickerHourModeAnnouncement": "Selecione horários",
|
||||||
|
"timePickerMinuteModeAnnouncement": "Selecione Minutos"
|
||||||
}
|
}
|
||||||
|
@ -30,5 +30,7 @@
|
|||||||
"selectAllButtonLabel": "ВЫБРАТЬ ВСЕ",
|
"selectAllButtonLabel": "ВЫБРАТЬ ВСЕ",
|
||||||
"viewLicensesButtonLabel": "ЛИЦЕНЗИИ",
|
"viewLicensesButtonLabel": "ЛИЦЕНЗИИ",
|
||||||
"anteMeridiemAbbreviation": "АМ",
|
"anteMeridiemAbbreviation": "АМ",
|
||||||
"postMeridiemAbbreviation": "PM"
|
"postMeridiemAbbreviation": "PM",
|
||||||
|
"timePickerHourModeAnnouncement": "ВЫБРАТЬ ЧАСЫ",
|
||||||
|
"timePickerMinuteModeAnnouncement": "ВЫБРАТЬ МИНУТЫ"
|
||||||
}
|
}
|
||||||
|
@ -27,5 +27,7 @@
|
|||||||
"selectAllButtonLabel": "سبھی منتخب کریں",
|
"selectAllButtonLabel": "سبھی منتخب کریں",
|
||||||
"viewLicensesButtonLabel": "لائسنسز دیکھیں",
|
"viewLicensesButtonLabel": "لائسنسز دیکھیں",
|
||||||
"anteMeridiemAbbreviation": "AM",
|
"anteMeridiemAbbreviation": "AM",
|
||||||
"postMeridiemAbbreviation": "PM"
|
"postMeridiemAbbreviation": "PM",
|
||||||
|
"timePickerHourModeAnnouncement": "گھنٹے منتخب کریں",
|
||||||
|
"timePickerMinuteModeAnnouncement": "منٹ منتخب کریں"
|
||||||
}
|
}
|
||||||
|
@ -27,5 +27,7 @@
|
|||||||
"nextMonthTooltip": "下个月",
|
"nextMonthTooltip": "下个月",
|
||||||
"previousMonthTooltip": "上个月",
|
"previousMonthTooltip": "上个月",
|
||||||
"anteMeridiemAbbreviation": "上午",
|
"anteMeridiemAbbreviation": "上午",
|
||||||
"postMeridiemAbbreviation": "下午"
|
"postMeridiemAbbreviation": "下午",
|
||||||
|
"timePickerHourModeAnnouncement": "选择小时",
|
||||||
|
"timePickerMinuteModeAnnouncement": "选择分钟"
|
||||||
}
|
}
|
||||||
|
@ -316,6 +316,12 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get postMeridiemAbbreviation => _nameToValue['postMeridiemAbbreviation'];
|
String get postMeridiemAbbreviation => _nameToValue['postMeridiemAbbreviation'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get timePickerHourModeAnnouncement => _nameToValue['timePickerHourModeAnnouncement'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get timePickerMinuteModeAnnouncement => _nameToValue['timePickerMinuteModeAnnouncement'];
|
||||||
|
|
||||||
/// The [TimeOfDayFormat] corresponding to one of the following supported
|
/// The [TimeOfDayFormat] corresponding to one of the following supported
|
||||||
/// patterns:
|
/// patterns:
|
||||||
///
|
///
|
||||||
|
@ -140,57 +140,58 @@ void main() {
|
|||||||
],
|
],
|
||||||
child: new MediaQuery(
|
child: new MediaQuery(
|
||||||
data: new MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat),
|
data: new MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat),
|
||||||
child: new Directionality(
|
child: new Material(
|
||||||
textDirection: TextDirection.ltr,
|
child: new Directionality(
|
||||||
child: new Navigator(
|
textDirection: TextDirection.ltr,
|
||||||
onGenerateRoute: (RouteSettings settings) {
|
child: new Navigator(
|
||||||
return new MaterialPageRoute<dynamic>(builder: (BuildContext context) {
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0));
|
return new MaterialPageRoute<dynamic>(builder: (BuildContext context) {
|
||||||
return new Container();
|
return new FlatButton(
|
||||||
});
|
onPressed: () {
|
||||||
},
|
showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0));
|
||||||
|
},
|
||||||
|
child: const Text('X'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Pump once, because the dialog shows up asynchronously.
|
|
||||||
await tester.pump();
|
await tester.tap(find.text('X'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
|
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
|
||||||
await mediaQueryBoilerplate(tester, false);
|
await mediaQueryBoilerplate(tester, false);
|
||||||
|
|
||||||
final CustomPaint dialPaint = tester.widget(find.descendant(
|
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
|
|
||||||
matching: find.byType(CustomPaint),
|
|
||||||
));
|
|
||||||
final dynamic dialPainter = dialPaint.painter;
|
final dynamic dialPainter = dialPaint.painter;
|
||||||
final List<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
||||||
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11);
|
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11);
|
||||||
expect(dialPainter.primaryInnerLabels, null);
|
expect(dialPainter.primaryInnerLabels, null);
|
||||||
|
|
||||||
final List<TextPainter> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
||||||
expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11);
|
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11);
|
||||||
expect(dialPainter.secondaryInnerLabels, null);
|
expect(dialPainter.secondaryInnerLabels, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
|
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
|
||||||
await mediaQueryBoilerplate(tester, true);
|
await mediaQueryBoilerplate(tester, true);
|
||||||
|
|
||||||
final CustomPaint dialPaint = tester.widget(find.descendant(
|
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
|
|
||||||
matching: find.byType(CustomPaint),
|
|
||||||
));
|
|
||||||
final dynamic dialPainter = dialPaint.painter;
|
final dynamic dialPainter = dialPaint.painter;
|
||||||
final List<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
||||||
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
|
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels00To23);
|
||||||
final List<TextPainter> primaryInnerLabels = dialPainter.primaryInnerLabels;
|
final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels;
|
||||||
expect(primaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
|
expect(primaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11TwoDigit);
|
||||||
|
|
||||||
final List<TextPainter> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
||||||
expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
|
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text), labels00To23);
|
||||||
final List<TextPainter> secondaryInnerLabels = dialPainter.secondaryInnerLabels;
|
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels;
|
||||||
expect(secondaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
|
expect(secondaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text), labels12To11TwoDigit);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -597,6 +597,7 @@ const Map<Type, DistanceFunction<dynamic>> _kStandardDistanceFunctions = const <
|
|||||||
Offset: _offsetDistance,
|
Offset: _offsetDistance,
|
||||||
int: _intDistance,
|
int: _intDistance,
|
||||||
double: _doubleDistance,
|
double: _doubleDistance,
|
||||||
|
Rect: _rectDistance,
|
||||||
};
|
};
|
||||||
|
|
||||||
int _intDistance(int a, int b) => (b - a).abs();
|
int _intDistance(int a, int b) => (b - a).abs();
|
||||||
@ -610,6 +611,13 @@ double _maxComponentColorDistance(Color a, Color b) {
|
|||||||
return delta.toDouble();
|
return delta.toDouble();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _rectDistance(Rect a, Rect b) {
|
||||||
|
double delta = math.max<double>((a.left - b.left).abs(), (a.top - b.top).abs());
|
||||||
|
delta = math.max<double>(delta, (a.right - b.right).abs());
|
||||||
|
delta = math.max<double>(delta, (a.bottom - b.bottom).abs());
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
/// Asserts that two values are within a certain distance from each other.
|
/// Asserts that two values are within a certain distance from each other.
|
||||||
///
|
///
|
||||||
/// The distance is computed by a [DistanceFunction].
|
/// The distance is computed by a [DistanceFunction].
|
||||||
@ -669,11 +677,23 @@ class _IsWithinDistance<T> extends Matcher {
|
|||||||
'double value, but it returned $distance.'
|
'double value, but it returned $distance.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
matchState['distance'] = distance;
|
||||||
return distance <= epsilon;
|
return distance <= epsilon;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Description describe(Description description) => description.add('$value (±$epsilon)');
|
Description describe(Description description) => description.add('$value (±$epsilon)');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describeMismatch(
|
||||||
|
Object object,
|
||||||
|
Description mismatchDescription,
|
||||||
|
Map<dynamic, dynamic> matchState,
|
||||||
|
bool verbose,
|
||||||
|
) {
|
||||||
|
mismatchDescription.add('was ${matchState['distance']} away from the desired value.');
|
||||||
|
return mismatchDescription;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MoreOrLessEquals extends Matcher {
|
class _MoreOrLessEquals extends Matcher {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user