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