Add a new MatrixTransition and refactor ScaleTransition and RotationT… (#131084)
â¦ransition to derive from it. The MatrixTransition class uses a callback to handle any value => Matrix animation. The alignment and filterQuality logic that was in ScaleTransition and RotationTransition is now factored in MatrixTransition. The ScaleTransition.scale and RotationTransition.turns getters had to be kept because they're still referenced in https://github.com/flutter/packages/tree/main/packages/animations, and https://github.com/flutter/packages/flutter/test/. I plan to remove the references there, once this PR is generally available, and then remove the getters here. A RotationTransition test was updated to use matrixMoreOrLessEquals because using Matrix4.rotationZ doesn't have the special cases Transform.Rotation had, and zeroes in matrix weren't exactly zeroes. fixes #130946
This commit is contained in:
parent
1cfba2620a
commit
58019b3428
@ -225,6 +225,90 @@ class SlideTransition extends AnimatedWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature for the callback to [MatrixTransition.onTransform].
|
||||
///
|
||||
/// Computes a [Matrix4] to be used in the [MatrixTransition] transformed widget
|
||||
/// from the [MatrixTransition.animation] value.
|
||||
typedef TransformCallback = Matrix4 Function(double animationValue);
|
||||
|
||||
/// Animates the [Matrix4] of a transformed widget.
|
||||
///
|
||||
/// The [onTransform] callback computes a [Matrix4] from the animated value, it
|
||||
/// is called every time the [animation] changes its value.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ScaleTransition], which animates the scale of a widget, by providing a
|
||||
/// matrix which scales along the X and Y axis.
|
||||
/// * [RotationTransition], which animates the rotation of a widget, by
|
||||
/// providing a matrix which rotates along the Z axis.
|
||||
class MatrixTransition extends AnimatedWidget {
|
||||
/// Creates a matrix transition.
|
||||
///
|
||||
/// The [alignment] argument defaults to [Alignment.center].
|
||||
const MatrixTransition({
|
||||
super.key,
|
||||
required Animation<double> animation,
|
||||
required this.onTransform,
|
||||
this.alignment = Alignment.center,
|
||||
this.filterQuality,
|
||||
this.child,
|
||||
}) : super(listenable: animation);
|
||||
|
||||
/// The callback to compute a [Matrix4] from the [animation]. It's called
|
||||
/// every time [animation] changes its value.
|
||||
final TransformCallback onTransform;
|
||||
|
||||
/// The animation that controls the matrix of the child.
|
||||
///
|
||||
/// The matrix will be computed from the animation with the [onTransform]
|
||||
/// callback.
|
||||
Animation<double> get animation => listenable as Animation<double>;
|
||||
|
||||
/// The alignment of the origin of the coordinate system in which the
|
||||
/// transform takes place, relative to the size of the box.
|
||||
///
|
||||
/// For example, to set the origin of the transform to bottom middle, you can
|
||||
/// use an alignment of (0.0, 1.0).
|
||||
final Alignment alignment;
|
||||
|
||||
/// The filter quality with which to apply the transform as a bitmap operation.
|
||||
///
|
||||
/// When the animation is stopped (either in [AnimationStatus.dismissed] or
|
||||
/// [AnimationStatus.completed]), the filter quality argument will be ignored.
|
||||
///
|
||||
/// {@macro flutter.widgets.Transform.optional.FilterQuality}
|
||||
final FilterQuality? filterQuality;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// The ImageFilter layer created by setting filterQuality will introduce
|
||||
// a saveLayer call. This is usually worthwhile when animating the layer,
|
||||
// but leaving it in the layer tree before the animation has started or after
|
||||
// it has finished significantly hurts performance.
|
||||
final bool useFilterQuality;
|
||||
switch (animation.status) {
|
||||
case AnimationStatus.dismissed:
|
||||
case AnimationStatus.completed:
|
||||
useFilterQuality = false;
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
useFilterQuality = true;
|
||||
}
|
||||
return Transform(
|
||||
transform: onTransform(animation.value),
|
||||
alignment: alignment,
|
||||
filterQuality: useFilterQuality ? filterQuality : null,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates the scale of a transformed widget.
|
||||
///
|
||||
/// Here's an illustration of the [ScaleTransition] widget, with it's [alignment]
|
||||
@ -246,67 +330,26 @@ class SlideTransition extends AnimatedWidget {
|
||||
/// position based on the value of a rectangle relative to a bounding box.
|
||||
/// * [SizeTransition], a widget that animates its own size and clips and
|
||||
/// aligns its child.
|
||||
class ScaleTransition extends AnimatedWidget {
|
||||
class ScaleTransition extends MatrixTransition {
|
||||
/// Creates a scale transition.
|
||||
///
|
||||
/// The [scale] argument must not be null. The [alignment] argument defaults
|
||||
/// to [Alignment.center].
|
||||
/// The [alignment] argument defaults to [Alignment.center].
|
||||
const ScaleTransition({
|
||||
super.key,
|
||||
required Animation<double> scale,
|
||||
this.alignment = Alignment.center,
|
||||
this.filterQuality,
|
||||
this.child,
|
||||
}) : super(listenable: scale);
|
||||
super.alignment = Alignment.center,
|
||||
super.filterQuality,
|
||||
super.child,
|
||||
}) : super(animation: scale, onTransform: _handleScaleMatrix);
|
||||
|
||||
/// The animation that controls the scale of the child.
|
||||
Animation<double> get scale => animation;
|
||||
|
||||
/// The callback that controls the scale of the child.
|
||||
///
|
||||
/// If the current value of the scale animation is v, the child will be
|
||||
/// If the current value of the animation is v, the child will be
|
||||
/// painted v times its normal size.
|
||||
Animation<double> get scale => listenable as Animation<double>;
|
||||
|
||||
/// The alignment of the origin of the coordinate system in which the scale
|
||||
/// takes place, relative to the size of the box.
|
||||
///
|
||||
/// For example, to set the origin of the scale to bottom middle, you can use
|
||||
/// an alignment of (0.0, 1.0).
|
||||
final Alignment alignment;
|
||||
|
||||
/// The filter quality with which to apply the transform as a bitmap operation.
|
||||
///
|
||||
/// When the animation is stopped (either in [AnimationStatus.dismissed] or
|
||||
/// [AnimationStatus.completed]), the filter quality argument will be ignored.
|
||||
///
|
||||
/// {@macro flutter.widgets.Transform.optional.FilterQuality}
|
||||
final FilterQuality? filterQuality;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// The ImageFilter layer created by setting filterQuality will introduce
|
||||
// a saveLayer call. This is usually worthwhile when animating the layer,
|
||||
// but leaving it in the layer tree before the animation has started or after
|
||||
// it has finished significantly hurts performance.
|
||||
final bool useFilterQuality;
|
||||
switch (scale.status) {
|
||||
case AnimationStatus.dismissed:
|
||||
case AnimationStatus.completed:
|
||||
useFilterQuality = false;
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
useFilterQuality = true;
|
||||
}
|
||||
return Transform.scale(
|
||||
scale: scale.value,
|
||||
alignment: alignment,
|
||||
filterQuality: useFilterQuality ? filterQuality : null,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
static Matrix4 _handleScaleMatrix(double value) => Matrix4.diagonal3Values(value, value, 1.0);
|
||||
}
|
||||
|
||||
/// Animates the rotation of a widget.
|
||||
@ -328,66 +371,26 @@ class ScaleTransition extends AnimatedWidget {
|
||||
/// widget.
|
||||
/// * [SizeTransition], a widget that animates its own size and clips and
|
||||
/// aligns its child.
|
||||
class RotationTransition extends AnimatedWidget {
|
||||
class RotationTransition extends MatrixTransition {
|
||||
/// Creates a rotation transition.
|
||||
///
|
||||
/// The [turns] argument must not be null.
|
||||
const RotationTransition({
|
||||
super.key,
|
||||
required Animation<double> turns,
|
||||
this.alignment = Alignment.center,
|
||||
this.filterQuality,
|
||||
this.child,
|
||||
}) : super(listenable: turns);
|
||||
super.alignment = Alignment.center,
|
||||
super.filterQuality,
|
||||
super.child,
|
||||
}) : super(animation: turns, onTransform: _handleTurnsMatrix);
|
||||
|
||||
/// The animation that controls the rotation of the child.
|
||||
///
|
||||
/// If the current value of the turns animation is v, the child will be
|
||||
/// rotated v * 2 * pi radians before being painted.
|
||||
Animation<double> get turns => listenable as Animation<double>;
|
||||
Animation<double> get turns => animation;
|
||||
|
||||
/// The alignment of the origin of the coordinate system around which the
|
||||
/// rotation occurs, relative to the size of the box.
|
||||
/// The callback that controls the rotation of the child.
|
||||
///
|
||||
/// For example, to set the origin of the rotation to top right corner, use
|
||||
/// an alignment of (1.0, -1.0) or use [Alignment.topRight]
|
||||
final Alignment alignment;
|
||||
|
||||
/// The filter quality with which to apply the transform as a bitmap operation.
|
||||
///
|
||||
/// When the animation is stopped (either in [AnimationStatus.dismissed] or
|
||||
/// [AnimationStatus.completed]), the filter quality argument will be ignored.
|
||||
///
|
||||
/// {@macro flutter.widgets.Transform.optional.FilterQuality}
|
||||
final FilterQuality? filterQuality;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// The ImageFilter layer created by setting filterQuality will introduce
|
||||
// a saveLayer call. This is usually worthwhile when animating the layer,
|
||||
// but leaving it in the layer tree before the animation has started or after
|
||||
// it has finished significantly hurts performance.
|
||||
final bool useFilterQuality;
|
||||
switch (turns.status) {
|
||||
case AnimationStatus.dismissed:
|
||||
case AnimationStatus.completed:
|
||||
useFilterQuality = false;
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
useFilterQuality = true;
|
||||
}
|
||||
return Transform.rotate(
|
||||
angle: turns.value * math.pi * 2.0,
|
||||
alignment: alignment,
|
||||
filterQuality: useFilterQuality ? filterQuality : null,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
/// If the current value of the animation is v, the child will be rotated
|
||||
/// v * 2 * pi radians before being painted.
|
||||
static Matrix4 _handleTurnsMatrix(double value) => Matrix4.rotationZ(value * math.pi * 2.0);
|
||||
}
|
||||
|
||||
/// Animates its own size and clips and aligns its child.
|
||||
|
@ -296,6 +296,67 @@ void main() {
|
||||
expect(actualPositionedBox.widthFactor, 1.0);
|
||||
});
|
||||
|
||||
testWidgets('MatrixTransition animates', (WidgetTester tester) async {
|
||||
final AnimationController controller = AnimationController(vsync: const TestVSync());
|
||||
final Widget widget = MatrixTransition(
|
||||
alignment: Alignment.topRight,
|
||||
onTransform: (double value) => Matrix4.translationValues(value, value, value),
|
||||
animation: controller,
|
||||
child: const Text(
|
||||
'Matrix',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(widget);
|
||||
Transform actualTransformedBox = tester.widget(find.byType(Transform));
|
||||
Matrix4 actualTransform = actualTransformedBox.transform;
|
||||
expect(actualTransform, equals(Matrix4.rotationZ(0.0)));
|
||||
|
||||
controller.value = 0.5;
|
||||
await tester.pump();
|
||||
actualTransformedBox = tester.widget(find.byType(Transform));
|
||||
actualTransform = actualTransformedBox.transform;
|
||||
expect(actualTransform, Matrix4.fromList(<double>[
|
||||
1.0, 0.0, 0.0, 0.5,
|
||||
0.0, 1.0, 0.0, 0.5,
|
||||
0.0, 0.0, 1.0, 0.5,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
])..transpose());
|
||||
|
||||
controller.value = 0.75;
|
||||
await tester.pump();
|
||||
actualTransformedBox = tester.widget(find.byType(Transform));
|
||||
actualTransform = actualTransformedBox.transform;
|
||||
expect(actualTransform, Matrix4.fromList(<double>[
|
||||
1.0, 0.0, 0.0, 0.75,
|
||||
0.0, 1.0, 0.0, 0.75,
|
||||
0.0, 0.0, 1.0, 0.75,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
])..transpose());
|
||||
});
|
||||
|
||||
testWidgets('MatrixTransition maintains chosen alignment during animation', (WidgetTester tester) async {
|
||||
final AnimationController controller = AnimationController(vsync: const TestVSync());
|
||||
final Widget widget = MatrixTransition(
|
||||
alignment: Alignment.topRight,
|
||||
onTransform: (double value) => Matrix4.identity(),
|
||||
animation: controller,
|
||||
child: const Text('Matrix', textDirection: TextDirection.ltr),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(widget);
|
||||
MatrixTransition actualTransformedBox = tester.widget(find.byType(MatrixTransition));
|
||||
Alignment actualAlignment = actualTransformedBox.alignment;
|
||||
expect(actualAlignment, Alignment.topRight);
|
||||
|
||||
controller.value = 0.5;
|
||||
await tester.pump();
|
||||
actualTransformedBox = tester.widget(find.byType(MatrixTransition));
|
||||
actualAlignment = actualTransformedBox.alignment;
|
||||
expect(actualAlignment, Alignment.topRight);
|
||||
});
|
||||
|
||||
testWidgets('RotationTransition animates', (WidgetTester tester) async {
|
||||
final AnimationController controller = AnimationController(vsync: const TestVSync());
|
||||
final Widget widget = RotationTransition(
|
||||
@ -316,23 +377,23 @@ void main() {
|
||||
await tester.pump();
|
||||
actualRotatedBox = tester.widget(find.byType(Transform));
|
||||
actualTurns = actualRotatedBox.transform;
|
||||
expect(actualTurns, Matrix4.fromList(<double>[
|
||||
expect(actualTurns, matrixMoreOrLessEquals(Matrix4.fromList(<double>[
|
||||
-1.0, 0.0, 0.0, 0.0,
|
||||
0.0, -1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
])..transpose());
|
||||
])..transpose()));
|
||||
|
||||
controller.value = 0.75;
|
||||
await tester.pump();
|
||||
actualRotatedBox = tester.widget(find.byType(Transform));
|
||||
actualTurns = actualRotatedBox.transform;
|
||||
expect(actualTurns, Matrix4.fromList(<double>[
|
||||
expect(actualTurns, matrixMoreOrLessEquals(Matrix4.fromList(<double>[
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
-1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
])..transpose());
|
||||
])..transpose()));
|
||||
});
|
||||
|
||||
testWidgets('RotationTransition maintains chosen alignment during animation', (WidgetTester tester) async {
|
||||
@ -457,6 +518,69 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('MatrixTransition', () {
|
||||
testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async {
|
||||
final AnimationController controller = AnimationController(vsync: const TestVSync());
|
||||
final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
|
||||
final Widget widget = Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MatrixTransition(
|
||||
animation: animation,
|
||||
onTransform: (double value) => Matrix4.identity(),
|
||||
filterQuality: FilterQuality.none,
|
||||
child: const Text('Matrix Transition'),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(widget);
|
||||
|
||||
// Validate that expensive layer is not left in tree before animation has started.
|
||||
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
|
||||
|
||||
controller.value = 0.25;
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
tester.layers,
|
||||
contains(isA<ImageFilterLayer>().having(
|
||||
(ImageFilterLayer layer) => layer.imageFilter.toString(),
|
||||
'image filter',
|
||||
startsWith('ImageFilter.matrix('),
|
||||
)),
|
||||
);
|
||||
|
||||
controller.value = 0.5;
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
tester.layers,
|
||||
contains(isA<ImageFilterLayer>().having(
|
||||
(ImageFilterLayer layer) => layer.imageFilter.toString(),
|
||||
'image filter',
|
||||
startsWith('ImageFilter.matrix('),
|
||||
)),
|
||||
);
|
||||
|
||||
controller.value = 0.75;
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
tester.layers,
|
||||
contains(isA<ImageFilterLayer>().having(
|
||||
(ImageFilterLayer layer) => layer.imageFilter.toString(),
|
||||
'image filter',
|
||||
startsWith('ImageFilter.matrix('),
|
||||
)),
|
||||
);
|
||||
|
||||
controller.value = 1;
|
||||
await tester.pump();
|
||||
|
||||
// Validate that expensive layer is not left in tree after animation has finished.
|
||||
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
|
||||
});
|
||||
});
|
||||
|
||||
group('ScaleTransition', () {
|
||||
testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async {
|
||||
final AnimationController controller = AnimationController(vsync: const TestVSync());
|
||||
|
Loading…
x
Reference in New Issue
Block a user