image.dart 14.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:ui' as ui show Image;
6

7 8
import 'package:flutter/animation.dart';

9 10
import 'box.dart';
import 'object.dart';
11

12
export 'package:flutter/painting.dart' show
13
  BoxFit,
14 15
  ImageRepeat;

16
/// An image in the render tree.
17 18
///
/// The render image attempts to find a size for itself that fits in the given
19
/// constraints and preserves the image's intrinsic aspect ratio.
20 21 22
///
/// The image is painted using [paintImage], which describes the meanings of the
/// various fields on this class in more detail.
23
class RenderImage extends RenderBox {
24
  /// Creates a render box that displays an image.
Ian Hickson's avatar
Ian Hickson committed
25
  ///
26
  /// The [scale], [alignment], [repeat], [matchTextDirection] and [filterQuality] arguments
Ian Hickson's avatar
Ian Hickson committed
27 28
  /// must not be null. The [textDirection] argument must not be null if
  /// [alignment] will need resolving or if [matchTextDirection] is true.
29
  RenderImage({
30
    ui.Image? image,
31
    this.debugImageLabel,
32 33
    double? width,
    double? height,
34
    double scale = 1.0,
35
    Color? color,
36
    Animation<double>? opacity,
37 38
    BlendMode? colorBlendMode,
    BoxFit? fit,
39 40
    AlignmentGeometry alignment = Alignment.center,
    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,
Ian Hickson's avatar
Ian Hickson committed
47 48 49
  }) : assert(scale != null),
       assert(repeat != null),
       assert(alignment != null),
50
       assert(filterQuality != null),
Ian Hickson's avatar
Ian Hickson committed
51
       assert(matchTextDirection != null),
52
       assert(isAntiAlias != null),
Ian Hickson's avatar
Ian Hickson committed
53 54 55 56 57
       _image = image,
       _width = width,
       _height = height,
       _scale = scale,
       _color = color,
58
       _opacity = opacity,
Ian Hickson's avatar
Ian Hickson committed
59 60 61 62 63 64
       _colorBlendMode = colorBlendMode,
       _fit = fit,
       _alignment = alignment,
       _repeat = repeat,
       _centerSlice = centerSlice,
       _matchTextDirection = matchTextDirection,
65
       _invertColors = invertColors,
66
       _textDirection = textDirection,
67
       _isAntiAlias = isAntiAlias,
68
       _filterQuality = filterQuality {
Adam Barth's avatar
Adam Barth committed
69 70
    _updateColorFilter();
  }
71

72 73
  Alignment? _resolvedAlignment;
  bool? _flipHorizontally;
Ian Hickson's avatar
Ian Hickson committed
74 75

  void _resolve() {
76
    if (_resolvedAlignment != null) {
Ian Hickson's avatar
Ian Hickson committed
77
      return;
78
    }
Ian Hickson's avatar
Ian Hickson committed
79 80 81 82 83 84 85 86 87 88
    _resolvedAlignment = alignment.resolve(textDirection);
    _flipHorizontally = matchTextDirection && textDirection == TextDirection.rtl;
  }

  void _markNeedResolution() {
    _resolvedAlignment = null;
    _flipHorizontally = null;
    markNeedsPaint();
  }

89
  /// The image to display.
90 91 92
  ui.Image? get image => _image;
  ui.Image? _image;
  set image(ui.Image? value) {
93
    if (value == _image) {
94
      return;
95 96 97 98 99 100 101 102
    }
    // 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();
103 104
    _image = value;
    markNeedsPaint();
105
    if (_width == null || _height == null) {
106
      markNeedsLayout();
107
    }
108 109
  }

110
  /// A string used to identify the source of the image.
111
  String? debugImageLabel;
112

113
  /// If non-null, requires the image to have this width.
114 115 116
  ///
  /// If null, the image will pick a size that best preserves its intrinsic
  /// aspect ratio.
117 118 119
  double? get width => _width;
  double? _width;
  set width(double? value) {
120
    if (value == _width) {
121
      return;
122
    }
123 124 125 126
    _width = value;
    markNeedsLayout();
  }

127 128 129 130
  /// 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.
131 132 133
  double? get height => _height;
  double? _height;
  set height(double? value) {
134
    if (value == _height) {
135
      return;
136
    }
137 138 139 140
    _height = value;
    markNeedsLayout();
  }

141
  /// Specifies the image's scale.
142 143 144 145
  ///
  /// Used when determining the best display size for the image.
  double get scale => _scale;
  double _scale;
146
  set scale(double value) {
147
    assert(value != null);
148
    if (value == _scale) {
149
      return;
150
    }
151 152 153 154
    _scale = value;
    markNeedsLayout();
  }

155
  ColorFilter? _colorFilter;
Adam Barth's avatar
Adam Barth committed
156 157

