From 05a22fe0ab2e74635db6628a7fc125e1a1f20203 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 23 Oct 2017 11:05:23 -0700 Subject: [PATCH] FittedBox RTL (#12662) --- .../flutter/lib/src/rendering/proxy_box.dart | 54 ++++- .../lib/src/rendering/shifted_box.dart | 4 +- packages/flutter/lib/src/widgets/basic.dart | 22 +- .../flutter/test/widgets/fitted_box_test.dart | 226 ++++++++++++++++++ 4 files changed, 291 insertions(+), 15 deletions(-) diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 8e65727a49..05d38312ed 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -1764,15 +1764,30 @@ class RenderFittedBox extends RenderProxyBox { /// /// The [fit] and [alignment] arguments must not be null. RenderFittedBox({ - RenderBox child, BoxFit fit: BoxFit.contain, - Alignment alignment: Alignment.center + AlignmentGeometry alignment: Alignment.center, + TextDirection textDirection, + RenderBox child, }) : assert(fit != null), assert(alignment != null), _fit = fit, _alignment = alignment, + _textDirection = textDirection, super(child); + Alignment _resolvedAlignment; + + void _resolve() { + if (_resolvedAlignment != null) + return; + _resolvedAlignment = alignment.resolve(textDirection); + } + + void _markNeedResolution() { + _resolvedAlignment = null; + markNeedsPaint(); + } + /// How to inscribe the child into the space allocated during layout. BoxFit get fit => _fit; BoxFit _fit; @@ -1790,17 +1805,36 @@ class RenderFittedBox extends RenderProxyBox { /// An alignment of (0.0, 0.0) aligns the child to the top-left corner of its /// parent's bounds. An alignment of (1.0, 0.5) aligns the child to the middle /// of the right edge of its parent's bounds. - Alignment get alignment => _alignment; - Alignment _alignment; - set alignment(Alignment value) { - assert(value != null && value.x != null && value.y != null); + /// + /// If this is set to a [AlignmentDirectional] object, then + /// [textDirection] must not be null. + AlignmentGeometry get alignment => _alignment; + AlignmentGeometry _alignment; + set alignment(AlignmentGeometry value) { + assert(value != null); if (_alignment == value) return; _alignment = value; _clearPaintData(); - markNeedsPaint(); + _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; + _clearPaintData(); + _markNeedResolution(); + } + + // TODO(ianh): The intrinsic dimensions of this box are wrong. + @override void performLayout() { if (child != null) { @@ -1828,12 +1862,13 @@ class RenderFittedBox extends RenderProxyBox { _hasVisualOverflow = false; _transform = new Matrix4.identity(); } else { + _resolve(); final Size childSize = child.size; final FittedSizes sizes = applyBoxFit(_fit, childSize, size); final double scaleX = sizes.destination.width / sizes.source.width; final double scaleY = sizes.destination.height / sizes.source.height; - final Rect sourceRect = _alignment.inscribe(sizes.source, Offset.zero & childSize); - final Rect destinationRect = _alignment.inscribe(sizes.destination, Offset.zero & size); + final Rect sourceRect = _resolvedAlignment.inscribe(sizes.source, Offset.zero & childSize); + final Rect destinationRect = _resolvedAlignment.inscribe(sizes.destination, Offset.zero & size); _hasVisualOverflow = sourceRect.width < childSize.width || sourceRect.height < childSize.width; _transform = new Matrix4.translationValues(destinationRect.left, destinationRect.top, 0.0) ..scale(scaleX, scaleY, 1.0) @@ -1894,6 +1929,7 @@ class RenderFittedBox extends RenderProxyBox { super.debugFillProperties(description); description.add(new EnumProperty('fit', fit)); description.add(new DiagnosticsProperty('alignment', alignment)); + description.add(new EnumProperty('textDirection', textDirection, defaultValue: null)); } } diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index a959bc7cc7..10238b86f6 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -229,7 +229,7 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { /// The [alignment] argument must not be null. RenderAligningShiftedBox({ AlignmentGeometry alignment: Alignment.center, - TextDirection textDirection, + @required TextDirection textDirection, RenderBox child, }) : assert(alignment != null), _alignment = alignment, @@ -265,7 +265,7 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { AlignmentGeometry _alignment; /// Sets the alignment to a new value, and triggers a layout update. /// - /// The new alignment must not be null or have any null properties. + /// The new alignment must not be null. set alignment(AlignmentGeometry value) { assert(value != null); if (_alignment == value) diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 70ccea3825..fe6a99893f 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -961,7 +961,7 @@ class FittedBox extends SingleChildRenderObjectWidget { Key key, this.fit: BoxFit.contain, this.alignment: Alignment.center, - Widget child + Widget child, }) : assert(fit != null), assert(alignment != null), super(key: key, child: child); @@ -974,16 +974,30 @@ class FittedBox extends SingleChildRenderObjectWidget { /// An alignment of (-1.0, -1.0) aligns the child to the top-left corner of its /// parent's bounds. An alignment of (1.0, 0.0) aligns the child to the middle /// of the right edge of its parent's bounds. - final Alignment alignment; + final AlignmentGeometry alignment; @override - RenderFittedBox createRenderObject(BuildContext context) => new RenderFittedBox(fit: fit, alignment: alignment); + RenderFittedBox createRenderObject(BuildContext context) { + return new RenderFittedBox( + fit: fit, + alignment: alignment, + textDirection: Directionality.of(context), + ); + } @override void updateRenderObject(BuildContext context, RenderFittedBox renderObject) { renderObject ..fit = fit - ..alignment = alignment; + ..alignment = alignment + ..textDirection = Directionality.of(context); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(new EnumProperty('fit', fit)); + description.add(new DiagnosticsProperty('alignment', alignment)); } } diff --git a/packages/flutter/test/widgets/fitted_box_test.dart b/packages/flutter/test/widgets/fitted_box_test.dart index 82c218b92f..23182874e3 100644 --- a/packages/flutter/test/widgets/fitted_box_test.dart +++ b/packages/flutter/test/widgets/fitted_box_test.dart @@ -112,4 +112,230 @@ void main() { expect(insidePoint, equals(outsidePoint)); }); + + testWidgets('FittedBox with no child', (WidgetTester tester) async { + final Key key = new UniqueKey(); + await tester.pumpWidget( + new Center( + child: new FittedBox( + key: key, + fit: BoxFit.cover, + ), + ), + ); + + final RenderBox box = tester.firstRenderObject(find.byKey(key)); + expect(box.size.width, 0.0); + expect(box.size.height, 0.0); + }); + + testWidgets('Child can be aligned multiple ways in a row', (WidgetTester tester) async { + final Key outside = new UniqueKey(); + final Key inside = new UniqueKey(); + + { // align RTL + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Center( + child: new Container( + width: 100.0, + height: 100.0, + child: new FittedBox( + key: outside, + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomEnd, + child: new Container( + key: inside, + width: 10.0, + height: 10.0, + ), + ), + ), + ), + ), + ); + + final RenderBox outsideBox = tester.firstRenderObject(find.byKey(outside)); + expect(outsideBox.size.width, 100.0); + expect(outsideBox.size.height, 100.0); + + final RenderBox insideBox = tester.firstRenderObject(find.byKey(inside)); + expect(insideBox.size.width, 10.0); + expect(insideBox.size.height, 10.0); + + final Offset insideTopLeft = insideBox.localToGlobal(const Offset(0.0, 0.0)); + final Offset outsideTopLeft = outsideBox.localToGlobal(const Offset(0.0, 90.0)); + final Offset insideBottomRight = insideBox.localToGlobal(const Offset(10.0, 10.0)); + final Offset outsideBottomRight = outsideBox.localToGlobal(const Offset(10.0, 100.0)); + + expect(insideTopLeft, equals(outsideTopLeft)); + expect(insideBottomRight, equals(outsideBottomRight)); + } + + { // change direction + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 100.0, + child: new FittedBox( + key: outside, + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomEnd, + child: new Container( + key: inside, + width: 10.0, + height: 10.0, + ), + ), + ), + ), + ), + ); + + final RenderBox outsideBox = tester.firstRenderObject(find.byKey(outside)); + expect(outsideBox.size.width, 100.0); + expect(outsideBox.size.height, 100.0); + + final RenderBox insideBox = tester.firstRenderObject(find.byKey(inside)); + expect(insideBox.size.width, 10.0); + expect(insideBox.size.height, 10.0); + + final Offset insideTopLeft = insideBox.localToGlobal(const Offset(0.0, 0.0)); + final Offset outsideTopLeft = outsideBox.localToGlobal(const Offset(90.0, 90.0)); + final Offset insideBottomRight = insideBox.localToGlobal(const Offset(10.0, 10.0)); + final Offset outsideBottomRight = outsideBox.localToGlobal(const Offset(100.0, 100.0)); + + expect(insideTopLeft, equals(outsideTopLeft)); + expect(insideBottomRight, equals(outsideBottomRight)); + } + + { // change alignment + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 100.0, + child: new FittedBox( + key: outside, + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.center, + child: new Container( + key: inside, + width: 10.0, + height: 10.0, + ), + ), + ), + ), + ), + ); + + final RenderBox outsideBox = tester.firstRenderObject(find.byKey(outside)); + expect(outsideBox.size.width, 100.0); + expect(outsideBox.size.height, 100.0); + + final RenderBox insideBox = tester.firstRenderObject(find.byKey(inside)); + expect(insideBox.size.width, 10.0); + expect(insideBox.size.height, 10.0); + + final Offset insideTopLeft = insideBox.localToGlobal(const Offset(0.0, 0.0)); + final Offset outsideTopLeft = outsideBox.localToGlobal(const Offset(45.0, 45.0)); + final Offset insideBottomRight = insideBox.localToGlobal(const Offset(10.0, 10.0)); + final Offset outsideBottomRight = outsideBox.localToGlobal(const Offset(55.0, 55.0)); + + expect(insideTopLeft, equals(outsideTopLeft)); + expect(insideBottomRight, equals(outsideBottomRight)); + } + + { // change size + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 100.0, + child: new FittedBox( + key: outside, + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.center, + child: new Container( + key: inside, + width: 30.0, + height: 10.0, + ), + ), + ), + ), + ), + ); + + final RenderBox outsideBox = tester.firstRenderObject(find.byKey(outside)); + expect(outsideBox.size.width, 100.0); + expect(outsideBox.size.height, 100.0); + + final RenderBox insideBox = tester.firstRenderObject(find.byKey(inside)); + expect(insideBox.size.width, 30.0); + expect(insideBox.size.height, 10.0); + + final Offset insideTopLeft = insideBox.localToGlobal(const Offset(0.0, 0.0)); + final Offset outsideTopLeft = outsideBox.localToGlobal(const Offset(35.0, 45.0)); + final Offset insideBottomRight = insideBox.localToGlobal(const Offset(30.0, 10.0)); + final Offset outsideBottomRight = outsideBox.localToGlobal(const Offset(65.0, 55.0)); + + expect(insideTopLeft, equals(outsideTopLeft)); + expect(insideBottomRight, equals(outsideBottomRight)); + } + + { // change fit + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + width: 100.0, + height: 100.0, + child: new FittedBox( + key: outside, + fit: BoxFit.fill, + alignment: AlignmentDirectional.center, + child: new Container( + key: inside, + width: 30.0, + height: 10.0, + ), + ), + ), + ), + ), + ); + + final RenderBox outsideBox = tester.firstRenderObject(find.byKey(outside)); + expect(outsideBox.size.width, 100.0); + expect(outsideBox.size.height, 100.0); + + final RenderBox insideBox = tester.firstRenderObject(find.byKey(inside)); + expect(insideBox.size.width, 30.0); + expect(insideBox.size.height, 10.0); + + final Offset insideTopLeft = insideBox.localToGlobal(const Offset(0.0, 0.0)); + final Offset outsideTopLeft = outsideBox.localToGlobal(const Offset(0.0, 0.0)); + final Offset insideBottomRight = insideBox.localToGlobal(const Offset(30.0, 10.0)); + final Offset outsideBottomRight = outsideBox.localToGlobal(const Offset(100.0, 100.0)); + + expect(insideTopLeft, equals(outsideTopLeft)); + expect(insideBottomRight, equals(outsideBottomRight)); + } + }); }