Commit e04bf328 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Image RTL (#12230)

parent ff45d506
...@@ -321,9 +321,20 @@ class _BoxDecorationPainter extends BoxPainter { ...@@ -321,9 +321,20 @@ class _BoxDecorationPainter extends BoxPainter {
ImageInfo _image; ImageInfo _image;
void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) { void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
// TODO(ianh): factor this out into a DecorationImage.paint method.
final DecorationImage backgroundImage = _decoration.image; final DecorationImage backgroundImage = _decoration.image;
if (backgroundImage == null) if (backgroundImage == null)
return; 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); final ImageStream newImageStream = backgroundImage.image.resolve(configuration);
if (newImageStream.key != _imageStream?.key) { if (newImageStream.key != _imageStream?.key) {
_imageStream?.removeListener(_imageListener); _imageStream?.removeListener(_imageListener);
...@@ -350,9 +361,10 @@ class _BoxDecorationPainter extends BoxPainter { ...@@ -350,9 +361,10 @@ class _BoxDecorationPainter extends BoxPainter {
image: image, image: image,
colorFilter: backgroundImage.colorFilter, colorFilter: backgroundImage.colorFilter,
fit: backgroundImage.fit, fit: backgroundImage.fit,
alignment: backgroundImage.alignment, alignment: backgroundImage.alignment.resolve(configuration.textDirection),
centerSlice: backgroundImage.centerSlice, centerSlice: backgroundImage.centerSlice,
repeat: backgroundImage.repeat, repeat: backgroundImage.repeat,
flipHorizontally: flipHorizontally,
); );
if (clipPath != null) if (clipPath != null)
......
...@@ -35,15 +35,20 @@ enum ImageRepeat { ...@@ -35,15 +35,20 @@ enum ImageRepeat {
class DecorationImage { class DecorationImage {
/// Creates an image to show in a [BoxDecoration]. /// 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({ const DecorationImage({
@required this.image, @required this.image,
this.colorFilter, this.colorFilter,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.centerSlice, this.centerSlice,
this.repeat: ImageRepeat.noRepeat, 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. /// The image to be painted into the decoration.
/// ///
...@@ -64,12 +69,23 @@ class DecorationImage { ...@@ -64,12 +69,23 @@ class DecorationImage {
/// How to align the image within its bounds. /// 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 /// The alignment aligns the given position in the image to the given position
/// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle /// in the layout bounds. For example, a [FractionalOffset] alignment of (0.0,
/// of the right edge of its layout bounds. /// 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]. /// Defaults to [FractionalOffset.center].
final FractionalOffset alignment; final FractionalOffsetGeometry alignment;
/// The center slice for a nine-patch image. /// The center slice for a nine-patch image.
/// ///
...@@ -92,6 +108,15 @@ class DecorationImage { ...@@ -92,6 +108,15 @@ class DecorationImage {
/// by the image. /// by the image.
final ImageRepeat repeat; 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 @override
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
if (identical(this, other)) if (identical(this, other))
...@@ -104,11 +129,12 @@ class DecorationImage { ...@@ -104,11 +129,12 @@ class DecorationImage {
&& fit == typedOther.fit && fit == typedOther.fit
&& alignment == typedOther.alignment && alignment == typedOther.alignment
&& centerSlice == typedOther.centerSlice && centerSlice == typedOther.centerSlice
&& repeat == typedOther.repeat; && repeat == typedOther.repeat
&& matchTextDirection == typedOther.matchTextDirection;
} }
@override @override
int get hashCode => hashValues(image, colorFilter, fit, alignment, centerSlice, repeat); int get hashCode => hashValues(image, colorFilter, fit, alignment, centerSlice, repeat, matchTextDirection);
@override @override
String toString() { String toString() {
...@@ -120,33 +146,44 @@ class DecorationImage { ...@@ -120,33 +146,44 @@ class DecorationImage {
!(fit == BoxFit.fill && centerSlice != null) && !(fit == BoxFit.fill && centerSlice != null) &&
!(fit == BoxFit.scaleDown && centerSlice == null)) !(fit == BoxFit.scaleDown && centerSlice == null))
properties.add('$fit'); properties.add('$fit');
if (alignment != null)
properties.add('$alignment'); properties.add('$alignment');
if (centerSlice != null) if (centerSlice != null)
properties.add('centerSlice: $centerSlice'); properties.add('centerSlice: $centerSlice');
if (repeat != ImageRepeat.noRepeat) if (repeat != ImageRepeat.noRepeat)
properties.add('$repeat'); properties.add('$repeat');
if (matchTextDirection)
properties.add('match text direction');
return '$runtimeType(${properties.join(", ")})'; return '$runtimeType(${properties.join(", ")})';
} }
} }
/// Paints an image into the given rectangle on the canvas. /// 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. /// * `canvas`: The canvas onto which the image will be painted.
///
/// * `rect`: The region of the canvas into 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 /// The image might not fill the entire rectangle (e.g., depending on the
/// `fit`). If `rect` is empty, nothing is painted. /// `fit`). If `rect` is empty, nothing is painted.
///
/// * `image`: The image to paint onto the canvas. /// * `image`: The image to paint onto the canvas.
///
/// * `colorFilter`: If non-null, the color filter to apply when painting the /// * `colorFilter`: If non-null, the color filter to apply when painting the
/// image. /// image.
///
/// * `fit`: How the image should be inscribed into `rect`. If null, the /// * `fit`: How the image should be inscribed into `rect`. If null, the
/// default behavior depends on `centerSlice`. If `centerSlice` is also null, /// default behavior depends on `centerSlice`. If `centerSlice` is also null,
/// the default behavior is [BoxFit.scaleDown]. If `centerSlice` is /// the default behavior is [BoxFit.scaleDown]. If `centerSlice` is
/// non-null, the default behavior is [BoxFit.fill]. See [BoxFit] for /// non-null, the default behavior is [BoxFit.fill]. See [BoxFit] for
/// details. /// 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. /// * `alignment`: How the destination rectangle defined by applying `fit` is
/// See [ImageRepeat] for details. /// 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 /// * `centerSlice`: The image is drawn in nine portions described by splitting
/// the image by drawing two horizontal lines and two vertical lines, where /// the image by drawing two horizontal lines and two vertical lines, where
/// `centerSlice` describes the rectangle formed by the four points where /// `centerSlice` describes the rectangle formed by the four points where
...@@ -157,11 +194,19 @@ class DecorationImage { ...@@ -157,11 +194,19 @@ class DecorationImage {
/// remaining five regions are drawn by stretching them to fit such that they /// remaining five regions are drawn by stretching them to fit such that they
/// exactly cover the destination rectangle while maintaining their relative /// exactly cover the destination rectangle while maintaining their relative
/// positions. /// positions.
/// * `alignment`: How the destination rectangle defined by applying `fit` is ///
/// aligned within `rect`. For example, if `fit` is [BoxFit.contain] and /// * `repeat`: If the image does not fill `rect`, whether and how the image
/// `alignment` is [FractionalOffset.bottomRight], the image will be as large /// should be repeated to fill `rect`. By default, the image is not repeated.
/// as possible within `rect` and placed with its bottom right corner at the /// See [ImageRepeat] for details.
/// bottom right corner of `rect`. ///
/// * `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: /// See also:
/// ///
...@@ -174,12 +219,16 @@ void paintImage({ ...@@ -174,12 +219,16 @@ void paintImage({
@required ui.Image image, @required ui.Image image,
ColorFilter colorFilter, ColorFilter colorFilter,
BoxFit fit, BoxFit fit,
FractionalOffset alignment, FractionalOffset alignment: FractionalOffset.center,
Rect centerSlice, Rect centerSlice,
ImageRepeat repeat: ImageRepeat.noRepeat, ImageRepeat repeat: ImageRepeat.noRepeat,
bool flipHorizontally: false,
}) { }) {
assert(canvas != null); assert(canvas != null);
assert(image != null); assert(image != null);
assert(alignment != null);
assert(repeat != null);
assert(flipHorizontally != null);
if (rect.isEmpty) if (rect.isEmpty)
return; return;
Size outputSize = rect.size; Size outputSize = rect.size;
...@@ -219,16 +268,23 @@ void paintImage({ ...@@ -219,16 +268,23 @@ void paintImage({
// to nearest-neighbor. // to nearest-neighbor.
paint.filterQuality = FilterQuality.low; paint.filterQuality = FilterQuality.low;
} }
final double dx = (outputSize.width - destinationSize.width) * (alignment?.dx ?? 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 ?? 0.5); final double dy = (outputSize.height - destinationSize.height) * alignment.dy;
final Offset destinationPosition = rect.topLeft.translate(dx, dy); final Offset destinationPosition = rect.topLeft.translate(dx, dy);
final Rect destinationRect = destinationPosition & destinationSize; final Rect destinationRect = destinationPosition & destinationSize;
if (repeat != ImageRepeat.noRepeat) { final bool needSave = repeat != ImageRepeat.noRepeat || flipHorizontally;
if (needSave)
canvas.save(); canvas.save();
if (repeat != ImageRepeat.noRepeat)
canvas.clipRect(rect); 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) { if (centerSlice == null) {
final Rect sourceRect = (alignment ?? FractionalOffset.center).inscribe( final Rect sourceRect = alignment.inscribe(
fittedSizes.source, Offset.zero & inputSize fittedSizes.source, Offset.zero & inputSize
); );
for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
...@@ -237,7 +293,7 @@ void paintImage({ ...@@ -237,7 +293,7 @@ void paintImage({
for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
canvas.drawImageNine(image, centerSlice, tileRect, paint); canvas.drawImageNine(image, centerSlice, tileRect, paint);
} }
if (repeat != ImageRepeat.noRepeat) if (needSave)
canvas.restore(); canvas.restore();
} }
......
...@@ -20,6 +20,10 @@ export 'package:flutter/painting.dart' show ...@@ -20,6 +20,10 @@ export 'package:flutter/painting.dart' show
/// various fields on this class in more detail. /// various fields on this class in more detail.
class RenderImage extends RenderBox { class RenderImage extends RenderBox {
/// Creates a render box that displays an image. /// 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({ RenderImage({
ui.Image image, ui.Image image,
double width, double width,
...@@ -28,10 +32,16 @@ class RenderImage extends RenderBox { ...@@ -28,10 +32,16 @@ class RenderImage extends RenderBox {
Color color, Color color,
BlendMode colorBlendMode, BlendMode colorBlendMode,
BoxFit fit, BoxFit fit,
FractionalOffset alignment, FractionalOffsetGeometry alignment: FractionalOffset.center,
ImageRepeat repeat: ImageRepeat.noRepeat, ImageRepeat repeat: ImageRepeat.noRepeat,
Rect centerSlice Rect centerSlice,
}) : _image = image, bool matchTextDirection: false,
TextDirection textDirection,
}) : assert(scale != null),
assert(repeat != null),
assert(alignment != null),
assert(matchTextDirection != null),
_image = image,
_width = width, _width = width,
_height = height, _height = height,
_scale = scale, _scale = scale,
...@@ -40,10 +50,28 @@ class RenderImage extends RenderBox { ...@@ -40,10 +50,28 @@ class RenderImage extends RenderBox {
_fit = fit, _fit = fit,
_alignment = alignment, _alignment = alignment,
_repeat = repeat, _repeat = repeat,
_centerSlice = centerSlice { _centerSlice = centerSlice,
_matchTextDirection = matchTextDirection,
_textDirection = textDirection {
_updateColorFilter(); _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. /// The image to display.
ui.Image get image => _image; ui.Image get image => _image;
ui.Image _image; ui.Image _image;
...@@ -147,19 +175,24 @@ class RenderImage extends RenderBox { ...@@ -147,19 +175,24 @@ class RenderImage extends RenderBox {
} }
/// How to align the image within its bounds. /// How to align the image within its bounds.
FractionalOffset get alignment => _alignment; ///
FractionalOffset _alignment; /// If this is set to a text-direction-dependent value, [textDirection] must
set alignment(FractionalOffset value) { /// not be null.
FractionalOffsetGeometry get alignment => _alignment;
FractionalOffsetGeometry _alignment;
set alignment(FractionalOffsetGeometry value) {
assert(value != null);
if (value == _alignment) if (value == _alignment)
return; return;
_alignment = value; _alignment = value;
markNeedsPaint(); _markNeedResolution();
} }
/// How to repeat this image if it doesn't fill its layout bounds. /// How to repeat this image if it doesn't fill its layout bounds.
ImageRepeat get repeat => _repeat; ImageRepeat get repeat => _repeat;
ImageRepeat _repeat; ImageRepeat _repeat;
set repeat(ImageRepeat value) { set repeat(ImageRepeat value) {
assert(value != null);
if (value == _repeat) if (value == _repeat)
return; return;
_repeat = value; _repeat = value;
...@@ -182,6 +215,44 @@ class RenderImage extends RenderBox { ...@@ -182,6 +215,44 @@ class RenderImage extends RenderBox {
markNeedsPaint(); 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. /// Find a size for the render image within the given constraints.
/// ///
/// - The dimensions of the RenderImage must fit within the constraints. /// - The dimensions of the RenderImage must fit within the constraints.
...@@ -246,15 +317,19 @@ class RenderImage extends RenderBox { ...@@ -246,15 +317,19 @@ class RenderImage extends RenderBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_image == null) if (_image == null)
return; return;
_resolve();
assert(_resolvedAlignment != null);
assert(_flipHorizontally != null);
paintImage( paintImage(
canvas: context.canvas, canvas: context.canvas,
rect: offset & size, rect: offset & size,
image: _image, image: _image,
colorFilter: _colorFilter, colorFilter: _colorFilter,
fit: _fit, fit: _fit,
alignment: _alignment, alignment: _resolvedAlignment,
centerSlice: _centerSlice, centerSlice: _centerSlice,
repeat: _repeat repeat: _repeat,
flipHorizontally: _flipHorizontally,
); );
} }
...@@ -271,5 +346,7 @@ class RenderImage extends RenderBox { ...@@ -271,5 +346,7 @@ class RenderImage extends RenderBox {
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment, defaultValue: null)); description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment, defaultValue: null));
description.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); description.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat));
description.add(new DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null)); description.add(new DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null));
description.add(new FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction'));
description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
} }
} }
...@@ -17,7 +17,7 @@ import 'layer.dart'; ...@@ -17,7 +17,7 @@ import 'layer.dart';
import 'node.dart'; import 'node.dart';
import 'semantics.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/gestures.dart' show HitTestEntry, HitTestResult;
export 'package:flutter/painting.dart'; export 'package:flutter/painting.dart';
......
...@@ -98,20 +98,20 @@ class RenderPadding extends RenderShiftedBox { ...@@ -98,20 +98,20 @@ class RenderPadding extends RenderShiftedBox {
assert(padding.isNonNegative), assert(padding.isNonNegative),
_textDirection = textDirection, _textDirection = textDirection,
_padding = padding, _padding = padding,
super(child) { super(child);
_applyUpdate();
}
// The resolved absolute insets.
EdgeInsets _resolvedPadding; EdgeInsets _resolvedPadding;
void _applyUpdate() { void _resolve() {
final EdgeInsets resolvedPadding = padding.resolve(textDirection); if (_resolvedPadding != null)
assert(resolvedPadding.isNonNegative); return;
if (_resolvedPadding != resolvedPadding) { _resolvedPadding = padding.resolve(textDirection);
_resolvedPadding = resolvedPadding; assert(_resolvedPadding.isNonNegative);
markNeedsLayout();
} }
void _markNeedResolution() {
_resolvedPadding = null;
markNeedsLayout();
} }
/// The amount to pad the child in each dimension. /// The amount to pad the child in each dimension.
...@@ -126,21 +126,25 @@ class RenderPadding extends RenderShiftedBox { ...@@ -126,21 +126,25 @@ class RenderPadding extends RenderShiftedBox {
if (_padding == value) if (_padding == value)
return; return;
_padding = value; _padding = value;
_applyUpdate(); _markNeedResolution();
} }
/// The text direction with which to resolve [padding]. /// 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 get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
if (_textDirection == value) if (_textDirection == value)
return; return;
_textDirection = value; _textDirection = value;
_applyUpdate(); _markNeedResolution();
} }
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
if (child != null) // next line relies on double.INFINITY absorption if (child != null) // next line relies on double.INFINITY absorption
...@@ -150,6 +154,7 @@ class RenderPadding extends RenderShiftedBox { ...@@ -150,6 +154,7 @@ class RenderPadding extends RenderShiftedBox {
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
if (child != null) // next line relies on double.INFINITY absorption if (child != null) // next line relies on double.INFINITY absorption
...@@ -159,6 +164,7 @@ class RenderPadding extends RenderShiftedBox { ...@@ -159,6 +164,7 @@ class RenderPadding extends RenderShiftedBox {
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
if (child != null) // next line relies on double.INFINITY absorption if (child != null) // next line relies on double.INFINITY absorption
...@@ -168,6 +174,7 @@ class RenderPadding extends RenderShiftedBox { ...@@ -168,6 +174,7 @@ class RenderPadding extends RenderShiftedBox {
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right; final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
if (child != null) // next line relies on double.INFINITY absorption if (child != null) // next line relies on double.INFINITY absorption
...@@ -177,6 +184,7 @@ class RenderPadding extends RenderShiftedBox { ...@@ -177,6 +184,7 @@ class RenderPadding extends RenderShiftedBox {
@override @override
void performLayout() { void performLayout() {
_resolve();
assert(_resolvedPadding != null); assert(_resolvedPadding != null);
if (child == null) { if (child == null) {
size = constraints.constrain(new Size( size = constraints.constrain(new Size(
...@@ -226,19 +234,19 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { ...@@ -226,19 +234,19 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox {
}) : assert(alignment != null), }) : assert(alignment != null),
_alignment = alignment, _alignment = alignment,
_textDirection = textDirection, _textDirection = textDirection,
super(child) { super(child);
_applyUpdate();
}
// The resolved absolute alignment.
FractionalOffset _resolvedAlignment; FractionalOffset _resolvedAlignment;
void _applyUpdate() { void _resolve() {
final FractionalOffset resolvedAlignment = alignment.resolve(textDirection); if (_resolvedAlignment != null)
if (_resolvedAlignment != resolvedAlignment) { return;
_resolvedAlignment = resolvedAlignment; _resolvedAlignment = alignment.resolve(textDirection);
markNeedsLayout();
} }
void _markNeedResolution() {
_resolvedAlignment = null;
markNeedsLayout();
} }
/// How to align the child. /// How to align the child.
...@@ -251,7 +259,7 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { ...@@ -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 /// For example, a value of 0.5 means that the center of the child is aligned
/// with the center of the parent. /// 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. /// [textDirection] must not be null.
FractionalOffsetGeometry get alignment => _alignment; FractionalOffsetGeometry get alignment => _alignment;
FractionalOffsetGeometry _alignment; FractionalOffsetGeometry _alignment;
...@@ -263,17 +271,20 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { ...@@ -263,17 +271,20 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox {
if (_alignment == value) if (_alignment == value)
return; return;
_alignment = value; _alignment = value;
_applyUpdate(); _markNeedResolution();
} }
/// The text direction with which to resolve [alignment]. /// 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 get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
if (_textDirection == value) if (_textDirection == value)
return; return;
_textDirection = value; _textDirection = value;
_applyUpdate(); _markNeedResolution();
} }
/// Apply the current [alignment] to the [child]. /// Apply the current [alignment] to the [child].
...@@ -285,10 +296,12 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { ...@@ -285,10 +296,12 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox {
/// This method must be called after the child has been laid out and /// This method must be called after the child has been laid out and
/// this object's own size has been set. /// this object's own size has been set.
void alignChild() { void alignChild() {
_resolve();
assert(child != null); assert(child != null);
assert(!child.debugNeedsLayout); assert(!child.debugNeedsLayout);
assert(child.hasSize); assert(child.hasSize);
assert(hasSize); assert(hasSize);
assert(_resolvedAlignment != null);
final BoxParentData childParentData = child.parentData; final BoxParentData childParentData = child.parentData;
childParentData.offset = _resolvedAlignment.alongOffset(size - child.size); childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
} }
......
...@@ -37,22 +37,26 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R ...@@ -37,22 +37,26 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
_padding = padding, _padding = padding,
_textDirection = textDirection { _textDirection = textDirection {
this.child = child; this.child = child;
_applyUpdate();
} }
// The resolved absolute insets.
EdgeInsets _resolvedPadding; EdgeInsets _resolvedPadding;
void _applyUpdate() { void _resolve() {
final EdgeInsets resolvedPadding = padding.resolve(textDirection); if (_resolvedPadding != null)
assert(resolvedPadding.isNonNegative); return;
if (_resolvedPadding != resolvedPadding) { _resolvedPadding = padding.resolve(textDirection);
_resolvedPadding = resolvedPadding; assert(_resolvedPadding.isNonNegative);
markNeedsLayout();
} }
void _markNeedResolution() {
_resolvedPadding = null;
markNeedsLayout();
} }
/// The amount to pad the child in each dimension. /// The amount to pad the child in each dimension.
///
/// If this is set to an [EdgeInsetsDirectional] object, then [textDirection]
/// must not be null.
EdgeInsetsGeometry get padding => _padding; EdgeInsetsGeometry get padding => _padding;
EdgeInsetsGeometry _padding; EdgeInsetsGeometry _padding;
set padding(EdgeInsetsGeometry value) { set padding(EdgeInsetsGeometry value) {
...@@ -61,17 +65,20 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R ...@@ -61,17 +65,20 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
if (_padding == value) if (_padding == value)
return; return;
_padding = value; _padding = value;
_applyUpdate(); _markNeedResolution();
} }
/// The text direction with which to resolve [padding]. /// 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 get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
if (_textDirection == value) if (_textDirection == value)
return; return;
_textDirection = value; _textDirection = value;
_applyUpdate(); _markNeedResolution();
} }
/// The padding in the scroll direction on the side nearest the 0.0 scroll direction. /// 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<R ...@@ -82,6 +89,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
assert(constraints != null); assert(constraints != null);
assert(constraints.axisDirection != null); assert(constraints.axisDirection != null);
assert(constraints.growthDirection != null); assert(constraints.growthDirection != null);
assert(_resolvedPadding != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up: case AxisDirection.up:
return _resolvedPadding.bottom; return _resolvedPadding.bottom;
...@@ -103,6 +111,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R ...@@ -103,6 +111,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
assert(constraints != null); assert(constraints != null);
assert(constraints.axisDirection != null); assert(constraints.axisDirection != null);
assert(constraints.growthDirection != null); assert(constraints.growthDirection != null);
assert(_resolvedPadding != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up: case AxisDirection.up:
return _resolvedPadding.top; return _resolvedPadding.top;
...@@ -125,6 +134,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R ...@@ -125,6 +134,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
double get mainAxisPadding { double get mainAxisPadding {
assert(constraints != null); assert(constraints != null);
assert(constraints.axis != null); assert(constraints.axis != null);
assert(_resolvedPadding != null);
return _resolvedPadding.along(constraints.axis); return _resolvedPadding.along(constraints.axis);
} }
...@@ -137,6 +147,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R ...@@ -137,6 +147,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
double get crossAxisPadding { double get crossAxisPadding {
assert(constraints != null); assert(constraints != null);
assert(constraints.axis != null); assert(constraints.axis != null);
assert(_resolvedPadding != null);
switch (constraints.axis) { switch (constraints.axis) {
case Axis.horizontal: case Axis.horizontal:
return _resolvedPadding.vertical; return _resolvedPadding.vertical;
...@@ -154,6 +165,8 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R ...@@ -154,6 +165,8 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
@override @override
void performLayout() { void performLayout() {
_resolve();
assert(_resolvedPadding != null);
final double beforePadding = this.beforePadding; final double beforePadding = this.beforePadding;
final double afterPadding = this.afterPadding; final double afterPadding = this.afterPadding;
final double mainAxisPadding = this.mainAxisPadding; final double mainAxisPadding = this.mainAxisPadding;
...@@ -248,6 +261,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R ...@@ -248,6 +261,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
assert(constraints != null); assert(constraints != null);
assert(constraints.axisDirection != null); assert(constraints.axisDirection != null);
assert(constraints.growthDirection != null); assert(constraints.growthDirection != null);
assert(_resolvedPadding != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up: case AxisDirection.up:
case AxisDirection.down: case AxisDirection.down:
......
...@@ -309,7 +309,6 @@ class RenderStack extends RenderBox ...@@ -309,7 +309,6 @@ class RenderStack extends RenderBox
_fit = fit, _fit = fit,
_overflow = overflow { _overflow = overflow {
addAll(children); addAll(children);
_applyUpdate();
} }
bool _hasVisualOverflow = false; bool _hasVisualOverflow = false;
...@@ -320,15 +319,17 @@ class RenderStack extends RenderBox ...@@ -320,15 +319,17 @@ class RenderStack extends RenderBox
child.parentData = new StackParentData(); child.parentData = new StackParentData();
} }
// The resolved absolute insets.
FractionalOffset _resolvedAlignment; FractionalOffset _resolvedAlignment;
void _applyUpdate() { void _resolve() {
final FractionalOffset resolvedAlignment = alignment.resolve(textDirection); if (_resolvedAlignment != null)
if (_resolvedAlignment != resolvedAlignment) { return;
_resolvedAlignment = resolvedAlignment; _resolvedAlignment = alignment.resolve(textDirection);
markNeedsLayout();
} }
void _markNeedResolution() {
_resolvedAlignment = null;
markNeedsLayout();
} }
/// How to align the non-positioned children in the stack. /// How to align the non-positioned children in the stack.
...@@ -337,24 +338,30 @@ class RenderStack extends RenderBox ...@@ -337,24 +338,30 @@ class RenderStack extends RenderBox
/// the points determined by [alignment] are co-located. For example, if the /// the points determined by [alignment] are co-located. For example, if the
/// [alignment] is [FractionalOffset.topLeft], then the top left corner of /// [alignment] is [FractionalOffset.topLeft], then the top left corner of
/// each non-positioned child will be located at the same global coordinate. /// each non-positioned child will be located at the same global coordinate.
///
/// If this is set to a [FractionalOffsetDirectional] object, then
/// [textDirection] must not be null.
FractionalOffsetGeometry get alignment => _alignment; FractionalOffsetGeometry get alignment => _alignment;
FractionalOffsetGeometry _alignment; FractionalOffsetGeometry _alignment;
set alignment(FractionalOffsetGeometry value) { set alignment(FractionalOffsetGeometry value) {
assert(value != null); assert(value != null);
if (_alignment != value) { if (_alignment == value)
return;
_alignment = value; _alignment = value;
_applyUpdate(); _markNeedResolution();
}
} }
/// The text direction with which to resolve [alignment]. /// 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 get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
if (_textDirection != value) { if (_textDirection == value)
return;
_textDirection = value; _textDirection = value;
_applyUpdate(); _markNeedResolution();
}
} }
/// How to size the non-positioned children in the stack. /// How to size the non-positioned children in the stack.
...@@ -426,6 +433,8 @@ class RenderStack extends RenderBox ...@@ -426,6 +433,8 @@ class RenderStack extends RenderBox
@override @override
void performLayout() { void performLayout() {
_resolve();
assert(_resolvedAlignment != null);
_hasVisualOverflow = false; _hasVisualOverflow = false;
bool hasNonPositionedChildren = false; bool hasNonPositionedChildren = false;
......
...@@ -6,7 +6,7 @@ import 'dart:async'; ...@@ -6,7 +6,7 @@ import 'dart:async';
import 'dart:io' show File; import 'dart:io' show File;
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui show Image; 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:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
...@@ -36,8 +36,9 @@ class ImageConfiguration { ...@@ -36,8 +36,9 @@ class ImageConfiguration {
this.bundle, this.bundle,
this.devicePixelRatio, this.devicePixelRatio,
this.locale, this.locale,
this.textDirection,
this.size, this.size,
this.platform this.platform,
}); });
/// Creates an object holding the configuration information for an [ImageProvider]. /// Creates an object holding the configuration information for an [ImageProvider].
...@@ -48,15 +49,17 @@ class ImageConfiguration { ...@@ -48,15 +49,17 @@ class ImageConfiguration {
AssetBundle bundle, AssetBundle bundle,
double devicePixelRatio, double devicePixelRatio,
Locale locale, Locale locale,
TextDirection textDirection,
Size size, Size size,
String platform String platform,
}) { }) {
return new ImageConfiguration( return new ImageConfiguration(
bundle: bundle ?? this.bundle, bundle: bundle ?? this.bundle,
devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
locale: locale ?? this.locale, locale: locale ?? this.locale,
textDirection: textDirection ?? this.textDirection,
size: size ?? this.size, size: size ?? this.size,
platform: platform ?? this.platform platform: platform ?? this.platform,
); );
} }
...@@ -70,6 +73,9 @@ class ImageConfiguration { ...@@ -70,6 +73,9 @@ class ImageConfiguration {
/// The language and region for which to select the image. /// The language and region for which to select the image.
final Locale locale; 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. /// The size at which the image will be rendered.
final Size size; final Size size;
...@@ -92,6 +98,7 @@ class ImageConfiguration { ...@@ -92,6 +98,7 @@ class ImageConfiguration {
return typedOther.bundle == bundle return typedOther.bundle == bundle
&& typedOther.devicePixelRatio == devicePixelRatio && typedOther.devicePixelRatio == devicePixelRatio
&& typedOther.locale == locale && typedOther.locale == locale
&& typedOther.textDirection == textDirection
&& typedOther.size == size && typedOther.size == size
&& typedOther.platform == platform; && typedOther.platform == platform;
} }
...@@ -122,6 +129,12 @@ class ImageConfiguration { ...@@ -122,6 +129,12 @@ class ImageConfiguration {
result.write('locale: $locale'); result.write('locale: $locale');
hasArguments = true; hasArguments = true;
} }
if (textDirection != null) {
if (hasArguments)
result.write(', ');
result.write('textDirection: $textDirection');
hasArguments = true;
}
if (size != null) { if (size != null) {
if (hasArguments) if (hasArguments)
result.write(', '); result.write(', ');
......
...@@ -224,8 +224,9 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -224,8 +224,9 @@ class AssetImage extends AssetBundleImageProvider {
final SplayTreeMap<double, String> mapping = new SplayTreeMap<double, String>(); final SplayTreeMap<double, String> mapping = new SplayTreeMap<double, String>();
for (String candidate in candidates) for (String candidate in candidates)
mapping[_parseScale(candidate)] = candidate; mapping[_parseScale(candidate)] = candidate;
// TODO(ianh): implement support for config.locale, config.size, config.platform // TODO(ianh): implement support for config.locale, config.textDirection,
// (then document this over in the Image.asset docs) // config.size, config.platform (then document this over in the Image.asset
// docs)
return _findNearest(mapping, config.devicePixelRatio); return _findNearest(mapping, config.devicePixelRatio);
} }
......
...@@ -161,8 +161,8 @@ class AnimatedList extends StatefulWidget { ...@@ -161,8 +161,8 @@ class AnimatedList extends StatefulWidget {
/// AnimatedListState animatedList = AnimatedList.of(context); /// AnimatedListState animatedList = AnimatedList.of(context);
/// ``` /// ```
static AnimatedListState of(BuildContext context, { bool nullOk: false }) { static AnimatedListState of(BuildContext context, { bool nullOk: false }) {
assert(nullOk != null);
assert(context != null); assert(context != null);
assert(nullOk != null);
final AnimatedListState result = context.ancestorStateOfType(const TypeMatcher<AnimatedListState>()); final AnimatedListState result = context.ancestorStateOfType(const TypeMatcher<AnimatedListState>());
if (nullOk || result != null) if (nullOk || result != null)
return result; return result;
......
...@@ -3826,7 +3826,8 @@ class RichText extends LeafRenderObjectWidget { ...@@ -3826,7 +3826,8 @@ class RichText extends LeafRenderObjectWidget {
class RawImage extends LeafRenderObjectWidget { class RawImage extends LeafRenderObjectWidget {
/// Creates a widget that displays an image. /// 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({ const RawImage({
Key key, Key key,
this.image, this.image,
...@@ -3836,11 +3837,14 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -3836,11 +3837,14 @@ class RawImage extends LeafRenderObjectWidget {
this.color, this.color,
this.colorBlendMode, this.colorBlendMode,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.centerSlice this.centerSlice,
this.matchTextDirection: false,
}) : assert(scale != null), }) : assert(scale != null),
assert(alignment != null),
assert(repeat != null), assert(repeat != null),
assert(matchTextDirection != null),
super(key: key); super(key: key);
/// The image to display. /// The image to display.
...@@ -3884,10 +3888,23 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -3884,10 +3888,23 @@ class RawImage extends LeafRenderObjectWidget {
/// How to align the image within its bounds. /// 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 /// The alignment aligns the given position in the image to the given position
/// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle /// in the layout bounds. For example, a [FractionalOffset] alignment of (0.0,
/// of the right edge of its layout bounds. /// 0.0) aligns the image to the top-left corner of its layout bounds, while a
final FractionalOffset alignment; /// [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. /// How to paint any portions of the layout bounds not covered by the image.
final ImageRepeat repeat; final ImageRepeat repeat;
...@@ -3901,8 +3918,26 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -3901,8 +3918,26 @@ class RawImage extends LeafRenderObjectWidget {
/// the center slice will be stretched only vertically. /// the center slice will be stretched only vertically.
final Rect centerSlice; 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 @override
RenderImage createRenderObject(BuildContext context) { RenderImage createRenderObject(BuildContext context) {
assert((!matchTextDirection && alignment is FractionalOffset) || debugCheckHasDirectionality(context));
return new RenderImage( return new RenderImage(
image: image, image: image,
width: width, width: width,
...@@ -3913,7 +3948,9 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -3913,7 +3948,9 @@ class RawImage extends LeafRenderObjectWidget {
fit: fit, fit: fit,
alignment: alignment, alignment: alignment,
repeat: repeat, 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 { ...@@ -3929,7 +3966,9 @@ class RawImage extends LeafRenderObjectWidget {
..alignment = alignment ..alignment = alignment
..fit = fit ..fit = fit
..repeat = repeat ..repeat = repeat
..centerSlice = centerSlice; ..centerSlice = centerSlice
..matchTextDirection = matchTextDirection
..textDirection = matchTextDirection || alignment is! FractionalOffset ? Directionality.of(context) : null;
} }
@override @override
...@@ -3942,9 +3981,10 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -3942,9 +3981,10 @@ class RawImage extends LeafRenderObjectWidget {
description.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null)); description.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null));
description.add(new EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null)); description.add(new EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null));
description.add(new EnumProperty<BoxFit>('fit', fit, defaultValue: null)); description.add(new EnumProperty<BoxFit>('fit', fit, defaultValue: null));
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment, defaultValue: null)); description.add(new DiagnosticsProperty<FractionalOffsetGeometry>('alignment', alignment, defaultValue: null));
description.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); description.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat));
description.add(new DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null)); description.add(new DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null));
description.add(new FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction'));
} }
} }
......
...@@ -71,7 +71,7 @@ class DecoratedBox extends SingleChildRenderObjectWidget { ...@@ -71,7 +71,7 @@ class DecoratedBox extends SingleChildRenderObjectWidget {
return new RenderDecoratedBox( return new RenderDecoratedBox(
decoration: decoration, decoration: decoration,
position: position, position: position,
configuration: createLocalImageConfiguration(context) configuration: createLocalImageConfiguration(context),
); );
} }
......
...@@ -60,7 +60,8 @@ class FadeInImage extends StatefulWidget { ...@@ -60,7 +60,8 @@ class FadeInImage extends StatefulWidget {
/// then cross-fades to display the [image]. /// then cross-fades to display the [image].
/// ///
/// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve], /// 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({ const FadeInImage({
Key key, Key key,
@required this.placeholder, @required this.placeholder,
...@@ -72,15 +73,18 @@ class FadeInImage extends StatefulWidget { ...@@ -72,15 +73,18 @@ class FadeInImage extends StatefulWidget {
this.width, this.width,
this.height, this.height,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.matchTextDirection: false,
}) : assert(placeholder != null), }) : assert(placeholder != null),
assert(image != null), assert(image != null),
assert(fadeOutDuration != null), assert(fadeOutDuration != null),
assert(fadeOutCurve != null), assert(fadeOutCurve != null),
assert(fadeInDuration != null), assert(fadeInDuration != null),
assert(fadeInCurve != null), assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null), assert(repeat != null),
assert(matchTextDirection != null),
super(key: key); super(key: key);
/// Creates a widget that uses a placeholder image stored in memory while /// Creates a widget that uses a placeholder image stored in memory while
...@@ -94,8 +98,9 @@ class FadeInImage extends StatefulWidget { ...@@ -94,8 +98,9 @@ class FadeInImage extends StatefulWidget {
/// [ImageProvider]s (see also [ImageInfo.scale]). /// [ImageProvider]s (see also [ImageInfo.scale]).
/// ///
/// The [placeholder], [image], [placeholderScale], [imageScale], /// The [placeholder], [image], [placeholderScale], [imageScale],
/// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve] and /// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve],
/// [repeat] arguments must not be null. /// [alignment], [repeat], and [matchTextDirection] arguments must not be
/// null.
/// ///
/// See also: /// See also:
/// ///
...@@ -116,8 +121,9 @@ class FadeInImage extends StatefulWidget { ...@@ -116,8 +121,9 @@ class FadeInImage extends StatefulWidget {
this.width, this.width,
this.height, this.height,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.matchTextDirection: false,
}) : assert(placeholder != null), }) : assert(placeholder != null),
assert(image != null), assert(image != null),
assert(placeholderScale != null), assert(placeholderScale != null),
...@@ -126,7 +132,9 @@ class FadeInImage extends StatefulWidget { ...@@ -126,7 +132,9 @@ class FadeInImage extends StatefulWidget {
assert(fadeOutCurve != null), assert(fadeOutCurve != null),
assert(fadeInDuration != null), assert(fadeInDuration != null),
assert(fadeInCurve != null), assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null), assert(repeat != null),
assert(matchTextDirection != null),
placeholder = new MemoryImage(placeholder, scale: placeholderScale), placeholder = new MemoryImage(placeholder, scale: placeholderScale),
image = new NetworkImage(image, scale: imageScale), image = new NetworkImage(image, scale: imageScale),
super(key: key); super(key: key);
...@@ -146,8 +154,8 @@ class FadeInImage extends StatefulWidget { ...@@ -146,8 +154,8 @@ class FadeInImage extends StatefulWidget {
/// exact asset specified will be used. /// exact asset specified will be used.
/// ///
/// The [placeholder], [image], [imageScale], [fadeOutDuration], /// The [placeholder], [image], [imageScale], [fadeOutDuration],
/// [fadeOutCurve], [fadeInDuration], [fadeInCurve] and [repeat] arguments /// [fadeOutCurve], [fadeInDuration], [fadeInCurve], [alignment], [repeat],
/// must not be null. /// and [matchTextDirection] arguments must not be null.
/// ///
/// See also: /// See also:
/// ///
...@@ -169,8 +177,9 @@ class FadeInImage extends StatefulWidget { ...@@ -169,8 +177,9 @@ class FadeInImage extends StatefulWidget {
this.width, this.width,
this.height, this.height,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.matchTextDirection: false,
}) : assert(placeholder != null), }) : assert(placeholder != null),
assert(image != null), assert(image != null),
placeholder = placeholderScale != null placeholder = placeholderScale != null
...@@ -181,7 +190,9 @@ class FadeInImage extends StatefulWidget { ...@@ -181,7 +190,9 @@ class FadeInImage extends StatefulWidget {
assert(fadeOutCurve != null), assert(fadeOutCurve != null),
assert(fadeInDuration != null), assert(fadeInDuration != null),
assert(fadeInCurve != null), assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null), assert(repeat != null),
assert(matchTextDirection != null),
image = new NetworkImage(image, scale: imageScale), image = new NetworkImage(image, scale: imageScale),
super(key: key); super(key: key);
...@@ -227,14 +238,41 @@ class FadeInImage extends StatefulWidget { ...@@ -227,14 +238,41 @@ class FadeInImage extends StatefulWidget {
/// How to align the image within its bounds. /// 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 /// The alignment aligns the given position in the image to the given position
/// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle /// in the layout bounds. For example, a [FractionalOffset] alignment of (0.0,
/// of the right edge of its layout bounds. /// 0.0) aligns the image to the top-left corner of its layout bounds, while a
final FractionalOffset alignment; /// [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. /// How to paint any portions of the layout bounds not covered by the image.
final ImageRepeat repeat; 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 @override
State<StatefulWidget> createState() => new _FadeInImageState(); State<StatefulWidget> createState() => new _FadeInImageState();
} }
...@@ -456,6 +494,7 @@ class _FadeInImageState extends State<FadeInImage> with TickerProviderStateMixin ...@@ -456,6 +494,7 @@ class _FadeInImageState extends State<FadeInImage> with TickerProviderStateMixin
fit: widget.fit, fit: widget.fit,
alignment: widget.alignment, alignment: widget.alignment,
repeat: widget.repeat, repeat: widget.repeat,
matchTextDirection: widget.matchTextDirection,
); );
} }
......
...@@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/services.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'localizations.dart';
import 'media_query.dart'; import 'media_query.dart';
export 'package:flutter/services.dart' show export 'package:flutter/services.dart' show
...@@ -39,7 +40,8 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si ...@@ -39,7 +40,8 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si
return new ImageConfiguration( return new ImageConfiguration(
bundle: DefaultAssetBundle.of(context), bundle: DefaultAssetBundle.of(context),
devicePixelRatio: MediaQuery.of(context, nullOk: true)?.devicePixelRatio ?? 1.0, 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, size: size,
platform: defaultTargetPlatform, platform: defaultTargetPlatform,
); );
...@@ -101,7 +103,8 @@ class Image extends StatefulWidget { ...@@ -101,7 +103,8 @@ class Image extends StatefulWidget {
/// To show an image from the network or from an asset bundle, consider using /// To show an image from the network or from an asset bundle, consider using
/// [new Image.network] and [new Image.asset] respectively. /// [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({ const Image({
Key key, Key key,
@required this.image, @required this.image,
...@@ -110,12 +113,16 @@ class Image extends StatefulWidget { ...@@ -110,12 +113,16 @@ class Image extends StatefulWidget {
this.color, this.color,
this.colorBlendMode, this.colorBlendMode,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.centerSlice, this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package, this.package,
}) : assert(image != null), }) : assert(image != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key); super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from the network. /// Creates a widget that displays an [ImageStream] obtained from the network.
...@@ -129,12 +136,16 @@ class Image extends StatefulWidget { ...@@ -129,12 +136,16 @@ class Image extends StatefulWidget {
this.color, this.color,
this.colorBlendMode, this.colorBlendMode,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.centerSlice, this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package, this.package,
}) : image = new NetworkImage(src, scale: scale), }) : image = new NetworkImage(src, scale: scale),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key); super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from a [File]. /// Creates a widget that displays an [ImageStream] obtained from a [File].
...@@ -151,12 +162,16 @@ class Image extends StatefulWidget { ...@@ -151,12 +162,16 @@ class Image extends StatefulWidget {
this.color, this.color,
this.colorBlendMode, this.colorBlendMode,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.centerSlice, this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package, this.package,
}) : image = new FileImage(file, scale: scale), }) : image = new FileImage(file, scale: scale),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key); super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from an asset /// Creates a widget that displays an [ImageStream] obtained from an asset
...@@ -181,8 +196,9 @@ class Image extends StatefulWidget { ...@@ -181,8 +196,9 @@ class Image extends StatefulWidget {
// /// size-aware asset resolution will be attempted also, with the given // /// size-aware asset resolution will be attempted also, with the given
// /// dimensions interpreted as logical pixels. // /// dimensions interpreted as logical pixels.
// /// // ///
// /// * If the images have platform or locale variants, the current platform // /// * If the images have platform, locale, or directionality variants, the
// /// and locale is taken into account during asset resolution as well. // /// current platform, locale, and directionality are taken into account
// /// during asset resolution as well.
/// ///
/// The [name] and [repeat] arguments must not be null. /// The [name] and [repeat] arguments must not be null.
/// ///
...@@ -277,14 +293,18 @@ class Image extends StatefulWidget { ...@@ -277,14 +293,18 @@ class Image extends StatefulWidget {
this.color, this.color,
this.colorBlendMode, this.colorBlendMode,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.centerSlice, this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package, this.package,
}) : image = scale != null }) : image = scale != null
? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package) ? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
: new AssetImage(name, bundle: bundle, package: package), : new AssetImage(name, bundle: bundle, package: package),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key); super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from a [Uint8List]. /// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
...@@ -298,12 +318,16 @@ class Image extends StatefulWidget { ...@@ -298,12 +318,16 @@ class Image extends StatefulWidget {
this.color, this.color,
this.colorBlendMode, this.colorBlendMode,
this.fit, this.fit,
this.alignment, this.alignment: FractionalOffset.center,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.centerSlice, this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package, this.package,
}) : image = new MemoryImage(bytes, scale: scale), }) : image = new MemoryImage(bytes, scale: scale),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key); super(key: key);
/// The image to display. /// The image to display.
...@@ -342,10 +366,23 @@ class Image extends StatefulWidget { ...@@ -342,10 +366,23 @@ class Image extends StatefulWidget {
/// How to align the image within its bounds. /// 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 /// The alignment aligns the given position in the image to the given position
/// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle /// in the layout bounds. For example, a [FractionalOffset] alignment of (0.0,
/// of the right edge of its layout bounds. /// 0.0) aligns the image to the top-left corner of its layout bounds, while a
final FractionalOffset alignment; /// [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. /// How to paint any portions of the layout bounds not covered by the image.
final ImageRepeat repeat; final ImageRepeat repeat;
...@@ -359,6 +396,23 @@ class Image extends StatefulWidget { ...@@ -359,6 +396,23 @@ class Image extends StatefulWidget {
/// the center slice will be stretched only vertically. /// the center slice will be stretched only vertically.
final Rect centerSlice; 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 /// Whether to continue showing the old image (true), or briefly show nothing
/// (false), when the image provider changes. /// (false), when the image provider changes.
final bool gaplessPlayback; final bool gaplessPlayback;
...@@ -379,9 +433,10 @@ class Image extends StatefulWidget { ...@@ -379,9 +433,10 @@ class Image extends StatefulWidget {
description.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null)); description.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null));
description.add(new EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null)); description.add(new EnumProperty<BlendMode>('colorBlendMode', colorBlendMode, defaultValue: null));
description.add(new EnumProperty<BoxFit>('fit', fit, defaultValue: null)); description.add(new EnumProperty<BoxFit>('fit', fit, defaultValue: null));
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment, defaultValue: null)); description.add(new DiagnosticsProperty<FractionalOffsetGeometry>('alignment', alignment, defaultValue: null));
description.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); description.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat));
description.add(new DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null)); description.add(new DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null));
description.add(new FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction'));
} }
} }
...@@ -448,7 +503,8 @@ class _ImageState extends State<Image> { ...@@ -448,7 +503,8 @@ class _ImageState extends State<Image> {
fit: widget.fit, fit: widget.fit,
alignment: widget.alignment, alignment: widget.alignment,
repeat: widget.repeat, repeat: widget.repeat,
centerSlice: widget.centerSlice centerSlice: widget.centerSlice,
matchTextDirection: widget.matchTextDirection,
); );
} }
......
...@@ -385,9 +385,16 @@ class Localizations extends StatefulWidget { ...@@ -385,9 +385,16 @@ class Localizations extends StatefulWidget {
/// The locale of the Localizations widget for the widget tree that /// The locale of the Localizations widget for the widget tree that
/// corresponds to [BuildContext] `context`. /// 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(context != null);
assert(nullOk != null);
final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope); final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope);
if (nullOk && scope == null)
return null;
assert(scope != null, 'a Localizations ancestor was not found'); assert(scope != null, 'a Localizations ancestor was not found');
return scope.localizationsState.locale; return scope.localizationsState.locale;
} }
......
...@@ -173,6 +173,8 @@ class MediaQuery extends InheritedWidget { ...@@ -173,6 +173,8 @@ class MediaQuery extends InheritedWidget {
/// If you use this from a widget (e.g. in its build function), consider /// If you use this from a widget (e.g. in its build function), consider
/// calling [debugCheckHasMediaQuery]. /// calling [debugCheckHasMediaQuery].
static MediaQueryData of(BuildContext context, { bool nullOk: false }) { static MediaQueryData of(BuildContext context, { bool nullOk: false }) {
assert(context != null);
assert(nullOk != null);
final MediaQuery query = context.inheritFromWidgetOfExactType(MediaQuery); final MediaQuery query = context.inheritFromWidgetOfExactType(MediaQuery);
if (query != null) if (query != null)
return query.data; return query.data;
......
...@@ -74,6 +74,7 @@ void main() { ...@@ -74,6 +74,7 @@ void main() {
' constraints: BoxConstraints(25.0<=w<=100.0, 25.0<=h<=100.0)\n' ' constraints: BoxConstraints(25.0<=w<=100.0, 25.0<=h<=100.0)\n'
' size: Size(25.0, 25.0)\n' ' size: Size(25.0, 25.0)\n'
' image: [10×10]\n' ' image: [10×10]\n'
' alignment: FractionalOffset.center\n'
), ),
); );
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -278,6 +278,24 @@ abstract class PaintPattern { ...@@ -278,6 +278,24 @@ abstract class PaintPattern {
/// If no call to [Canvas.drawParagraph] was made, then this results in failure. /// If no call to [Canvas.drawParagraph] was made, then this results in failure.
void paragraph({ ui.Paragraph paragraph, Offset offset }); 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. /// Provides a custom matcher.
/// ///
/// Each method call after the last matched call (if any) will be passed to /// Each method call after the last matched call (if any) will be passed to
...@@ -472,6 +490,11 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp ...@@ -472,6 +490,11 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp
_predicates.add(new _FunctionPaintPredicate(#drawParagraph, <dynamic>[paragraph, offset])); _predicates.add(new _FunctionPaintPredicate(#drawParagraph, <dynamic>[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 @override
void something(PaintPatternPredicate predicate) { void something(PaintPatternPredicate predicate) {
_predicates.add(new _SomethingPaintPredicate(predicate)); _predicates.add(new _SomethingPaintPredicate(predicate));
...@@ -800,6 +823,41 @@ class _ArcPaintPredicate extends _DrawCommandPaintPredicate { ...@@ -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<dynamic> 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<String> 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 { class _SomethingPaintPredicate extends _PaintPredicate {
_SomethingPaintPredicate(this.predicate); _SomethingPaintPredicate(this.predicate);
......
...@@ -21,6 +21,26 @@ void main() { ...@@ -21,6 +21,26 @@ void main() {
alignment: const FractionalOffset(0.5, 0.5), alignment: const FractionalOffset(0.5, 0.5),
), ),
); );
await tester.pumpWidget(
const Align(
key: const GlobalObjectKey<Null>(null),
alignment: FractionalOffset.topLeft,
),
);
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.rtl,
child: const Align(
key: const GlobalObjectKey<Null>(null),
alignment: FractionalOffsetDirectional.topStart,
),
));
await tester.pumpWidget(
const Align(
key: const GlobalObjectKey<Null>(null),
alignment: FractionalOffset.topLeft,
),
);
}); });
testWidgets('Align control test (LTR)', (WidgetTester tester) async { testWidgets('Align control test (LTR)', (WidgetTester tester) async {
......
// 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<TestImageProvider> {
@override
Future<TestImageProvider> obtainKey(ImageConfiguration configuration) {
return new SynchronousFuture<TestImageProvider>(this);
}
@override
ImageStreamCompleter load(TestImageProvider key) {
return new OneFrameImageStreamCompleter(
new SynchronousFuture<ImageInfo>(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
...@@ -21,6 +21,26 @@ void main() { ...@@ -21,6 +21,26 @@ void main() {
child: child, child: child,
)); ));
expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(0.0, 0.0)); expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(0.0, 0.0));
await tester.pumpWidget(
const Padding(
key: const GlobalObjectKey<Null>(null),
padding: const EdgeInsets.only(left: 1.0),
),
);
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.rtl,
child: const Padding(
key: const GlobalObjectKey<Null>(null),
padding: const EdgeInsetsDirectional.only(start: 1.0),
),
));
await tester.pumpWidget(
const Padding(
key: const GlobalObjectKey<Null>(null),
padding: const EdgeInsets.only(left: 1.0),
),
);
}); });
testWidgets('Container padding/margin RTL', (WidgetTester tester) async { testWidgets('Container padding/margin RTL', (WidgetTester tester) async {
......
...@@ -610,4 +610,23 @@ void main() { ...@@ -610,4 +610,23 @@ void main() {
expect(tester.getTopLeft(find.byKey(key)), const Offset(50.0, 0.0)); 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,
),
);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment