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