  void _updateColorFilter() {
158
    if (_color == null) {
Adam Barth's avatar
Adam Barth committed
159
      _colorFilter = null;
160
    } else {
161
      _colorFilter = ColorFilter.mode(_color!, _colorBlendMode ?? BlendMode.srcIn);
162
    }
Adam Barth's avatar
Adam Barth committed
163 164
  }

165
  /// If non-null, this color is blended with each image pixel using [colorBlendMode].
166 167 168
  Color? get color => _color;
  Color? _color;
  set color(Color? value) {
169
    if (value == _color) {
170
      return;
171
    }
Adam Barth's avatar
Adam Barth committed
172 173
    _color = value;
    _updateColorFilter();
174 175 176
    markNeedsPaint();
  }

177 178 179 180 181
  /// 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) {
182
    if (value == _opacity) {
183
      return;
184
    }
185

186
    if (attached) {
187
      _opacity?.removeListener(markNeedsPaint);
188
    }
189
    _opacity = value;
190
    if (attached) {
191
      value?.addListener(markNeedsPaint);
192
    }
193 194
  }

195 196
  /// Used to set the filterQuality of the image.
  ///
197 198 199 200 201 202 203
  /// 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) {
    assert(value != null);
204
    if (value == _filterQuality) {
205
      return;
206
    }
207 208 209 210 211
    _filterQuality = value;
    markNeedsPaint();
  }


212 213 214 215 216 217 218 219
  /// 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.
220 221 222
  BlendMode? get colorBlendMode => _colorBlendMode;
  BlendMode? _colorBlendMode;
  set colorBlendMode(BlendMode? value) {
223
    if (value == _colorBlendMode) {
224
      return;
225
    }
226
    _colorBlendMode = value;
227 228 229 230
    _updateColorFilter();
    markNeedsPaint();
  }

Adam Barth's avatar
Adam Barth committed
231
  /// How to inscribe the image into the space allocated during layout.
232 233 234
  ///
  /// The default varies based on the other fields. See the discussion at
  /// [paintImage].
235 236 237
  BoxFit? get fit => _fit;
  BoxFit? _fit;
  set fit(BoxFit? value) {
238
    if (value == _fit) {
239
      return;
240
    }
241 242 243 244
    _fit = value;
    markNeedsPaint();
  }

245
  /// How to align the image within its bounds.
Ian Hickson's avatar
Ian Hickson committed
246 247 248
  ///
  /// If this is set to a text-direction-dependent value, [textDirection] must
  /// not be null.
249 250 251
  AlignmentGeometry get alignment => _alignment;
  AlignmentGeometry _alignment;
  set alignment(AlignmentGeometry value) {
Ian Hickson's avatar
Ian Hickson committed
252
    assert(value != null);
253
    if (value == _alignment) {
254
      return;
255
    }
256
    _alignment = value;
Ian Hickson's avatar
Ian Hickson committed
257
    _markNeedResolution();
258 259
  }

260
  /// How to repeat this image if it doesn't fill its layout bounds.
261
  ImageRepeat get repeat => _repeat;
262
  ImageRepeat _repeat;
263
  set repeat(ImageRepeat value) {
Ian Hickson's avatar
Ian Hickson committed
264
    assert(value != null);
265
    if (value == _repeat) {
266
      return;
267
    }
268 269 270 271
    _repeat = value;
    markNeedsPaint();
  }

272 273 274 275 276 277 278
  /// 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.
279 280 281
  Rect? get centerSlice => _centerSlice;
  Rect? _centerSlice;
  set centerSlice(Rect? value) {
282
    if (value == _centerSlice) {
283
      return;
284
    }
285 286 287 288
    _centerSlice = value;
    markNeedsPaint();
  }

289 290
  /// Whether to invert the colors of the image.
  ///
291
  /// Inverting the colors of an image applies a new color filter to the paint.
292 293 294 295 296
  /// 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) {
297
    if (value == _invertColors) {
298
      return;
299
    }
300 301 302 303
    _invertColors = value;
    markNeedsPaint();
  }

Ian Hickson's avatar
Ian Hickson committed
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
  /// 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) {
    assert(value != null);
322
    if (value == _matchTextDirection) {
Ian Hickson's avatar
Ian Hickson committed
323
      return;
324
    }
Ian Hickson's avatar
Ian Hickson committed
325 326 327 328 329 330 331 332 333
    _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.
