Added AnimatedSize. (#5260)
Added a widget that implicitly animates the size of it child.
This commit is contained in:
@@ -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';
|
||||
|
||||
139
packages/flutter/lib/src/rendering/animated_size.dart
Normal file
139
packages/flutter/lib/src/rendering/animated_size.dart
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
packages/flutter/lib/src/widgets/animated_size.dart
Normal file
62
packages/flutter/lib/src/widgets/animated_size.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
172
packages/flutter/test/widget/animated_size_test.dart
Normal file
172
packages/flutter/test/widget/animated_size_test.dart
Normal file
@@ -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<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));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user