Add error callbacks to other image resolving code (#53329)
This commit is contained in:
parent
0d111bc91a
commit
ed67c47982
@ -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...)
|
||||
|
Loading…
x
Reference in New Issue
Block a user