diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 0e22f4d491..c8e3df2c4e 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -290,3 +290,114 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState description.add('has height'); } } + +/// Animated version of [Positioned] which automatically transitions the child's +/// position over a given duration whenever the given positon changes. +/// +/// Only works if it's the child of a [Stack]. +class AnimatedPositioned extends AnimatedWidgetBase { + AnimatedPositioned({ + Key key, + this.child, + this.left, + this.top, + this.right, + this.bottom, + this.width, + this.height, + Curve curve: Curves.linear, + Duration duration + }) : super(key: key, curve: curve, duration: duration) { + assert(left == null || right == null || width == null); + assert(top == null || bottom == null || height == null); + } + + AnimatedPositioned.fromRect({ + Key key, + this.child, + Rect rect, + Curve curve: Curves.linear, + Duration duration + }) : left = rect.left, + top = rect.top, + width = rect.width, + height = rect.height, + right = null, + bottom = null, + super(key: key, curve: curve, duration: duration); + + final Widget child; + + /// The offset of the child's left edge from the left of the stack. + final double left; + + /// The offset of the child's top edge from the top of the stack. + final double top; + + /// The offset of the child's right edge from the right of the stack. + final double right; + + /// The offset of the child's bottom edge from the bottom of the stack. + final double bottom; + + /// The child's width. + /// + /// Only two out of the three horizontal values (left, right, width) can be + /// set. The third must be null. + final double width; + + /// The child's height. + /// + /// Only two out of the three vertical values (top, bottom, height) can be + /// set. The third must be null. + final double height; + + _AnimatedPositionedState createState() => new _AnimatedPositionedState(); +} + +class _AnimatedPositionedState extends AnimatedWidgetBaseState { + AnimatedValue _left; + AnimatedValue _top; + AnimatedValue _right; + AnimatedValue _bottom; + AnimatedValue _width; + AnimatedValue _height; + + void forEachVariable(VariableVisitor visitor) { + // TODO(ianh): Use constructor tear-offs when it becomes possible + _left = visitor(_left, config.left, (dynamic value) => new AnimatedValue(value)); + _top = visitor(_top, config.top, (dynamic value) => new AnimatedValue(value)); + _right = visitor(_right, config.right, (dynamic value) => new AnimatedValue(value)); + _bottom = visitor(_bottom, config.bottom, (dynamic value) => new AnimatedValue(value)); + _width = visitor(_width, config.width, (dynamic value) => new AnimatedValue(value)); + _height = visitor(_height, config.height, (dynamic value) => new AnimatedValue(value)); + } + + Widget build(BuildContext context) { + return new Positioned( + child: config.child, + left: _left?.value, + top: _top?.value, + right: _right?.value, + bottom: _bottom?.value, + width: _width?.value, + height: _height?.value + ); + } + + void debugFillDescription(List description) { + super.debugFillDescription(description); + if (_left != null) + description.add('has left'); + if (_top != null) + description.add('has top'); + if (_right != null) + description.add('has right'); + if (_bottom != null) + description.add('has bottom'); + if (_width != null) + description.add('has width'); + if (_height != null) + description.add('has height'); + } +} diff --git a/packages/flutter/test/widget/animated_container_test.dart b/packages/flutter/test/widget/animated_container_test.dart index fac5763fea..a4ba63797b 100644 --- a/packages/flutter/test/widget/animated_container_test.dart +++ b/packages/flutter/test/widget/animated_container_test.dart @@ -30,7 +30,7 @@ void main() { ) ); - RenderDecoratedBox box = key.currentState.context.findRenderObject(); + RenderDecoratedBox box = key.currentContext.findRenderObject(); actualDecoration = box.decoration; expect(actualDecoration.backgroundColor, equals(decorationA.backgroundColor)); @@ -42,7 +42,7 @@ void main() { ) ); - expect(key.currentState.context.findRenderObject(), equals(box)); + expect(key.currentContext.findRenderObject(), equals(box)); actualDecoration = box.decoration; expect(actualDecoration.backgroundColor, equals(decorationA.backgroundColor)); diff --git a/packages/flutter/test/widget/animated_positioned_test.dart b/packages/flutter/test/widget/animated_positioned_test.dart new file mode 100644 index 0000000000..3a3a61234f --- /dev/null +++ b/packages/flutter/test/widget/animated_positioned_test.dart @@ -0,0 +1,215 @@ +// Copyright 2015 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 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:test/test.dart'; + +void main() { + test('AnimatedPositioned - basics', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + + RenderBox box; + + tester.pumpWidget( + new Stack( + [ + new AnimatedPositioned( + child: new Container(key: key), + left: 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(Point.origin)), equals(const Point(50.0 + 70.0 / 2.0, 30.0 + 110.0 / 2.0))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(50.0 + 70.0 / 2.0, 30.0 + 110.0 / 2.0))); + + tester.pumpWidget( + new Stack( + [ + new AnimatedPositioned( + child: new Container(key: key), + left: 37.0, + top: 31.0, + width: 59.0, + height: 71.0, + duration: const Duration(seconds: 2) + ) + ] + ) + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(50.0 + 70.0 / 2.0, 30.0 + 110.0 / 2.0))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(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))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(37.0 + 59.0 / 2.0, 31.0 + 71.0 / 2.0))); + + }); + }); + + test('AnimatedPositioned - interrupted animation', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + + RenderBox box; + + tester.pumpWidget( + new Stack( + [ + new AnimatedPositioned( + child: new Container(key: key), + left: 0.0, + top: 0.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2) + ) + ] + ) + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(50.0, 50.0))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(50.0, 50.0))); + + tester.pumpWidget( + new Stack( + [ + new AnimatedPositioned( + child: new Container(key: key), + left: 100.0, + top: 100.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2) + ) + ] + ) + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(50.0, 50.0))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(100.0, 100.0))); + + tester.pumpWidget( + new Stack( + [ + new AnimatedPositioned( + child: new Container(key: key), + left: 150.0, + top: 150.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2) + ) + ] + ) + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(100.0, 100.0))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(150.0, 150.0))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(200.0, 200.0))); + + }); + }); + + test('AnimatedPositioned - switching variables', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + + RenderBox box; + + tester.pumpWidget( + new Stack( + [ + new AnimatedPositioned( + child: new Container(key: key), + left: 0.0, + top: 0.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2) + ) + ] + ) + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(50.0, 50.0))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(50.0, 50.0))); + + tester.pumpWidget( + new Stack( + [ + new AnimatedPositioned( + child: new Container(key: key), + left: 0.0, + top: 100.0, + right: 100.0, // 700.0 from the left + height: 100.0, + duration: const Duration(seconds: 2) + ) + ] + ) + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(350.0, 50.0))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(350.0, 100.0))); + + tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Point.origin)), equals(const Point(350.0, 150.0))); + + }); + }); + +}