Add ability to specify Image widget opacity as an animation (#83379)
This commit is contained in:
parent
59f6cc7ac9
commit
a84bb4eb3c
@ -360,6 +360,8 @@ void debugFlushLastFrameImageSizeInfo() {
|
|||||||
///
|
///
|
||||||
/// * `scale`: The number of image pixels for each logical pixel.
|
/// * `scale`: The number of image pixels for each logical pixel.
|
||||||
///
|
///
|
||||||
|
/// * `opacity`: The opacity to paint the image onto the canvas with.
|
||||||
|
///
|
||||||
/// * `colorFilter`: If non-null, the color filter to apply when painting the
|
/// * `colorFilter`: If non-null, the color filter to apply when painting the
|
||||||
/// image.
|
/// image.
|
||||||
///
|
///
|
||||||
@ -420,6 +422,7 @@ void paintImage({
|
|||||||
required ui.Image image,
|
required ui.Image image,
|
||||||
String? debugImageLabel,
|
String? debugImageLabel,
|
||||||
double scale = 1.0,
|
double scale = 1.0,
|
||||||
|
double opacity = 1.0,
|
||||||
ColorFilter? colorFilter,
|
ColorFilter? colorFilter,
|
||||||
BoxFit? fit,
|
BoxFit? fit,
|
||||||
Alignment alignment = Alignment.center,
|
Alignment alignment = Alignment.center,
|
||||||
@ -473,6 +476,7 @@ void paintImage({
|
|||||||
final Paint paint = Paint()..isAntiAlias = isAntiAlias;
|
final Paint paint = Paint()..isAntiAlias = isAntiAlias;
|
||||||
if (colorFilter != null)
|
if (colorFilter != null)
|
||||||
paint.colorFilter = colorFilter;
|
paint.colorFilter = colorFilter;
|
||||||
|
paint.color = Color.fromRGBO(0, 0, 0, opacity);
|
||||||
paint.filterQuality = filterQuality;
|
paint.filterQuality = filterQuality;
|
||||||
paint.invertColors = invertColors;
|
paint.invertColors = invertColors;
|
||||||
final double halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0;
|
final double halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0;
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
import 'dart:ui' as ui show Image;
|
import 'dart:ui' as ui show Image;
|
||||||
|
|
||||||
|
import 'package:flutter/animation.dart';
|
||||||
|
|
||||||
import 'box.dart';
|
import 'box.dart';
|
||||||
import 'object.dart';
|
import 'object.dart';
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ class RenderImage extends RenderBox {
|
|||||||
double? height,
|
double? height,
|
||||||
double scale = 1.0,
|
double scale = 1.0,
|
||||||
Color? color,
|
Color? color,
|
||||||
|
Animation<double>? opacity,
|
||||||
BlendMode? colorBlendMode,
|
BlendMode? colorBlendMode,
|
||||||
BoxFit? fit,
|
BoxFit? fit,
|
||||||
AlignmentGeometry alignment = Alignment.center,
|
AlignmentGeometry alignment = Alignment.center,
|
||||||
@ -52,6 +55,7 @@ class RenderImage extends RenderBox {
|
|||||||
_height = height,
|
_height = height,
|
||||||
_scale = scale,
|
_scale = scale,
|
||||||
_color = color,
|
_color = color,
|
||||||
|
_opacity = opacity,
|
||||||
_colorBlendMode = colorBlendMode,
|
_colorBlendMode = colorBlendMode,
|
||||||
_fit = fit,
|
_fit = fit,
|
||||||
_alignment = alignment,
|
_alignment = alignment,
|
||||||
@ -163,6 +167,21 @@ class RenderImage extends RenderBox {
|
|||||||
markNeedsPaint();
|
markNeedsPaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If non-null, the value from the [Animation] is multiplied with the opacity
|
||||||
|
/// of each image pixel before painting onto the canvas.
|
||||||
|
Animation<double>? get opacity => _opacity;
|
||||||
|
Animation<double>? _opacity;
|
||||||
|
set opacity(Animation<double>? value) {
|
||||||
|
if (value == _opacity)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (attached)
|
||||||
|
_opacity?.removeListener(markNeedsPaint);
|
||||||
|
_opacity = value;
|
||||||
|
if (attached)
|
||||||
|
value?.addListener(markNeedsPaint);
|
||||||
|
}
|
||||||
|
|
||||||
/// Used to set the filterQuality of the image
|
/// Used to set the filterQuality of the image
|
||||||
/// Use the [FilterQuality.low] quality setting to scale the image, which corresponds to
|
/// Use the [FilterQuality.low] quality setting to scale the image, which corresponds to
|
||||||
/// bilinear interpolation, rather than the default [FilterQuality.none] which corresponds
|
/// bilinear interpolation, rather than the default [FilterQuality.none] which corresponds
|
||||||
@ -381,6 +400,18 @@ class RenderImage extends RenderBox {
|
|||||||
size = _sizeForConstraints(constraints);
|
size = _sizeForConstraints(constraints);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void attach(covariant PipelineOwner owner) {
|
||||||
|
super.attach(owner);
|
||||||
|
_opacity?.addListener(markNeedsPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void detach() {
|
||||||
|
_opacity?.removeListener(markNeedsPaint);
|
||||||
|
super.detach();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(PaintingContext context, Offset offset) {
|
void paint(PaintingContext context, Offset offset) {
|
||||||
if (_image == null)
|
if (_image == null)
|
||||||
@ -394,6 +425,7 @@ class RenderImage extends RenderBox {
|
|||||||
image: _image!,
|
image: _image!,
|
||||||
debugImageLabel: debugImageLabel,
|
debugImageLabel: debugImageLabel,
|
||||||
scale: _scale,
|
scale: _scale,
|
||||||
|
opacity: _opacity?.value ?? 1.0,
|
||||||
colorFilter: _colorFilter,
|
colorFilter: _colorFilter,
|
||||||
fit: _fit,
|
fit: _fit,
|
||||||
alignment: _resolvedAlignment!,
|
alignment: _resolvedAlignment!,
|
||||||
@ -414,6 +446,7 @@ class RenderImage extends RenderBox {
|
|||||||
properties.add(DoubleProperty('height', height, defaultValue: null));
|
properties.add(DoubleProperty('height', height, defaultValue: null));
|
||||||
properties.add(DoubleProperty('scale', scale, defaultValue: 1.0));
|
properties.add(DoubleProperty('scale', scale, defaultValue: 1.0));
|
||||||
properties.add(ColorProperty('color', color, defaultValue: null));
|
properties.add(ColorProperty('color', color, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<Animation<double>?>('opacity', opacity, defaultValue: null));
|
||||||
properties.add(EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null));
|
properties.add(EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null));
|
||||||
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
|
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
|
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'dart:ui' as ui show Image, ImageFilter, TextHeightBehavior;
|
import 'dart:ui' as ui show Image, ImageFilter, TextHeightBehavior;
|
||||||
|
|
||||||
|
import 'package:flutter/animation.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
@ -5915,6 +5916,7 @@ class RawImage extends LeafRenderObjectWidget {
|
|||||||
this.height,
|
this.height,
|
||||||
this.scale = 1.0,
|
this.scale = 1.0,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.opacity,
|
||||||
this.colorBlendMode,
|
this.colorBlendMode,
|
||||||
this.fit,
|
this.fit,
|
||||||
this.alignment = Alignment.center,
|
this.alignment = Alignment.center,
|
||||||
@ -5961,6 +5963,13 @@ class RawImage extends LeafRenderObjectWidget {
|
|||||||
/// If non-null, this color is blended with each image pixel using [colorBlendMode].
|
/// If non-null, this color is blended with each image pixel using [colorBlendMode].
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
|
||||||
|
/// If non-null, the value from the [Animation] is multiplied with the opacity
|
||||||
|
/// of each image pixel before painting onto the canvas.
|
||||||
|
///
|
||||||
|
/// This is more efficient than using [FadeTransition] to change the opacity
|
||||||
|
/// of an image.
|
||||||
|
final Animation<double>? opacity;
|
||||||
|
|
||||||
/// Used to set the filterQuality of the image
|
/// Used to set the filterQuality of the image
|
||||||
/// Use the "low" quality setting to scale the image, which corresponds to
|
/// Use the "low" quality setting to scale the image, which corresponds to
|
||||||
/// bilinear interpolation, rather than the default "none" which corresponds
|
/// bilinear interpolation, rather than the default "none" which corresponds
|
||||||
@ -6070,6 +6079,7 @@ class RawImage extends LeafRenderObjectWidget {
|
|||||||
height: height,
|
height: height,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
color: color,
|
color: color,
|
||||||
|
opacity: opacity,
|
||||||
colorBlendMode: colorBlendMode,
|
colorBlendMode: colorBlendMode,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
@ -6122,6 +6132,7 @@ class RawImage extends LeafRenderObjectWidget {
|
|||||||
properties.add(DoubleProperty('height', height, defaultValue: null));
|
properties.add(DoubleProperty('height', height, defaultValue: null));
|
||||||
properties.add(DoubleProperty('scale', scale, defaultValue: 1.0));
|
properties.add(DoubleProperty('scale', scale, defaultValue: 1.0));
|
||||||
properties.add(ColorProperty('color', color, defaultValue: null));
|
properties.add(ColorProperty('color', color, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<Animation<double>?>('opacity', opacity, defaultValue: null));
|
||||||
properties.add(EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null));
|
properties.add(EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null));
|
||||||
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
|
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
|
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
|
||||||
|
@ -11,7 +11,6 @@ import 'basic.dart';
|
|||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'image.dart';
|
import 'image.dart';
|
||||||
import 'implicit_animations.dart';
|
import 'implicit_animations.dart';
|
||||||
import 'transitions.dart';
|
|
||||||
|
|
||||||
// Examples can assume:
|
// Examples can assume:
|
||||||
// late Uint8List bytes;
|
// late Uint8List bytes;
|
||||||
@ -62,7 +61,7 @@ import 'transitions.dart';
|
|||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
class FadeInImage extends StatelessWidget {
|
class FadeInImage extends StatefulWidget {
|
||||||
/// Creates a widget that displays a [placeholder] while an [image] is loading,
|
/// Creates a widget that displays a [placeholder] while an [image] is loading,
|
||||||
/// then fades-out the placeholder and fades-in the image.
|
/// then fades-out the placeholder and fades-in the image.
|
||||||
///
|
///
|
||||||
@ -356,22 +355,42 @@ class FadeInImage extends StatelessWidget {
|
|||||||
/// once the image has loaded.
|
/// once the image has loaded.
|
||||||
final String? imageSemanticLabel;
|
final String? imageSemanticLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FadeInImage> createState() => _FadeInImageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FadeInImageState extends State<FadeInImage> {
|
||||||
|
static const Animation<double> _kOpaqueAnimation = AlwaysStoppedAnimation<double>(1.0);
|
||||||
|
|
||||||
|
// These ProxyAnimations are changed to the fade in animation by
|
||||||
|
// [_AnimatedFadeOutFadeInState]. Otherwise these animations are reset to
|
||||||
|
// their defaults by [_resetAnimations].
|
||||||
|
final ProxyAnimation _imageAnimation = ProxyAnimation(_kOpaqueAnimation);
|
||||||
|
final ProxyAnimation _placeholderAnimation = ProxyAnimation(_kOpaqueAnimation);
|
||||||
|
|
||||||
|
void _resetAnimations() {
|
||||||
|
_imageAnimation.parent = _kOpaqueAnimation;
|
||||||
|
_placeholderAnimation.parent = _kOpaqueAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
Image _image({
|
Image _image({
|
||||||
required ImageProvider image,
|
required ImageProvider image,
|
||||||
ImageErrorWidgetBuilder? errorBuilder,
|
ImageErrorWidgetBuilder? errorBuilder,
|
||||||
ImageFrameBuilder? frameBuilder,
|
ImageFrameBuilder? frameBuilder,
|
||||||
|
required Animation<double> opacity,
|
||||||
}) {
|
}) {
|
||||||
assert(image != null);
|
assert(image != null);
|
||||||
return Image(
|
return Image(
|
||||||
image: image,
|
image: image,
|
||||||
errorBuilder: errorBuilder,
|
errorBuilder: errorBuilder,
|
||||||
frameBuilder: frameBuilder,
|
frameBuilder: frameBuilder,
|
||||||
width: width,
|
opacity: opacity,
|
||||||
height: height,
|
width: widget.width,
|
||||||
fit: fit,
|
height: widget.height,
|
||||||
alignment: alignment,
|
fit: widget.fit,
|
||||||
repeat: repeat,
|
alignment: widget.alignment,
|
||||||
matchTextDirection: matchTextDirection,
|
repeat: widget.repeat,
|
||||||
|
matchTextDirection: widget.matchTextDirection,
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
excludeFromSemantics: true,
|
excludeFromSemantics: true,
|
||||||
);
|
);
|
||||||
@ -380,28 +399,37 @@ class FadeInImage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget result = _image(
|
Widget result = _image(
|
||||||
image: image,
|
image: widget.image,
|
||||||
errorBuilder: imageErrorBuilder,
|
errorBuilder: widget.imageErrorBuilder,
|
||||||
|
opacity: _imageAnimation,
|
||||||
frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
|
frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
|
||||||
if (wasSynchronouslyLoaded)
|
if (wasSynchronouslyLoaded) {
|
||||||
|
_resetAnimations();
|
||||||
return child;
|
return child;
|
||||||
|
}
|
||||||
return _AnimatedFadeOutFadeIn(
|
return _AnimatedFadeOutFadeIn(
|
||||||
target: child,
|
target: child,
|
||||||
placeholder: _image(image: placeholder, errorBuilder: placeholderErrorBuilder),
|
targetProxyAnimation: _imageAnimation,
|
||||||
|
placeholder: _image(
|
||||||
|
image: widget.placeholder,
|
||||||
|
errorBuilder: widget.placeholderErrorBuilder,
|
||||||
|
opacity: _placeholderAnimation,
|
||||||
|
),
|
||||||
|
placeholderProxyAnimation: _placeholderAnimation,
|
||||||
isTargetLoaded: frame != null,
|
isTargetLoaded: frame != null,
|
||||||
fadeInDuration: fadeInDuration,
|
fadeInDuration: widget.fadeInDuration,
|
||||||
fadeOutDuration: fadeOutDuration,
|
fadeOutDuration: widget.fadeOutDuration,
|
||||||
fadeInCurve: fadeInCurve,
|
fadeInCurve: widget.fadeInCurve,
|
||||||
fadeOutCurve: fadeOutCurve,
|
fadeOutCurve: widget.fadeOutCurve,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!excludeFromSemantics) {
|
if (!widget.excludeFromSemantics) {
|
||||||
result = Semantics(
|
result = Semantics(
|
||||||
container: imageSemanticLabel != null,
|
container: widget.imageSemanticLabel != null,
|
||||||
image: true,
|
image: true,
|
||||||
label: imageSemanticLabel ?? '',
|
label: widget.imageSemanticLabel ?? '',
|
||||||
child: result,
|
child: result,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -414,7 +442,9 @@ class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
|
|||||||
const _AnimatedFadeOutFadeIn({
|
const _AnimatedFadeOutFadeIn({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.target,
|
required this.target,
|
||||||
|
required this.targetProxyAnimation,
|
||||||
required this.placeholder,
|
required this.placeholder,
|
||||||
|
required this.placeholderProxyAnimation,
|
||||||
required this.isTargetLoaded,
|
required this.isTargetLoaded,
|
||||||
required this.fadeOutDuration,
|
required this.fadeOutDuration,
|
||||||
required this.fadeOutCurve,
|
required this.fadeOutCurve,
|
||||||
@ -430,7 +460,9 @@ class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
|
|||||||
super(key: key, duration: fadeInDuration + fadeOutDuration);
|
super(key: key, duration: fadeInDuration + fadeOutDuration);
|
||||||
|
|
||||||
final Widget target;
|
final Widget target;
|
||||||
|
final ProxyAnimation targetProxyAnimation;
|
||||||
final Widget placeholder;
|
final Widget placeholder;
|
||||||
|
final ProxyAnimation placeholderProxyAnimation;
|
||||||
final bool isTargetLoaded;
|
final bool isTargetLoaded;
|
||||||
final Duration fadeInDuration;
|
final Duration fadeInDuration;
|
||||||
final Duration fadeOutDuration;
|
final Duration fadeOutDuration;
|
||||||
@ -494,6 +526,9 @@ class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_Animate
|
|||||||
// for the full animation when the new target image becomes ready.
|
// for the full animation when the new target image becomes ready.
|
||||||
controller.value = controller.upperBound;
|
controller.value = controller.upperBound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
widget.targetProxyAnimation.parent = _targetOpacityAnimation;
|
||||||
|
widget.placeholderProxyAnimation.parent = _placeholderOpacityAnimation;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isValid(Tween<double> tween) {
|
bool _isValid(Tween<double> tween) {
|
||||||
@ -502,13 +537,8 @@ class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_Animate
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Widget target = FadeTransition(
|
|
||||||
opacity: _targetOpacityAnimation!,
|
|
||||||
child: widget.target,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_placeholderOpacityAnimation!.isCompleted) {
|
if (_placeholderOpacityAnimation!.isCompleted) {
|
||||||
return target;
|
return widget.target;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
@ -518,11 +548,8 @@ class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_Animate
|
|||||||
// but it allows the Stack to avoid a call to Directionality.of()
|
// but it allows the Stack to avoid a call to Directionality.of()
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
target,
|
widget.target,
|
||||||
FadeTransition(
|
widget.placeholder,
|
||||||
opacity: _placeholderOpacityAnimation!,
|
|
||||||
child: widget.placeholder,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -330,6 +330,7 @@ class Image extends StatefulWidget {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.opacity,
|
||||||
this.colorBlendMode,
|
this.colorBlendMode,
|
||||||
this.fit,
|
this.fit,
|
||||||
this.alignment = Alignment.center,
|
this.alignment = Alignment.center,
|
||||||
@ -389,6 +390,7 @@ class Image extends StatefulWidget {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.opacity,
|
||||||
this.colorBlendMode,
|
this.colorBlendMode,
|
||||||
this.fit,
|
this.fit,
|
||||||
this.alignment = Alignment.center,
|
this.alignment = Alignment.center,
|
||||||
@ -451,6 +453,7 @@ class Image extends StatefulWidget {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.opacity,
|
||||||
this.colorBlendMode,
|
this.colorBlendMode,
|
||||||
this.fit,
|
this.fit,
|
||||||
this.alignment = Alignment.center,
|
this.alignment = Alignment.center,
|
||||||
@ -612,6 +615,7 @@ class Image extends StatefulWidget {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.opacity,
|
||||||
this.colorBlendMode,
|
this.colorBlendMode,
|
||||||
this.fit,
|
this.fit,
|
||||||
this.alignment = Alignment.center,
|
this.alignment = Alignment.center,
|
||||||
@ -681,6 +685,7 @@ class Image extends StatefulWidget {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.opacity,
|
||||||
this.colorBlendMode,
|
this.colorBlendMode,
|
||||||
this.fit,
|
this.fit,
|
||||||
this.alignment = Alignment.center,
|
this.alignment = Alignment.center,
|
||||||
@ -922,6 +927,20 @@ class Image extends StatefulWidget {
|
|||||||
/// If non-null, this color is blended with each image pixel using [colorBlendMode].
|
/// If non-null, this color is blended with each image pixel using [colorBlendMode].
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
|
||||||
|
/// If non-null, the value from the [Animation] is multiplied with the opacity
|
||||||
|
/// of each image pixel before painting onto the canvas.
|
||||||
|
///
|
||||||
|
/// This is more efficient than using [FadeTransition] to change the opacity
|
||||||
|
/// of an image, since this avoids creating a new composited layer. Composited
|
||||||
|
/// layers may double memory usage as the image is painted onto an offscreen
|
||||||
|
/// render target.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AlwaysStoppedAnimation], which allows you to create an [Animation]
|
||||||
|
/// from a single opacity value.
|
||||||
|
final Animation<double>? opacity;
|
||||||
|
|
||||||
/// The rendering quality of the image.
|
/// The rendering quality of the image.
|
||||||
///
|
///
|
||||||
/// If the image is of a high quality and its pixels are perfectly aligned
|
/// If the image is of a high quality and its pixels are perfectly aligned
|
||||||
@ -1071,6 +1090,7 @@ class Image extends StatefulWidget {
|
|||||||
properties.add(DoubleProperty('width', width, defaultValue: null));
|
properties.add(DoubleProperty('width', width, defaultValue: null));
|
||||||
properties.add(DoubleProperty('height', height, defaultValue: null));
|
properties.add(DoubleProperty('height', height, defaultValue: null));
|
||||||
properties.add(ColorProperty('color', color, defaultValue: null));
|
properties.add(ColorProperty('color', color, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<Animation<double>?>('opacity', opacity, defaultValue: null));
|
||||||
properties.add(EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null));
|
properties.add(EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null));
|
||||||
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
|
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
|
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
|
||||||
@ -1326,6 +1346,7 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
|
|||||||
height: widget.height,
|
height: widget.height,
|
||||||
scale: _imageInfo?.scale ?? 1.0,
|
scale: _imageInfo?.scale ?? 1.0,
|
||||||
color: widget.color,
|
color: widget.color,
|
||||||
|
opacity: widget.opacity,
|
||||||
colorBlendMode: widget.colorBlendMode,
|
colorBlendMode: widget.colorBlendMode,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
alignment: widget.alignment,
|
alignment: widget.alignment,
|
||||||
|
@ -43,14 +43,12 @@ class FadeInImageParts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FadeInImageElements {
|
class FadeInImageElements {
|
||||||
const FadeInImageElements(this.rawImageElement, this.fadeTransitionElement);
|
const FadeInImageElements(this.rawImageElement);
|
||||||
|
|
||||||
final Element rawImageElement;
|
final Element rawImageElement;
|
||||||
final Element? fadeTransitionElement;
|
|
||||||
|
|
||||||
RawImage get rawImage => rawImageElement.widget as RawImage;
|
RawImage get rawImage => rawImageElement.widget as RawImage;
|
||||||
FadeTransition? get fadeTransition => fadeTransitionElement?.widget as FadeTransition?;
|
double get opacity => rawImage.opacity?.value ?? 1.0;
|
||||||
double get opacity => fadeTransition == null ? 1 : fadeTransition!.opacity.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoadTestImageProvider extends ImageProvider<Object> {
|
class LoadTestImageProvider extends ImageProvider<Object> {
|
||||||
@ -78,11 +76,8 @@ FadeInImageParts findFadeInImage(WidgetTester tester) {
|
|||||||
final Iterable<Element> rawImageElements = tester.elementList(find.byType(RawImage));
|
final Iterable<Element> rawImageElements = tester.elementList(find.byType(RawImage));
|
||||||
ComponentElement? fadeInImageElement;
|
ComponentElement? fadeInImageElement;
|
||||||
for (final Element rawImageElement in rawImageElements) {
|
for (final Element rawImageElement in rawImageElements) {
|
||||||
Element? fadeTransitionElement;
|
|
||||||
rawImageElement.visitAncestorElements((Element ancestor) {
|
rawImageElement.visitAncestorElements((Element ancestor) {
|
||||||
if (ancestor.widget is FadeTransition) {
|
if (ancestor.widget is FadeInImage) {
|
||||||
fadeTransitionElement = ancestor;
|
|
||||||
} else if (ancestor.widget is FadeInImage) {
|
|
||||||
if (fadeInImageElement == null) {
|
if (fadeInImageElement == null) {
|
||||||
fadeInImageElement = ancestor as ComponentElement;
|
fadeInImageElement = ancestor as ComponentElement;
|
||||||
} else {
|
} else {
|
||||||
@ -93,7 +88,7 @@ FadeInImageParts findFadeInImage(WidgetTester tester) {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
expect(fadeInImageElement, isNotNull);
|
expect(fadeInImageElement, isNotNull);
|
||||||
elements.add(FadeInImageElements(rawImageElement, fadeTransitionElement));
|
elements.add(FadeInImageElements(rawImageElement));
|
||||||
}
|
}
|
||||||
if (elements.length == 2) {
|
if (elements.length == 2) {
|
||||||
return FadeInImageParts(fadeInImageElement!, elements.last, elements.first);
|
return FadeInImageParts(fadeInImageElement!, elements.last, elements.first);
|
||||||
|
@ -753,6 +753,19 @@ void main() {
|
|||||||
expect(renderer.colorBlendMode, BlendMode.clear);
|
expect(renderer.colorBlendMode, BlendMode.clear);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Image opacity parameter', (WidgetTester tester) async {
|
||||||
|
const Animation<double> opacity = AlwaysStoppedAnimation<double>(0.5);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Image(
|
||||||
|
excludeFromSemantics: true,
|
||||||
|
image: _TestImageProvider(),
|
||||||
|
opacity: opacity,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final RenderImage renderer = tester.renderObject<RenderImage>(find.byType(Image));
|
||||||
|
expect(renderer.opacity, opacity);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Precache', (WidgetTester tester) async {
|
testWidgets('Precache', (WidgetTester tester) async {
|
||||||
final _TestImageProvider provider = _TestImageProvider();
|
final _TestImageProvider provider = _TestImageProvider();
|
||||||
late Future<void> precache;
|
late Future<void> precache;
|
||||||
@ -1721,6 +1734,57 @@ void main() {
|
|||||||
skip: kIsWeb, // https://github.com/flutter/flutter/issues/54292.
|
skip: kIsWeb, // https://github.com/flutter/flutter/issues/54292.
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Image opacity',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final Key key = UniqueKey();
|
||||||
|
await tester.pumpWidget(RepaintBoundary(
|
||||||
|
key: key,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
children: <Widget>[
|
||||||
|
Image.memory(
|
||||||
|
Uint8List.fromList(kBlueRectPng),
|
||||||
|
opacity: const AlwaysStoppedAnimation<double>(0.25),
|
||||||
|
),
|
||||||
|
Image.memory(
|
||||||
|
Uint8List.fromList(kBlueRectPng),
|
||||||
|
opacity: const AlwaysStoppedAnimation<double>(0.5),
|
||||||
|
),
|
||||||
|
Image.memory(
|
||||||
|
Uint8List.fromList(kBlueRectPng),
|
||||||
|
opacity: const AlwaysStoppedAnimation<double>(0.75),
|
||||||
|
),
|
||||||
|
Image.memory(
|
||||||
|
Uint8List.fromList(kBlueRectPng),
|
||||||
|
opacity: const AlwaysStoppedAnimation<double>(1.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
// precacheImage is needed, or the image in the golden file will be empty.
|
||||||
|
if (!kIsWeb) {
|
||||||
|
final Finder allImages = find.byType(Image);
|
||||||
|
for (final Element e in allImages.evaluate()) {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final Image image = e.widget as Image;
|
||||||
|
await precacheImage(image.image, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byKey(key),
|
||||||
|
matchesGoldenFile('transparent_image.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // https://github.com/flutter/flutter/issues/54292.
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('Reports image size when painted', (WidgetTester tester) async {
|
testWidgets('Reports image size when painted', (WidgetTester tester) async {
|
||||||
late ImageSizeInfo imageSizeInfo;
|
late ImageSizeInfo imageSizeInfo;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user