diff --git a/packages/flutter/lib/rendering.dart b/packages/flutter/lib/rendering.dart index 9e93e161c4..106881a15d 100644 --- a/packages/flutter/lib/rendering.dart +++ b/packages/flutter/lib/rendering.dart @@ -22,6 +22,7 @@ /// initialized with those features. library rendering; +export 'src/rendering/animated_size.dart'; export 'src/rendering/auto_layout.dart'; export 'src/rendering/binding.dart'; export 'src/rendering/block.dart'; diff --git a/packages/flutter/lib/src/rendering/animated_size.dart b/packages/flutter/lib/src/rendering/animated_size.dart new file mode 100644 index 0000000000..d050f87d28 --- /dev/null +++ b/packages/flutter/lib/src/rendering/animated_size.dart @@ -0,0 +1,139 @@ +// Copyright 2016 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/animation.dart'; +import 'package:meta/meta.dart'; + +import 'box.dart'; +import 'object.dart'; +import 'shifted_box.dart'; + +/// A render object that animates its size to its child's size over a given +/// [duration] and with a given [curve]. In case the child's size is animating +/// as opposed to abruptly changing size, the parent behaves like a normal +/// container. +/// +/// In case the child overflows the current animated size of the parent, it gets +/// clipped automatically. +class RenderAnimatedSize extends RenderAligningShiftedBox { + /// Creates a render object that animates its size to match its child. + /// The [duration] and [curve] arguments define the animation. The [alignment] + /// argument is used to align the child in the case where the parent is not + /// (yet) the same size as the child. + /// + /// The arguments [duration], [curve], and [alignment] should not be null. + RenderAnimatedSize({ + Curve curve: Curves.linear, + RenderBox child, + FractionalOffset alignment: FractionalOffset.center, + @required Duration duration + }) : super(child: child, alignment: alignment) { + assert(duration != null); + assert(curve != null); + _controller = new AnimationController( + duration: duration + )..addListener(() { + if (_controller.value != _lastValue) + markNeedsLayout(); + }); + _animation = new CurvedAnimation( + parent: _controller, + curve: curve + ); + } + + AnimationController _controller; + CurvedAnimation _animation; + SizeTween _sizeTween = new SizeTween(); + bool _didChangeTargetSizeLastFrame = false; + bool _hasVisualOverflow; + double _lastValue; + + /// The duration of the animation. + Duration get duration => _controller.duration; + set duration(Duration value) { + assert(value != null); + if (value == _controller.duration) + return; + _controller.duration = value; + } + + /// The curve of the animation. + Curve get curve => _animation.curve; + set curve(Curve value) { + assert(value != null); + if (value == _animation.curve) + return; + _animation.curve = value; + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + if (_animatedSize != _sizeTween.end && !_controller.isAnimating) + _controller.forward(); + } + + @override + void detach() { + _controller.stop(); + super.detach(); + } + + Size get _animatedSize { + return _sizeTween.evaluate(_animation); + } + + @override + void performLayout() { + _lastValue = _controller.value; + _hasVisualOverflow = false; + + if (child == null) { + size = _sizeTween.begin = _sizeTween.end = constraints.smallest; + return; + } + + child.layout(constraints, parentUsesSize: true); + if (_sizeTween.end != child.size) { + _sizeTween.begin = _animatedSize ?? child.size; + _sizeTween.end = child.size; + + if (_didChangeTargetSizeLastFrame) { + size = child.size; + _controller.stop(); + } else { + // Don't register first change (i.e. when _targetSize == _sourceSize) + // as a last-frame change. + if (_sizeTween.end != _sizeTween.begin) + _didChangeTargetSizeLastFrame = true; + + _lastValue = 0.0; + _controller.forward(from: 0.0); + + size = constraints.constrain(_animatedSize); + } + } else { + _didChangeTargetSizeLastFrame = false; + + size = constraints.constrain(_animatedSize); + } + + alignChild(); + + if (size.width < _sizeTween.end.width || + size.height < _sizeTween.end.height) + _hasVisualOverflow = true; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child != null && _hasVisualOverflow) { + final Rect rect = Point.origin & size; + context.pushClipRect(needsCompositing, offset, rect, super.paint); + } else { + super.paint(context, offset); + } + } +} diff --git a/packages/flutter/lib/src/widgets/animated_size.dart b/packages/flutter/lib/src/widgets/animated_size.dart new file mode 100644 index 0000000000..96af3857b6 --- /dev/null +++ b/packages/flutter/lib/src/widgets/animated_size.dart @@ -0,0 +1,62 @@ +// Copyright 2016 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/rendering.dart'; +import 'package:meta/meta.dart'; + +import 'basic.dart'; +import 'framework.dart'; + +/// Animated widget that automatically transitions its size over a given +/// duration whenever the given child's size changes. +class AnimatedSize extends SingleChildRenderObjectWidget { + /// Creates a widget that animates its size to match that of its child. + /// + /// The [curve] and [duration] arguments must not be null. + AnimatedSize({ + Key key, + Widget child, + this.alignment: FractionalOffset.center, + this.curve: Curves.linear, + @required this.duration + }) : super(key: key, child: child); + + /// The alignment of the child within the parent when the parent is not yet + /// the same size as the child. + /// + /// The x and y values of the alignment control the horizontal and vertical + /// alignment, respectively. An x value of 0.0 means that the left edge of + /// the child is aligned with the left edge of the parent whereas an x value + /// of 1.0 means that the right edge of the child is aligned with the right + /// edge of the parent. Other values interpolate (and extrapolate) linearly. + /// For example, a value of 0.5 means that the center of the child is aligned + /// with the center of the parent. + final FractionalOffset alignment; + + /// The animation curve when transitioning this widget's size to match the + /// child's size. + final Curve curve; + + /// The duration when transitioning this widget's size to match the child's + /// size. + final Duration duration; + + @override + RenderAnimatedSize createRenderObject(BuildContext context) { + return new RenderAnimatedSize( + alignment: alignment, + duration: duration, + curve: curve + ); + } + + @override + void updateRenderObject(BuildContext context, + RenderAnimatedSize renderObject) { + renderObject + ..alignment = alignment + ..duration = duration + ..curve = curve; + } +} diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 0c4e3be3c1..b2d16c2d2c 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:meta/meta.dart'; +import 'package:vector_math/vector_math_64.dart'; + import 'basic.dart'; import 'container.dart'; import 'framework.dart'; -import 'package:meta/meta.dart'; -import 'package:vector_math/vector_math_64.dart'; - /// An interpolation between two [BoxConstraint]s. class BoxConstraintsTween extends Tween { /// Creates a box constraints tween. diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index f944f3e6c1..684159137c 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -8,6 +8,7 @@ /// To use, import `package:flutter/widgets.dart`. library widgets; +export 'src/widgets/animated_size.dart'; export 'src/widgets/app.dart'; export 'src/widgets/auto_layout.dart'; export 'src/widgets/banner.dart'; diff --git a/packages/flutter/test/widget/animated_size_test.dart b/packages/flutter/test/widget/animated_size_test.dart new file mode 100644 index 0000000000..aabc499981 --- /dev/null +++ b/packages/flutter/test/widget/animated_size_test.dart @@ -0,0 +1,172 @@ +// Copyright 2016 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'; + +class TestPaintingContext implements PaintingContext { + final List invocations = []; + + @override + void noSuchMethod(Invocation invocation) { + invocations.add(invocation); + } +} + +void main() { + testWidgets('AnimatedSize test', (WidgetTester tester) async { + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + child: new SizedBox( + width: 100.0, + height: 100.0 + ) + ) + ) + ); + + RenderBox box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: new Duration(milliseconds: 200), + child: new SizedBox( + width: 200.0, + height: 200.0 + ) + ) + ) + ); + + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(150.0)); + expect(box.size.height, equals(150.0)); + + TestPaintingContext context = new TestPaintingContext(); + box.paint(context, Offset.zero); + expect(context.invocations.first.memberName, equals(#pushClipRect)); + + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(200.0)); + expect(box.size.height, equals(200.0)); + + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: new Duration(milliseconds: 200), + child: new SizedBox( + width: 100.0, + height: 100.0 + ) + ) + ) + ); + + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(150.0)); + expect(box.size.height, equals(150.0)); + + context = new TestPaintingContext(); + box.paint(context, Offset.zero); + expect(context.invocations.first.memberName, equals(#paintChild)); + + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + }); + + testWidgets('AnimatedSize constrained test', (WidgetTester tester) async { + await tester.pumpWidget( + new Center( + child: new SizedBox ( + width: 100.0, + height: 100.0, + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + child: new SizedBox( + width: 100.0, + height: 100.0 + ) + ) + ) + ) + ); + + RenderBox box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + + await tester.pumpWidget( + new Center( + child: new SizedBox ( + width: 100.0, + height: 100.0, + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + child: new SizedBox( + width: 200.0, + height: 200.0 + ) + ) + ) + ) + ); + + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + }); + + testWidgets('AnimatedSize with AnimatedContainer', (WidgetTester tester) async { + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + child: new AnimatedContainer( + duration: const Duration(milliseconds: 100), + width: 100.0, + height: 100.0 + ) + ) + ) + ); + + RenderBox box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + child: new AnimatedContainer( + duration: const Duration(milliseconds: 100), + width: 200.0, + height: 200.0 + ) + ) + ) + ); + + await tester.pump(const Duration(milliseconds: 1)); // register change + await tester.pump(const Duration(milliseconds: 49)); + expect(box.size.width, equals(150.0)); + expect(box.size.height, equals(150.0)); + await tester.pump(const Duration(milliseconds: 50)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(200.0)); + expect(box.size.height, equals(200.0)); + }); +}