diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart index 011848688f..a1ce6f0fdf 100644 --- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart +++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart @@ -416,11 +416,7 @@ class _BottomNavigationBarState extends State with TickerPr child: new Center( child: new Stack( children: [ - new Positioned( - left: 0.0, - top: 0.0, - right: 0.0, - bottom: 0.0, + new Positioned.fill( child: new CustomPaint( painter: new _RadialPainter( circles: _circles.toList(), diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 8d40ede94e..cebb449409 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -464,8 +464,8 @@ class InputDecorator extends StatelessWidget { top += topPaddingIncrement + baseStyle.fontSize - labelStyle.fontSize; stackChildren.add( - new AnimatedPositioned( - left: 0.0, + new AnimatedPositionedDirectional( + start: 0.0, top: top, duration: _kTransitionDuration, curve: _kTransitionCurve, diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index c9335d8f64..5d662aa461 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -482,6 +482,11 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState /// position over a given duration whenever the given position changes. /// /// Only works if it's the child of a [Stack]. +/// +/// See also: +/// +/// * [AnimatedPositionedDirectional], which adapts to the ambient +/// [Directionality]. class AnimatedPositioned extends ImplicitlyAnimatedWidget { /// Creates a widget that animates its position implicitly. /// @@ -608,6 +613,123 @@ class _AnimatedPositionedState extends AnimatedWidgetBaseState new _AnimatedPositionedDirectionalState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(new DoubleProperty('start', start, defaultValue: null)); + description.add(new DoubleProperty('top', top, defaultValue: null)); + description.add(new DoubleProperty('end', end, defaultValue: null)); + description.add(new DoubleProperty('bottom', bottom, defaultValue: null)); + description.add(new DoubleProperty('width', width, defaultValue: null)); + description.add(new DoubleProperty('height', height, defaultValue: null)); + } +} + +class _AnimatedPositionedDirectionalState extends AnimatedWidgetBaseState { + Tween _start; + Tween _top; + Tween _end; + Tween _bottom; + Tween _width; + Tween _height; + + @override + void forEachTween(TweenVisitor visitor) { + _start = visitor(_start, widget.start, (dynamic value) => new Tween(begin: value)); + _top = visitor(_top, widget.top, (dynamic value) => new Tween(begin: value)); + _end = visitor(_end, widget.end, (dynamic value) => new Tween(begin: value)); + _bottom = visitor(_bottom, widget.bottom, (dynamic value) => new Tween(begin: value)); + _width = visitor(_width, widget.width, (dynamic value) => new Tween(begin: value)); + _height = visitor(_height, widget.height, (dynamic value) => new Tween(begin: value)); + } + + @override + Widget build(BuildContext context) { + return new PositionedDirectional( + child: widget.child, + start: _start?.evaluate(animation), + top: _top?.evaluate(animation), + end: _end?.evaluate(animation), + bottom: _bottom?.evaluate(animation), + width: _width?.evaluate(animation), + height: _height?.evaluate(animation) + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(new ObjectFlagProperty>.has('start', _start)); + description.add(new ObjectFlagProperty>.has('top', _top)); + description.add(new ObjectFlagProperty>.has('end', _end)); + description.add(new ObjectFlagProperty>.has('bottom', _bottom)); + description.add(new ObjectFlagProperty>.has('width', _width)); + description.add(new ObjectFlagProperty>.has('height', _height)); + } +} + /// Animated version of [Opacity] which automatically transitions the child's /// opacity over a given duration whenever the given opacity changes. /// diff --git a/packages/flutter/test/widgets/animated_positioned_test.dart b/packages/flutter/test/widgets/animated_positioned_test.dart index d8baef1b7e..eb7d4d4e95 100644 --- a/packages/flutter/test/widgets/animated_positioned_test.dart +++ b/packages/flutter/test/widgets/animated_positioned_test.dart @@ -20,7 +20,7 @@ void main() { expect(positioned, hasOneLineDescription); }); - testWidgets('AnimatedPositioned - basics', (WidgetTester tester) async { + testWidgets('AnimatedPositioned - basics (VISUAL)', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); RenderBox box; @@ -35,10 +35,10 @@ void main() { top: 30.0, width: 70.0, height: 110.0, - duration: const Duration(seconds: 2) - ) - ] - ) + duration: const Duration(seconds: 2), + ), + ], + ), ); box = key.currentContext.findRenderObject(); @@ -59,25 +59,27 @@ void main() { top: 31.0, width: 59.0, height: 71.0, - duration: const Duration(seconds: 2) - ) - ] - ) + duration: const Duration(seconds: 2), + ), + ], + ), ); + const Offset first = const Offset(50.0 + 70.0 / 2.0, 30.0 + 110.0 / 2.0); + const Offset last = const Offset(37.0 + 59.0 / 2.0, 31.0 + 71.0 / 2.0); + box = key.currentContext.findRenderObject(); - expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(50.0 + 70.0 / 2.0, 30.0 + 110.0 / 2.0))); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(first)); await tester.pump(const Duration(seconds: 1)); box = key.currentContext.findRenderObject(); - expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(50.0 - (50.0 - 37.0) / 2.0 + (70.0 - (70.0 - 59.0) / 2.0) / 2.0, - 30.0 + (31.0 - 30.0) / 2.0 + (110.0 - (110.0 - 71.0) / 2.0) / 2.0))); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(Offset.lerp(first, last, 0.5))); await tester.pump(const Duration(seconds: 1)); box = key.currentContext.findRenderObject(); - expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(37.0 + 59.0 / 2.0, 31.0 + 71.0 / 2.0))); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(last)); expect(box, hasAGoodToStringDeep); expect( @@ -100,6 +102,178 @@ void main() { ); }); + testWidgets('AnimatedPositioned - basics (LTR)', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + + RenderBox box; + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 50.0, + top: 30.0, + width: 70.0, + height: 110.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(50.0 + 70.0 / 2.0, 30.0 + 110.0 / 2.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(50.0 + 70.0 / 2.0, 30.0 + 110.0 / 2.0))); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 37.0, + top: 31.0, + width: 59.0, + height: 71.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + const Offset first = const Offset(50.0 + 70.0 / 2.0, 30.0 + 110.0 / 2.0); + const Offset last = const Offset(37.0 + 59.0 / 2.0, 31.0 + 71.0 / 2.0); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(first)); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(Offset.lerp(first, last, 0.5))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(last)); + + expect(box, hasAGoodToStringDeep); + expect( + box.toStringDeep(minLevel: DiagnosticLevel.info), + equalsIgnoringHashCodes( + 'RenderLimitedBox#00000\n' + ' │ parentData: top=31.0; left=37.0; width=59.0; height=71.0;\n' + ' │ offset=Offset(37.0, 31.0) (can use size)\n' + ' │ constraints: BoxConstraints(w=59.0, h=71.0)\n' + ' │ size: Size(59.0, 71.0)\n' + ' │ maxWidth: 0.0\n' + ' │ maxHeight: 0.0\n' + ' │\n' + ' └─child: RenderConstrainedBox#00000\n' + ' parentData: (can use size)\n' + ' constraints: BoxConstraints(w=59.0, h=71.0)\n' + ' size: Size(59.0, 71.0)\n' + ' additionalConstraints: BoxConstraints(biggest)\n', + ), + ); + }); + + testWidgets('AnimatedPositioned - basics (RTL)', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + + RenderBox box; + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 50.0, + top: 30.0, + width: 70.0, + height: 110.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(800.0 - 50.0 - 70.0 / 2.0, 30.0 + 110.0 / 2.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(800.0 - 50.0 - 70.0 / 2.0, 30.0 + 110.0 / 2.0))); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 37.0, + top: 31.0, + width: 59.0, + height: 71.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + const Offset first = const Offset(800.0 - 50.0 - 70.0 / 2.0, 30.0 + 110.0 / 2.0); + const Offset last = const Offset(800.0 - 37.0 - 59.0 / 2.0, 31.0 + 71.0 / 2.0); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(first)); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(Offset.lerp(first, last, 0.5))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(last)); + + expect(box, hasAGoodToStringDeep); + expect( + box.toStringDeep(minLevel: DiagnosticLevel.info), + equalsIgnoringHashCodes( + 'RenderLimitedBox#00000\n' + ' │ parentData: top=31.0; right=37.0; width=59.0; height=71.0;\n' + ' │ offset=Offset(704.0, 31.0) (can use size)\n' + ' │ constraints: BoxConstraints(w=59.0, h=71.0)\n' + ' │ size: Size(59.0, 71.0)\n' + ' │ maxWidth: 0.0\n' + ' │ maxHeight: 0.0\n' + ' │\n' + ' └─child: RenderConstrainedBox#00000\n' + ' parentData: (can use size)\n' + ' constraints: BoxConstraints(w=59.0, h=71.0)\n' + ' size: Size(59.0, 71.0)\n' + ' additionalConstraints: BoxConstraints(biggest)\n' + ), + ); + }); + testWidgets('AnimatedPositioned - interrupted animation', (WidgetTester tester) async { final GlobalKey key = new GlobalKey();