Commit 6e9bbca4 authored by Dragoș Tiselice's avatar Dragoș Tiselice Committed by GitHub

Added AnimatedSize. (#5260)

Added a widget that implicitly animates the size of it child.
parent f28cf641
......@@ -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';
......
// 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);
}
}
}
// 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;
}
}
......@@ -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<BoxConstraints> {
/// Creates a box constraints tween.
......
......@@ -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';
......
// 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<Invocation> invocations = <Invocation>[];
@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));
});
}
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