diff --git a/packages/flutter/lib/src/painting/box_decoration.dart b/packages/flutter/lib/src/painting/box_decoration.dart index 7c400a9e01..e37abeed2f 100644 --- a/packages/flutter/lib/src/painting/box_decoration.dart +++ b/packages/flutter/lib/src/painting/box_decoration.dart @@ -321,9 +321,20 @@ class _BoxDecorationPainter extends BoxPainter { ImageInfo _image; void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) { + // TODO(ianh): factor this out into a DecorationImage.paint method. final DecorationImage backgroundImage = _decoration.image; if (backgroundImage == null) return; + + bool flipHorizontally = false; + if (backgroundImage.matchTextDirection) { + // We check this first so that the assert will fire immediately, not just when the + // image is ready. + assert(configuration.textDirection != null, 'matchTextDirection can only be used when a TextDirection is available.'); + if (configuration.textDirection == TextDirection.rtl) + flipHorizontally = true; + } + final ImageStream newImageStream = backgroundImage.image.resolve(configuration); if (newImageStream.key != _imageStream?.key) { _imageStream?.removeListener(_imageListener); @@ -350,9 +361,10 @@ class _BoxDecorationPainter extends BoxPainter { image: image, colorFilter: backgroundImage.colorFilter, fit: backgroundImage.fit, - alignment: backgroundImage.alignment, + alignment: backgroundImage.alignment.resolve(configuration.textDirection), centerSlice: backgroundImage.centerSlice, repeat: backgroundImage.repeat, + flipHorizontally: flipHorizontally, ); if (clipPath != null) diff --git a/packages/flutter/lib/src/painting/images.dart b/packages/flutter/lib/src/painting/images.dart index 8c8cabb867..446daebaf2 100644 --- a/packages/flutter/lib/src/painting/images.dart +++ b/packages/flutter/lib/src/painting/images.dart @@ -35,15 +35,20 @@ enum ImageRepeat { class DecorationImage { /// Creates an image to show in a [BoxDecoration]. /// - /// The [image] argument must not be null. + /// The [image], [alignment], [repeat], and [matchTextDirection] arguments + /// must not be null. const DecorationImage({ @required this.image, this.colorFilter, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.centerSlice, this.repeat: ImageRepeat.noRepeat, - }) : assert(image != null); + this.matchTextDirection: false, + }) : assert(image != null), + assert(alignment != null), + assert(repeat != null), + assert(matchTextDirection != null); /// The image to be painted into the decoration. /// @@ -64,12 +69,23 @@ class DecorationImage { /// How to align the image within its bounds. /// - /// An alignment of (0.0, 0.0) aligns the image to the top-left corner of its - /// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle - /// of the right edge of its layout bounds. + /// The alignment aligns the given position in the image to the given position + /// in the layout bounds. For example, a [FractionalOffset] alignment of (0.0, + /// 0.0) aligns the image to the top-left corner of its layout bounds, while a + /// [FractionalOffset] alignment of (1.0, 1.0) aligns the bottom right of the + /// image with the bottom right corner of its layout bounds. Similarly, an + /// alignment of (0.5, 1.0) aligns the bottom middle of the image with the + /// middle of the bottom edge of its layout bounds. + /// + /// To display a subpart of an image, consider using a [CustomPainter] and + /// [Canvas.drawImageRect]. + /// + /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a + /// [FractionalOffsetDirectional]), then a [TextDirection] must be available + /// when the image is painted. /// /// Defaults to [FractionalOffset.center]. - final FractionalOffset alignment; + final FractionalOffsetGeometry alignment; /// The center slice for a nine-patch image. /// @@ -92,6 +108,15 @@ class DecorationImage { /// by the image. final ImageRepeat repeat; + /// 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. + final bool matchTextDirection; + @override bool operator ==(dynamic other) { if (identical(this, other)) @@ -104,11 +129,12 @@ class DecorationImage { && fit == typedOther.fit && alignment == typedOther.alignment && centerSlice == typedOther.centerSlice - && repeat == typedOther.repeat; + && repeat == typedOther.repeat + && matchTextDirection == typedOther.matchTextDirection; } @override - int get hashCode => hashValues(image, colorFilter, fit, alignment, centerSlice, repeat); + int get hashCode => hashValues(image, colorFilter, fit, alignment, centerSlice, repeat, matchTextDirection); @override String toString() { @@ -120,33 +146,44 @@ class DecorationImage { !(fit == BoxFit.fill && centerSlice != null) && !(fit == BoxFit.scaleDown && centerSlice == null)) properties.add('$fit'); - if (alignment != null) - properties.add('$alignment'); + properties.add('$alignment'); if (centerSlice != null) properties.add('centerSlice: $centerSlice'); if (repeat != ImageRepeat.noRepeat) properties.add('$repeat'); + if (matchTextDirection) + properties.add('match text direction'); return '$runtimeType(${properties.join(", ")})'; } } /// Paints an image into the given rectangle on the canvas. /// +/// The arguments have the following meanings: +/// /// * `canvas`: The canvas onto which the image will be painted. +/// /// * `rect`: The region of the canvas into which the image will be painted. /// The image might not fill the entire rectangle (e.g., depending on the /// `fit`). If `rect` is empty, nothing is painted. +/// /// * `image`: The image to paint onto the canvas. +/// /// * `colorFilter`: If non-null, the color filter to apply when painting the /// image. +/// /// * `fit`: How the image should be inscribed into `rect`. If null, the /// default behavior depends on `centerSlice`. If `centerSlice` is also null, /// the default behavior is [BoxFit.scaleDown]. If `centerSlice` is /// non-null, the default behavior is [BoxFit.fill]. See [BoxFit] for /// details. -/// * `repeat`: If the image does not fill `rect`, whether and how the image -/// should be repeated to fill `rect`. By default, the image is not repeated. -/// See [ImageRepeat] for details. +/// +/// * `alignment`: How the destination rectangle defined by applying `fit` is +/// aligned within `rect`. For example, if `fit` is [BoxFit.contain] and +/// `alignment` is [FractionalOffset.bottomRight], the image will be as large +/// as possible within `rect` and placed with its bottom right corner at the +/// bottom right corner of `rect`. Defaults to [FractionalOffset.center]. +/// /// * `centerSlice`: The image is drawn in nine portions described by splitting /// the image by drawing two horizontal lines and two vertical lines, where /// `centerSlice` describes the rectangle formed by the four points where @@ -157,11 +194,19 @@ class DecorationImage { /// remaining five regions are drawn by stretching them to fit such that they /// exactly cover the destination rectangle while maintaining their relative /// positions. -/// * `alignment`: How the destination rectangle defined by applying `fit` is -/// aligned within `rect`. For example, if `fit` is [BoxFit.contain] and -/// `alignment` is [FractionalOffset.bottomRight], the image will be as large -/// as possible within `rect` and placed with its bottom right corner at the -/// bottom right corner of `rect`. +/// +/// * `repeat`: If the image does not fill `rect`, whether and how the image +/// should be repeated to fill `rect`. By default, the image is not repeated. +/// See [ImageRepeat] for details. +/// +/// * `flipHorizontally`: Whether to flip the image horizontally. This is +/// occasionally used with images in right-to-left environments, for images +/// that were designed for left-to-right locales (or vice versa). Be careful, +/// when using this, to not flip images with integral shadows, text, or other +/// effects that will look incorrect when flipped. +/// +/// The `canvas`, `rect`, `image`, `alignment`, `repeat`, and `flipHorizontally` +/// arguments must not be null. /// /// See also: /// @@ -174,12 +219,16 @@ void paintImage({ @required ui.Image image, ColorFilter colorFilter, BoxFit fit, - FractionalOffset alignment, + FractionalOffset alignment: FractionalOffset.center, Rect centerSlice, ImageRepeat repeat: ImageRepeat.noRepeat, + bool flipHorizontally: false, }) { assert(canvas != null); assert(image != null); + assert(alignment != null); + assert(repeat != null); + assert(flipHorizontally != null); if (rect.isEmpty) return; Size outputSize = rect.size; @@ -219,16 +268,23 @@ void paintImage({ // to nearest-neighbor. paint.filterQuality = FilterQuality.low; } - final double dx = (outputSize.width - destinationSize.width) * (alignment?.dx ?? 0.5); - final double dy = (outputSize.height - destinationSize.height) * (alignment?.dy ?? 0.5); + final double dx = (outputSize.width - destinationSize.width) * (flipHorizontally ? 1.0 - alignment.dx : alignment.dx); + final double dy = (outputSize.height - destinationSize.height) * alignment.dy; final Offset destinationPosition = rect.topLeft.translate(dx, dy); final Rect destinationRect = destinationPosition & destinationSize; - if (repeat != ImageRepeat.noRepeat) { + final bool needSave = repeat != ImageRepeat.noRepeat || flipHorizontally; + if (needSave) canvas.save(); + if (repeat != ImageRepeat.noRepeat) canvas.clipRect(rect); + if (flipHorizontally) { + final double dx = -(rect.left + rect.width / 2.0); + canvas.translate(-dx, 0.0); + canvas.scale(-1.0, 1.0); + canvas.translate(dx, 0.0); } if (centerSlice == null) { - final Rect sourceRect = (alignment ?? FractionalOffset.center).inscribe( + final Rect sourceRect = alignment.inscribe( fittedSizes.source, Offset.zero & inputSize ); for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) @@ -237,7 +293,7 @@ void paintImage({ for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) canvas.drawImageNine(image, centerSlice, tileRect, paint); } - if (repeat != ImageRepeat.noRepeat) + if (needSave) canvas.restore(); } diff --git a/packages/flutter/lib/src/rendering/image.dart b/packages/flutter/lib/src/rendering/image.dart index beea2bf05e..8739f74853 100644 --- a/packages/flutter/lib/src/rendering/image.dart +++ b/packages/flutter/lib/src/rendering/image.dart @@ -20,6 +20,10 @@ export 'package:flutter/painting.dart' show /// various fields on this class in more detail. class RenderImage extends RenderBox { /// Creates a render box that displays an image. + /// + /// The [scale], [alignment], [repeat], and [matchTextDirection] 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, double width, @@ -28,22 +32,46 @@ class RenderImage extends RenderBox { Color color, BlendMode colorBlendMode, BoxFit fit, - FractionalOffset alignment, + FractionalOffsetGeometry alignment: FractionalOffset.center, ImageRepeat repeat: ImageRepeat.noRepeat, - Rect centerSlice - }) : _image = image, - _width = width, - _height = height, - _scale = scale, - _color = color, - _colorBlendMode = colorBlendMode, - _fit = fit, - _alignment = alignment, - _repeat = repeat, - _centerSlice = centerSlice { + Rect centerSlice, + bool matchTextDirection: false, + TextDirection textDirection, + }) : assert(scale != null), + assert(repeat != null), + assert(alignment != null), + assert(matchTextDirection != null), + _image = image, + _width = width, + _height = height, + _scale = scale, + _color = color, + _colorBlendMode = colorBlendMode, + _fit = fit, + _alignment = alignment, + _repeat = repeat, + _centerSlice = centerSlice, + _matchTextDirection = matchTextDirection, + _textDirection = textDirection { _updateColorFilter(); } + FractionalOffset _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; @@ -147,19 +175,24 @@ class RenderImage extends RenderBox { } /// How to align the image within its bounds. - FractionalOffset get alignment => _alignment; - FractionalOffset _alignment; - set alignment(FractionalOffset value) { + /// + /// If this is set to a text-direction-dependent value, [textDirection] must + /// not be null. + FractionalOffsetGeometry get alignment => _alignment; + FractionalOffsetGeometry _alignment; + set alignment(FractionalOffsetGeometry value) { + assert(value != null); if (value == _alignment) return; _alignment = value; - markNeedsPaint(); + _markNeedResolution(); } /// How to repeat this image if it doesn't fill its layout bounds. ImageRepeat get repeat => _repeat; ImageRepeat _repeat; set repeat(ImageRepeat value) { + assert(value != null); if (value == _repeat) return; _repeat = value; @@ -182,6 +215,44 @@ class RenderImage extends RenderBox { 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) { + assert(value != null); + 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(); + } + /// Find a size for the render image within the given constraints. /// /// - The dimensions of the RenderImage must fit within the constraints. @@ -246,15 +317,19 @@ class RenderImage extends RenderBox { 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, colorFilter: _colorFilter, fit: _fit, - alignment: _alignment, + alignment: _resolvedAlignment, centerSlice: _centerSlice, - repeat: _repeat + repeat: _repeat, + flipHorizontally: _flipHorizontally, ); } @@ -271,5 +346,7 @@ class RenderImage extends RenderBox { description.add(new DiagnosticsProperty('alignment', alignment, defaultValue: null)); description.add(new EnumProperty('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); description.add(new DiagnosticsProperty('centerSlice', centerSlice, defaultValue: null)); + description.add(new FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction')); + description.add(new EnumProperty('textDirection', textDirection, defaultValue: null)); } } diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index a3922df8be..5d63678a68 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -17,7 +17,7 @@ import 'layer.dart'; import 'node.dart'; import 'semantics.dart'; -export 'package:flutter/foundation.dart' show FlutterError, InformationCollector, DiagnosticsNode, DiagnosticsProperty, StringProperty, DoubleProperty, EnumProperty, IntProperty, DiagnosticPropertiesBuilder; +export 'package:flutter/foundation.dart' show FlutterError, InformationCollector, DiagnosticsNode, DiagnosticsProperty, StringProperty, DoubleProperty, EnumProperty, FlagProperty, IntProperty, DiagnosticPropertiesBuilder; export 'package:flutter/gestures.dart' show HitTestEntry, HitTestResult; export 'package:flutter/painting.dart'; diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index 881d5ea03f..2f85669456 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -98,20 +98,20 @@ class RenderPadding extends RenderShiftedBox { assert(padding.isNonNegative), _textDirection = textDirection, _padding = padding, - super(child) { - _applyUpdate(); - } + super(child); - // The resolved absolute insets. EdgeInsets _resolvedPadding; - void _applyUpdate() { - final EdgeInsets resolvedPadding = padding.resolve(textDirection); - assert(resolvedPadding.isNonNegative); - if (_resolvedPadding != resolvedPadding) { - _resolvedPadding = resolvedPadding; - markNeedsLayout(); - } + void _resolve() { + if (_resolvedPadding != null) + return; + _resolvedPadding = padding.resolve(textDirection); + assert(_resolvedPadding.isNonNegative); + } + + void _markNeedResolution() { + _resolvedPadding = null; + markNeedsLayout(); } /// The amount to pad the child in each dimension. @@ -126,21 +126,25 @@ class RenderPadding extends RenderShiftedBox { if (_padding == value) return; _padding = value; - _applyUpdate(); + _markNeedResolution(); } /// The text direction with which to resolve [padding]. + /// + /// This may be changed to null, but only after the [padding] has been changed + /// to a value that does not depend on the direction. TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { if (_textDirection == value) return; _textDirection = value; - _applyUpdate(); + _markNeedResolution(); } @override double computeMinIntrinsicWidth(double height) { + _resolve(); final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; if (child != null) // next line relies on double.INFINITY absorption @@ -150,6 +154,7 @@ class RenderPadding extends RenderShiftedBox { @override double computeMaxIntrinsicWidth(double height) { + _resolve(); final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; if (child != null) // next line relies on double.INFINITY absorption @@ -159,6 +164,7 @@ class RenderPadding extends RenderShiftedBox { @override double computeMinIntrinsicHeight(double width) { + _resolve(); final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; if (child != null) // next line relies on double.INFINITY absorption @@ -168,6 +174,7 @@ class RenderPadding extends RenderShiftedBox { @override double computeMaxIntrinsicHeight(double width) { + _resolve(); final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; if (child != null) // next line relies on double.INFINITY absorption @@ -177,6 +184,7 @@ class RenderPadding extends RenderShiftedBox { @override void performLayout() { + _resolve(); assert(_resolvedPadding != null); if (child == null) { size = constraints.constrain(new Size( @@ -226,19 +234,19 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { }) : assert(alignment != null), _alignment = alignment, _textDirection = textDirection, - super(child) { - _applyUpdate(); - } + super(child); - // The resolved absolute alignment. FractionalOffset _resolvedAlignment; - void _applyUpdate() { - final FractionalOffset resolvedAlignment = alignment.resolve(textDirection); - if (_resolvedAlignment != resolvedAlignment) { - _resolvedAlignment = resolvedAlignment; - markNeedsLayout(); - } + void _resolve() { + if (_resolvedAlignment != null) + return; + _resolvedAlignment = alignment.resolve(textDirection); + } + + void _markNeedResolution() { + _resolvedAlignment = null; + markNeedsLayout(); } /// How to align the child. @@ -251,7 +259,7 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { /// For example, a value of 0.5 means that the center of the child is aligned /// with the center of the parent. /// - /// If this is set to an [FractionalOffsetDirectional] object, then + /// If this is set to a [FractionalOffsetDirectional] object, then /// [textDirection] must not be null. FractionalOffsetGeometry get alignment => _alignment; FractionalOffsetGeometry _alignment; @@ -263,17 +271,20 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { if (_alignment == value) return; _alignment = value; - _applyUpdate(); + _markNeedResolution(); } /// The text direction with which to resolve [alignment]. + /// + /// This may be changed to null, but only after [alignment] has been changed + /// to a value that does not depend on the direction. TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { if (_textDirection == value) return; _textDirection = value; - _applyUpdate(); + _markNeedResolution(); } /// Apply the current [alignment] to the [child]. @@ -285,10 +296,12 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { /// This method must be called after the child has been laid out and /// this object's own size has been set. void alignChild() { + _resolve(); assert(child != null); assert(!child.debugNeedsLayout); assert(child.hasSize); assert(hasSize); + assert(_resolvedAlignment != null); final BoxParentData childParentData = child.parentData; childParentData.offset = _resolvedAlignment.alongOffset(size - child.size); } diff --git a/packages/flutter/lib/src/rendering/sliver_padding.dart b/packages/flutter/lib/src/rendering/sliver_padding.dart index 87a1f2bb0b..8d01bf9c8d 100644 --- a/packages/flutter/lib/src/rendering/sliver_padding.dart +++ b/packages/flutter/lib/src/rendering/sliver_padding.dart @@ -37,22 +37,26 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin _padding; EdgeInsetsGeometry _padding; set padding(EdgeInsetsGeometry value) { @@ -61,17 +65,20 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { if (_textDirection == value) return; _textDirection = value; - _applyUpdate(); + _markNeedResolution(); } /// The padding in the scroll direction on the side nearest the 0.0 scroll direction. @@ -82,6 +89,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin _alignment; FractionalOffsetGeometry _alignment; set alignment(FractionalOffsetGeometry value) { assert(value != null); - if (_alignment != value) { - _alignment = value; - _applyUpdate(); - } + if (_alignment == value) + return; + _alignment = value; + _markNeedResolution(); } /// The text direction with which to resolve [alignment]. + /// + /// This may be changed to null, but only after the [alignment] has been changed + /// to a value that does not depend on the direction. TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { - if (_textDirection != value) { - _textDirection = value; - _applyUpdate(); - } + if (_textDirection == value) + return; + _textDirection = value; + _markNeedResolution(); } /// How to size the non-positioned children in the stack. @@ -426,6 +433,8 @@ class RenderStack extends RenderBox @override void performLayout() { + _resolve(); + assert(_resolvedAlignment != null); _hasVisualOverflow = false; bool hasNonPositionedChildren = false; diff --git a/packages/flutter/lib/src/services/image_provider.dart b/packages/flutter/lib/src/services/image_provider.dart index bd46311216..39669d0e2f 100644 --- a/packages/flutter/lib/src/services/image_provider.dart +++ b/packages/flutter/lib/src/services/image_provider.dart @@ -6,7 +6,7 @@ import 'dart:async'; import 'dart:io' show File; import 'dart:typed_data'; import 'dart:ui' as ui show Image; -import 'dart:ui' show Size, Locale, hashValues; +import 'dart:ui' show Size, Locale, TextDirection, hashValues; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; @@ -36,8 +36,9 @@ class ImageConfiguration { this.bundle, this.devicePixelRatio, this.locale, + this.textDirection, this.size, - this.platform + this.platform, }); /// Creates an object holding the configuration information for an [ImageProvider]. @@ -48,15 +49,17 @@ class ImageConfiguration { AssetBundle bundle, double devicePixelRatio, Locale locale, + TextDirection textDirection, Size size, - String platform + String platform, }) { return new ImageConfiguration( bundle: bundle ?? this.bundle, devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, locale: locale ?? this.locale, + textDirection: textDirection ?? this.textDirection, size: size ?? this.size, - platform: platform ?? this.platform + platform: platform ?? this.platform, ); } @@ -70,6 +73,9 @@ class ImageConfiguration { /// The language and region for which to select the image. final Locale locale; + /// The reading direction of the language for which to select the image. + final TextDirection textDirection; + /// The size at which the image will be rendered. final Size size; @@ -92,6 +98,7 @@ class ImageConfiguration { return typedOther.bundle == bundle && typedOther.devicePixelRatio == devicePixelRatio && typedOther.locale == locale + && typedOther.textDirection == textDirection && typedOther.size == size && typedOther.platform == platform; } @@ -122,6 +129,12 @@ class ImageConfiguration { result.write('locale: $locale'); hasArguments = true; } + if (textDirection != null) { + if (hasArguments) + result.write(', '); + result.write('textDirection: $textDirection'); + hasArguments = true; + } if (size != null) { if (hasArguments) result.write(', '); diff --git a/packages/flutter/lib/src/services/image_resolution.dart b/packages/flutter/lib/src/services/image_resolution.dart index 656401150b..e0e605f410 100644 --- a/packages/flutter/lib/src/services/image_resolution.dart +++ b/packages/flutter/lib/src/services/image_resolution.dart @@ -224,8 +224,9 @@ class AssetImage extends AssetBundleImageProvider { final SplayTreeMap mapping = new SplayTreeMap(); for (String candidate in candidates) mapping[_parseScale(candidate)] = candidate; - // TODO(ianh): implement support for config.locale, config.size, config.platform - // (then document this over in the Image.asset docs) + // TODO(ianh): implement support for config.locale, config.textDirection, + // config.size, config.platform (then document this over in the Image.asset + // docs) return _findNearest(mapping, config.devicePixelRatio); } diff --git a/packages/flutter/lib/src/widgets/animated_list.dart b/packages/flutter/lib/src/widgets/animated_list.dart index ddbaff4ac7..08ff3da3f4 100644 --- a/packages/flutter/lib/src/widgets/animated_list.dart +++ b/packages/flutter/lib/src/widgets/animated_list.dart @@ -161,8 +161,8 @@ class AnimatedList extends StatefulWidget { /// AnimatedListState animatedList = AnimatedList.of(context); /// ``` static AnimatedListState of(BuildContext context, { bool nullOk: false }) { - assert(nullOk != null); assert(context != null); + assert(nullOk != null); final AnimatedListState result = context.ancestorStateOfType(const TypeMatcher()); if (nullOk || result != null) return result; diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index a56a982ec2..9dba5315a9 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -3826,7 +3826,8 @@ class RichText extends LeafRenderObjectWidget { class RawImage extends LeafRenderObjectWidget { /// Creates a widget that displays an image. /// - /// The [scale] and [repeat] arguments must not be null. + /// The [scale], [alignment], [repeat], and [matchTextDirection] arguments must + /// not be null. const RawImage({ Key key, this.image, @@ -3836,11 +3837,14 @@ class RawImage extends LeafRenderObjectWidget { this.color, this.colorBlendMode, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.repeat: ImageRepeat.noRepeat, - this.centerSlice + this.centerSlice, + this.matchTextDirection: false, }) : assert(scale != null), + assert(alignment != null), assert(repeat != null), + assert(matchTextDirection != null), super(key: key); /// The image to display. @@ -3884,10 +3888,23 @@ class RawImage extends LeafRenderObjectWidget { /// How to align the image within its bounds. /// - /// An alignment of (0.0, 0.0) aligns the image to the top-left corner of its - /// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle - /// of the right edge of its layout bounds. - final FractionalOffset alignment; + /// The alignment aligns the given position in the image to the given position + /// in the layout bounds. For example, a [FractionalOffset] alignment of (0.0, + /// 0.0) aligns the image to the top-left corner of its layout bounds, while a + /// [FractionalOffset] alignment of (1.0, 1.0) aligns the bottom right of the + /// image with the bottom right corner of its layout bounds. Similarly, an + /// alignment of (0.5, 1.0) aligns the bottom middle of the image with the + /// middle of the bottom edge of its layout bounds. + /// + /// To display a subpart of an image, consider using a [CustomPainter] and + /// [Canvas.drawImageRect]. + /// + /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a + /// [FractionalOffsetDirectional]), then an ambient [Directionality] widget + /// must be in scope. + /// + /// Defaults to [FractionalOffset.center]. + final FractionalOffsetGeometry alignment; /// How to paint any portions of the layout bounds not covered by the image. final ImageRepeat repeat; @@ -3901,8 +3918,26 @@ class RawImage extends LeafRenderObjectWidget { /// the center slice will be stretched only vertically. final Rect centerSlice; + /// 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 true, there must be an ambient [Directionality] widget in + /// scope. + final bool matchTextDirection; + @override RenderImage createRenderObject(BuildContext context) { + assert((!matchTextDirection && alignment is FractionalOffset) || debugCheckHasDirectionality(context)); return new RenderImage( image: image, width: width, @@ -3913,7 +3948,9 @@ class RawImage extends LeafRenderObjectWidget { fit: fit, alignment: alignment, repeat: repeat, - centerSlice: centerSlice + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + textDirection: matchTextDirection || alignment is! FractionalOffset ? Directionality.of(context) : null, ); } @@ -3929,7 +3966,9 @@ class RawImage extends LeafRenderObjectWidget { ..alignment = alignment ..fit = fit ..repeat = repeat - ..centerSlice = centerSlice; + ..centerSlice = centerSlice + ..matchTextDirection = matchTextDirection + ..textDirection = matchTextDirection || alignment is! FractionalOffset ? Directionality.of(context) : null; } @override @@ -3942,9 +3981,10 @@ class RawImage extends LeafRenderObjectWidget { description.add(new DiagnosticsProperty('color', color, defaultValue: null)); description.add(new EnumProperty('colorBlendMode', colorBlendMode, defaultValue: null)); description.add(new EnumProperty('fit', fit, defaultValue: null)); - description.add(new DiagnosticsProperty('alignment', alignment, defaultValue: null)); + description.add(new DiagnosticsProperty('alignment', alignment, defaultValue: null)); description.add(new EnumProperty('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); description.add(new DiagnosticsProperty('centerSlice', centerSlice, defaultValue: null)); + description.add(new FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction')); } } diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index 676212df8c..b61503c938 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -71,7 +71,7 @@ class DecoratedBox extends SingleChildRenderObjectWidget { return new RenderDecoratedBox( decoration: decoration, position: position, - configuration: createLocalImageConfiguration(context) + configuration: createLocalImageConfiguration(context), ); } diff --git a/packages/flutter/lib/src/widgets/fade_in_image.dart b/packages/flutter/lib/src/widgets/fade_in_image.dart index d7ae6692fd..aded85759c 100644 --- a/packages/flutter/lib/src/widgets/fade_in_image.dart +++ b/packages/flutter/lib/src/widgets/fade_in_image.dart @@ -60,7 +60,8 @@ class FadeInImage extends StatefulWidget { /// then cross-fades to display the [image]. /// /// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve], - /// [fadeInDuration], [fadeInCurve] and [repeat] arguments must not be null. + /// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and + /// [matchTextDirection] arguments must not be null. const FadeInImage({ Key key, @required this.placeholder, @@ -72,15 +73,18 @@ class FadeInImage extends StatefulWidget { this.width, this.height, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.repeat: ImageRepeat.noRepeat, + this.matchTextDirection: false, }) : assert(placeholder != null), assert(image != null), assert(fadeOutDuration != null), assert(fadeOutCurve != null), assert(fadeInDuration != null), assert(fadeInCurve != null), + assert(alignment != null), assert(repeat != null), + assert(matchTextDirection != null), super(key: key); /// Creates a widget that uses a placeholder image stored in memory while @@ -94,8 +98,9 @@ class FadeInImage extends StatefulWidget { /// [ImageProvider]s (see also [ImageInfo.scale]). /// /// The [placeholder], [image], [placeholderScale], [imageScale], - /// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve] and - /// [repeat] arguments must not be null. + /// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve], + /// [alignment], [repeat], and [matchTextDirection] arguments must not be + /// null. /// /// See also: /// @@ -116,8 +121,9 @@ class FadeInImage extends StatefulWidget { this.width, this.height, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.repeat: ImageRepeat.noRepeat, + this.matchTextDirection: false, }) : assert(placeholder != null), assert(image != null), assert(placeholderScale != null), @@ -126,7 +132,9 @@ class FadeInImage extends StatefulWidget { assert(fadeOutCurve != null), assert(fadeInDuration != null), assert(fadeInCurve != null), + assert(alignment != null), assert(repeat != null), + assert(matchTextDirection != null), placeholder = new MemoryImage(placeholder, scale: placeholderScale), image = new NetworkImage(image, scale: imageScale), super(key: key); @@ -146,8 +154,8 @@ class FadeInImage extends StatefulWidget { /// exact asset specified will be used. /// /// The [placeholder], [image], [imageScale], [fadeOutDuration], - /// [fadeOutCurve], [fadeInDuration], [fadeInCurve] and [repeat] arguments - /// must not be null. + /// [fadeOutCurve], [fadeInDuration], [fadeInCurve], [alignment], [repeat], + /// and [matchTextDirection] arguments must not be null. /// /// See also: /// @@ -169,8 +177,9 @@ class FadeInImage extends StatefulWidget { this.width, this.height, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.repeat: ImageRepeat.noRepeat, + this.matchTextDirection: false, }) : assert(placeholder != null), assert(image != null), placeholder = placeholderScale != null @@ -181,7 +190,9 @@ class FadeInImage extends StatefulWidget { assert(fadeOutCurve != null), assert(fadeInDuration != null), assert(fadeInCurve != null), + assert(alignment != null), assert(repeat != null), + assert(matchTextDirection != null), image = new NetworkImage(image, scale: imageScale), super(key: key); @@ -227,14 +238,41 @@ class FadeInImage extends StatefulWidget { /// How to align the image within its bounds. /// - /// An alignment of (0.0, 0.0) aligns the image to the top-left corner of its - /// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle - /// of the right edge of its layout bounds. - final FractionalOffset alignment; + /// The alignment aligns the given position in the image to the given position + /// in the layout bounds. For example, a [FractionalOffset] alignment of (0.0, + /// 0.0) aligns the image to the top-left corner of its layout bounds, while a + /// [FractionalOffset] alignment of (1.0, 1.0) aligns the bottom right of the + /// image with the bottom right corner of its layout bounds. Similarly, an + /// alignment of (0.5, 1.0) aligns the bottom middle of the image with the + /// middle of the bottom edge of its layout bounds. + /// + /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a + /// [FractionalOffsetDirectional]), then an ambient [Directionality] widget + /// must be in scope. + /// + /// Defaults to [FractionalOffset.center]. + final FractionalOffsetGeometry alignment; /// How to paint any portions of the layout bounds not covered by the image. final ImageRepeat repeat; + /// 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 true, there must be an ambient [Directionality] widget in + /// scope. + final bool matchTextDirection; + @override State createState() => new _FadeInImageState(); } @@ -282,8 +320,8 @@ class _ImageProviderResolver { void resolve(ImageProvider provider) { final ImageStream oldImageStream = _imageStream; _imageStream = provider.resolve(createLocalImageConfiguration( - state.context, - size: widget.width != null && widget.height != null ? new Size(widget.width, widget.height) : null + state.context, + size: widget.width != null && widget.height != null ? new Size(widget.width, widget.height) : null )); assert(_imageStream != null); @@ -456,6 +494,7 @@ class _FadeInImageState extends State with TickerProviderStateMixin fit: widget.fit, alignment: widget.alignment, repeat: widget.repeat, + matchTextDirection: widget.matchTextDirection, ); } diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart index f9cdd51ac6..2ff5375bd7 100644 --- a/packages/flutter/lib/src/widgets/image.dart +++ b/packages/flutter/lib/src/widgets/image.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'basic.dart'; import 'framework.dart'; +import 'localizations.dart'; import 'media_query.dart'; export 'package:flutter/services.dart' show @@ -39,7 +40,8 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si return new ImageConfiguration( bundle: DefaultAssetBundle.of(context), devicePixelRatio: MediaQuery.of(context, nullOk: true)?.devicePixelRatio ?? 1.0, - // TODO(ianh): provide the locale + locale: Localizations.localeOf(context, nullOk: true), + textDirection: Directionality.of(context), size: size, platform: defaultTargetPlatform, ); @@ -101,7 +103,8 @@ class Image extends StatefulWidget { /// To show an image from the network or from an asset bundle, consider using /// [new Image.network] and [new Image.asset] respectively. /// - /// The [image] and [repeat] arguments must not be null. + /// The [image], [alignment], [repeat], and [matchTextDirection] arguments + /// must not be null. const Image({ Key key, @required this.image, @@ -110,12 +113,16 @@ class Image extends StatefulWidget { this.color, this.colorBlendMode, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.repeat: ImageRepeat.noRepeat, this.centerSlice, + this.matchTextDirection: false, this.gaplessPlayback: false, this.package, }) : assert(image != null), + assert(alignment != null), + assert(repeat != null), + assert(matchTextDirection != null), super(key: key); /// Creates a widget that displays an [ImageStream] obtained from the network. @@ -129,12 +136,16 @@ class Image extends StatefulWidget { this.color, this.colorBlendMode, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.repeat: ImageRepeat.noRepeat, this.centerSlice, + this.matchTextDirection: false, this.gaplessPlayback: false, this.package, }) : image = new NetworkImage(src, scale: scale), + assert(alignment != null), + assert(repeat != null), + assert(matchTextDirection != null), super(key: key); /// Creates a widget that displays an [ImageStream] obtained from a [File]. @@ -151,12 +162,16 @@ class Image extends StatefulWidget { this.color, this.colorBlendMode, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.repeat: ImageRepeat.noRepeat, this.centerSlice, + this.matchTextDirection: false, this.gaplessPlayback: false, this.package, }) : image = new FileImage(file, scale: scale), + assert(alignment != null), + assert(repeat != null), + assert(matchTextDirection != null), super(key: key); /// Creates a widget that displays an [ImageStream] obtained from an asset @@ -181,8 +196,9 @@ class Image extends StatefulWidget { // /// size-aware asset resolution will be attempted also, with the given // /// dimensions interpreted as logical pixels. // /// - // /// * If the images have platform or locale variants, the current platform - // /// and locale is taken into account during asset resolution as well. + // /// * If the images have platform, locale, or directionality variants, the + // /// current platform, locale, and directionality are taken into account + // /// during asset resolution as well. /// /// The [name] and [repeat] arguments must not be null. /// @@ -277,15 +293,19 @@ class Image extends StatefulWidget { this.color, this.colorBlendMode, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.repeat: ImageRepeat.noRepeat, this.centerSlice, + this.matchTextDirection: false, this.gaplessPlayback: false, this.package, }) : image = scale != null - ? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package) - : new AssetImage(name, bundle: bundle, package: package), - super(key: key); + ? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package) + : new AssetImage(name, bundle: bundle, package: package), + assert(alignment != null), + assert(repeat != null), + assert(matchTextDirection != null), + super(key: key); /// Creates a widget that displays an [ImageStream] obtained from a [Uint8List]. /// @@ -298,12 +318,16 @@ class Image extends StatefulWidget { this.color, this.colorBlendMode, this.fit, - this.alignment, + this.alignment: FractionalOffset.center, this.repeat: ImageRepeat.noRepeat, this.centerSlice, + this.matchTextDirection: false, this.gaplessPlayback: false, this.package, }) : image = new MemoryImage(bytes, scale: scale), + assert(alignment != null), + assert(repeat != null), + assert(matchTextDirection != null), super(key: key); /// The image to display. @@ -342,10 +366,23 @@ class Image extends StatefulWidget { /// How to align the image within its bounds. /// - /// An alignment of (0.0, 0.0) aligns the image to the top-left corner of its - /// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle - /// of the right edge of its layout bounds. - final FractionalOffset alignment; + /// The alignment aligns the given position in the image to the given position + /// in the layout bounds. For example, a [FractionalOffset] alignment of (0.0, + /// 0.0) aligns the image to the top-left corner of its layout bounds, while a + /// [FractionalOffset] alignment of (1.0, 1.0) aligns the bottom right of the + /// image with the bottom right corner of its layout bounds. Similarly, an + /// alignment of (0.5, 1.0) aligns the bottom middle of the image with the + /// middle of the bottom edge of its layout bounds. + /// + /// To display a subpart of an image, consider using a [CustomPainter] and + /// [Canvas.drawImageRect]. + /// + /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a + /// [FractionalOffsetDirectional]), then an ambient [Directionality] widget + /// must be in scope. + /// + /// Defaults to [FractionalOffset.center]. + final FractionalOffsetGeometry alignment; /// How to paint any portions of the layout bounds not covered by the image. final ImageRepeat repeat; @@ -359,6 +396,23 @@ class Image extends StatefulWidget { /// the center slice will be stretched only vertically. final Rect centerSlice; + /// 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 true, there must be an ambient [Directionality] widget in + /// scope. + final bool matchTextDirection; + /// Whether to continue showing the old image (true), or briefly show nothing /// (false), when the image provider changes. final bool gaplessPlayback; @@ -379,9 +433,10 @@ class Image extends StatefulWidget { description.add(new DiagnosticsProperty('color', color, defaultValue: null)); description.add(new EnumProperty('colorBlendMode', colorBlendMode, defaultValue: null)); description.add(new EnumProperty('fit', fit, defaultValue: null)); - description.add(new DiagnosticsProperty('alignment', alignment, defaultValue: null)); + description.add(new DiagnosticsProperty('alignment', alignment, defaultValue: null)); description.add(new EnumProperty('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); description.add(new DiagnosticsProperty('centerSlice', centerSlice, defaultValue: null)); + description.add(new FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction')); } } @@ -448,7 +503,8 @@ class _ImageState extends State { fit: widget.fit, alignment: widget.alignment, repeat: widget.repeat, - centerSlice: widget.centerSlice + centerSlice: widget.centerSlice, + matchTextDirection: widget.matchTextDirection, ); } diff --git a/packages/flutter/lib/src/widgets/localizations.dart b/packages/flutter/lib/src/widgets/localizations.dart index baf375a135..0ca84de1a4 100644 --- a/packages/flutter/lib/src/widgets/localizations.dart +++ b/packages/flutter/lib/src/widgets/localizations.dart @@ -385,9 +385,16 @@ class Localizations extends StatefulWidget { /// The locale of the Localizations widget for the widget tree that /// corresponds to [BuildContext] `context`. - static Locale localeOf(BuildContext context) { + /// + /// If no [Localizations] widget is in scope then the [Localizations.localeOf] + /// method will throw an exception, unless the `nullOk` argument is set to + /// true, in which case it returns null. + static Locale localeOf(BuildContext context, { bool nullOk: false }) { assert(context != null); + assert(nullOk != null); final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope); + if (nullOk && scope == null) + return null; assert(scope != null, 'a Localizations ancestor was not found'); return scope.localizationsState.locale; } diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index 84d7d54a8d..356fbee190 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -173,6 +173,8 @@ class MediaQuery extends InheritedWidget { /// If you use this from a widget (e.g. in its build function), consider /// calling [debugCheckHasMediaQuery]. static MediaQueryData of(BuildContext context, { bool nullOk: false }) { + assert(context != null); + assert(nullOk != null); final MediaQuery query = context.inheritFromWidgetOfExactType(MediaQuery); if (query != null) return query.data; diff --git a/packages/flutter/test/rendering/image_test.dart b/packages/flutter/test/rendering/image_test.dart index 468fe94f7c..f9bedb5bb4 100644 --- a/packages/flutter/test/rendering/image_test.dart +++ b/packages/flutter/test/rendering/image_test.dart @@ -74,6 +74,7 @@ void main() { ' constraints: BoxConstraints(25.0<=w<=100.0, 25.0<=h<=100.0)\n' ' size: Size(25.0, 25.0)\n' ' image: [10×10]\n' + ' alignment: FractionalOffset.center\n' ), ); diff --git a/packages/flutter/test/rendering/mock_canvas.dart b/packages/flutter/test/rendering/mock_canvas.dart index 44a9e22359..20aa7950cd 100644 --- a/packages/flutter/test/rendering/mock_canvas.dart +++ b/packages/flutter/test/rendering/mock_canvas.dart @@ -2,7 +2,7 @@ // 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 Paragraph; +import 'dart:ui' as ui show Paragraph, Image; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -278,6 +278,24 @@ abstract class PaintPattern { /// If no call to [Canvas.drawParagraph] was made, then this results in failure. void paragraph({ ui.Paragraph paragraph, Offset offset }); + /// Indicates that an image is expected next. + /// + /// The next call to [Canvas.drawImageRect] is examined, and its arguments + /// compared to those passed to _this_ method. + /// + /// If no call to [Canvas.drawImageRect] was made, then this results in + /// failure. + /// + /// Any calls made between the last matched call (if any) and the + /// [Canvas.drawImageRect] call are ignored. + /// + /// The [Paint]-related arguments (`color`, `strokeWidth`, `hasMaskFilter`, + /// `style`) are compared against the state of the [Paint] object after the + /// painting has completed, not at the time of the call. If the same [Paint] + /// object is reused multiple times, then this may not match the actual + /// arguments as they were seen by the method. + void drawImageRect({ ui.Image image, Rect source, Rect destination, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }); + /// Provides a custom matcher. /// /// Each method call after the last matched call (if any) will be passed to @@ -472,6 +490,11 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp _predicates.add(new _FunctionPaintPredicate(#drawParagraph, [paragraph, offset])); } + @override + void drawImageRect({ ui.Image image, Rect source, Rect destination, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) { + _predicates.add(new _DrawImageRectPaintPredicate(image: image, source: source, destination: destination, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style)); + } + @override void something(PaintPatternPredicate predicate) { _predicates.add(new _SomethingPaintPredicate(predicate)); @@ -800,6 +823,41 @@ class _ArcPaintPredicate extends _DrawCommandPaintPredicate { ); } +class _DrawImageRectPaintPredicate extends _DrawCommandPaintPredicate { + _DrawImageRectPaintPredicate({ this.image, this.source, this.destination, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) : super( + #drawImageRect, 'an image', 4, 3, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style + ); + + final ui.Image image; + final Rect source; + final Rect destination; + + @override + void verifyArguments(List arguments) { + super.verifyArguments(arguments); + final ui.Image imageArgument = arguments[0]; + if (image != null && imageArgument != image) + throw 'It called $methodName with an image, $imageArgument, which was not exactly the expected image ($image).'; + final Rect sourceArgument = arguments[1]; + if (source != null && sourceArgument != source) + throw 'It called $methodName with a source rectangle, $sourceArgument, which was not exactly the expected rectangle ($source).'; + final Rect destinationArgument = arguments[2]; + if (destination != null && destinationArgument != destination) + throw 'It called $methodName with a destination rectangle, $destinationArgument, which was not exactly the expected rectangle ($destination).'; + } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + if (image != null) + description.add('image $image'); + if (source != null) + description.add('source $source'); + if (destination != null) + description.add('destination $destination'); + } +} + class _SomethingPaintPredicate extends _PaintPredicate { _SomethingPaintPredicate(this.predicate); diff --git a/packages/flutter/test/widgets/align_test.dart b/packages/flutter/test/widgets/align_test.dart index 5269a5b36c..8f25d42ba5 100644 --- a/packages/flutter/test/widgets/align_test.dart +++ b/packages/flutter/test/widgets/align_test.dart @@ -21,6 +21,26 @@ void main() { alignment: const FractionalOffset(0.5, 0.5), ), ); + + await tester.pumpWidget( + const Align( + key: const GlobalObjectKey(null), + alignment: FractionalOffset.topLeft, + ), + ); + await tester.pumpWidget(const Directionality( + textDirection: TextDirection.rtl, + child: const Align( + key: const GlobalObjectKey(null), + alignment: FractionalOffsetDirectional.topStart, + ), + )); + await tester.pumpWidget( + const Align( + key: const GlobalObjectKey(null), + alignment: FractionalOffset.topLeft, + ), + ); }); testWidgets('Align control test (LTR)', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/image_rtl_test.dart b/packages/flutter/test/widgets/image_rtl_test.dart new file mode 100644 index 0000000000..bb7e95604b --- /dev/null +++ b/packages/flutter/test/widgets/image_rtl_test.dart @@ -0,0 +1,583 @@ +// Copyright 2017 The Chromium 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:async'; +import 'dart:ui' as ui show Image; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +class TestImageProvider extends ImageProvider { + @override + Future obtainKey(ImageConfiguration configuration) { + return new SynchronousFuture(this); + } + + @override + ImageStreamCompleter load(TestImageProvider key) { + return new OneFrameImageStreamCompleter( + new SynchronousFuture(new ImageInfo(image: new TestImage())) + ); + } +} + +class TestImage extends ui.Image { + @override + int get width => 16; + + @override + int get height => 9; + + // @override + // void dispose() { } +} + +void main() { + testWidgets('DecorationImage RTL with alignment topEnd and match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + decoration: new BoxDecoration( + image: new DecorationImage( + image: new TestImageProvider(), + alignment: FractionalOffsetDirectional.topEnd, + repeat: ImageRepeat.repeatX, + matchTextDirection: true, + ), + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..clipRect(rect: new Rect.fromLTRB(0.0, 0.0, 100.0, 50.0)) + ..translate(x: 50.0, y: 0.0) + ..scale(x: -1.0, y: 1.0) + ..translate(x: -50.0, y: 0.0) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(4.0, 0.0, 20.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(20.0, 0.0, 36.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(36.0, 0.0, 52.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(52.0, 0.0, 68.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(68.0, 0.0, 84.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 0.0, 100.0, 9.0)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale()..scale())); + }); + + testWidgets('DecorationImage LTR with alignment topEnd (and pointless match)', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + decoration: new BoxDecoration( + image: new DecorationImage( + image: new TestImageProvider(), + alignment: FractionalOffsetDirectional.topEnd, + repeat: ImageRepeat.repeatX, + matchTextDirection: true, + ), + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..clipRect(rect: new Rect.fromLTRB(0.0, 0.0, 100.0, 50.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(4.0, 0.0, 20.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(20.0, 0.0, 36.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(36.0, 0.0, 52.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(52.0, 0.0, 68.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(68.0, 0.0, 84.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 0.0, 100.0, 9.0)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale())); + }); + + testWidgets('DecorationImage RTL with alignment topEnd', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + decoration: new BoxDecoration( + image: new DecorationImage( + image: new TestImageProvider(), + alignment: FractionalOffsetDirectional.topEnd, + repeat: ImageRepeat.repeatX, + ), + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..clipRect(rect: new Rect.fromLTRB(0.0, 0.0, 100.0, 50.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(16.0, 0.0, 32.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(32.0, 0.0, 48.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(48.0, 0.0, 64.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(64.0, 0.0, 80.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(80.0, 0.0, 96.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(96.0, 0.0, 112.0, 9.0)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale())); + }); + + testWidgets('DecorationImage LTR with alignment topEnd', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + decoration: new BoxDecoration( + image: new DecorationImage( + image: new TestImageProvider(), + alignment: FractionalOffsetDirectional.topEnd, + repeat: ImageRepeat.repeatX, + ), + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..clipRect(rect: new Rect.fromLTRB(0.0, 0.0, 100.0, 50.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(4.0, 0.0, 20.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(20.0, 0.0, 36.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(36.0, 0.0, 52.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(52.0, 0.0, 68.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(68.0, 0.0, 84.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 0.0, 100.0, 9.0)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale())); + }); + + testWidgets('DecorationImage RTL with alignment center-right and match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + decoration: new BoxDecoration( + image: new DecorationImage( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + matchTextDirection: true, + ), + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..translate(x: 50.0, y: 0.0) + ..scale(x: -1.0, y: 1.0) + ..translate(x: -50.0, y: 0.0) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(0.0, 20.5, 16.0, 29.5)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale()..scale())); + expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); + }); + + testWidgets('DecorationImage RTL with alignment center-right and no match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + decoration: new BoxDecoration( + image: new DecorationImage( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + ), + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 20.5, 100.0, 29.5)) + ); + expect(find.byType(Container), isNot(paints..scale())); + expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); + }); + + testWidgets('DecorationImage LTR with alignment center-right and match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + decoration: new BoxDecoration( + image: new DecorationImage( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + matchTextDirection: true + ), + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 20.5, 100.0, 29.5)) + ); + expect(find.byType(Container), isNot(paints..scale())); + expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); + }); + + testWidgets('DecorationImage LTR with alignment center-right and no match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + decoration: new BoxDecoration( + image: new DecorationImage( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + matchTextDirection: true + ), + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 20.5, 100.0, 29.5)) + ); + expect(find.byType(Container), isNot(paints..scale())); + expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); + }); + + testWidgets('Image RTL with alignment topEnd and match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffsetDirectional.topEnd, + repeat: ImageRepeat.repeatX, + matchTextDirection: true, + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..clipRect(rect: new Rect.fromLTRB(0.0, 0.0, 100.0, 50.0)) + ..translate(x: 50.0, y: 0.0) + ..scale(x: -1.0, y: 1.0) + ..translate(x: -50.0, y: 0.0) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(4.0, 0.0, 20.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(20.0, 0.0, 36.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(36.0, 0.0, 52.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(52.0, 0.0, 68.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(68.0, 0.0, 84.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 0.0, 100.0, 9.0)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale()..scale())); + }); + + testWidgets('Image LTR with alignment topEnd (and pointless match)', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffsetDirectional.topEnd, + repeat: ImageRepeat.repeatX, + matchTextDirection: true, + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..clipRect(rect: new Rect.fromLTRB(0.0, 0.0, 100.0, 50.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(4.0, 0.0, 20.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(20.0, 0.0, 36.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(36.0, 0.0, 52.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(52.0, 0.0, 68.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(68.0, 0.0, 84.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 0.0, 100.0, 9.0)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale())); + }); + + testWidgets('Image RTL with alignment topEnd', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffsetDirectional.topEnd, + repeat: ImageRepeat.repeatX, + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..clipRect(rect: new Rect.fromLTRB(0.0, 0.0, 100.0, 50.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(16.0, 0.0, 32.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(32.0, 0.0, 48.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(48.0, 0.0, 64.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(64.0, 0.0, 80.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(80.0, 0.0, 96.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(96.0, 0.0, 112.0, 9.0)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale())); + }); + + testWidgets('Image LTR with alignment topEnd', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffsetDirectional.topEnd, + repeat: ImageRepeat.repeatX, + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..clipRect(rect: new Rect.fromLTRB(0.0, 0.0, 100.0, 50.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(4.0, 0.0, 20.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(20.0, 0.0, 36.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(36.0, 0.0, 52.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(52.0, 0.0, 68.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(68.0, 0.0, 84.0, 9.0)) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 0.0, 100.0, 9.0)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale())); + }); + + testWidgets('Image RTL with alignment center-right and match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + matchTextDirection: true, + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..translate(x: 50.0, y: 0.0) + ..scale(x: -1.0, y: 1.0) + ..translate(x: -50.0, y: 0.0) + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(0.0, 20.5, 16.0, 29.5)) + ..restore() + ); + expect(find.byType(Container), isNot(paints..scale()..scale())); + expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); + }); + + testWidgets('Image RTL with alignment center-right and no match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 20.5, 100.0, 29.5)) + ); + expect(find.byType(Container), isNot(paints..scale())); + expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); + }); + + testWidgets('Image LTR with alignment center-right and match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + matchTextDirection: true + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 20.5, 100.0, 29.5)) + ); + expect(find.byType(Container), isNot(paints..scale())); + expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); + }); + + testWidgets('Image LTR with alignment center-right and no match', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 50.0, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + matchTextDirection: true + ), + ), + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + expect(find.byType(Container), paints + ..drawImageRect(source: new Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: new Rect.fromLTRB(84.0, 20.5, 100.0, 29.5)) + ); + expect(find.byType(Container), isNot(paints..scale())); + expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); + }); + + testWidgets('Image - Switch needing direction', (WidgetTester tester) async { + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + matchTextDirection: false, + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffsetDirectional.centerEnd, + matchTextDirection: true, + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Image( + image: new TestImageProvider(), + alignment: FractionalOffset.centerRight, + matchTextDirection: false, + ), + ), + Duration.ZERO, + EnginePhase.layout, // so that we don't try to paint the fake images + ); + }); +} \ No newline at end of file diff --git a/packages/flutter/test/widgets/rtl_test.dart b/packages/flutter/test/widgets/rtl_test.dart index 5bb9579dd7..f129ba9a78 100644 --- a/packages/flutter/test/widgets/rtl_test.dart +++ b/packages/flutter/test/widgets/rtl_test.dart @@ -21,6 +21,26 @@ void main() { child: child, )); expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(0.0, 0.0)); + + await tester.pumpWidget( + const Padding( + key: const GlobalObjectKey(null), + padding: const EdgeInsets.only(left: 1.0), + ), + ); + await tester.pumpWidget(const Directionality( + textDirection: TextDirection.rtl, + child: const Padding( + key: const GlobalObjectKey(null), + padding: const EdgeInsetsDirectional.only(start: 1.0), + ), + )); + await tester.pumpWidget( + const Padding( + key: const GlobalObjectKey(null), + padding: const EdgeInsets.only(left: 1.0), + ), + ); }); testWidgets('Container padding/margin RTL', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/stack_test.dart b/packages/flutter/test/widgets/stack_test.dart index c411df9716..b15e644bb2 100644 --- a/packages/flutter/test/widgets/stack_test.dart +++ b/packages/flutter/test/widgets/stack_test.dart @@ -610,4 +610,23 @@ void main() { expect(tester.getTopLeft(find.byKey(key)), const Offset(50.0, 0.0)); }); + + testWidgets('Can change the text direction of a Stack', (WidgetTester tester) async { + await tester.pumpWidget( + new Stack( + alignment: FractionalOffset.center, + ), + ); + await tester.pumpWidget( + new Stack( + alignment: FractionalOffsetDirectional.topStart, + textDirection: TextDirection.rtl, + ), + ); + await tester.pumpWidget( + new Stack( + alignment: FractionalOffset.center, + ), + ); + }); }