Add AnimatedPositionedDirectional (#12228)
This widget makes it easier to animated Positioned widgets with awareness of the ambient Directionality. Fixes #11998
This commit is contained in:
parent
36e7138e4f
commit
824db696b8
@ -416,11 +416,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
|
|||||||
child: new Center(
|
child: new Center(
|
||||||
child: new Stack(
|
child: new Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new Positioned(
|
new Positioned.fill(
|
||||||
left: 0.0,
|
|
||||||
top: 0.0,
|
|
||||||
right: 0.0,
|
|
||||||
bottom: 0.0,
|
|
||||||
child: new CustomPaint(
|
child: new CustomPaint(
|
||||||
painter: new _RadialPainter(
|
painter: new _RadialPainter(
|
||||||
circles: _circles.toList(),
|
circles: _circles.toList(),
|
||||||
|
@ -464,8 +464,8 @@ class InputDecorator extends StatelessWidget {
|
|||||||
top += topPaddingIncrement + baseStyle.fontSize - labelStyle.fontSize;
|
top += topPaddingIncrement + baseStyle.fontSize - labelStyle.fontSize;
|
||||||
|
|
||||||
stackChildren.add(
|
stackChildren.add(
|
||||||
new AnimatedPositioned(
|
new AnimatedPositionedDirectional(
|
||||||
left: 0.0,
|
start: 0.0,
|
||||||
top: top,
|
top: top,
|
||||||
duration: _kTransitionDuration,
|
duration: _kTransitionDuration,
|
||||||
curve: _kTransitionCurve,
|
curve: _kTransitionCurve,
|
||||||
|
@ -482,6 +482,11 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer>
|
|||||||
/// position over a given duration whenever the given position changes.
|
/// position over a given duration whenever the given position changes.
|
||||||
///
|
///
|
||||||
/// Only works if it's the child of a [Stack].
|
/// Only works if it's the child of a [Stack].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AnimatedPositionedDirectional], which adapts to the ambient
|
||||||
|
/// [Directionality].
|
||||||
class AnimatedPositioned extends ImplicitlyAnimatedWidget {
|
class AnimatedPositioned extends ImplicitlyAnimatedWidget {
|
||||||
/// Creates a widget that animates its position implicitly.
|
/// Creates a widget that animates its position implicitly.
|
||||||
///
|
///
|
||||||
@ -608,6 +613,123 @@ class _AnimatedPositionedState extends AnimatedWidgetBaseState<AnimatedPositione
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Animated version of [PositionedDirectional] which automatically transitions the child's
|
||||||
|
/// position over a given duration whenever the given position changes.
|
||||||
|
///
|
||||||
|
/// Only works if it's the child of a [Stack].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AnimatedPositioned], which specifies the widget's position visually.
|
||||||
|
class AnimatedPositionedDirectional extends ImplicitlyAnimatedWidget {
|
||||||
|
/// Creates a widget that animates its position implicitly.
|
||||||
|
///
|
||||||
|
/// Only two out of the three horizontal values ([start], [end],
|
||||||
|
/// [width]), and only two out of the three vertical values ([top],
|
||||||
|
/// [bottom], [height]), can be set. In each case, at least one of
|
||||||
|
/// the three must be null.
|
||||||
|
///
|
||||||
|
/// The [curve] and [duration] arguments must not be null.
|
||||||
|
const AnimatedPositionedDirectional({
|
||||||
|
Key key,
|
||||||
|
@required this.child,
|
||||||
|
this.start,
|
||||||
|
this.top,
|
||||||
|
this.end,
|
||||||
|
this.bottom,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
Curve curve: Curves.linear,
|
||||||
|
@required Duration duration,
|
||||||
|
}) : assert(start == null || end == null || width == null),
|
||||||
|
assert(top == null || bottom == null || height == null),
|
||||||
|
super(key: key, curve: curve, duration: duration);
|
||||||
|
|
||||||
|
/// The widget below this widget in the tree.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// The offset of the child's start edge from the start of the stack.
|
||||||
|
final double start;
|
||||||
|
|
||||||
|
/// The offset of the child's top edge from the top of the stack.
|
||||||
|
final double top;
|
||||||
|
|
||||||
|
/// The offset of the child's end edge from the end of the stack.
|
||||||
|
final double end;
|
||||||
|
|
||||||
|
/// 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 (start, end, 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;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AnimatedPositionedDirectionalState createState() => 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<AnimatedPositionedDirectional> {
|
||||||
|
Tween<double> _start;
|
||||||
|
Tween<double> _top;
|
||||||
|
Tween<double> _end;
|
||||||
|
Tween<double> _bottom;
|
||||||
|
Tween<double> _width;
|
||||||
|
Tween<double> _height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void forEachTween(TweenVisitor<dynamic> visitor) {
|
||||||
|
_start = visitor(_start, widget.start, (dynamic value) => new Tween<double>(begin: value));
|
||||||
|
_top = visitor(_top, widget.top, (dynamic value) => new Tween<double>(begin: value));
|
||||||
|
_end = visitor(_end, widget.end, (dynamic value) => new Tween<double>(begin: value));
|
||||||
|
_bottom = visitor(_bottom, widget.bottom, (dynamic value) => new Tween<double>(begin: value));
|
||||||
|
_width = visitor(_width, widget.width, (dynamic value) => new Tween<double>(begin: value));
|
||||||
|
_height = visitor(_height, widget.height, (dynamic value) => new Tween<double>(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<Tween<double>>.has('start', _start));
|
||||||
|
description.add(new ObjectFlagProperty<Tween<double>>.has('top', _top));
|
||||||
|
description.add(new ObjectFlagProperty<Tween<double>>.has('end', _end));
|
||||||
|
description.add(new ObjectFlagProperty<Tween<double>>.has('bottom', _bottom));
|
||||||
|
description.add(new ObjectFlagProperty<Tween<double>>.has('width', _width));
|
||||||
|
description.add(new ObjectFlagProperty<Tween<double>>.has('height', _height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Animated version of [Opacity] which automatically transitions the child's
|
/// Animated version of [Opacity] which automatically transitions the child's
|
||||||
/// opacity over a given duration whenever the given opacity changes.
|
/// opacity over a given duration whenever the given opacity changes.
|
||||||
///
|
///
|
||||||
|
@ -20,7 +20,7 @@ void main() {
|
|||||||
expect(positioned, hasOneLineDescription);
|
expect(positioned, hasOneLineDescription);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('AnimatedPositioned - basics', (WidgetTester tester) async {
|
testWidgets('AnimatedPositioned - basics (VISUAL)', (WidgetTester tester) async {
|
||||||
final GlobalKey key = new GlobalKey();
|
final GlobalKey key = new GlobalKey();
|
||||||
|
|
||||||
RenderBox box;
|
RenderBox box;
|
||||||
@ -35,10 +35,10 @@ void main() {
|
|||||||
top: 30.0,
|
top: 30.0,
|
||||||
width: 70.0,
|
width: 70.0,
|
||||||
height: 110.0,
|
height: 110.0,
|
||||||
duration: const Duration(seconds: 2)
|
duration: const Duration(seconds: 2),
|
||||||
)
|
),
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
box = key.currentContext.findRenderObject();
|
box = key.currentContext.findRenderObject();
|
||||||
@ -59,25 +59,27 @@ void main() {
|
|||||||
top: 31.0,
|
top: 31.0,
|
||||||
width: 59.0,
|
width: 59.0,
|
||||||
height: 71.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();
|
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));
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
box = key.currentContext.findRenderObject();
|
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,
|
expect(box.localToGlobal(box.size.center(Offset.zero)), equals(Offset.lerp(first, last, 0.5)));
|
||||||
30.0 + (31.0 - 30.0) / 2.0 + (110.0 - (110.0 - 71.0) / 2.0) / 2.0)));
|
|
||||||
|
|
||||||
await tester.pump(const Duration(seconds: 1));
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
box = key.currentContext.findRenderObject();
|
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(box, hasAGoodToStringDeep);
|
||||||
expect(
|
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: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
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: <none> (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: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
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: <none> (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 {
|
testWidgets('AnimatedPositioned - interrupted animation', (WidgetTester tester) async {
|
||||||
final GlobalKey key = new GlobalKey();
|
final GlobalKey key = new GlobalKey();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user