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 { ...@@ -66,10 +66,21 @@ class FadeInImage extends StatefulWidget {
/// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve], /// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve],
/// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and /// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and
/// [matchTextDirection] arguments must not be null. /// [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({ const FadeInImage({
Key key, Key key,
@required this.placeholder, @required this.placeholder,
@required this.image, @required this.image,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.placeholderSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300), this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut, this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700), this.fadeInDuration = const Duration(milliseconds: 700),
...@@ -118,6 +129,9 @@ class FadeInImage extends StatefulWidget { ...@@ -118,6 +129,9 @@ class FadeInImage extends StatefulWidget {
@required String image, @required String image,
double placeholderScale = 1.0, double placeholderScale = 1.0,
double imageScale = 1.0, double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.placeholderSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300), this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut, this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700), this.fadeInDuration = const Duration(milliseconds: 700),
...@@ -174,6 +188,9 @@ class FadeInImage extends StatefulWidget { ...@@ -174,6 +188,9 @@ class FadeInImage extends StatefulWidget {
AssetBundle bundle, AssetBundle bundle,
double placeholderScale, double placeholderScale,
double imageScale = 1.0, double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.placeholderSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300), this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut, this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700), this.fadeInDuration = const Duration(milliseconds: 700),
...@@ -284,6 +301,24 @@ class FadeInImage extends StatefulWidget { ...@@ -284,6 +301,24 @@ class FadeInImage extends StatefulWidget {
/// scope. /// scope.
final bool matchTextDirection; 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 @override
State<StatefulWidget> createState() => _FadeInImageState(); State<StatefulWidget> createState() => _FadeInImageState();
} }
...@@ -491,11 +526,17 @@ class _FadeInImageState extends State<FadeInImage> with TickerProviderStateMixin ...@@ -491,11 +526,17 @@ class _FadeInImageState extends State<FadeInImage> with TickerProviderStateMixin
: _imageResolver._imageInfo; : _imageResolver._imageInfo;
} }
String get _semanticLabel {
return _isShowingPlaceholder
? widget.placeholderSemanticLabel
: widget.imageSemanticLabel;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(_phase != FadeInImagePhase.start); assert(_phase != FadeInImagePhase.start);
final ImageInfo imageInfo = _imageInfo; final ImageInfo imageInfo = _imageInfo;
return RawImage( final RawImage image = RawImage(
image: imageInfo?.image, image: imageInfo?.image,
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
...@@ -507,6 +548,17 @@ class _FadeInImageState extends State<FadeInImage> with TickerProviderStateMixin ...@@ -507,6 +548,17 @@ class _FadeInImageState extends State<FadeInImage> with TickerProviderStateMixin
repeat: widget.repeat, repeat: widget.repeat,
matchTextDirection: widget.matchTextDirection, matchTextDirection: widget.matchTextDirection,
); );
if (widget.excludeFromSemantics) {
return image;
}
return Semantics(
container: _semanticLabel != null,
image: true,
label: _semanticLabel == null ? '' : _semanticLabel,
child: image,
);
} }
@override @override
......
...@@ -116,5 +116,93 @@ Future<void> main() async { ...@@ -116,5 +116,93 @@ Future<void> main() async {
expect(displayedImage().image, isNot(same(placeholderImage))); // placeholder replaced expect(displayedImage().image, isNot(same(placeholderImage))); // placeholder replaced
expect(displayedImage().image, same(secondPlaceholderImage)); 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