Skip to content

Commit 119e0ea

Browse files
authored
circleAvatar: foreground Image uses background Image as a fall-back (flutter#71783)
* foregroundImage property added * fixed documentaion nits * test for fallback to background image on foreground image failover * golden test
1 parent be8b6bf commit 119e0ea

File tree

2 files changed

+75
-0
lines changed

2 files changed

+75
-0
lines changed

packages/flutter/lib/src/material/circle_avatar.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ import 'theme_data.dart';
1717
/// such an image, the user's initials. A given user's initials should
1818
/// always be paired with the same background color, for consistency.
1919
///
20+
/// If [foregroundImage] fails then [backgroundImage] is used. If
21+
/// [backgroundImage] fails too, [backgroundColor] is used.
22+
///
2023
/// The [onBackgroundImageError] parameter must be null if the [backgroundImage]
2124
/// is null.
25+
/// The [onForegroundImageError] parameter must be null if the [foregroundImage]
26+
/// is null.
2227
///
2328
/// {@tool snippet}
2429
///
@@ -60,13 +65,16 @@ class CircleAvatar extends StatelessWidget {
6065
this.child,
6166
this.backgroundColor,
6267
this.backgroundImage,
68+
this.foregroundImage,
6369
this.onBackgroundImageError,
70+
this.onForegroundImageError,
6471
this.foregroundColor,
6572
this.radius,
6673
this.minRadius,
6774
this.maxRadius,
6875
}) : assert(radius == null || (minRadius == null && maxRadius == null)),
6976
assert(backgroundImage != null || onBackgroundImageError == null),
77+
assert(foregroundImage != null || onForegroundImageError== null),
7078
super(key: key);
7179

7280
/// The widget below this widget in the tree.
@@ -95,13 +103,24 @@ class CircleAvatar extends StatelessWidget {
95103
/// The background image of the circle. Changing the background
96104
/// image will cause the avatar to animate to the new image.
97105
///
106+
/// Typically used as a fallback image for [foregroundImage].
107+
///
98108
/// If the [CircleAvatar] is to have the user's initials, use [child] instead.
99109
final ImageProvider? backgroundImage;
100110

111+
/// The foreground image of the circle.
112+
///
113+
/// Typically used as profile image. For fallback use [backgroundImage].
114+
final ImageProvider? foregroundImage;
115+
101116
/// An optional error callback for errors emitted when loading
102117
/// [backgroundImage].
103118
final ImageErrorListener? onBackgroundImageError;
104119

120+
/// An optional error callback for errors emitted when loading
121+
/// [foregroundImage].
122+
final ImageErrorListener? onForegroundImageError;
123+
105124
/// The size of the avatar, expressed as the radius (half the diameter).
106125
///
107126
/// If [radius] is specified, then neither [minRadius] nor [maxRadius] may be
@@ -217,6 +236,16 @@ class CircleAvatar extends StatelessWidget {
217236
: null,
218237
shape: BoxShape.circle,
219238
),
239+
foregroundDecoration: foregroundImage != null
240+
? BoxDecoration(
241+
image: DecorationImage(
242+
image: foregroundImage!,
243+
onError: onForegroundImageError,
244+
fit: BoxFit.cover,
245+
),
246+
shape: BoxShape.circle,
247+
)
248+
: null,
220249
child: child == null
221250
? null
222251
: Center(

packages/flutter/test/material/circle_avatar_test.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart';
99
import 'package:flutter_test/flutter_test.dart';
1010

1111
import '../image_data.dart';
12+
import '../painting/mocks_for_image_cache.dart';
1213

1314
void main() {
1415
testWidgets('CircleAvatar with dark background color', (WidgetTester tester) async {
@@ -72,6 +73,51 @@ void main() {
7273
expect(decoration.image!.fit, equals(BoxFit.cover));
7374
});
7475

76+
testWidgets('CircleAvatar with image foreground', (WidgetTester tester) async {
77+
await tester.pumpWidget(
78+
wrap(
79+
child: CircleAvatar(
80+
foregroundImage: MemoryImage(Uint8List.fromList(kBlueRectPng)),
81+
radius: 50.0,
82+
),
83+
),
84+
);
85+
86+
final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
87+
expect(box.size, equals(const Size(100.0, 100.0)));
88+
final RenderDecoratedBox child = box.child! as RenderDecoratedBox;
89+
final BoxDecoration decoration = child.decoration as BoxDecoration;
90+
expect(decoration.image!.fit, equals(BoxFit.cover));
91+
});
92+
93+
testWidgets('CircleAvatar backgroundImage is used as a fallback for foregroundImage', (WidgetTester tester) async {
94+
final ErrorImageProvider errorImage = ErrorImageProvider();
95+
bool caughtForegroundImageError = false;
96+
await tester.pumpWidget(
97+
wrap(
98+
child: RepaintBoundary(
99+
child: CircleAvatar(
100+
foregroundImage: errorImage,
101+
backgroundImage: MemoryImage(Uint8List.fromList(kBlueRectPng)),
102+
radius: 50.0,
103+
onForegroundImageError: (_,__) => caughtForegroundImageError = true,
104+
),
105+
),
106+
),
107+
);
108+
109+
expect(caughtForegroundImageError, true);
110+
final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
111+
expect(box.size, equals(const Size(100.0, 100.0)));
112+
final RenderDecoratedBox child = box.child! as RenderDecoratedBox;
113+
final BoxDecoration decoration = child.decoration as BoxDecoration;
114+
expect(decoration.image!.fit, equals(BoxFit.cover));
115+
await expectLater(
116+
find.byType(CircleAvatar),
117+
matchesGoldenFile('circle_avatar.fallback.png'),
118+
);
119+
});
120+
75121
testWidgets('CircleAvatar with foreground color', (WidgetTester tester) async {
76122
final Color foregroundColor = Colors.red.shade100;
77123
await tester.pumpWidget(

0 commit comments

Comments
 (0)