Commit baf3b45e authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add RTL support to AnimatedContainer (#11910)

Also, fix the interpolation between visual and directional fractional
offsets. The interpolation now works visually in whatever direction the
result eventually gets resolved into.

Fixes #11847
Fixes #11357
parent 74b0bf64
......@@ -752,10 +752,10 @@ class EdgeInsetsDirectional extends EdgeInsetsGeometry {
EdgeInsets resolve(TextDirection direction) {
assert(direction != null);
switch (direction) {
case TextDirection.ltr:
return new EdgeInsets.fromLTRB(start, top, end, bottom);
case TextDirection.rtl:
return new EdgeInsets.fromLTRB(end, top, start, bottom);
case TextDirection.ltr:
return new EdgeInsets.fromLTRB(start, top, end, bottom);
}
return null;
}
......@@ -856,10 +856,10 @@ class _MixedEdgeInsets extends EdgeInsetsGeometry {
EdgeInsets resolve(TextDirection direction) {
assert(direction != null);
switch (direction) {
case TextDirection.rtl:
return new EdgeInsets.fromLTRB(_end + _left, _top, _start + _right, _bottom);
case TextDirection.ltr:
return new EdgeInsets.fromLTRB(_start + _left, _top, _end + _right, _bottom);
case TextDirection.rtl:
return new EdgeInsets.fromLTRB(_end + _left, _top, _start + _left, _bottom);
}
return null;
}
......
......@@ -11,11 +11,16 @@ import 'package:flutter/painting.dart';
/// appropriate for rectangles.
///
/// See [Tween] for a discussion on how to use interpolation objects.
///
/// See also:
///
/// * [FractionalOffsetGeometryTween], which interpolates between two
/// [FractionalOffsetGeometry] objects.
class FractionalOffsetTween extends Tween<FractionalOffset> {
/// Creates a fractional offset tween.
///
/// The [begin] and [end] properties may be null; the null value
/// is treated as meaning the top left corner.
/// is treated as meaning the center.
FractionalOffsetTween({ FractionalOffset begin, FractionalOffset end })
: super(begin: begin, end: end);
......@@ -23,3 +28,29 @@ class FractionalOffsetTween extends Tween<FractionalOffset> {
@override
FractionalOffset lerp(double t) => FractionalOffset.lerp(begin, end, t);
}
/// An interpolation between two [FractionalOffsetGeometry].
///
/// This class specializes the interpolation of [Tween<FractionalOffsetGeometry>]
/// to be appropriate for rectangles.
///
/// See [Tween] for a discussion on how to use interpolation objects.
///
/// See also:
///
/// * [FractionalOffsetTween], which interpolates between two
/// [FractionalOffset] objects.
class FractionalOffsetGeometryTween extends Tween<FractionalOffsetGeometry> {
/// Creates a fractional offset geometry tween.
///
/// The [begin] and [end] properties may be null; the null value
/// is treated as meaning the center.
FractionalOffsetGeometryTween({
FractionalOffsetGeometry begin,
FractionalOffsetGeometry end,
}) : super(begin: begin, end: end);
/// Returns the value this variable has at the given animation clock value.
@override
FractionalOffsetGeometry lerp(double t) => FractionalOffsetGeometry.lerp(begin, end, t);
}
......@@ -29,6 +29,7 @@ export 'package:flutter/rendering.dart' show
FlowDelegate,
FlowPaintingContext,
FractionalOffsetTween,
FractionalOffsetGeometryTween,
HitTestBehavior,
LayerLink,
MainAxisAlignment,
......
......@@ -61,6 +61,11 @@ class DecorationTween extends Tween<Decoration> {
/// [EdgeInsets.lerp].
///
/// See [Tween] for a discussion on how to use interpolation objects.
///
/// See also:
///
/// * [EdgeInsetsGeometryTween], which interpolates between two
/// [EdgeInsetsGeometry] objects.
class EdgeInsetsTween extends Tween<EdgeInsets> {
/// Creates an [EdgeInsets] tween.
///
......@@ -73,6 +78,28 @@ class EdgeInsetsTween extends Tween<EdgeInsets> {
EdgeInsets lerp(double t) => EdgeInsets.lerp(begin, end, t);
}
/// An interpolation between two [EdgeInsetsGeometry]s.
///
/// This class specializes the interpolation of [Tween<EdgeInsetsGeometry>] to
/// use [EdgeInsetsGeometry.lerp].
///
/// See [Tween] for a discussion on how to use interpolation objects.
///
/// See also:
///
/// * [EdgeInsetsTween], which interpolates between two [EdgeInsets] objects.
class EdgeInsetsGeometryTween extends Tween<EdgeInsetsGeometry> {
/// Creates an [EdgeInsetsGeometry] tween.
///
/// The [begin] and [end] properties may be null; the null value
/// is treated as an [EdgeInsetsGeometry] with no inset.
EdgeInsetsGeometryTween({ EdgeInsetsGeometry begin, EdgeInsetsGeometry end }) : super(begin: begin, end: end);
/// Returns the value this variable has at the given animation clock value.
@override
EdgeInsetsGeometry lerp(double t) => EdgeInsetsGeometry.lerp(begin, end, t);
}
/// An interpolation between two [BorderRadius]s.
///
/// This class specializes the interpolation of [Tween<BorderRadius>] to use
......@@ -358,11 +385,11 @@ class AnimatedContainer extends ImplicitlyAnimatedWidget {
/// constraints are unbounded, then the child will be shrink-wrapped instead.
///
/// Ignored if [child] is null.
final FractionalOffset alignment;
final FractionalOffsetGeometry alignment;
/// Empty space to inscribe inside the [decoration]. The [child], if any, is
/// placed inside this padding.
final EdgeInsets padding;
final EdgeInsetsGeometry padding;
/// The decoration to paint behind the [child].
///
......@@ -383,7 +410,7 @@ class AnimatedContainer extends ImplicitlyAnimatedWidget {
final BoxConstraints constraints;
/// Empty space to surround the [decoration] and [child].
final EdgeInsets margin;
final EdgeInsetsGeometry margin;
/// The transformation matrix to apply before painting the container.
final Matrix4 transform;
......@@ -394,33 +421,33 @@ class AnimatedContainer extends ImplicitlyAnimatedWidget {
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment, showName: false, defaultValue: null));
description.add(new DiagnosticsProperty<EdgeInsets>('padding', padding, defaultValue: null));
description.add(new DiagnosticsProperty<FractionalOffsetGeometry>('alignment', alignment, showName: false, defaultValue: null));
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
description.add(new DiagnosticsProperty<Decoration>('bg', decoration, defaultValue: null));
description.add(new DiagnosticsProperty<Decoration>('fg', foregroundDecoration, defaultValue: null));
description.add(new DiagnosticsProperty<BoxConstraints>('constraints', constraints, defaultValue: null, showName: false));
description.add(new DiagnosticsProperty<EdgeInsets>('margin', margin, defaultValue: null));
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin, defaultValue: null));
description.add(new ObjectFlagProperty<Matrix4>.has('transform', transform));
}
}
class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer> {
FractionalOffsetTween _alignment;
EdgeInsetsTween _padding;
FractionalOffsetGeometryTween _alignment;
EdgeInsetsGeometryTween _padding;
DecorationTween _decoration;
DecorationTween _foregroundDecoration;
BoxConstraintsTween _constraints;
EdgeInsetsTween _margin;
EdgeInsetsGeometryTween _margin;
Matrix4Tween _transform;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_alignment = visitor(_alignment, widget.alignment, (dynamic value) => new FractionalOffsetTween(begin: value));
_padding = visitor(_padding, widget.padding, (dynamic value) => new EdgeInsetsTween(begin: value));
_alignment = visitor(_alignment, widget.alignment, (dynamic value) => new FractionalOffsetGeometryTween(begin: value));
_padding = visitor(_padding, widget.padding, (dynamic value) => new EdgeInsetsGeometryTween(begin: value));
_decoration = visitor(_decoration, widget.decoration, (dynamic value) => new DecorationTween(begin: value));
_foregroundDecoration = visitor(_foregroundDecoration, widget.foregroundDecoration, (dynamic value) => new DecorationTween(begin: value));
_constraints = visitor(_constraints, widget.constraints, (dynamic value) => new BoxConstraintsTween(begin: value));
_margin = visitor(_margin, widget.margin, (dynamic value) => new EdgeInsetsTween(begin: value));
_margin = visitor(_margin, widget.margin, (dynamic value) => new EdgeInsetsGeometryTween(begin: value));
_transform = visitor(_transform, widget.transform, (dynamic value) => new Matrix4Tween(begin: value));
}
......@@ -441,12 +468,12 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer>
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<FractionalOffsetTween>('alignment', _alignment, showName: false, defaultValue: null));
description.add(new DiagnosticsProperty<EdgeInsetsTween>('padding', _padding, defaultValue: null));
description.add(new DiagnosticsProperty<FractionalOffsetGeometryTween>('alignment', _alignment, showName: false, defaultValue: null));
description.add(new DiagnosticsProperty<EdgeInsetsGeometryTween>('padding', _padding, defaultValue: null));
description.add(new DiagnosticsProperty<DecorationTween>('bg', _decoration, defaultValue: null));
description.add(new DiagnosticsProperty<DecorationTween>('fg', _foregroundDecoration, defaultValue: null));
description.add(new DiagnosticsProperty<BoxConstraintsTween>('constraints', _constraints, showName: false, defaultValue: null));
description.add(new DiagnosticsProperty<EdgeInsetsTween>('margin', _margin, defaultValue: null));
description.add(new DiagnosticsProperty<EdgeInsetsGeometryTween>('margin', _margin, defaultValue: null));
description.add(new ObjectFlagProperty<Matrix4Tween>.has('transform', _transform));
}
}
......
......@@ -73,15 +73,15 @@ void main() {
expect(FractionalOffsetGeometry.lerp(directional1, directional2, 0.5), const FractionalOffsetDirectional(0.125 + (2.0 - 0.125) / 2.0, 0.625 + (3.0 - 0.625) / 2.0));
expect(FractionalOffsetGeometry.lerp(directional2, directional2, 0.5), directional2);
expect(FractionalOffsetGeometry.lerp(directional1, normal2, 0.5).resolve(TextDirection.ltr), const FractionalOffset(1.0 + 1.0 / 16.0, 0.625 + (3.0 - 0.625) / 2.0));
expect(FractionalOffsetGeometry.lerp(directional1, normal2, 0.5).resolve(TextDirection.rtl), const FractionalOffset(1.0 + 15.0 / 16.0, 0.625 + (3.0 - 0.625) / 2.0));
expect(FractionalOffsetGeometry.lerp(directional1, normal2, 0.5).resolve(TextDirection.ltr), const FractionalOffset(1.0 + 1.0 / 16.0, 0.625 + (3.0 - 0.625) / 2.0));
expect(FractionalOffsetGeometry.lerp(directional1, normal2, 0.5).resolve(TextDirection.rtl), new FractionalOffset(lerpDouble(0.875, 2.0, 0.5), 0.625 + (3.0 - 0.625) / 2.0));
expect(FractionalOffsetGeometry.lerp(directional1, mixed1, 0.5).resolve(TextDirection.ltr), new FractionalOffset(1.0 / 32.0 + 2.5 / 16.0, lerpDouble(0.625, 0.5625 + 0.6875, 0.5)));
expect(FractionalOffsetGeometry.lerp(directional1, mixed1, 0.5).resolve(TextDirection.rtl), new FractionalOffset(1.0 / 32.0 + 1.0 - 2.5 / 16.0, lerpDouble(0.625, 0.5625 + 0.6875, 0.5)));
expect(FractionalOffsetGeometry.lerp(mixed1, mixed2, 0.5).resolve(TextDirection.ltr), new FractionalOffset(3.0 + 5.0 / 8.0, lerpDouble(0.5625 + 0.6875, 6.0, 0.5)));
expect(FractionalOffsetGeometry.lerp(mixed1, mixed2, 0.5).resolve(TextDirection.rtl), new FractionalOffset(2.0 - 41.0 / 16.0, lerpDouble(0.5625 + 0.6875, 6.0, 0.5)));
expect(FractionalOffsetGeometry.lerp(normal1, normal2, 0.5), const FractionalOffset(0.25 + (2.0 - 0.25) / 2.0, 0.875 + (3.0 - 0.875) / 2.0));
expect(FractionalOffsetGeometry.lerp(normal1, mixed1, 0.5).resolve(TextDirection.ltr), new FractionalOffset(lerpDouble(0.25, 0.0625, 0.5) + lerpDouble(0.0, 0.1875, 0.5), lerpDouble(0.875, 0.5625 + 0.6875, 0.5)));
expect(FractionalOffsetGeometry.lerp(normal1, mixed1, 0.5).resolve(TextDirection.rtl), new FractionalOffset(lerpDouble(0.25, 0.0625, 0.5) + 1.0 - lerpDouble(0.0, 0.1875, 0.5), lerpDouble(0.875, 0.5625 + 0.6875, 0.5)));
expect(FractionalOffsetGeometry.lerp(normal1, mixed1, 0.5).resolve(TextDirection.rtl), new FractionalOffset(lerpDouble(0.25, 0.0625 + 0.8125, 0.5), lerpDouble(0.875, 0.5625 + 0.6875, 0.5)));
expect(FractionalOffsetGeometry.lerp(null, mixed1, 0.5).resolve(TextDirection.ltr), FractionalOffsetGeometry.lerp(FractionalOffset.center, mixed1, 0.5).resolve(TextDirection.ltr));
expect(FractionalOffsetGeometry.lerp(mixed2, null, 0.25).resolve(TextDirection.ltr), FractionalOffsetGeometry.lerp(FractionalOffset.center, mixed2, 0.75).resolve(TextDirection.ltr));
expect(FractionalOffsetGeometry.lerp(directional1, null, 1.0), FractionalOffsetDirectional.center);
......@@ -98,6 +98,35 @@ void main() {
expect(FractionalOffsetGeometry.lerp(mixed1, mixed2, 0.25), mixed3);
});
test('lerp commutes with resolve', () {
final List<FractionalOffsetGeometry> offsets = <FractionalOffsetGeometry>[
const FractionalOffset(-1.0, 0.65),
const FractionalOffsetDirectional(-1.0, 0.45),
const FractionalOffsetDirectional(0.125, 0.625),
const FractionalOffset(0.25, 0.875),
const FractionalOffset(0.0625, 0.5625).add(const FractionalOffsetDirectional(0.1875, 0.6875)),
const FractionalOffsetDirectional(2.0, 3.0),
const FractionalOffset(2.0, 3.0),
const FractionalOffset(2.0, 3.0).add(const FractionalOffsetDirectional(5.0, 3.0)),
const FractionalOffset(10.0, 20.0).add(const FractionalOffsetDirectional(30.0, 50.0)),
const FractionalOffset(70.0, 110.0).add(const FractionalOffsetDirectional(130.0, 170.0)),
const FractionalOffset(25.0, 42.5).add(const FractionalOffsetDirectional(55.0, 80.0)),
];
final List<double> times = <double>[ 0.0, 0.25, 0.5, 0.75, 1.0 ];
for (TextDirection direction in TextDirection.values) {
for (FractionalOffsetGeometry a in offsets) {
for (FractionalOffsetGeometry b in offsets) {
for (double t in times) {
expect(FractionalOffsetGeometry.lerp(a, b, t).resolve(direction),
FractionalOffset.lerp(a.resolve(direction), b.resolve(direction), t));
}
}
}
}
});
test('FractionalOffsetGeometry add/subtract', () {
final FractionalOffsetGeometry directional = const FractionalOffsetDirectional(1.0, 2.0);
final FractionalOffsetGeometry normal = const FractionalOffset(3.0, 5.0);
......@@ -136,10 +165,10 @@ void main() {
test('FractionalOffsetGeometry toString', () {
expect(const FractionalOffset(1.0001, 2.0001).toString(), 'FractionalOffset(1.0, 2.0)');
expect(const FractionalOffset(0.0, 0.0).toString(), 'FractionalOffset.topLeft');
expect(const FractionalOffset(0.0, 1.0).add(const FractionalOffsetDirectional(1.0, 0.0)).toString(), 'FractionalOffsetDirectional.bottomEnd');
expect(const FractionalOffset(0.0, 1.0).add(const FractionalOffsetDirectional(1.0, 0.0)).toString(), 'FractionalOffset.bottomLeft in RTL or FractionalOffset.bottomRight in LTR');
expect(const FractionalOffset(0.0001, 0.0001).toString(), 'FractionalOffset(0.0, 0.0)');
expect(const FractionalOffset(0.0, 0.0).toString(), 'FractionalOffset.topLeft');
expect(const FractionalOffsetDirectional(0.0, 0.0).toString(), 'FractionalOffsetDirectional.topStart');
expect(const FractionalOffset(1.0, 1.0).add(const FractionalOffsetDirectional(1.0, 1.0)).toString(), 'FractionalOffset(1.0, 2.0) + FractionalOffsetDirectional(1.0, 0.0)');
expect(const FractionalOffset(1.0, 1.0).add(const FractionalOffsetDirectional(1.0, 1.0)).toString(), 'FractionalOffset(1.0, 2.0) in RTL or FractionalOffset(2.0, 2.0) in LTR');
});
}
......@@ -139,6 +139,90 @@ void main() {
expect(tester.binding.transientCallbackCount, 0);
});
testWidgets('AnimatedContainer padding visual-to-directional animation', (WidgetTester tester) async {
final Key target = new UniqueKey();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.rtl,
child: new AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.only(right: 50.0),
child: new SizedBox.expand(key: target),
),
),
);
expect(tester.getSize(find.byKey(target)), const Size(750.0, 600.0));
expect(tester.getTopRight(find.byKey(target)), const Offset(750.0, 0.0));
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.rtl,
child: new AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsetsDirectional.only(start: 100.0),
child: new SizedBox.expand(key: target),
),
),
);
expect(tester.getSize(find.byKey(target)), const Size(750.0, 600.0));
expect(tester.getTopRight(find.byKey(target)), const Offset(750.0, 0.0));
await tester.pump(const Duration(milliseconds: 100));
expect(tester.getSize(find.byKey(target)), const Size(725.0, 600.0));
expect(tester.getTopRight(find.byKey(target)), const Offset(725.0, 0.0));
await tester.pump(const Duration(milliseconds: 500));
expect(tester.getSize(find.byKey(target)), const Size(700.0, 600.0));
expect(tester.getTopRight(find.byKey(target)), const Offset(700.0, 0.0));
});
testWidgets('AnimatedContainer alignment visual-to-directional animation', (WidgetTester tester) async {
final Key target = new UniqueKey();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.rtl,
child: new AnimatedContainer(
duration: const Duration(milliseconds: 200),
alignment: FractionalOffset.topRight,
child: new SizedBox(key: target, width: 100.0, height: 200.0),
),
),
);
expect(tester.getSize(find.byKey(target)), const Size(100.0, 200.0));
expect(tester.getTopRight(find.byKey(target)), const Offset(800.0, 0.0));
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.rtl,
child: new AnimatedContainer(
duration: const Duration(milliseconds: 200),
alignment: FractionalOffsetDirectional.bottomStart,
child: new SizedBox(key: target, width: 100.0, height: 200.0),
),
),
);
expect(tester.getSize(find.byKey(target)), const Size(100.0, 200.0));
expect(tester.getTopRight(find.byKey(target)), const Offset(800.0, 0.0));
await tester.pump(const Duration(milliseconds: 100));
expect(tester.getSize(find.byKey(target)), const Size(100.0, 200.0));
expect(tester.getTopRight(find.byKey(target)), const Offset(800.0, 200.0));
await tester.pump(const Duration(milliseconds: 500));
expect(tester.getSize(find.byKey(target)), const Size(100.0, 200.0));
expect(tester.getTopRight(find.byKey(target)), const Offset(800.0, 400.0));
});
testWidgets('Animation rerun', (WidgetTester tester) async {
await tester.pumpWidget(
new Center(
......
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