Unverified Commit 810a29d6 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Semantics framework updates (#18758)

Changes:

- Move the SemanticsConfiguration update from RenderToggleable to each subclass, so that Switch can use toggled.
- Add image, liveRegion, toggled properties to Semantics, SemanticsConfiguration, SemanticsNode
- Added semanticsLabel and excludeFromSemantics to Image (the latter so that we avoid creating a semantics node)
- Added onDismiss semantics action which maps to the modal escape on iOS and dismiss action on Android.
- Added dismiss and liveRegion to snackbar widget
- Updated custom painter semantics to handle image, liveRegion, toggle
- Updated relevant tests to use correct flag/action
parent 687f059a
...@@ -272,6 +272,7 @@ class RecipeCard extends StatelessWidget { ...@@ -272,6 +272,7 @@ class RecipeCard extends StatelessWidget {
recipe.imagePath, recipe.imagePath,
package: recipe.imagePackage, package: recipe.imagePackage,
fit: BoxFit.contain, fit: BoxFit.contain,
semanticLabel: recipe.name,
), ),
), ),
new Expanded( new Expanded(
......
...@@ -241,6 +241,12 @@ class _RenderCheckbox extends RenderToggleable { ...@@ -241,6 +241,12 @@ class _RenderCheckbox extends RenderToggleable {
super.value = newValue; super.value = newValue;
} }
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isChecked = value == true;
}
// The square outer bounds of the checkbox at t, with the specified origin. // The square outer bounds of the checkbox at t, with the specified origin.
// At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width) // At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width)
// At t == 0.5, .. is _kEdgeSize - _kStrokeWidth // At t == 0.5, .. is _kEdgeSize - _kStrokeWidth
......
...@@ -236,6 +236,8 @@ class _RenderRadio extends RenderToggleable { ...@@ -236,6 +236,8 @@ class _RenderRadio extends RenderToggleable {
@override @override
void describeSemanticsConfiguration(SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
config.isInMutuallyExclusiveGroup = true; config
..isInMutuallyExclusiveGroup = true
..isChecked = value == true;
} }
} }
...@@ -1713,6 +1713,10 @@ class _PersistentBottomSheetState extends State<_PersistentBottomSheet> { ...@@ -1713,6 +1713,10 @@ class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
}, },
child: new Semantics( child: new Semantics(
container: true, container: true,
onDismiss: () {
close();
widget.onClosing();
},
child: new BottomSheet( child: new BottomSheet(
animationController: widget.animationController, animationController: widget.animationController,
enableDrag: widget.enableDrag, enableDrag: widget.enableDrag,
......
...@@ -224,7 +224,11 @@ class SnackBar extends StatelessWidget { ...@@ -224,7 +224,11 @@ class SnackBar extends StatelessWidget {
); );
}, },
child: new Semantics( child: new Semantics(
liveRegion: true,
container: true, container: true,
onDismiss: () {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
},
child: new Dismissible( child: new Dismissible(
key: const Key('dismissible'), key: const Key('dismissible'),
direction: DismissDirection.down, direction: DismissDirection.down,
......
...@@ -419,6 +419,12 @@ class _RenderSwitch extends RenderToggleable { ...@@ -419,6 +419,12 @@ class _RenderSwitch extends RenderToggleable {
markNeedsPaint(); markNeedsPaint();
} }
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isToggled = value == true;
}
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
......
...@@ -339,7 +339,6 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -339,7 +339,6 @@ abstract class RenderToggleable extends RenderConstrainedBox {
config.isEnabled = isInteractive; config.isEnabled = isInteractive;
if (isInteractive) if (isInteractive)
config.onTap = _handleTap; config.onTap = _handleTap;
config.isChecked = _value == true;
} }
@override @override
......
...@@ -846,6 +846,15 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -846,6 +846,15 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.namesRoute != null) { if (properties.namesRoute != null) {
config.namesRoute = properties.namesRoute; config.namesRoute = properties.namesRoute;
} }
if (properties.liveRegion != null) {
config.liveRegion = properties.liveRegion;
}
if (properties.toggled != null) {
config.isToggled = properties.toggled;
}
if (properties.image != null) {
config.isImage = properties.image;
}
if (properties.label != null) { if (properties.label != null) {
config.label = properties.label; config.label = properties.label;
} }
...@@ -912,6 +921,9 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -912,6 +921,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.onDidLoseAccessibilityFocus != null) { if (properties.onDidLoseAccessibilityFocus != null) {
config.onDidLoseAccessibilityFocus = properties.onDidLoseAccessibilityFocus; config.onDidLoseAccessibilityFocus = properties.onDidLoseAccessibilityFocus;
} }
if (properties.onDismiss != null) {
config.onDismiss = properties.onDismiss;
}
newChild.updateWith( newChild.updateWith(
config: config, config: config,
......
...@@ -3168,6 +3168,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3168,6 +3168,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool explicitChildNodes, bool explicitChildNodes,
bool enabled, bool enabled,
bool checked, bool checked,
bool toggled,
bool selected, bool selected,
bool button, bool button,
bool header, bool header,
...@@ -3178,6 +3179,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3178,6 +3179,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool scopesRoute, bool scopesRoute,
bool namesRoute, bool namesRoute,
bool hidden, bool hidden,
bool image,
bool liveRegion,
bool isSwitch,
String label, String label,
String value, String value,
String increasedValue, String increasedValue,
...@@ -3186,6 +3190,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3186,6 +3190,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
TextDirection textDirection, TextDirection textDirection,
SemanticsSortKey sortKey, SemanticsSortKey sortKey,
VoidCallback onTap, VoidCallback onTap,
VoidCallback onDismiss,
VoidCallback onLongPress, VoidCallback onLongPress,
VoidCallback onScrollLeft, VoidCallback onScrollLeft,
VoidCallback onScrollRight, VoidCallback onScrollRight,
...@@ -3207,6 +3212,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3207,6 +3212,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_explicitChildNodes = explicitChildNodes, _explicitChildNodes = explicitChildNodes,
_enabled = enabled, _enabled = enabled,
_checked = checked, _checked = checked,
_toggled = toggled,
_selected = selected, _selected = selected,
_button = button, _button = button,
_header = header, _header = header,
...@@ -3216,7 +3222,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3216,7 +3222,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_obscured = obscured, _obscured = obscured,
_scopesRoute = scopesRoute, _scopesRoute = scopesRoute,
_namesRoute = namesRoute, _namesRoute = namesRoute,
_liveRegion = liveRegion,
_hidden = hidden, _hidden = hidden,
_image = image,
_onDismiss = onDismiss,
_label = label, _label = label,
_value = value, _value = value,
_increasedValue = increasedValue, _increasedValue = increasedValue,
...@@ -3408,6 +3417,38 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3408,6 +3417,38 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the [SemanticsNode.isImage] semantic to the given
/// value.
bool get image => _image;
bool _image;
set image(bool value) {
if (_image == value)
return;
_image = value;
}
/// If non-null, sets the [SemanticsNode.isLiveRegion] semantic to the given
/// value.
bool get liveRegion => _liveRegion;
bool _liveRegion;
set liveRegion(bool value) {
if (_liveRegion == value)
return;
_liveRegion = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.isToggled] semantic to the given
/// value.
bool get toggled => _toggled;
bool _toggled;
set toggled(bool value) {
if (_toggled == value)
return;
_toggled = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.label] semantic to the given value. /// If non-null, sets the [SemanticsNode.label] semantic to the given value.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
...@@ -3516,6 +3557,24 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3516,6 +3557,24 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// The handler for [SemanticsAction.dismiss].
///
/// This is a request to dismiss the currently focused node.
///
/// TalkBack users on Android can trigger this action in the local context
/// menu, and VoiceOver users on iOS can trigger this action with a standard
/// gesture or menu option.
VoidCallback get onDismiss => _onDismiss;
VoidCallback _onDismiss;
set onDismiss(VoidCallback handler) {
if (_onDismiss == handler)
return;
final bool hadValue = _onDismiss != null;
_onDismiss = handler;
if ((handler != null) == hadValue)
markNeedsSemanticsUpdate();
}
/// The handler for [SemanticsAction.longPress]. /// The handler for [SemanticsAction.longPress].
/// ///
/// This is the semantic equivalent of a user pressing and holding the screen /// This is the semantic equivalent of a user pressing and holding the screen
...@@ -3848,11 +3907,15 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3848,11 +3907,15 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.explicitChildNodes = explicitChildNodes; config.explicitChildNodes = explicitChildNodes;
assert((scopesRoute == true && explicitChildNodes == true) || scopesRoute != true, assert((scopesRoute == true && explicitChildNodes == true) || scopesRoute != true,
'explicitChildNodes must be set to true if scopes route is true'); 'explicitChildNodes must be set to true if scopes route is true');
assert(!(toggled == true && checked == true),
'A semantics node cannot be toggled and checked at the same time');
if (enabled != null) if (enabled != null)
config.isEnabled = enabled; config.isEnabled = enabled;
if (checked != null) if (checked != null)
config.isChecked = checked; config.isChecked = checked;
if (toggled != null)
config.isToggled = toggled;
if (selected != null) if (selected != null)
config.isSelected = selected; config.isSelected = selected;
if (button != null) if (button != null)
...@@ -3869,6 +3932,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3869,6 +3932,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isObscured = obscured; config.isObscured = obscured;
if (hidden != null) if (hidden != null)
config.isHidden = hidden; config.isHidden = hidden;
if (image != null)
config.isImage = image;
if (label != null) if (label != null)
config.label = label; config.label = label;
if (value != null) if (value != null)
...@@ -3883,6 +3948,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3883,6 +3948,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.scopesRoute = scopesRoute; config.scopesRoute = scopesRoute;
if (namesRoute != null) if (namesRoute != null)
config.namesRoute = namesRoute; config.namesRoute = namesRoute;
if (liveRegion != null)
config.liveRegion = liveRegion;
if (textDirection != null) if (textDirection != null)
config.textDirection = textDirection; config.textDirection = textDirection;
if (sortKey != null) if (sortKey != null)
...@@ -3894,6 +3961,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3894,6 +3961,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.onTap = _performTap; config.onTap = _performTap;
if (onLongPress != null) if (onLongPress != null)
config.onLongPress = _performLongPress; config.onLongPress = _performLongPress;
if (onDismiss != null)
config.onDismiss = _performDismiss;
if (onScrollLeft != null) if (onScrollLeft != null)
config.onScrollLeft = _performScrollLeft; config.onScrollLeft = _performScrollLeft;
if (onScrollRight != null) if (onScrollRight != null)
...@@ -3936,6 +4005,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3936,6 +4005,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
onLongPress(); onLongPress();
} }
void _performDismiss() {
if (onDismiss != null)
onDismiss();
}
void _performScrollLeft() { void _performScrollLeft() {
if (onScrollLeft != null) if (onScrollLeft != null)
onScrollLeft(); onScrollLeft();
......
...@@ -419,6 +419,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -419,6 +419,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.enabled, this.enabled,
this.checked, this.checked,
this.selected, this.selected,
this.toggled,
this.button, this.button,
this.header, this.header,
this.textField, this.textField,
...@@ -428,6 +429,8 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -428,6 +429,8 @@ class SemanticsProperties extends DiagnosticableTree {
this.obscured, this.obscured,
this.scopesRoute, this.scopesRoute,
this.namesRoute, this.namesRoute,
this.image,
this.liveRegion,
this.label, this.label,
this.value, this.value,
this.increasedValue, this.increasedValue,
...@@ -451,6 +454,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -451,6 +454,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.onSetSelection, this.onSetSelection,
this.onDidGainAccessibilityFocus, this.onDidGainAccessibilityFocus,
this.onDidLoseAccessibilityFocus, this.onDidLoseAccessibilityFocus,
this.onDismiss,
this.customSemanticsActions, this.customSemanticsActions,
}); });
...@@ -465,8 +469,17 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -465,8 +469,17 @@ class SemanticsProperties extends DiagnosticableTree {
/// If non-null, indicates that this subtree represents a checkbox /// If non-null, indicates that this subtree represents a checkbox
/// or similar widget with a "checked" state, and what its current /// or similar widget with a "checked" state, and what its current
/// state is. /// state is.
///
/// This is mutually exclusive with [toggled].
final bool checked; final bool checked;
/// If non-null, indicates that this subtree represents a toggle switch
/// or similar widget with an "on" state, and what its current
/// state is.
///
/// This is mutually exclusive with [checked].
final bool toggled;
/// If non-null indicates that this subtree represents something that can be /// If non-null indicates that this subtree represents something that can be
/// in a selected or unselected state, and what its current state is. /// in a selected or unselected state, and what its current state is.
/// ///
...@@ -553,6 +566,36 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -553,6 +566,36 @@ class SemanticsProperties extends DiagnosticableTree {
/// * [SemanticsFlag.namesRoute] for a description of how the name is used. /// * [SemanticsFlag.namesRoute] for a description of how the name is used.
final bool namesRoute; final bool namesRoute;
/// If non-null, whether the node represents an image.
///
/// See also:
///
/// * [SemanticsFlag.image], for the flag this setting controls.
final bool image;
/// If non-null, whether the node should be considered a live region.
///
/// On Android, when a live region semantics node is first created TalkBack
/// will make a polite announcement of the current label. This announcement
/// occurs even if the node is not focused. Subsequent polite announcements
/// can be made by sending a [UpdateLiveRegionEvent] semantics event. The
/// announcement will only be made if the node's label has changed since the
/// last update.
///
/// On iOS, no announcements are made but the node is marked as
/// `UIAccessibilityTraitUpdatesFrequently`.
///
/// An example of a live region is the [Snackbar] widget. When it appears
/// on the screen it may be difficult to focus to read the label. A live
/// region causes an initial polite announcement to be generated
/// automatically.
///
/// See also:
/// * [SemanticsFlag.liveRegion], the semantics flag this setting controls.
/// * [SemanticsConfiguration.liveRegion], for a full description of a live region.
/// * [UpdateLiveRegionEvent], to trigger a polite announcement of a live region.
final bool liveRegion;
/// Provides a textual description of the widget. /// Provides a textual description of the widget.
/// ///
/// If a label is provided, there must either by an ambient [Directionality] /// If a label is provided, there must either by an ambient [Directionality]
...@@ -815,6 +858,15 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -815,6 +858,15 @@ class SemanticsProperties extends DiagnosticableTree {
/// * [FocusNode], [FocusScope], [FocusManager], which manage the input focus /// * [FocusNode], [FocusScope], [FocusManager], which manage the input focus
final VoidCallback onDidLoseAccessibilityFocus; final VoidCallback onDidLoseAccessibilityFocus;
/// The handler for [SemanticsAction.dismiss].
///
/// This is a request to dismiss the currently focused node.
///
/// TalkBack users on Android can trigger this action in the local context
/// menu, and VoiceOver users on iOS can trigger this action with a standard
/// gesture or menu option.
final VoidCallback onDismiss;
/// A map from each supported [CustomSemanticsAction] to a provided handler. /// A map from each supported [CustomSemanticsAction] to a provided handler.
/// ///
/// The handler associated with each custom action is called whenever a /// The handler associated with each custom action is called whenever a
...@@ -2346,6 +2398,20 @@ class SemanticsConfiguration { ...@@ -2346,6 +2398,20 @@ class SemanticsConfiguration {
_onScrollLeft = value; _onScrollLeft = value;
} }
/// The handler for [SemanticsAction.dismiss].
///
/// This is a request to dismiss the currently focused node.
///
/// TalkBack users on Android can trigger this action in the local context
/// menu, and VoiceOver users on iOS can trigger this action with a standard
/// gesture or menu option.
VoidCallback get onDismiss => _onDismiss;
VoidCallback _onDismiss;
set onDismiss(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.dismiss, value);
_onDismiss = value;
}
/// The handler for [SemanticsAction.scrollRight]. /// The handler for [SemanticsAction.scrollRight].
/// ///
/// This is the semantic equivalent of a user moving their finger across the /// This is the semantic equivalent of a user moving their finger across the
...@@ -2771,6 +2837,34 @@ class SemanticsConfiguration { ...@@ -2771,6 +2837,34 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.namesRoute, value); _setFlag(SemanticsFlag.namesRoute, value);
} }
/// Whether the semantics node represents an image.
bool get isImage => _hasFlag(SemanticsFlag.isImage);
set isImage(bool value) {
_setFlag(SemanticsFlag.isImage, value);
}
/// Whether the semantics node is a live region.
///
/// On Android, when a live region semantics node is first created TalkBack
/// will make a polite announcement of the current label. This announcement
/// occurs even if the node is not focused. Subsequent polite announcements
/// can be made by sending a [UpdateLiveRegionEvent] semantics event. The
/// announcement will only be made if the node's label has changed since the
/// last update.
///
/// An example of a live region is the [Snackbar] widget. When it appears
/// on the screen it may be difficult to focus to read the label. A live
/// region causes an initial polite announcement to be generated
/// automatically.
///
/// See also:
///
/// * [SemanticsFlag.isLiveRegion], the semantics flag that this setting controls.
bool get liveRegion => _hasFlag(SemanticsFlag.isLiveRegion);
set liveRegion(bool value) {
_setFlag(SemanticsFlag.isLiveRegion, value);
}
/// The reading direction for the text in [label], [value], [hint], /// The reading direction for the text in [label], [value], [hint],
/// [increasedValue], and [decreasedValue]. /// [increasedValue], and [decreasedValue].
TextDirection get textDirection => _textDirection; TextDirection get textDirection => _textDirection;
...@@ -2804,7 +2898,8 @@ class SemanticsConfiguration { ...@@ -2804,7 +2898,8 @@ class SemanticsConfiguration {
} }
/// If this node has Boolean state that can be controlled by the user, whether /// If this node has Boolean state that can be controlled by the user, whether
/// that state is on or off, corresponding to true and false, respectively. /// that state is checked or unchecked, corresponding to true and false,
/// respectively.
/// ///
/// Do not call the setter for this field if the owning [RenderObject] doesn't /// Do not call the setter for this field if the owning [RenderObject] doesn't
/// have checked/unchecked state that can be controlled by the user. /// have checked/unchecked state that can be controlled by the user.
...@@ -2817,6 +2912,20 @@ class SemanticsConfiguration { ...@@ -2817,6 +2912,20 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.isChecked, value); _setFlag(SemanticsFlag.isChecked, value);
} }
/// If this node has Boolean state that can be controlled by the user, whether
/// that state is on or off, corresponding to true and false, respectively.
///
/// Do not call the setter for this field if the owning [RenderObject] doesn't
/// have on/off state that can be controlled by the user.
///
/// The getter returns null if the owning [RenderObject] does not have
/// on/off state.
bool get isToggled => _hasFlag(SemanticsFlag.hasToggledState) ? _hasFlag(SemanticsFlag.isToggled) : null;
set isToggled(bool value) {
_setFlag(SemanticsFlag.hasToggledState, true);
_setFlag(SemanticsFlag.isToggled, value);
}
/// Whether the owning RenderObject corresponds to UI that allows the user to /// Whether the owning RenderObject corresponds to UI that allows the user to
/// pick one of several mutually exclusive options. /// pick one of several mutually exclusive options.
/// ///
......
...@@ -133,3 +133,21 @@ class TapSemanticEvent extends SemanticsEvent { ...@@ -133,3 +133,21 @@ class TapSemanticEvent extends SemanticsEvent {
@override @override
Map<String, dynamic> getDataMap() => const <String, dynamic>{}; Map<String, dynamic> getDataMap() => const <String, dynamic>{};
} }
/// An event which triggers a polite announcement of a live region.
///
/// This requires that the semantics node has already been marked as a live
/// region. On Android, TalkBack will make a verbal announcement, as long as
/// the label of the semantics node has changed since the last live region
/// update. iOS does not currently support this event.
///
/// See also:
///
/// * [SemanticsFlag.liveRegion], for a description of live regions.
class UpdateLiveRegionEvent extends SemanticsEvent {
/// Creates a new [UpdateLiveRegionEvent].
const UpdateLiveRegionEvent() : super('updateLiveRegion');
@override
Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}
...@@ -5080,6 +5080,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5080,6 +5080,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool enabled, bool enabled,
bool checked, bool checked,
bool selected, bool selected,
bool toggled,
bool button, bool button,
bool header, bool header,
bool textField, bool textField,
...@@ -5089,6 +5090,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5089,6 +5090,8 @@ class Semantics extends SingleChildRenderObjectWidget {
bool scopesRoute, bool scopesRoute,
bool namesRoute, bool namesRoute,
bool hidden, bool hidden,
bool image,
bool liveRegion,
String label, String label,
String value, String value,
String increasedValue, String increasedValue,
...@@ -5107,6 +5110,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5107,6 +5110,7 @@ class Semantics extends SingleChildRenderObjectWidget {
VoidCallback onCopy, VoidCallback onCopy,
VoidCallback onCut, VoidCallback onCut,
VoidCallback onPaste, VoidCallback onPaste,
VoidCallback onDismiss,
MoveCursorHandler onMoveCursorForwardByCharacter, MoveCursorHandler onMoveCursorForwardByCharacter,
MoveCursorHandler onMoveCursorBackwardByCharacter, MoveCursorHandler onMoveCursorBackwardByCharacter,
SetSelectionHandler onSetSelection, SetSelectionHandler onSetSelection,
...@@ -5121,6 +5125,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5121,6 +5125,7 @@ class Semantics extends SingleChildRenderObjectWidget {
properties: new SemanticsProperties( properties: new SemanticsProperties(
enabled: enabled, enabled: enabled,
checked: checked, checked: checked,
toggled: toggled,
selected: selected, selected: selected,
button: button, button: button,
header: header, header: header,
...@@ -5131,6 +5136,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5131,6 +5136,8 @@ class Semantics extends SingleChildRenderObjectWidget {
scopesRoute: scopesRoute, scopesRoute: scopesRoute,
namesRoute: namesRoute, namesRoute: namesRoute,
hidden: hidden, hidden: hidden,
image: image,
liveRegion: liveRegion,
label: label, label: label,
value: value, value: value,
increasedValue: increasedValue, increasedValue: increasedValue,
...@@ -5153,8 +5160,10 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5153,8 +5160,10 @@ class Semantics extends SingleChildRenderObjectWidget {
onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter, onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter,
onDidGainAccessibilityFocus: onDidGainAccessibilityFocus, onDidGainAccessibilityFocus: onDidGainAccessibilityFocus,
onDidLoseAccessibilityFocus: onDidLoseAccessibilityFocus, onDidLoseAccessibilityFocus: onDidLoseAccessibilityFocus,
onDismiss: onDismiss,
onSetSelection: onSetSelection,
customSemanticsActions: customSemanticsActions, customSemanticsActions: customSemanticsActions,
onSetSelection: onSetSelection,), ),
); );
/// Creates a semantic annotation using [SemanticsProperties]. /// Creates a semantic annotation using [SemanticsProperties].
...@@ -5208,16 +5217,19 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5208,16 +5217,19 @@ class Semantics extends SingleChildRenderObjectWidget {
explicitChildNodes: explicitChildNodes, explicitChildNodes: explicitChildNodes,
enabled: properties.enabled, enabled: properties.enabled,
checked: properties.checked, checked: properties.checked,
toggled: properties.toggled,
selected: properties.selected, selected: properties.selected,
button: properties.button, button: properties.button,
header: properties.header, header: properties.header,
textField: properties.textField, textField: properties.textField,
focused: properties.focused, focused: properties.focused,
liveRegion: properties.liveRegion,
inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup, inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup,
obscured: properties.obscured, obscured: properties.obscured,
scopesRoute: properties.scopesRoute, scopesRoute: properties.scopesRoute,
namesRoute: properties.namesRoute, namesRoute: properties.namesRoute,
hidden: properties.hidden, hidden: properties.hidden,
image: properties.image,
label: properties.label, label: properties.label,
value: properties.value, value: properties.value,
increasedValue: properties.increasedValue, increasedValue: properties.increasedValue,
...@@ -5234,6 +5246,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5234,6 +5246,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onIncrease: properties.onIncrease, onIncrease: properties.onIncrease,
onDecrease: properties.onDecrease, onDecrease: properties.onDecrease,
onCopy: properties.onCopy, onCopy: properties.onCopy,
onDismiss: properties.onDismiss,
onCut: properties.onCut, onCut: properties.onCut,
onPaste: properties.onPaste, onPaste: properties.onPaste,
onMoveCursorForwardByCharacter: properties.onMoveCursorForwardByCharacter, onMoveCursorForwardByCharacter: properties.onMoveCursorForwardByCharacter,
...@@ -5261,10 +5274,11 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5261,10 +5274,11 @@ class Semantics extends SingleChildRenderObjectWidget {
void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) { void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) {
renderObject renderObject
..container = container ..container = container
..scopesRoute = properties.scopesRoute
..explicitChildNodes = explicitChildNodes ..explicitChildNodes = explicitChildNodes
..scopesRoute = properties.scopesRoute
..enabled = properties.enabled ..enabled = properties.enabled
..checked = properties.checked ..checked = properties.checked
..toggled = properties.toggled
..selected = properties.selected ..selected = properties.selected
..button = properties.button ..button = properties.button
..header = properties.header ..header = properties.header
...@@ -5273,6 +5287,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5273,6 +5287,8 @@ class Semantics extends SingleChildRenderObjectWidget {
..inMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup ..inMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup
..obscured = properties.obscured ..obscured = properties.obscured
..hidden = properties.hidden ..hidden = properties.hidden
..image = properties.image
..liveRegion = properties.liveRegion
..label = properties.label ..label = properties.label
..value = properties.value ..value = properties.value
..increasedValue = properties.increasedValue ..increasedValue = properties.increasedValue
...@@ -5288,6 +5304,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5288,6 +5304,7 @@ class Semantics extends SingleChildRenderObjectWidget {
..onScrollUp = properties.onScrollUp ..onScrollUp = properties.onScrollUp
..onScrollDown = properties.onScrollDown ..onScrollDown = properties.onScrollDown
..onIncrease = properties.onIncrease ..onIncrease = properties.onIncrease
..onDismiss = properties.onDismiss
..onDecrease = properties.onDecrease ..onDecrease = properties.onDecrease
..onCopy = properties.onCopy ..onCopy = properties.onCopy
..onCut = properties.onCut ..onCut = properties.onCut
......
...@@ -120,9 +120,13 @@ class Image extends StatefulWidget { ...@@ -120,9 +120,13 @@ class Image extends StatefulWidget {
/// widget should be placed in a context that sets tight layout constraints. /// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which /// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes. /// will result in ugly layout changes.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
const Image({ const Image({
Key key, Key key,
@required this.image, @required this.image,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width, this.width,
this.height, this.height,
this.color, this.color,
...@@ -152,9 +156,13 @@ class Image extends StatefulWidget { ...@@ -152,9 +156,13 @@ class Image extends StatefulWidget {
/// ///
/// An optional [headers] argument can be used to send custom HTTP headers /// An optional [headers] argument can be used to send custom HTTP headers
/// with the image request. /// with the image request.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
Image.network(String src, { Image.network(String src, {
Key key, Key key,
double scale = 1.0, double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width, this.width,
this.height, this.height,
this.color, this.color,
...@@ -183,9 +191,13 @@ class Image extends StatefulWidget { ...@@ -183,9 +191,13 @@ class Image extends StatefulWidget {
/// ///
/// On Android, this may require the /// On Android, this may require the
/// `android.permission.READ_EXTERNAL_STORAGE` permission. /// `android.permission.READ_EXTERNAL_STORAGE` permission.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
Image.file(File file, { Image.file(File file, {
Key key, Key key,
double scale = 1.0, double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width, this.width,
this.height, this.height,
this.color, this.color,
...@@ -217,6 +229,8 @@ class Image extends StatefulWidget { ...@@ -217,6 +229,8 @@ class Image extends StatefulWidget {
/// ///
/// * If the `scale` argument is provided and is not null, then the exact /// * If the `scale` argument is provided and is not null, then the exact
/// asset specified will be used. /// asset specified will be used.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
// //
// TODO(ianh): Implement the following (see ../services/image_resolution.dart): // TODO(ianh): Implement the following (see ../services/image_resolution.dart):
// /// // ///
...@@ -319,6 +333,8 @@ class Image extends StatefulWidget { ...@@ -319,6 +333,8 @@ class Image extends StatefulWidget {
Image.asset(String name, { Image.asset(String name, {
Key key, Key key,
AssetBundle bundle, AssetBundle bundle,
this.semanticLabel,
this.excludeFromSemantics = false,
double scale, double scale,
this.width, this.width,
this.height, this.height,
...@@ -347,9 +363,13 @@ class Image extends StatefulWidget { ...@@ -347,9 +363,13 @@ class Image extends StatefulWidget {
/// widget should be placed in a context that sets tight layout constraints. /// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which /// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes. /// will result in ugly layout changes.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
Image.memory(Uint8List bytes, { Image.memory(Uint8List bytes, {
Key key, Key key,
double scale = 1.0, double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width, this.width,
this.height, this.height,
this.color, this.color,
...@@ -472,6 +492,18 @@ class Image extends StatefulWidget { ...@@ -472,6 +492,18 @@ class Image extends StatefulWidget {
/// (false), when the image provider changes. /// (false), when the image provider changes.
final bool gaplessPlayback; final bool gaplessPlayback;
/// A Semantic description of the image.
///
/// Used to provide a description of the image to TalkBack on Andoid, and
/// VoiceOver on iOS.
final String semanticLabel;
/// Whether to exclude this image from semantics.
///
/// Useful for images which do not contribute meaningful information to an
/// application.
final bool excludeFromSemantics;
@override @override
_ImageState createState() => new _ImageState(); _ImageState createState() => new _ImageState();
...@@ -488,6 +520,8 @@ class Image extends StatefulWidget { ...@@ -488,6 +520,8 @@ class Image extends StatefulWidget {
properties.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); properties.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat));
properties.add(new DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null)); properties.add(new DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null));
properties.add(new FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction')); properties.add(new FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction'));
properties.add(new StringProperty('semanticLabel', semanticLabel, defaultValue: null));
properties.add(new DiagnosticsProperty<bool>('this.excludeFromSemantics', excludeFromSemantics));
} }
} }
...@@ -578,7 +612,7 @@ class _ImageState extends State<Image> { ...@@ -578,7 +612,7 @@ class _ImageState extends State<Image> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new RawImage( final RawImage image = RawImage(
image: _imageInfo?.image, image: _imageInfo?.image,
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
...@@ -591,6 +625,14 @@ class _ImageState extends State<Image> { ...@@ -591,6 +625,14 @@ class _ImageState extends State<Image> {
centerSlice: widget.centerSlice, centerSlice: widget.centerSlice,
matchTextDirection: widget.matchTextDirection, matchTextDirection: widget.matchTextDirection,
); );
if (widget.excludeFromSemantics)
return image;
return new Semantics(
container: widget.semanticLabel != null,
image: true,
label: widget.semanticLabel == null ? '' : widget.semanticLabel,
child: image,
);
} }
@override @override
......
...@@ -101,8 +101,8 @@ void main() { ...@@ -101,8 +101,8 @@ void main() {
rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0), rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
transform: null, transform: null,
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState, SemanticsFlag.hasToggledState,
SemanticsFlag.isChecked, SemanticsFlag.isToggled,
SemanticsFlag.hasEnabledState, SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled SemanticsFlag.isEnabled
], ],
......
...@@ -327,6 +327,7 @@ void _defineTests() { ...@@ -327,6 +327,7 @@ void _defineTests() {
key: const ValueKey<int>(1), key: const ValueKey<int>(1),
rect: new Rect.fromLTRB(1.0, 2.0, 3.0, 4.0), rect: new Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: new SemanticsProperties( properties: new SemanticsProperties(
onDismiss: () => performedActions.add(SemanticsAction.dismiss),
onTap: () => performedActions.add(SemanticsAction.tap), onTap: () => performedActions.add(SemanticsAction.tap),
onLongPress: () => performedActions.add(SemanticsAction.longPress), onLongPress: () => performedActions.add(SemanticsAction.longPress),
onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft), onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
...@@ -349,8 +350,7 @@ void _defineTests() { ...@@ -349,8 +350,7 @@ void _defineTests() {
)); ));
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet() final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
..remove(SemanticsAction.customAction) // customAction is not user-exposed. ..remove(SemanticsAction.customAction) // customAction is not user-exposed.
..remove(SemanticsAction.showOnScreen) // showOnScreen is not user-exposed ..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
..remove(SemanticsAction.dismiss); // TODO(jonahwilliams): update when dismiss is exposed.
const int expectedId = 2; const int expectedId = 2;
final TestSemantics expectedSemantics = new TestSemantics.root( final TestSemantics expectedSemantics = new TestSemantics.root(
...@@ -397,7 +397,7 @@ void _defineTests() { ...@@ -397,7 +397,7 @@ void _defineTests() {
testWidgets('Supports all flags', (WidgetTester tester) async { testWidgets('Supports all flags', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
// checked state and toggled state are mutually exclusive.
await tester.pumpWidget(new CustomPaint( await tester.pumpWidget(new CustomPaint(
painter: new _PainterWithSemantics( painter: new _PainterWithSemantics(
semantics: new CustomPainterSemantics( semantics: new CustomPainterSemantics(
...@@ -416,18 +416,17 @@ void _defineTests() { ...@@ -416,18 +416,17 @@ void _defineTests() {
obscured: true, obscured: true,
scopesRoute: true, scopesRoute: true,
namesRoute: true, namesRoute: true,
image: true,
liveRegion: true,
), ),
), ),
), ),
)); ));
// TODO(jonahwilliams): update when the following semantics flags are added. List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
final List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
flags flags
..remove(SemanticsFlag.isImage)
..remove(SemanticsFlag.hasToggledState) ..remove(SemanticsFlag.hasToggledState)
..remove(SemanticsFlag.isToggled) ..remove(SemanticsFlag.isToggled);
..remove(SemanticsFlag.isLiveRegion); TestSemantics expectedSemantics = new TestSemantics.root(
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 1,
...@@ -443,6 +442,50 @@ void _defineTests() { ...@@ -443,6 +442,50 @@ void _defineTests() {
); );
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true)); expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
await tester.pumpWidget(new CustomPaint(
painter: new _PainterWithSemantics(
semantics: new CustomPainterSemantics(
key: const ValueKey<int>(1),
rect: new Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: const SemanticsProperties(
enabled: true,
toggled: true,
selected: true,
hidden: true,
button: true,
textField: true,
focused: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
scopesRoute: true,
namesRoute: true,
image: true,
liveRegion: true,
),
),
),
));
flags = SemanticsFlag.values.values.toList();
flags
..remove(SemanticsFlag.hasCheckedState)
..remove(SemanticsFlag.isChecked);
expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 2,
rect: TestSemantics.fullScreen,
flags: flags,
),
]
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
semantics.dispose(); semantics.dispose();
}); });
......
...@@ -133,10 +133,12 @@ Widget buildImageAtRatio(String image, Key key, double ratio, bool inferSize, [A ...@@ -133,10 +133,12 @@ Widget buildImageAtRatio(String image, Key key, double ratio, bool inferSize, [A
child: inferSize ? child: inferSize ?
new Image( new Image(
key: key, key: key,
excludeFromSemantics: true,
image: new TestAssetImage(image) image: new TestAssetImage(image)
) : ) :
new Image( new Image(
key: key, key: key,
excludeFromSemantics: true,
image: new TestAssetImage(image), image: new TestAssetImage(image),
height: imageSize, height: imageSize,
width: imageSize, width: imageSize,
......
...@@ -12,6 +12,7 @@ import 'package:flutter/widgets.dart'; ...@@ -12,6 +12,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../painting/image_data.dart'; import '../painting/image_data.dart';
import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async { testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
...@@ -21,7 +22,8 @@ void main() { ...@@ -21,7 +22,8 @@ void main() {
new Container( new Container(
key: key, key: key,
child: new Image( child: new Image(
image: imageProvider1 image: imageProvider1,
excludeFromSemantics: true,
) )
), ),
null, null,
...@@ -42,7 +44,8 @@ void main() { ...@@ -42,7 +44,8 @@ void main() {
new Container( new Container(
key: key, key: key,
child: new Image( child: new Image(
image: imageProvider2 image: imageProvider2,
excludeFromSemantics: true,
) )
), ),
null, null,
...@@ -61,7 +64,8 @@ void main() { ...@@ -61,7 +64,8 @@ void main() {
key: key, key: key,
child: new Image( child: new Image(
gaplessPlayback: true, gaplessPlayback: true,
image: imageProvider1 image: imageProvider1,
excludeFromSemantics: true,
) )
), ),
null, null,
...@@ -83,7 +87,8 @@ void main() { ...@@ -83,7 +87,8 @@ void main() {
key: key, key: key,
child: new Image( child: new Image(
gaplessPlayback: true, gaplessPlayback: true,
image: imageProvider2 image: imageProvider2,
excludeFromSemantics: true,
) )
), ),
null, null,
...@@ -100,7 +105,8 @@ void main() { ...@@ -100,7 +105,8 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new Image( new Image(
key: key, key: key,
image: imageProvider1 image: imageProvider1,
excludeFromSemantics: true,
), ),
null, null,
EnginePhase.layout EnginePhase.layout
...@@ -119,7 +125,8 @@ void main() { ...@@ -119,7 +125,8 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new Image( new Image(
key: key, key: key,
image: imageProvider2 image: imageProvider2,
excludeFromSemantics: true,
), ),
null, null,
EnginePhase.layout EnginePhase.layout
...@@ -136,7 +143,8 @@ void main() { ...@@ -136,7 +143,8 @@ void main() {
new Image( new Image(
key: key, key: key,
gaplessPlayback: true, gaplessPlayback: true,
image: imageProvider1 image: imageProvider1,
excludeFromSemantics: true,
), ),
null, null,
EnginePhase.layout EnginePhase.layout
...@@ -156,6 +164,7 @@ void main() { ...@@ -156,6 +164,7 @@ void main() {
new Image( new Image(
key: key, key: key,
gaplessPlayback: true, gaplessPlayback: true,
excludeFromSemantics: true,
image: imageProvider2 image: imageProvider2
), ),
null, null,
...@@ -188,6 +197,7 @@ void main() { ...@@ -188,6 +197,7 @@ void main() {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
child: new Image( child: new Image(
excludeFromSemantics: true,
key: imageKey, key: imageKey,
image: imageProvider image: imageProvider
), ),
...@@ -214,6 +224,7 @@ void main() { ...@@ -214,6 +224,7 @@ void main() {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
child: new Image( child: new Image(
excludeFromSemantics: true,
key: imageKey, key: imageKey,
image: imageProvider image: imageProvider
), ),
...@@ -243,6 +254,7 @@ void main() { ...@@ -243,6 +254,7 @@ void main() {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
child: new Image( child: new Image(
excludeFromSemantics: true,
key: imageKey, key: imageKey,
image: imageProvider image: imageProvider
) )
...@@ -280,6 +292,7 @@ void main() { ...@@ -280,6 +292,7 @@ void main() {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
child: new Image( child: new Image(
excludeFromSemantics: true,
key: imageKey, key: imageKey,
image: imageProvider image: imageProvider
) )
...@@ -293,7 +306,7 @@ void main() { ...@@ -293,7 +306,7 @@ void main() {
testWidgets('Verify Image stops listening to ImageStream', (WidgetTester tester) async { testWidgets('Verify Image stops listening to ImageStream', (WidgetTester tester) async {
final TestImageProvider imageProvider = new TestImageProvider(); final TestImageProvider imageProvider = new TestImageProvider();
await tester.pumpWidget(new Image(image: imageProvider)); await tester.pumpWidget(new Image(image: imageProvider, excludeFromSemantics: true));
final State<Image> image = tester.state/*State<Image>*/(find.byType(Image)); final State<Image> image = tester.state/*State<Image>*/(find.byType(Image));
expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners), pixels: null)')); expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners), pixels: null)'));
imageProvider.complete(); imageProvider.complete();
...@@ -304,12 +317,13 @@ void main() { ...@@ -304,12 +317,13 @@ void main() {
}); });
testWidgets('Image.memory control test', (WidgetTester tester) async { testWidgets('Image.memory control test', (WidgetTester tester) async {
await tester.pumpWidget(new Image.memory(new Uint8List.fromList(kTransparentImage))); await tester.pumpWidget(new Image.memory(new Uint8List.fromList(kTransparentImage), excludeFromSemantics: true,));
}); });
testWidgets('Image color and colorBlend parameters', (WidgetTester tester) async { testWidgets('Image color and colorBlend parameters', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new Image( new Image(
excludeFromSemantics: true,
image: new TestImageProvider(), image: new TestImageProvider(),
color: const Color(0xFF00FF00), color: const Color(0xFF00FF00),
colorBlendMode: BlendMode.clear colorBlendMode: BlendMode.clear
...@@ -345,6 +359,7 @@ void main() { ...@@ -345,6 +359,7 @@ void main() {
testWidgets('TickerMode controls stream registration', (WidgetTester tester) async { testWidgets('TickerMode controls stream registration', (WidgetTester tester) async {
final TestImageStreamCompleter imageStreamCompleter = new TestImageStreamCompleter(); final TestImageStreamCompleter imageStreamCompleter = new TestImageStreamCompleter();
final Image image = new Image( final Image image = new Image(
excludeFromSemantics: true,
image: new TestImageProvider(streamCompleter: imageStreamCompleter), image: new TestImageProvider(streamCompleter: imageStreamCompleter),
); );
await tester.pumpWidget( await tester.pumpWidget(
...@@ -373,6 +388,7 @@ void main() { ...@@ -373,6 +388,7 @@ void main() {
new Container( new Container(
key: key, key: key,
child: new Image( child: new Image(
excludeFromSemantics: true,
image: imageProvider1 image: imageProvider1
) )
), ),
...@@ -396,6 +412,7 @@ void main() { ...@@ -396,6 +412,7 @@ void main() {
new Container( new Container(
key: key, key: key,
child: new Image( child: new Image(
excludeFromSemantics: true,
image: imageProvider2 image: imageProvider2
) )
), ),
...@@ -409,8 +426,8 @@ void main() { ...@@ -409,8 +426,8 @@ void main() {
}); });
testWidgets('Image State can be reconfigured to use another image', (WidgetTester tester) async { testWidgets('Image State can be reconfigured to use another image', (WidgetTester tester) async {
final Image image1 = new Image(image: new TestImageProvider()..complete(), width: 10.0); final Image image1 = new Image(image: new TestImageProvider()..complete(), width: 10.0, excludeFromSemantics: true);
final Image image2 = new Image(image: new TestImageProvider()..complete(), width: 20.0); final Image image2 = new Image(image: new TestImageProvider()..complete(), width: 20.0, excludeFromSemantics: true);
final Column column = new Column(children: <Widget>[image1, image2]); final Column column = new Column(children: <Widget>[image1, image2]);
await tester.pumpWidget(column, null, EnginePhase.layout); await tester.pumpWidget(column, null, EnginePhase.layout);
...@@ -425,6 +442,58 @@ void main() { ...@@ -425,6 +442,58 @@ void main() {
expect(renderObjects[1].image, isNotNull); expect(renderObjects[1].image, isNotNull);
expect(renderObjects[1].width, 10.0); expect(renderObjects[1].width, 10.0);
}); });
testWidgets('Image contributes semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Row(
children: <Widget>[
new Image(
image: new TestImageProvider(),
width: 100.0,
height: 100.0,
semanticLabel: 'test',
),
],
),
),
);
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: 'test',
rect: new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0),
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isImage],
)
]
), ignoreTransform: true));
semantics.dispose();
});
testWidgets('Image can exclude semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Image(
image: new TestImageProvider(),
width: 100.0,
height: 100.0,
excludeFromSemantics: true,
),
),
);
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[]
)));
semantics.dispose();
});
} }
class TestImageProvider extends ImageProvider<TestImageProvider> { class TestImageProvider extends ImageProvider<TestImageProvider> {
......
...@@ -21,7 +21,7 @@ Future<Null> main() async { ...@@ -21,7 +21,7 @@ Future<Null> main() async {
final GlobalKey imageKey = new GlobalKey(); final GlobalKey imageKey = new GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
home: new Image(image: fakeImageProvider, key: imageKey), home: new Image(image: fakeImageProvider, excludeFromSemantics: true, key: imageKey),
routes: <String, WidgetBuilder> { routes: <String, WidgetBuilder> {
'/page': (BuildContext context) => new Container() '/page': (BuildContext context) => new Container()
} }
......
...@@ -395,6 +395,7 @@ void main() { ...@@ -395,6 +395,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new Semantics( new Semantics(
container: true, container: true,
onDismiss: () => performedActions.add(SemanticsAction.dismiss),
onTap: () => performedActions.add(SemanticsAction.tap), onTap: () => performedActions.add(SemanticsAction.tap),
onLongPress: () => performedActions.add(SemanticsAction.longPress), onLongPress: () => performedActions.add(SemanticsAction.longPress),
onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft), onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
...@@ -416,8 +417,7 @@ void main() { ...@@ -416,8 +417,7 @@ void main() {
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet() final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
..remove(SemanticsAction.customAction) // customAction is not user-exposed. ..remove(SemanticsAction.customAction) // customAction is not user-exposed.
..remove(SemanticsAction.showOnScreen) // showOnScreen is not user-exposed ..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
..remove(SemanticsAction.dismiss); // TODO(jonahwilliams): remove when dismiss action is exposed.
const int expectedId = 1; const int expectedId = 1;
final TestSemantics expectedSemantics = new TestSemantics.root( final TestSemantics expectedSemantics = new TestSemantics.root(
...@@ -459,9 +459,10 @@ void main() { ...@@ -459,9 +459,10 @@ void main() {
testWidgets('Semantics widget supports all flags', (WidgetTester tester) async { testWidgets('Semantics widget supports all flags', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
// Note: checked state and toggled state are mutually exclusive.
await tester.pumpWidget( await tester.pumpWidget(
new Semantics( new Semantics(
key: const Key('a'),
container: true, container: true,
explicitChildNodes: true, explicitChildNodes: true,
// flags // flags
...@@ -477,15 +478,14 @@ void main() { ...@@ -477,15 +478,14 @@ void main() {
obscured: true, obscured: true,
scopesRoute: true, scopesRoute: true,
namesRoute: true, namesRoute: true,
image: true,
liveRegion: true,
) )
); );
// TODO(jonahwilliams): update when the following semantics flags are added.
final List<SemanticsFlag> flags = SemanticsFlag.values.values.toList(); final List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
flags flags
..remove(SemanticsFlag.isImage)
..remove(SemanticsFlag.hasToggledState) ..remove(SemanticsFlag.hasToggledState)
..remove(SemanticsFlag.isToggled) ..remove(SemanticsFlag.isToggled);
..remove(SemanticsFlag.isLiveRegion);
TestSemantics expectedSemantics = new TestSemantics.root( TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
...@@ -498,7 +498,9 @@ void main() { ...@@ -498,7 +498,9 @@ void main() {
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true)); expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
await tester.pumpWidget(new Semantics( await tester.pumpWidget(new Semantics(
key: const Key('b'),
container: true, container: true,
scopesRoute: false,
)); ));
expectedSemantics = new TestSemantics.root( expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
...@@ -510,6 +512,26 @@ void main() { ...@@ -510,6 +512,26 @@ void main() {
); );
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true)); expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
await tester.pumpWidget(
new Semantics(
key: const Key('c'),
toggled: true,
),
);
expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
rect: TestSemantics.fullScreen,
flags: <SemanticsFlag>[
SemanticsFlag.hasToggledState,
SemanticsFlag.isToggled,
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
semantics.dispose(); semantics.dispose();
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment