Unverified Commit ed67c479 authored by Dan Field's avatar Dan Field Committed by GitHub

Add error callbacks to other image resolving code (#53329)

parent 0d111bc9
......@@ -17,6 +17,9 @@ import 'theme_data.dart';
/// such an image, the user's initials. A given user's initials should
/// always be paired with the same background color, for consistency.
///
/// The [onBackgroundImageError] parameter must be null if the [backgroundImage]
/// is null.
///
/// {@tool snippet}
///
/// If the avatar is to have an image, the image should be specified in the
......@@ -57,11 +60,13 @@ class CircleAvatar extends StatelessWidget {
this.child,
this.backgroundColor,
this.backgroundImage,
this.onBackgroundImageError,
this.foregroundColor,
this.radius,
this.minRadius,
this.maxRadius,
}) : assert(radius == null || (minRadius == null && maxRadius == null)),
assert(backgroundImage != null || onBackgroundImageError == null),
super(key: key);
/// The widget below this widget in the tree.
......@@ -93,6 +98,10 @@ class CircleAvatar extends StatelessWidget {
/// If the [CircleAvatar] is to have the user's initials, use [child] instead.
final ImageProvider backgroundImage;
/// An optional error callback for errors emitted when loading
/// [backgroundImage].
final ImageErrorListener onBackgroundImageError;
/// The size of the avatar, expressed as the radius (half the diameter).
///
/// If [radius] is specified, then neither [minRadius] nor [maxRadius] may be
......@@ -200,7 +209,11 @@ class CircleAvatar extends StatelessWidget {
decoration: BoxDecoration(
color: effectiveBackgroundColor,
image: backgroundImage != null
? DecorationImage(image: backgroundImage, fit: BoxFit.cover)
? DecorationImage(
image: backgroundImage,
onError: onBackgroundImageError,
fit: BoxFit.cover,
)
: null,
shape: BoxShape.circle,
),
......
......@@ -145,7 +145,8 @@ class Ink extends StatefulWidget {
///
/// The `image` argument must not be null. If there is no
/// intention to render anything on this image, consider using a
/// [Container] with a [BoxDecoration.image] instead.
/// [Container] with a [BoxDecoration.image] instead. The `onImageError`
/// argument may be provided to listen for errors when resolving the image.
///
/// The `alignment`, `repeat`, and `matchTextDirection` arguments must not
/// be null either, but they have default values.
......@@ -155,6 +156,7 @@ class Ink extends StatefulWidget {
Key key,
this.padding,
@required ImageProvider image,
ImageErrorListener onImageError,
ColorFilter colorFilter,
BoxFit fit,
AlignmentGeometry alignment = Alignment.center,
......@@ -172,6 +174,7 @@ class Ink extends StatefulWidget {
decoration = BoxDecoration(
image: DecorationImage(
image: image,
onError: onImageError,
colorFilter: colorFilter,
fit: fit,
alignment: alignment,
......
......@@ -71,7 +71,9 @@ class Switch extends StatefulWidget {
this.inactiveThumbColor,
this.inactiveTrackColor,
this.activeThumbImage,
this.onActiveThumbImageError,
this.inactiveThumbImage,
this.onInactiveThumbImageError,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.focusColor,
......@@ -80,6 +82,8 @@ class Switch extends StatefulWidget {
this.autofocus = false,
}) : _switchType = _SwitchType.material,
assert(dragStartBehavior != null),
assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
super(key: key);
/// Creates a [CupertinoSwitch] if the target platform is iOS, creates a
......@@ -87,7 +91,8 @@ class Switch extends StatefulWidget {
///
/// If a [CupertinoSwitch] is created, the following parameters are
/// ignored: [activeTrackColor], [inactiveThumbColor], [inactiveTrackColor],
/// [activeThumbImage], [inactiveThumbImage], [materialTapTargetSize].
/// [activeThumbImage], [onActiveThumbImageError], [inactiveThumbImage],
/// [onInactiveImageThumbError], [materialTapTargetSize].
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
const Switch.adaptive({
......@@ -99,7 +104,9 @@ class Switch extends StatefulWidget {
this.inactiveThumbColor,
this.inactiveTrackColor,
this.activeThumbImage,
this.onActiveThumbImageError,
this.inactiveThumbImage,
this.onInactiveThumbImageError,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.focusColor,
......@@ -107,6 +114,8 @@ class Switch extends StatefulWidget {
this.focusNode,
this.autofocus = false,
}) : assert(autofocus != null),
assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
_switchType = _SwitchType.adaptive,
super(key: key);
......@@ -170,11 +179,19 @@ class Switch extends StatefulWidget {
/// Ignored if this switch is created with [Switch.adaptive].
final ImageProvider activeThumbImage;
/// An optional error callback for errors emitted when loading
/// [activeThumbImage].
final ImageErrorListener onActiveThumbImageError;
/// An image to use on the thumb of this switch when the switch is off.
///
/// Ignored if this switch is created with [Switch.adaptive].
final ImageProvider inactiveThumbImage;
/// An optional error callback for errors emitted when loading
/// [inactiveThumbImage].
final ImageErrorListener onInactiveThumbImageError;
/// Configures the minimum size of the tap target.
///
/// Defaults to [ThemeData.materialTapTargetSize].
......@@ -311,7 +328,9 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
hoverColor: hoverColor,
focusColor: focusColor,
activeThumbImage: widget.activeThumbImage,
onActiveThumbImageError: widget.onActiveThumbImageError,
inactiveThumbImage: widget.inactiveThumbImage,
onInactiveThumbImageError: widget.onInactiveThumbImageError,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: createLocalImageConfiguration(context),
......@@ -381,7 +400,9 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
this.hoverColor,
this.focusColor,
this.activeThumbImage,
this.onActiveThumbImageError,
this.inactiveThumbImage,
this.onInactiveThumbImageError,
this.activeTrackColor,
this.inactiveTrackColor,
this.configuration,
......@@ -399,7 +420,9 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
final Color hoverColor;
final Color focusColor;
final ImageProvider activeThumbImage;
final ImageErrorListener onActiveThumbImageError;
final ImageProvider inactiveThumbImage;
final ImageErrorListener onInactiveThumbImageError;
final Color activeTrackColor;
final Color inactiveTrackColor;
final ImageConfiguration configuration;
......@@ -420,7 +443,9 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
hoverColor: hoverColor,
focusColor: focusColor,
activeThumbImage: activeThumbImage,
onActiveThumbImageError: onActiveThumbImageError,
inactiveThumbImage: inactiveThumbImage,
onInactiveThumbImageError: onInactiveThumbImageError,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: configuration,
......@@ -442,7 +467,9 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
..hoverColor = hoverColor
..focusColor = focusColor
..activeThumbImage = activeThumbImage
..onActiveThumbImageError = onActiveThumbImageError
..inactiveThumbImage = inactiveThumbImage
..onInactiveThumbImageError = onInactiveThumbImageError
..activeTrackColor = activeTrackColor
..inactiveTrackColor = inactiveTrackColor
..configuration = configuration
......@@ -464,7 +491,9 @@ class _RenderSwitch extends RenderToggleable {
Color hoverColor,
Color focusColor,
ImageProvider activeThumbImage,
ImageErrorListener onActiveThumbImageError,
ImageProvider inactiveThumbImage,
ImageErrorListener onInactiveThumbImageError,
Color activeTrackColor,
Color inactiveTrackColor,
ImageConfiguration configuration,
......@@ -477,7 +506,9 @@ class _RenderSwitch extends RenderToggleable {
@required this.state,
}) : assert(textDirection != null),
_activeThumbImage = activeThumbImage,
_onActiveThumbImageError = onActiveThumbImageError,
_inactiveThumbImage = inactiveThumbImage,
_onInactiveThumbImageError = onInactiveThumbImageError,
_activeTrackColor = activeTrackColor,
_inactiveTrackColor = inactiveTrackColor,
_configuration = configuration,
......@@ -511,6 +542,16 @@ class _RenderSwitch extends RenderToggleable {
markNeedsPaint();
}
ImageErrorListener get onActiveThumbImageError => _onActiveThumbImageError;
ImageErrorListener _onActiveThumbImageError;
set onActiveThumbImageError(ImageErrorListener value) {
if (value == _onActiveThumbImageError) {
return;
}
_onActiveThumbImageError = value;
markNeedsPaint();
}
ImageProvider get inactiveThumbImage => _inactiveThumbImage;
ImageProvider _inactiveThumbImage;
set inactiveThumbImage(ImageProvider value) {
......@@ -520,6 +561,16 @@ class _RenderSwitch extends RenderToggleable {
markNeedsPaint();
}
ImageErrorListener get onInactiveThumbImageError => _onInactiveThumbImageError;
ImageErrorListener _onInactiveThumbImageError;
set onInactiveThumbImageError(ImageErrorListener value) {
if (value == _onInactiveThumbImageError) {
return;
}
_onInactiveThumbImageError = value;
markNeedsPaint();
}
Color get activeTrackColor => _activeTrackColor;
Color _activeTrackColor;
set activeTrackColor(Color value) {
......@@ -642,12 +693,13 @@ class _RenderSwitch extends RenderToggleable {
Color _cachedThumbColor;
ImageProvider _cachedThumbImage;
ImageErrorListener _cachedThumbErrorListener;
BoxPainter _cachedThumbPainter;
BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image) {
BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image, ImageErrorListener errorListener) {
return BoxDecoration(
color: color,
image: image == null ? null : DecorationImage(image: image),
image: image == null ? null : DecorationImage(image: image, onError: errorListener),
shape: BoxShape.circle,
boxShadow: kElevationToShadow[1],
);
......@@ -698,6 +750,10 @@ class _RenderSwitch extends RenderToggleable {
? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
: inactiveThumbImage;
final ImageErrorListener thumbErrorListener = isEnabled
? (currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError)
: onInactiveThumbImageError;
// Paint the track
final Paint paint = Paint()
..color = trackColor;
......@@ -721,10 +777,11 @@ class _RenderSwitch extends RenderToggleable {
try {
_isPainting = true;
BoxPainter thumbPainter;
if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage) {
if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage || thumbErrorListener != _cachedThumbErrorListener) {
_cachedThumbColor = thumbColor;
_cachedThumbImage = thumbImage;
_cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage).createBoxPainter(_handleDecorationChanged);
_cachedThumbErrorListener = thumbErrorListener;
_cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage, thumbErrorListener).createBoxPainter(_handleDecorationChanged);
}
thumbPainter = _cachedThumbPainter;
......
......@@ -40,6 +40,7 @@ class DecorationImage {
/// must not be null.
const DecorationImage({
@required this.image,
this.onError,
this.colorFilter,
this.fit,
this.alignment = Alignment.center,
......@@ -57,6 +58,9 @@ class DecorationImage {
/// application) or a [NetworkImage] (for an image obtained from the network).
final ImageProvider image;
/// An optional error callback for errors emitted when loading [image].
final ImageErrorListener onError;
/// A color filter to apply to the image before painting it.
final ColorFilter colorFilter;
......@@ -239,7 +243,10 @@ class DecorationImagePainter {
final ImageStream newImageStream = _details.image.resolve(configuration);
if (newImageStream.key != _imageStream?.key) {
final ImageStreamListener listener = ImageStreamListener(_handleImage);
final ImageStreamListener listener = ImageStreamListener(
_handleImage,
onError: _details.onError,
);
_imageStream?.removeListener(listener);
_imageStream = newImageStream;
_imageStream.addListener(listener);
......@@ -286,7 +293,10 @@ class DecorationImagePainter {
/// After this method has been called, the object is no longer usable.
@mustCallSuper
void dispose() {
_imageStream?.removeListener(ImageStreamListener(_handleImage));
_imageStream?.removeListener(ImageStreamListener(
_handleImage,
onError: _details.onError,
));
}
@override
......
......@@ -77,7 +77,9 @@ class FadeInImage extends StatelessWidget {
const FadeInImage({
Key key,
@required this.placeholder,
this.placeholderErrorBuilder,
@required this.image,
this.imageErrorBuilder,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
......@@ -132,7 +134,9 @@ class FadeInImage extends StatelessWidget {
FadeInImage.memoryNetwork({
Key key,
@required Uint8List placeholder,
this.placeholderErrorBuilder,
@required String image,
this.imageErrorBuilder,
double placeholderScale = 1.0,
double imageScale = 1.0,
this.excludeFromSemantics = false,
......@@ -200,7 +204,9 @@ class FadeInImage extends StatelessWidget {
FadeInImage.assetNetwork({
Key key,
@required String placeholder,
this.placeholderErrorBuilder,
@required String image,
this.imageErrorBuilder,
AssetBundle bundle,
double placeholderScale,
double imageScale = 1.0,
......@@ -239,9 +245,24 @@ class FadeInImage extends StatelessWidget {
/// Image displayed while the target [image] is loading.
final ImageProvider placeholder;
/// A builder function that is called if an error occurs during placeholder
/// image loading.
///
/// If this builder is not provided, any exceptions will be reported to
/// [FlutterError.onError]. If it is provided, the caller should either handle
/// the exception by providing a replacement widget, or rethrow the exception.
final ImageErrorWidgetBuilder placeholderErrorBuilder;
/// The target image that is displayed once it has loaded.
final ImageProvider image;
/// A builder function that is called if an error occurs during image loading.
///
/// If this builder is not provided, any exceptions will be reported to
/// [FlutterError.onError]. If it is provided, the caller should either handle
/// the exception by providing a replacement widget, or rethrow the exception.
final ImageErrorWidgetBuilder imageErrorBuilder;
/// The duration of the fade-out animation for the [placeholder].
final Duration fadeOutDuration;
......@@ -337,11 +358,13 @@ class FadeInImage extends StatelessWidget {
Image _image({
@required ImageProvider image,
ImageErrorWidgetBuilder errorBuilder,
ImageFrameBuilder frameBuilder,
}) {
assert(image != null);
return Image(
image: image,
errorBuilder: errorBuilder,
frameBuilder: frameBuilder,
width: width,
height: height,
......@@ -358,12 +381,13 @@ class FadeInImage extends StatelessWidget {
Widget build(BuildContext context) {
Widget result = _image(
image: image,
errorBuilder: imageErrorBuilder,
frameBuilder: (BuildContext context, Widget child, int frame, bool wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded)
return child;
return _AnimatedFadeOutFadeIn(
target: child,
placeholder: _image(image: placeholder),
placeholder: _image(image: placeholder, errorBuilder: placeholderErrorBuilder),
isTargetLoaded: frame != null,
fadeInDuration: fadeInDuration,
fadeOutDuration: fadeOutDuration,
......
......@@ -40,6 +40,22 @@ class SynchronousTestImageProvider extends ImageProvider<int> {
}
}
class SynchronousErrorTestImageProvider extends ImageProvider<int> {
const SynchronousErrorTestImageProvider(this.throwable);
final Object throwable;
@override
Future<int> obtainKey(ImageConfiguration configuration) {
throw throwable;
}
@override
ImageStreamCompleter load(int key, DecoderCallback decode) {
throw throwable;
}
}
class AsyncTestImageProvider extends ImageProvider<int> {
@override
Future<int> obtainKey(ImageConfiguration configuration) {
......@@ -269,6 +285,26 @@ void main() {
);
});
test('DecorationImage - error listener', () async {
String exception;
final DecorationImage backgroundImage = DecorationImage(
image: const SynchronousErrorTestImageProvider('threw'),
onError: (dynamic error, StackTrace stackTrace) {
exception = error as String;
}
);
backgroundImage.createPainter(() { }).paint(
TestCanvas(),
Rect.largest,
Path(),
ImageConfiguration.empty,
);
// Yield so that the exception callback gets called before we check it.
await null;
expect(exception, 'threw');
});
test('BoxDecoration.lerp - shapes', () {
// We don't lerp the shape, we just switch from one to the other at t=0.5.
// (Use a ShapeDecoration and ShapeBorder if you want to lerp the shapes...)
......
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