Unverified Commit 9e4c9640 authored by David Shuckerow's avatar David Shuckerow Committed by GitHub

Add animation tests for changes to the FloatingActionButtonLocation (#23677)

* Add more animation tests for rotation

* Test animation progress

* Progress on the tests

* Everything 'works'

* We don't need to guarantee the rotation of the fab as it exits.

* Remove print statement

* Cleanup extra lines

* Remove whitespace
parent 975ea595
......@@ -468,7 +468,7 @@ class _ScalingFabMotionAnimator extends FloatingActionButtonAnimator {
class _AnimationSwap<T> extends CompoundAnimation<T> {
/// Creates an [_AnimationSwap].
///
/// Both arguments must be non-null. Either can be an [AnimationMin] itself
/// Both arguments must be non-null. Either can be an [_AnimationSwap] itself
/// to combine multiple animations.
_AnimationSwap(Animation<T> first, Animation<T> next, this.parent, this.swapThreshold): super(first: first, next: next);
......
......@@ -522,7 +522,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
final bool newChildIsNull = widget.child == null;
if (oldChildIsNull == newChildIsNull && oldWidget.child?.key == widget.child?.key)
return;
if (oldWidget.fabMotionAnimator != widget.fabMotionAnimator || oldWidget.fabMoveAnimation != oldWidget.fabMoveAnimation) {
if (oldWidget.fabMotionAnimator != widget.fabMotionAnimator || oldWidget.fabMoveAnimation != widget.fabMoveAnimation) {
// Get the right scale and rotation animations to use for this widget.
_updateAnimations();
}
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -74,47 +76,168 @@ void main() {
});
testWidgets('interrupts in-progress animations without jumps', (WidgetTester tester) async {
final _GeometryListener geometryListener = _GeometryListener();
group('interrupts in-progress animations without jumps', () {
_GeometryListener geometryListener;
ScaffoldGeometry geometry;
_GeometryListenerState listenerState;
Size previousRect;
// The maximum amounts we expect the fab width and height to change during one step of a transition.
const double maxDeltaWidth = 12.0;
const double maxDeltaHeight = 12.0;
// Measure the delta in width and height of the fab, and check that it never grows
// by more than the expected maximum deltas.
void check() {
geometry = listenerState.cache.value;
final Size currentRect = geometry.floatingActionButtonArea?.size;
// Measure the delta in width and height of the rect, and check that it never grows
// by more than a safe amount.
if (previousRect != null && currentRect != null) {
final double deltaWidth = currentRect.width - previousRect.width;
final double deltaHeight = currentRect.height - previousRect.height;
expect(deltaWidth.abs(), lessThanOrEqualTo(maxDeltaWidth), reason: "The Floating Action Button's width should not change faster than $maxDeltaWidth per animation step.");
expect(deltaHeight.abs(), lessThanOrEqualTo(maxDeltaHeight), reason: "The Floating Action Button's width should not change faster than $maxDeltaHeight per animation step.");
Iterable<double> previousRotations;
// The maximum amounts we expect the fab width and height to change
// during one step of a transition.
const double maxDeltaWidth = 12.5;
const double maxDeltaHeight = 12.5;
// The maximum amounts we expect the fab icon to rotate during one step
// of a transition.
const double maxDeltaRotation = 0.09;
// We'll listen to the Scaffold's geometry for any 'jumps' to detect
// changes in the size and rotation of the fab.
void setupListener(WidgetTester tester) {
// Measure the delta in width and height of the fab, and check that it never grows
// by more than the expected maximum deltas.
void check() {
geometry = listenerState.cache.value;
final Size currentRect = geometry.floatingActionButtonArea?.size;
// Measure the delta in width and height of the rect, and check that
// it never grows by more than a safe amount.
if (previousRect != null && currentRect != null) {
final double deltaWidth = currentRect.width - previousRect.width;
final double deltaHeight = currentRect.height - previousRect.height;
expect(
deltaWidth.abs(),
lessThanOrEqualTo(maxDeltaWidth),
reason: "The Floating Action Button's width should not change "
'faster than $maxDeltaWidth per animation step.\n'
'Prevous rect: $previousRect, current rect: $currentRect',
);
expect(
deltaHeight.abs(),
lessThanOrEqualTo(maxDeltaHeight),
reason: "The Floating Action Button's width should not change "
'faster than $maxDeltaHeight per animation step.\n'
'Prevous rect: $previousRect, current rect: $currentRect',
);
}
previousRect = currentRect;
// Measure the delta in rotation.
// Check that it never grows by more than a safe amount.
//
// Note that there may be multiple transitions all active at
// the same time. We are concerned only with the closest one.
final Iterable<RotationTransition> rotationTransitions = tester.widgetList(
find.byType(RotationTransition),
);
final Iterable<double> currentRotations = rotationTransitions.map(
(RotationTransition t) => t.turns.value);
if (previousRotations != null && previousRotations.isNotEmpty
&& currentRotations != null && currentRotations.isNotEmpty
&& previousRect != null && currentRect != null) {
final List<double> deltas = <double>[];
for (double currentRotation in currentRotations) {
double minDelta;
for (double previousRotation in previousRotations) {
final double delta = (previousRotation - currentRotation).abs();
minDelta ??= delta;
minDelta = min(delta, minDelta);
}
deltas.add(minDelta);
}
if (deltas.where((double delta) => delta < maxDeltaRotation).isEmpty) {
fail("The Floating Action Button's rotation should not change "
'faster than $maxDeltaRotation per animation step.\n'
'Detected deltas were: $deltas\n'
'Previous values: $previousRotations, current values: $currentRotations\n'
'Prevous rect: $previousRect, current rect: $currentRect',);
}
}
previousRotations = currentRotations;
}
previousRect = currentRect;
}
// We'll listen to the Scaffold's geometry for any 'jumps' to a size of 1 to detect changes in the size and rotation of the fab.
// Creating a scaffold with the fab at endFloat
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
listenerState = tester.state(find.byType(_GeometryListener));
listenerState.geometryListenable.addListener(check);
// Moving the fab to centerFloat'
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
await tester.pumpAndSettle();
// Moving the fab to the top start after finishing the previous motion
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation(), listener: geometryListener));
listenerState = tester.state(find.byType(_GeometryListener));
listenerState.geometryListenable.addListener(check);
}
// Interrupting motion to move to the end float
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
await tester.pumpAndSettle();
setUp(() {
// We create the geometry listener here, but it can only be set up
// after it is pumped into the widget tree and a tester is
// available.
geometryListener = _GeometryListener();
geometry = null;
listenerState = null;
previousRect = null;
previousRotations = null;
});
testWidgets('moving the fab to centerFloat', (WidgetTester tester) async {
// Create a scaffold with the fab at endFloat
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
setupListener(tester);
// Move the fab to centerFloat'
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
await tester.pumpAndSettle();
});
testWidgets('interrupting motion towards the StartTop location.', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
setupListener(tester);
// Move the fab to the top start after creating the fab.
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation(), listener: geometryListener));
await tester.pump(kFloatingActionButtonSegue ~/ 2);
// Interrupt motion to move to the end float
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
await tester.pumpAndSettle();
});
testWidgets('interrupting entrance to remove the fab.', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(fab: null, location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
setupListener(tester);
// Animate the fab in.
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
await tester.pump(kFloatingActionButtonSegue ~/ 2);
// Remove the fab.
await tester.pumpWidget(
buildFrame(
fab: null,
location: FloatingActionButtonLocation.endFloat,
listener: geometryListener,
),
);
await tester.pumpAndSettle();
});
testWidgets('interrupting entrance of a new fab.', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
fab: null,
location: FloatingActionButtonLocation.endFloat,
listener: geometryListener,
),
);
setupListener(tester);
// Bring in a new fab.
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
await tester.pump(kFloatingActionButtonSegue ~/ 2);
// Interrupt motion to move the fab.
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endFloat,
listener: geometryListener,
),
);
await tester.pumpAndSettle();
});
});
});
......
......@@ -913,14 +913,25 @@ void main() {
await tester.pump(const Duration(milliseconds: 50));
ScaffoldGeometry geometry = listenerState.cache.value;
final Rect transitioningFabRect = geometry.floatingActionButtonArea;
final double transitioningRotation = tester.widget<RotationTransition>(
find.byType(RotationTransition),
).turns.value;
await tester.pump(const Duration(seconds: 3));
geometry = listenerState.cache.value;
final RenderBox floatingActionButtonBox = tester.renderObject(find.byKey(key));
final Rect fabRect = floatingActionButtonBox.localToGlobal(Offset.zero) & floatingActionButtonBox.size;
final double completedRotation = tester.widget<RotationTransition>(
find.byType(RotationTransition),
).turns.value;
expect(transitioningRotation, lessThan(1.0));
expect(completedRotation, equals(1.0));
expect(
geometry.floatingActionButtonArea,
fabRect
......
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