334 335 336
  TextDirection? get textDirection => _textDirection;
  TextDirection? _textDirection;
  set textDirection(TextDirection? value) {
337
    if (_textDirection == value) {
Ian Hickson's avatar
Ian Hickson committed
338
      return;
339
    }
Ian Hickson's avatar
Ian Hickson committed
340 341 342 343
    _textDirection = value;
    _markNeedResolution();
  }

344 345 346 347 348 349 350 351 352 353 354 355 356 357
  /// 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;
    }
    assert(value != null);
    _isAntiAlias = value;
    markNeedsPaint();
  }

358
  /// Find a size for the render image within the given constraints.
359 360
  ///
  ///  - The dimensions of the RenderImage must fit within the constraints.
361
  ///  - The aspect ratio of the RenderImage matches the intrinsic aspect
362 363 364
  ///    ratio of the image.
  ///  - The RenderImage's dimension are maximal subject to being smaller than
  ///    the intrinsic size of the image.
365
  Size _sizeForConstraints(BoxConstraints constraints) {
366
    // Folds the given |width| and |height| into |constraints| so they can all
367
    // be treated uniformly.
368
    constraints = BoxConstraints.tightFor(
369
      width: _width,
370
      height: _height,
371
    ).enforce(constraints);
372

373
    if (_image == null) {
374
      return constraints.smallest;
375
    }
376

377
    return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size(
378 379
      _image!.width.toDouble() / _scale,
      _image!.height.toDouble() / _scale,
Adam Barth's avatar
Adam Barth committed
380
    ));
381 382
  }

383
  @override
384
  double computeMinIntrinsicWidth(double height) {
385
    assert(height >= 0.0);
386
    if (_width == null && _height == null) {
387
      return 0.0;
388
    }
389
    return _sizeForConstraints(BoxConstraints.tightForFinite(height: height)).width;
390 391
  }

392
  @override
393
  double computeMaxIntrinsicWidth(double height) {
394
    assert(height >= 0.0);
395
    return _sizeForConstraints(BoxConstraints.tightForFinite(height: height)).width;
396 397
  }

398
  @override
399
  double computeMinIntrinsicHeight(double width) {
400
    assert(width >= 0.0);
401
    if (_width == null && _height == null) {
402
      return 0.0;
403
    }
404
    return _sizeForConstraints(BoxConstraints.tightForFinite(width: width)).height;
405 406
  }

407
  @override
408
  double computeMaxIntrinsicHeight(double width) {
409
    assert(width >= 0.0);
410
    return _sizeForConstraints(BoxConstraints.tightForFinite(width: width)).height;
411 412
  }

413
  @override
414
  bool hitTestSelf(Offset position) => true;
Adam Barth's avatar
Adam Barth committed
415

416 417 418 419 420
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return _sizeForConstraints(constraints);
  }

421
  @override
422 423 424 425
  void performLayout() {
    size = _sizeForConstraints(constraints);
  }

426 427 428 429 430 431 432 433 434 435 436 437
  @override
  void attach(covariant PipelineOwner owner) {
    super.attach(owner);
    _opacity?.addListener(markNeedsPaint);
  }

  @override
  void detach() {
    _opacity?.removeListener(markNeedsPaint);
    super.detach();
  }

438
  @override
439
  void paint(PaintingContext context, Offset offset) {
440
    if (_image == null) {
441
      return;
442
    }
Ian Hickson's avatar
Ian Hickson committed
443 444 445
    _resolve();
    assert(_resolvedAlignment != null);
    assert(_flipHorizontally != null);
446 447 448
    paintImage(
      canvas: context.canvas,
      rect: offset & size,
449
      image: _image!,
450
      debugImageLabel: debugImageLabel,
451
      scale: _scale,
452
      opacity: _opacity?.value ?? 1.0,
453 454
      colorFilter: _colorFilter,
      fit: _fit,
455
      alignment: _resolvedAlignment!,
456
      centerSlice: _centerSlice,
Ian Hickson's avatar
Ian Hickson committed
457
      repeat: _repeat,
458
      flipHorizontally: _flipHorizontally!,
459
      invertColors: invertColors,
460
      filterQuality: _filterQuality,
461
      isAntiAlias: _isAntiAlias,
462 463 464
    );
  }

465 466 467 468 469 470 471
  @override
  void dispose() {
    _image?.dispose();
    _image = null;
    super.dispose();
  }

472
  @override
473 474
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
475 476 477 478
    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));
479
    properties.add(ColorProperty('color', color, defaultValue: null));
480
    properties.add(DiagnosticsProperty<Animation<double>?>('opacity', opacity, defaultValue: null));
481 482 483 484 485 486 487
    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));
488
    properties.add(DiagnosticsProperty<bool>('invertColors', invertColors));
489
    properties.add(EnumProperty<FilterQuality>('filterQuality', filterQuality));
490
  }
491
}