| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:ui' as ui show Image; | |
| import 'package:flutter/animation.dart'; | |
| import 'box.dart'; | |
| import 'object.dart'; | |
| export 'package:flutter/painting.dart' show | |
| BoxFit, | |
| ImageRepeat; | |
| /// An image in the render tree. | |
| /// | |
| /// The render image attempts to find a size for itself that fits in the given | |
| /// constraints and preserves the image's intrinsic aspect ratio. | |
| /// | |
| /// The image is painted using [paintImage], which describes the meanings of the | |
| /// various fields on this class in more detail. | |
| class RenderImage extends RenderBox { | |
| /// Creates a render box that displays an image. | |
| /// | |
| /// The [scale], [alignment], [repeat], [matchTextDirection] and [filterQuality] arguments | |
| /// must not be null. The [textDirection] argument must not be null if | |
| /// [alignment] will need resolving or if [matchTextDirection] is true. | |
| RenderImage({ | |
| ui.Image? image, | |
| this.debugImageLabel, | |
| double? width, | |
| double? height, | |
| double scale = 1.0, | |
| Color? color, | |
| Animation<double>? opacity, | |
| BlendMode? colorBlendMode, | |
| BoxFit? fit, | |
| AlignmentGeometry alignment = Alignment.center, | |
| ImageRepeat repeat = ImageRepeat.noRepeat, | |
| Rect? centerSlice, | |
| bool matchTextDirection = false, | |
| TextDirection? textDirection, | |
| bool invertColors = false, | |
| bool isAntiAlias = false, | |
| FilterQuality filterQuality = FilterQuality.low, | |
| }) : _image = image, | |
| _width = width, | |
| _height = height, | |
| _scale = scale, | |
| _color = color, | |
| _opacity = opacity, | |
| _colorBlendMode = colorBlendMode, | |
| _fit = fit, | |
| _alignment = alignment, | |
| _repeat = repeat, | |
| _centerSlice = centerSlice, | |
| _matchTextDirection = matchTextDirection, | |
| _invertColors = invertColors, | |
| _textDirection = textDirection, | |
| _isAntiAlias = isAntiAlias, | |
| _filterQuality = filterQuality { | |
| _updateColorFilter(); | |
| } | |
| Alignment? _resolvedAlignment; | |
| bool? _flipHorizontally; | |
| void _resolve() { | |
| if (_resolvedAlignment != null) { | |
| return; | |
| } | |
| _resolvedAlignment = alignment.resolve(textDirection); | |
| _flipHorizontally = matchTextDirection && textDirection == TextDirection.rtl; | |
| } | |
| void _markNeedResolution() { | |
| _resolvedAlignment = null; | |
| _flipHorizontally = null; | |
| markNeedsPaint(); | |
| } | |
| /// The image to display. | |
| ui.Image? get image => _image; | |
| ui.Image? _image; | |
| set image(ui.Image? value) { | |
| if (value == _image) { | |
| return; | |
| } | |
| // If we get a clone of our image, it's the same underlying native data - | |
| // dispose of the new clone and return early. | |
| if (value != null && _image != null && value.isCloneOf(_image!)) { | |
| value.dispose(); | |
| return; | |
| } | |
| _image?.dispose(); | |
| _image = value; | |
| markNeedsPaint(); | |
| if (_width == null || _height == null) { | |
| markNeedsLayout(); | |
| } | |
| } | |
| /// A string used to identify the source of the image. | |
| String? debugImageLabel; | |
| /// If non-null, requires the image to have this width. | |
| /// | |
| /// If null, the image will pick a size that best preserves its intrinsic | |
| /// aspect ratio. | |
| double? get width => _width; | |
| double? _width; | |
| set width(double? value) { | |
| if (value == _width) { | |
| return; | |
| } | |
| _width = value; | |
| markNeedsLayout(); | |
| } | |
| /// If non-null, require the image to have this height. | |
| /// | |
| /// If null, the image will pick a size that best preserves its intrinsic | |
| /// aspect ratio. | |
| double? get height => _height; | |
| double? _height; | |
| set height(double? value) { | |
| if (value == _height) { | |
| return; | |
| } | |
| _height = value; | |
| markNeedsLayout(); | |
| } | |
| /// Specifies the image's scale. | |
| /// | |
| /// Used when determining the best display size for the image. | |
| double get scale => _scale; | |
| double _scale; | |
| set scale(double value) { | |
| if (value == _scale) { | |
| return; | |
| } | |
| _scale = value; | |
| markNeedsLayout(); | |
| } | |
| ColorFilter? _colorFilter; | |
| void _updateColorFilter() { | |
| if (_color == null) { | |
| _colorFilter = null; | |
| } else { | |
| _colorFilter = ColorFilter.mode(_color!, _colorBlendMode ?? BlendMode.srcIn); | |
| } | |
| } | |
| /// If non-null, this color is blended with each image pixel using [colorBlendMode]. | |
| Color? get color => _color; | |
| Color? _color; | |
| set color(Color? value) { | |
| if (value == _color) { | |
| return; | |
| } | |
| _color = value; | |
| _updateColorFilter(); | |
| 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. | |
| /// | |
| /// Use the [FilterQuality.low] quality setting to scale the image, which corresponds to | |
| /// bilinear interpolation, rather than the default [FilterQuality.none] which corresponds | |
| /// to nearest-neighbor. | |
| FilterQuality get filterQuality => _filterQuality; | |
| FilterQuality _filterQuality; | |
| set filterQuality(FilterQuality value) { | |
| if (value == _filterQuality) { | |
| return; | |
| } | |
| _filterQuality = value; | |
| markNeedsPaint(); | |
| } | |
| /// Used to combine [color] with this image. | |
| /// | |
| /// The default is [BlendMode.srcIn]. In terms of the blend mode, [color] is | |
| /// the source and this image is the destination. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [BlendMode], which includes an illustration of the effect of each blend mode. | |
| BlendMode? get colorBlendMode => _colorBlendMode; | |
| BlendMode? _colorBlendMode; | |
| set colorBlendMode(BlendMode? value) { | |
| if (value == _colorBlendMode) { | |
| return; | |
| } | |
| _colorBlendMode = value; | |
| _updateColorFilter(); | |
| markNeedsPaint(); | |
| } | |
| /// How to inscribe the image into the space allocated during layout. | |
| /// | |
| /// The default varies based on the other fields. See the discussion at | |
| /// [paintImage]. | |
| BoxFit? get fit => _fit; | |
| BoxFit? _fit; | |
| set fit(BoxFit? value) { | |
| if (value == _fit) { | |
| return; | |
| } | |
| _fit = value; | |
| markNeedsPaint(); | |
| } | |
| /// How to align the image within its bounds. | |
| /// | |
| /// If this is set to a text-direction-dependent value, [textDirection] must | |
| /// not be null. | |
| AlignmentGeometry get alignment => _alignment; | |
| AlignmentGeometry _alignment; | |
| set alignment(AlignmentGeometry value) { | |
| if (value == _alignment) { | |
| return; | |
| } | |
| _alignment = value; | |
| _markNeedResolution(); | |
| } | |
| /// How to repeat this image if it doesn't fill its layout bounds. | |
| ImageRepeat get repeat => _repeat; | |
| ImageRepeat _repeat; | |
| set repeat(ImageRepeat value) { | |
| if (value == _repeat) { | |
| return; | |
| } | |
| _repeat = value; | |
| markNeedsPaint(); | |
| } | |
| /// The center slice for a nine-patch image. | |
| /// | |
| /// The region of the image inside the center slice will be stretched both | |
| /// horizontally and vertically to fit the image into its destination. The | |
| /// region of the image above and below the center slice will be stretched | |
| /// only horizontally and the region of the image to the left and right of | |
| /// the center slice will be stretched only vertically. | |
| Rect? get centerSlice => _centerSlice; | |
| Rect? _centerSlice; | |
| set centerSlice(Rect? value) { | |
| if (value == _centerSlice) { | |
| return; | |
| } | |
| _centerSlice = value; | |
| markNeedsPaint(); | |
| } | |
| /// Whether to invert the colors of the image. | |
| /// | |
| /// Inverting the colors of an image applies a new color filter to the paint. | |
| /// If there is another specified color filter, the invert will be applied | |
| /// after it. This is primarily used for implementing smart invert on iOS. | |
| bool get invertColors => _invertColors; | |
| bool _invertColors; | |
| set invertColors(bool value) { | |
| if (value == _invertColors) { | |
| return; | |
| } | |
| _invertColors = value; | |
| markNeedsPaint(); | |
| } | |
| /// Whether to paint the image in the direction of the [TextDirection]. | |
| /// | |
| /// If this is true, then in [TextDirection.ltr] contexts, the image will be | |
| /// drawn with its origin in the top left (the "normal" painting direction for | |
| /// images); and in [TextDirection.rtl] contexts, the image will be drawn with | |
| /// a scaling factor of -1 in the horizontal direction so that the origin is | |
| /// in the top right. | |
| /// | |
| /// This is occasionally used with images in right-to-left environments, for | |
| /// images that were designed for left-to-right locales. Be careful, when | |
| /// using this, to not flip images with integral shadows, text, or other | |
| /// effects that will look incorrect when flipped. | |
| /// | |
| /// If this is set to true, [textDirection] must not be null. | |
| bool get matchTextDirection => _matchTextDirection; | |
| bool _matchTextDirection; | |
| set matchTextDirection(bool value) { | |
| if (value == _matchTextDirection) { | |
| return; | |
| } | |
| _matchTextDirection = value; | |
| _markNeedResolution(); | |
| } | |
| /// The text direction with which to resolve [alignment]. | |
| /// | |
| /// This may be changed to null, but only after the [alignment] and | |
| /// [matchTextDirection] properties have been changed to values that do not | |
| /// depend on the direction. | |
| TextDirection? get textDirection => _textDirection; | |
| TextDirection? _textDirection; | |
| set textDirection(TextDirection? value) { | |
| if (_textDirection == value) { | |
| return; | |
| } | |
| _textDirection = value; | |
| _markNeedResolution(); | |
| } | |
| /// Whether to paint the image with anti-aliasing. | |
| /// | |
| /// Anti-aliasing alleviates the sawtooth artifact when the image is rotated. | |
| bool get isAntiAlias => _isAntiAlias; | |
| bool _isAntiAlias; | |
| set isAntiAlias(bool value) { | |
| if (_isAntiAlias == value) { | |
| return; | |
| } | |
| _isAntiAlias = value; | |
| markNeedsPaint(); | |
| } | |
| /// Find a size for the render image within the given constraints. | |
| /// | |
| /// - The dimensions of the RenderImage must fit within the constraints. | |
| /// - The aspect ratio of the RenderImage matches the intrinsic aspect | |
| /// ratio of the image. | |
| /// - The RenderImage's dimension are maximal subject to being smaller than | |
| /// the intrinsic size of the image. | |
| Size _sizeForConstraints(BoxConstraints constraints) { | |
| // Folds the given |width| and |height| into |constraints| so they can all | |
| // be treated uniformly. | |
| constraints = BoxConstraints.tightFor( | |
| width: _width, | |
| height: _height, | |
| ).enforce(constraints); | |
| if (_image == null) { | |
| return constraints.smallest; | |
| } | |
| return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size( | |
| _image!.width.toDouble() / _scale, | |
| _image!.height.toDouble() / _scale, | |
| )); | |
| } | |
| @override | |
| double computeMinIntrinsicWidth(double height) { | |
| assert(height >= 0.0); | |
| if (_width == null && _height == null) { | |
| return 0.0; | |
| } | |
| return _sizeForConstraints(BoxConstraints.tightForFinite(height: height)).width; | |
| } | |
| @override | |
| double computeMaxIntrinsicWidth(double height) { | |
| assert(height >= 0.0); | |
| return _sizeForConstraints(BoxConstraints.tightForFinite(height: height)).width; | |
| } | |
| @override | |
| double computeMinIntrinsicHeight(double width) { | |
| assert(width >= 0.0); | |
| if (_width == null && _height == null) { | |
| return 0.0; | |
| } | |
| return _sizeForConstraints(BoxConstraints.tightForFinite(width: width)).height; | |
| } | |
| @override | |
| double computeMaxIntrinsicHeight(double width) { | |
| assert(width >= 0.0); | |
| return _sizeForConstraints(BoxConstraints.tightForFinite(width: width)).height; | |
| } | |
| @override | |
| bool hitTestSelf(Offset position) => true; | |
| @override | |
| Size computeDryLayout(BoxConstraints constraints) { | |
| return _sizeForConstraints(constraints); | |
| } | |
| @override | |
| void performLayout() { | |
| size = _sizeForConstraints(constraints); | |
| } | |
| @override | |
| void attach(covariant PipelineOwner owner) { | |
| super.attach(owner); | |
| _opacity?.addListener(markNeedsPaint); | |
| } | |
| @override | |
| void detach() { | |
| _opacity?.removeListener(markNeedsPaint); | |
| super.detach(); | |
| } | |
| @override | |
| void paint(PaintingContext context, Offset offset) { | |
| if (_image == null) { | |
| return; | |
| } | |
| _resolve(); | |
| assert(_resolvedAlignment != null); | |
| assert(_flipHorizontally != null); | |
| paintImage( | |
| canvas: context.canvas, | |
| rect: offset & size, | |
| image: _image!, | |
| debugImageLabel: debugImageLabel, | |
| scale: _scale, | |
| opacity: _opacity?.value ?? 1.0, | |
| colorFilter: _colorFilter, | |
| fit: _fit, | |
| alignment: _resolvedAlignment!, | |
| centerSlice: _centerSlice, | |
| repeat: _repeat, | |
| flipHorizontally: _flipHorizontally!, | |
| invertColors: invertColors, | |
| filterQuality: _filterQuality, | |
| isAntiAlias: _isAntiAlias, | |
| ); | |
| } | |
| @override | |
| void dispose() { | |
| _image?.dispose(); | |
| _image = null; | |
| super.dispose(); | |
| } | |
| @override | |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
| super.debugFillProperties(properties); | |
| properties.add(DiagnosticsProperty<ui.Image>('image', image)); | |
| properties.add(DoubleProperty('width', width, defaultValue: null)); | |
| properties.add(DoubleProperty('height', height, defaultValue: null)); | |
| properties.add(DoubleProperty('scale', scale, defaultValue: 1.0)); | |
| 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<BoxFit>('fit', fit, defaultValue: null)); | |
| properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null)); | |
| properties.add(EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); | |
| properties.add(DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null)); | |
| properties.add(FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction')); | |
| properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); | |
| properties.add(DiagnosticsProperty<bool>('invertColors', invertColors)); | |
| properties.add(EnumProperty<FilterQuality>('filterQuality', filterQuality)); | |
| } | |
| } |