circleAvatar: foreground Image uses background Image as a fall-back (#71783)
* foregroundImage property added
* fixed documentaion nits
* test for fallback to background image on foreground image failover
* golden test
diff --git a/packages/flutter/lib/src/material/circle_avatar.dart b/packages/flutter/lib/src/material/circle_avatar.dart
index 3ff4245..d449316 100644
--- a/packages/flutter/lib/src/material/circle_avatar.dart
+++ b/packages/flutter/lib/src/material/circle_avatar.dart
@@ -17,8 +17,13 @@
/// such an image, the user's initials. A given user's initials should
/// always be paired with the same background color, for consistency.
///
+/// If [foregroundImage] fails then [backgroundImage] is used. If
+/// [backgroundImage] fails too, [backgroundColor] is used.
+///
/// The [onBackgroundImageError] parameter must be null if the [backgroundImage]
/// is null.
+/// The [onForegroundImageError] parameter must be null if the [foregroundImage]
+/// is null.
///
/// {@tool snippet}
///
@@ -60,13 +65,16 @@
this.child,
this.backgroundColor,
this.backgroundImage,
+ this.foregroundImage,
this.onBackgroundImageError,
+ this.onForegroundImageError,
this.foregroundColor,
this.radius,
this.minRadius,
this.maxRadius,
}) : assert(radius == null || (minRadius == null && maxRadius == null)),
assert(backgroundImage != null || onBackgroundImageError == null),
+ assert(foregroundImage != null || onForegroundImageError== null),
super(key: key);
/// The widget below this widget in the tree.
@@ -95,13 +103,24 @@
/// The background image of the circle. Changing the background
/// image will cause the avatar to animate to the new image.
///
+ /// Typically used as a fallback image for [foregroundImage].
+ ///
/// If the [CircleAvatar] is to have the user's initials, use [child] instead.
final ImageProvider? backgroundImage;
+ /// The foreground image of the circle.
+ ///
+ /// Typically used as profile image. For fallback use [backgroundImage].
+ final ImageProvider? foregroundImage;
+
/// An optional error callback for errors emitted when loading
/// [backgroundImage].
final ImageErrorListener? onBackgroundImageError;
+ /// An optional error callback for errors emitted when loading
+ /// [foregroundImage].
+ final ImageErrorListener? onForegroundImageError;
+
/// The size of the avatar, expressed as the radius (half the diameter).
///
/// If [radius] is specified, then neither [minRadius] nor [maxRadius] may be
@@ -217,6 +236,16 @@
: null,
shape: BoxShape.circle,
),
+ foregroundDecoration: foregroundImage != null
+ ? BoxDecoration(
+ image: DecorationImage(
+ image: foregroundImage!,
+ onError: onForegroundImageError,
+ fit: BoxFit.cover,
+ ),
+ shape: BoxShape.circle,
+ )
+ : null,
child: child == null
? null
: Center(
diff --git a/packages/flutter/test/material/circle_avatar_test.dart b/packages/flutter/test/material/circle_avatar_test.dart
index cb4d31c..a49de4d 100644
--- a/packages/flutter/test/material/circle_avatar_test.dart
+++ b/packages/flutter/test/material/circle_avatar_test.dart
@@ -9,6 +9,7 @@
import 'package:flutter_test/flutter_test.dart';
import '../image_data.dart';
+import '../painting/mocks_for_image_cache.dart';
void main() {
testWidgets('CircleAvatar with dark background color', (WidgetTester tester) async {
@@ -72,6 +73,51 @@
expect(decoration.image!.fit, equals(BoxFit.cover));
});
+ testWidgets('CircleAvatar with image foreground', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ wrap(
+ child: CircleAvatar(
+ foregroundImage: MemoryImage(Uint8List.fromList(kBlueRectPng)),
+ radius: 50.0,
+ ),
+ ),
+ );
+
+ final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
+ expect(box.size, equals(const Size(100.0, 100.0)));
+ final RenderDecoratedBox child = box.child! as RenderDecoratedBox;
+ final BoxDecoration decoration = child.decoration as BoxDecoration;
+ expect(decoration.image!.fit, equals(BoxFit.cover));
+ });
+
+ testWidgets('CircleAvatar backgroundImage is used as a fallback for foregroundImage', (WidgetTester tester) async {
+ final ErrorImageProvider errorImage = ErrorImageProvider();
+ bool caughtForegroundImageError = false;
+ await tester.pumpWidget(
+ wrap(
+ child: RepaintBoundary(
+ child: CircleAvatar(
+ foregroundImage: errorImage,
+ backgroundImage: MemoryImage(Uint8List.fromList(kBlueRectPng)),
+ radius: 50.0,
+ onForegroundImageError: (_,__) => caughtForegroundImageError = true,
+ ),
+ ),
+ ),
+ );
+
+ expect(caughtForegroundImageError, true);
+ final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
+ expect(box.size, equals(const Size(100.0, 100.0)));
+ final RenderDecoratedBox child = box.child! as RenderDecoratedBox;
+ final BoxDecoration decoration = child.decoration as BoxDecoration;
+ expect(decoration.image!.fit, equals(BoxFit.cover));
+ await expectLater(
+ find.byType(CircleAvatar),
+ matchesGoldenFile('circle_avatar.fallback.png'),
+ );
+ });
+
testWidgets('CircleAvatar with foreground color', (WidgetTester tester) async {
final Color foregroundColor = Colors.red.shade100;
await tester.pumpWidget(