Commit 11678da3 authored by Muhammed Salih Guler's avatar Muhammed Salih Guler Committed by Jonah Williams

Add semantics label to FadeInImage. (#28799)

parent 0c7fe40e
......@@ -66,10 +66,21 @@ class FadeInImage extends StatefulWidget {
/// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve],
/// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and
/// [matchTextDirection] arguments must not be null.
///
/// There are two different semantic label for the class.
/// [placeholderSemanticLabel] is used for defining a semantics label for
/// [placeholder]. [imageSemanticLabel] is used for defining a semantics label
/// for [image]
///
/// If [excludeFromSemantics] is true, then [placeholderSemanticLabel] and
/// [imageSemanticLabel] will be ignored.
const FadeInImage({
Key key,
@required this.placeholder,
@required this.image,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.placeholderSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
......@@ -118,6 +129,9 @@ class FadeInImage extends StatefulWidget {
@required String image,
double placeholderScale = 1.0,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.placeholderSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
......@@ -174,6 +188,9 @@ class FadeInImage extends StatefulWidget {
AssetBundle bundle,
double placeholderScale,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.placeholderSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
......@@ -284,6 +301,24 @@ class FadeInImage extends StatefulWidget {
/// scope.
final bool matchTextDirection;
/// Whether to exclude this image from semantics.
///
/// Useful for images which do not contribute meaningful information to an
/// application.
final bool excludeFromSemantics;
/// A Semantic description of the [placeholder].
///
/// Used to provide a description of the [placeholder] to TalkBack on Android, and
/// VoiceOver on iOS.
final String placeholderSemanticLabel;
/// A Semantic description of the [image].
///
/// Used to provide a description of the [image] to TalkBack on Android, and
/// VoiceOver on iOS.
final String imageSemanticLabel;
@override
State<StatefulWidget> createState() => _FadeInImageState();
}
......@@ -491,11 +526,17 @@ class _FadeInImageState extends State<FadeInImage> with TickerProviderStateMixin
: _imageResolver._imageInfo;
}
String get _semanticLabel {
return _isShowingPlaceholder
? widget.placeholderSemanticLabel
: widget.imageSemanticLabel;
}
@override
Widget build(BuildContext context) {
assert(_phase != FadeInImagePhase.start);
final ImageInfo imageInfo = _imageInfo;
return RawImage(
final RawImage image = RawImage(
image: imageInfo?.image,
width: widget.width,
height: widget.height,
......@@ -507,6 +548,17 @@ class _FadeInImageState extends State<FadeInImage> with TickerProviderStateMixin
repeat: widget.repeat,
matchTextDirection: widget.matchTextDirection,
);
if (widget.excludeFromSemantics) {
return image;
}
return Semantics(
container: _semanticLabel != null,
image: true,
label: _semanticLabel == null ? '' : _semanticLabel,
child: image,
);
}
@override
......
......@@ -116,5 +116,93 @@ Future<void> main() async {
expect(displayedImage().image, isNot(same(placeholderImage))); // placeholder replaced
expect(displayedImage().image, same(secondPlaceholderImage));
});
group('semanticLabel', () {
const String placeholderSemanticText = 'Test placeholder semantic label';
const String imageSemanticText = 'Test image semantic label';
const Duration animationDuration = Duration(milliseconds: 50);
testWidgets('assigned correctly according to placeholder or image', (WidgetTester tester) async {
// The semantics widget that is created
Semantics displayedWidget() => tester.widget(find.byType(Semantics));
// The placeholder is expected to be already loaded
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
// The image which takes long to load
final TestImageProvider imageProvider = TestImageProvider(targetImage);
// Test case: Image and Placeholder semantic texts are provided.
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
imageSemanticLabel: imageSemanticText,
placeholderSemanticLabel: placeholderSemanticText
));
placeholderProvider.complete(); // load the placeholder
await tester.pump();
expect(displayedWidget().properties.label, same(placeholderSemanticText));
imageProvider.complete(); // load the image
for (int i = 0; i < 10; i += 1) {
await tester.pump(const Duration(milliseconds: 10)); // do the fadeout and fade in
}
expect(displayedWidget().properties.label, same(imageSemanticText));
});
testWidgets('assigned correctly with only one semantics text', (WidgetTester tester) async {
// The semantics widget that is created
Semantics displayedWidget() => tester.widget(find.byType(Semantics));
// The placeholder is expected to be already loaded
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
// The image which takes long to load
final TestImageProvider imageProvider = TestImageProvider(targetImage);
// Test case: Placeholder semantic text provided.
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
placeholderSemanticLabel: placeholderSemanticText
));
placeholderProvider.complete(); // load the placeholder
await tester.pump();
expect(displayedWidget().properties.label, same(placeholderSemanticText));
imageProvider.complete(); // load the image
for (int i = 0; i < 10; i += 1) {
await tester.pump(const Duration(milliseconds: 10)); // do the fadeout and fade in
}
expect(displayedWidget().properties.label, same(''));
});
testWidgets('assigned correctly without any semantics text', (WidgetTester tester) async {
// The semantics widget that is created
Semantics displayedWidget() => tester.widget(find.byType(Semantics));
// The placeholder is expected to be already loaded
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
// The image which takes long to load
final TestImageProvider imageProvider = TestImageProvider(targetImage);
// Test case: No semantic text provided.
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
));
placeholderProvider.complete(); // load the placeholder
await tester.pump();
expect(displayedWidget().properties.label, same(''));
imageProvider.complete(); // load the image
for (int i = 0; i < 10; i += 1) {
await tester.pump(const Duration(milliseconds: 10)); // do the fadeout and fade in
}
expect(displayedWidget().properties.label, same(''));
});
});
});
}
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