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 | |
5 | import 'dart:ui' as ui show Image; |
6 | |
7 | import 'package:flutter/animation.dart'; |
8 | import 'package:flutter/foundation.dart'; |
9 | |
10 | import 'box.dart'; |
11 | import 'object.dart'; |
12 | |
13 | export '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. |
24 | class 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 | |