1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:ui' as ui show Image;
6
7import 'package:flutter/animation.dart';
8import 'package:flutter/foundation.dart';
9
10import 'box.dart';
11import 'object.dart';
12
13export 'package:flutter/painting.dart' show
14 BoxFit,
15 ImageRepeat;
16
17/// An image in the render tree.
18///
19/// The render image attempts to find a size for itself that fits in the given
20/// constraints and preserves the image's intrinsic aspect ratio.
21///
22/// The image is painted using [paintImage], which describes the meanings of the
23/// various fields on this class in more detail.
24class RenderImage extends RenderBox {
25 /// Creates a render box that displays an image.
26 ///
27 /// The [textDirection] argument must not be null if [alignment] will need
28 /// resolving or if [matchTextDirection] is true.
29 RenderImage({
30 ui.Image? image,
31 this.debugImageLabel,
32 double? width,
33 double? height,
34 double scale = 1.0,
35 Color? color,
36 Animation<double>? opacity,
37 BlendMode? colorBlendMode,
38 BoxFit? fit,
39 AlignmentGeometry alignment = Alignment.center,
40 ImageRepeat repeat = ImageRepeat.noRepeat,
41 Rect? centerSlice,
42 bool matchTextDirection = false,
43 TextDirection? textDirection,
44 bool invertColors = false,
45 bool isAntiAlias = false,
46 FilterQuality filterQuality = FilterQuality.low,
47 }) : _image = image,
48 _width = width,
49 _height = height,
50 _scale = scale,
51 _color = color,
52 _opacity = opacity,
53 _colorBlendMode = colorBlendMode,
54 _fit = fit,
55 _alignment = alignment,
56 _repeat = repeat,
57 _centerSlice = centerSlice,
58 _matchTextDirection = matchTextDirection,
59 _invertColors = invertColors,
60 _textDirection = textDirection,
61 _isAntiAlias = isAntiAlias,
62 _filterQuality = filterQuality {
63 _updateColorFilter();
64 }
65
66 Alignment? _resolvedAlignment;
67 bool? _flipHorizontally;
68
69 void _resolve() {
70 if (_resolvedAlignment != null) {
71 return;
72 }
73 _resolvedAlignment = alignment.resolve(textDirection);
74 _flipHorizontally = matchTextDirection && textDirection == TextDirection.rtl;
75 }
76
77 void _markNeedResolution() {
78 _resolvedAlignment = null;
79 _flipHorizontally = null;
80 markNeedsPaint();
81 }
82
83 /// The image to display.
84 ui.Image? get image => _image;
85 ui.Image? _image;
86 set image(ui.Image? value) {
87 if (value == _image) {
88 return;
89 }
90 // If we get a clone of our image, it's the same underlying native data -
91 // dispose of the new clone and return early.
92 if (value != null && _image != null && value.isCloneOf(_image!)) {
93 value.dispose();
94 return;
95 }
96 _image?.dispose();
97 _image = value;
98 markNeedsPaint();
99 if (_width == null || _height == null) {
100 markNeedsLayout();
101 }
102 }
103
104 /// A string used to identify the source of the image.
105 String? debugImageLabel;
106
107 /// If non-null, requires the image to have this width.
108 ///
109 /// If null, the image will pick a size that best preserves its intrinsic
110 /// aspect ratio.
111 double? get width => _width;
112 double? _width;
113 set width(double? value) {
114 if (value == _width) {
115 return;
116 }
117 _width = value;
118 markNeedsLayout();
119 }
120
121 /// If non-null, require the image to have this height.
122 ///
123 /// If null, the image will pick a size that best preserves its intrinsic
124 /// aspect ratio.
125 double? get height => _height;
126 double? _height;
127 set height(double? value) {
128 if (value == _height) {
129 return;
130 }
131 _height = value;
132 markNeedsLayout();
133 }
134
135 /// Specifies the image's scale.
136 ///
137 /// Used when determining the best display size for the image.
138 double get scale => _scale;
139 double _scale;
140 set scale(double value) {
141 if (value == _scale) {
142 return;
143 }
144 _scale = value;
145 markNeedsLayout();
146 }
147
148 ColorFilter? _colorFilter;
149
150 void _updateColorFilter() {
151 if (_color == null) {
152 _colorFilter = null;
153 } else {
154 _colorFilter = ColorFilter.mode(_color!, _colorBlendMode ?? BlendMode.srcIn);
155 }
156 }
157
158 /// If non-null, this color is blended with each image pixel using [colorBlendMode].
159 Color? get color => _color;
160 Color? _color;
161 set color(Color? value) {
162 if (value == _color) {
163 return;
164 }
165 _color = value;
166 _updateColorFilter();
167 markNeedsPaint();
168 }
169
170 /// If non-null, the value from the [Animation] is multiplied with the opacity
171 /// of each image pixel before painting onto the canvas.
172 Animation<double>? get opacity => _opacity;
173 Animation<double>? _opacity;
174 set opacity(Animation<double>? value) {
175 if (value == _opacity) {
176 return;
177 }
178
179 if (attached) {
180 _opacity?.removeListener(markNeedsPaint);
181 }
182 _opacity = value;
183 if (attached) {
184 value?.addListener(markNeedsPaint);
185 }
186 }
187
188 /// Used to set the filterQuality of the image.
189 ///
190 /// Use the [FilterQuality.low] quality setting to scale the image, which corresponds to
191 /// bilinear interpolation, rather than the default [FilterQuality.none] which corresponds
192 /// to nearest-neighbor.
193 FilterQuality get filterQuality => _filterQuality;
194 FilterQuality _filterQuality;
195 set filterQuality(FilterQuality value) {
196 if (value == _filterQuality) {
197 return;
198 }
199 _filterQuality = value;
200 markNeedsPaint();
201 }
202
203
204 /// Used to combine [color] with this image.
205 ///
206 /// The default is [BlendMode.srcIn]. In terms of the blend mode, [color] is
207 /// the source and this image is the destination.
208 ///
209 /// See also:
210 ///
211 /// * [BlendMode], which includes an illustration of the effect of each blend mode.
212 BlendMode? get colorBlendMode => _colorBlendMode;
213 BlendMode? _colorBlendMode;
214 set colorBlendMode(BlendMode? value) {
215 if (value == _colorBlendMode) {
216 return;
217 }
218 _colorBlendMode = value;
219 _updateColorFilter();
220 markNeedsPaint();
221 }
222
223 /// How to inscribe the image into the space allocated during layout.
224 ///
225 /// The default varies based on the other fields. See the discussion at
226 /// [paintImage].
227 BoxFit? get fit => _fit;
228 BoxFit? _fit;
229 set fit(BoxFit? value) {
230 if (value == _fit) {
231 return;
232 }
233 _fit = value;
234 markNeedsPaint();
235 }
236
237 /// How to align the image within its bounds.
238 ///
239 /// If this is set to a text-direction-dependent value, [textDirection] must
240 /// not be null.
241 AlignmentGeometry get alignment => _alignment;
242 AlignmentGeometry _alignment;
243 set alignment(AlignmentGeometry value) {
244 if (value == _alignment) {
245 return;
246 }
247 _alignment = value;
248 _markNeedResolution();
249 }
250
251 /// How to repeat this image if it doesn't fill its layout bounds.
252 ImageRepeat get repeat => _repeat;
253 ImageRepeat _repeat;
254 set repeat(ImageRepeat value) {
255 if (value == _repeat) {
256 return;
257 }
258 _repeat = value;
259 markNeedsPaint();
260 }
261
262 /// The center slice for a nine-patch image.
263 ///
264 /// The region of the image inside the center slice will be stretched both
265 /// horizontally and vertically to fit the image into its destination. The
266 /// region of the image above and below the center slice will be stretched
267 /// only horizontally and the region of the image to the left and right of
268 /// the center slice will be stretched only vertically.
269 Rect? get centerSlice => _centerSlice;
270 Rect? _centerSlice;
271 set centerSlice(Rect? value) {
272 if (value == _centerSlice) {
273 return;
274 }
275 _centerSlice = value;
276 markNeedsPaint();
277 }
278
279 /// Whether to invert the colors of the image.
280 ///
281 /// Inverting the colors of an image applies a new color filter to the paint.
282 /// If there is another specified color filter, the invert will be applied
283 /// after it. This is primarily used for implementing smart invert on iOS.
284 bool get invertColors => _invertColors;
285 bool _invertColors;
286 set invertColors(bool value) {
287 if (value == _invertColors) {
288 return;
289 }
290 _invertColors = value;
291 markNeedsPaint();
292 }
293
294 /// Whether to paint the image in the direction of the [TextDirection].
295 ///
296 /// If this is true, then in [TextDirection.ltr] contexts, the image will be
297 /// drawn with its origin in the top left (the "normal" painting direction for
298 /// images); and in [TextDirection.rtl] contexts, the image will be drawn with
299 /// a scaling factor of -1 in the horizontal direction so that the origin is
300 /// in the top right.
301 ///
302 /// This is occasionally used with images in right-to-left environments, for
303 /// images that were designed for left-to-right locales. Be careful, when
304 /// using this, to not flip images with integral shadows, text, or other
305 /// effects that will look incorrect when flipped.
306 ///
307 /// If this is set to true, [textDirection] must not be null.
308 bool get matchTextDirection => _matchTextDirection;
309 bool _matchTextDirection;
310 set matchTextDirection(bool value) {
311 if (value == _matchTextDirection) {
312 return;
313 }
314 _matchTextDirection = value;
315 _markNeedResolution();
316 }
317
318 /// The text direction with which to resolve [alignment].
319 ///
320 /// This may be changed to null, but only after the [alignment] and
321 /// [matchTextDirection] properties have been changed to values that do not
322 /// depend on the direction.
323 TextDirection? get textDirection => _textDirection;
324 TextDirection? _textDirection;
325 set textDirection(TextDirection? value) {
326 if (_textDirection == value) {
327 return;
328 }
329 _textDirection = value;
330 _markNeedResolution();
331 }
332
333 /// Whether to paint the image with anti-aliasing.
334 ///
335 /// Anti-aliasing alleviates the sawtooth artifact when the image is rotated.
336 bool get isAntiAlias => _isAntiAlias;
337 bool _isAntiAlias;
338 set isAntiAlias(bool value) {
339 if (_isAntiAlias == value) {
340 return;
341 }
342 _isAntiAlias = value;
343 markNeedsPaint();
344 }
345
346 /// Find a size for the render image within the given constraints.
347 ///
348 /// - The dimensions of the RenderImage must fit within the constraints.
349 /// - The aspect ratio of the RenderImage matches the intrinsic aspect
350 /// ratio of the image.
351 /// - The RenderImage's dimension are maximal subject to being smaller than
352 /// the intrinsic size of the image.
353 Size _sizeForConstraints(BoxConstraints constraints) {
354 // Folds the given |width| and |height| into |constraints| so they can all
355 // be treated uniformly.
356 constraints = BoxConstraints.tightFor(
357 width: _width,
358 height: _height,
359 ).enforce(constraints);
360
361 if (_image == null) {
362 return constraints.smallest;
363 }
364
365 return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size(
366 _image!.width.toDouble() / _scale,
367 _image!.height.toDouble() / _scale,
368 ));
369 }
370
371 @override
372 double computeMinIntrinsicWidth(double height) {
373 assert(height >= 0.0);
374 if (_width == null && _height == null) {
375 return 0.0;
376 }
377 return _sizeForConstraints(BoxConstraints.tightForFinite(height: height)).width;
378 }
379
380 @override
381 double computeMaxIntrinsicWidth(double height) {
382 assert(height >= 0.0);
383 return _sizeForConstraints(BoxConstraints.tightForFinite(height: height)).width;
384 }
385
386 @override
387 double computeMinIntrinsicHeight(double width) {
388 assert(width >= 0.0);
389 if (_width == null && _height == null) {
390 return 0.0;
391 }
392 return _sizeForConstraints(BoxConstraints.tightForFinite(width: width)).height;
393 }
394
395 @override
396 double computeMaxIntrinsicHeight(double width) {
397 assert(width >= 0.0);
398 return _sizeForConstraints(BoxConstraints.tightForFinite(width: width)).height;
399 }
400
401 @override
402 bool hitTestSelf(Offset position) => true;
403
404 @override
405 @protected
406 Size computeDryLayout(covariant BoxConstraints constraints) {
407 return _sizeForConstraints(constraints);
408 }
409
410 @override
411 void performLayout() {
412 size = _sizeForConstraints(constraints);
413 }
414
415 @override
416 void attach(covariant PipelineOwner owner) {
417 super.attach(owner);
418 _opacity?.addListener(markNeedsPaint);
419 }
420
421 @override
422 void detach() {
423 _opacity?.removeListener(markNeedsPaint);
424 super.detach();
425 }
426
427 @override
428 void paint(PaintingContext context, Offset offset) {
429 if (_image == null) {
430 return;
431 }
432 _resolve();
433 assert(_resolvedAlignment != null);
434 assert(_flipHorizontally != null);
435 paintImage(
436 canvas: context.canvas,
437 rect: offset & size,
438 image: _image!,
439 debugImageLabel: debugImageLabel,
440 scale: _scale,
441 opacity: _opacity?.value ?? 1.0,
442 colorFilter: _colorFilter,
443 fit: _fit,
444 alignment: _resolvedAlignment!,
445 centerSlice: _centerSlice,
446 repeat: _repeat,
447 flipHorizontally: _flipHorizontally!,
448 invertColors: invertColors,
449 filterQuality: _filterQuality,
450 isAntiAlias: _isAntiAlias,
451 );
452 }
453
454 @override
455 void dispose() {
456 _image?.dispose();
457 _image = null;
458 super.dispose();
459 }
460
461 @override
462 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
463 super.debugFillProperties(properties);
464 properties.add(DiagnosticsProperty<ui.Image>('image', image));
465 properties.add(DoubleProperty('width', width, defaultValue: null));
466 properties.add(DoubleProperty('height', height, defaultValue: null));
467 properties.add(DoubleProperty('scale', scale, defaultValue: 1.0));
468 properties.add(ColorProperty('color', color, defaultValue: null));
469 properties.add(DiagnosticsProperty<Animation<double>?>('opacity', opacity, defaultValue: null));
470 properties.add(EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null));
471 properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
472 properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
473 properties.add(EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat));
474 properties.add(DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null));
475 properties.add(FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction'));
476 properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
477 properties.add(DiagnosticsProperty<bool>('invertColors', invertColors));
478 properties.add(EnumProperty<FilterQuality>('filterQuality', filterQuality));
479 }
480}
481