Skip to content

Commit

Permalink
Added parallax effect for cupertino fullscreenDialog (#50180)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kavantix committed Mar 28, 2020
1 parent 9450007 commit 6c9071d
Show file tree
Hide file tree
Showing 2 changed files with 220 additions and 12 deletions.
54 changes: 42 additions & 12 deletions packages/flutter/lib/src/cupertino/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -300,21 +300,24 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
Animation<double> secondaryAnimation,
Widget child,
) {
// Check if the route has an animation that's currently participating
// in a back swipe gesture.
//
// In the middle of a back gesture drag, let the transition be linear to
// match finger motions.
final bool linearTransition = isPopGestureInProgress(route);
if (route.fullscreenDialog) {
return CupertinoFullscreenDialogTransition(
animation: animation,
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
child: child,
linearTransition: linearTransition,
);
} else {
return CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
// Check if the route has an animation that's currently participating
// in a back swipe gesture.
//
// In the middle of a back gesture drag, let the transition be linear to
// match finger motions.
linearTransition: isPopGestureInProgress(route),
linearTransition: linearTransition,
child: _CupertinoBackGestureDetector<T>(
enabledCallback: () => _isPopGestureEnabled<T>(route),
onStartPopGesture: () => _startPopGesture<T>(route),
Expand Down Expand Up @@ -344,7 +347,7 @@ class CupertinoPageTransition extends StatelessWidget {
/// when this screen is being pushed.
/// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
/// when another screen is being pushed on top of this one.
/// * `linearTransition` is whether to perform primary transition linearly.
/// * `linearTransition` is whether to perform the transitions linearly.
/// Used to precisely track back gesture drags.
CupertinoPageTransition({
Key key,
Expand Down Expand Up @@ -422,29 +425,56 @@ class CupertinoPageTransition extends StatelessWidget {
/// screen from the bottom.
class CupertinoFullscreenDialogTransition extends StatelessWidget {
/// Creates an iOS-style transition used for summoning fullscreen dialogs.
///
/// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0
/// when this screen is being pushed.
/// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
/// when another screen is being pushed on top of this one.
/// * `linearTransition` is whether to perform the secondary transition linearly.
/// Used to precisely track back gesture drags.
CupertinoFullscreenDialogTransition({
Key key,
@required Animation<double> animation,
@required Animation<double> primaryRouteAnimation,
@required Animation<double> secondaryRouteAnimation,
@required this.child,
@required bool linearTransition,
}) : _positionAnimation = CurvedAnimation(
parent: animation,
parent: primaryRouteAnimation,
curve: Curves.linearToEaseOut,
// The curve must be flipped so that the reverse animation doesn't play
// an ease-in curve, which iOS does not use.
reverseCurve: Curves.linearToEaseOut.flipped,
).drive(_kBottomUpTween),
_secondaryPositionAnimation =
(linearTransition
? secondaryRouteAnimation
: CurvedAnimation(
parent: secondaryRouteAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
)
).drive(_kMiddleLeftTween),
super(key: key);

final Animation<Offset> _positionAnimation;
// When this page is becoming covered by another page.
final Animation<Offset> _secondaryPositionAnimation;

/// The widget below this widget in the tree.
final Widget child;

@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
return SlideTransition(
position: _positionAnimation,
child: child,
position: _secondaryPositionAnimation,
textDirection: textDirection,
transformHitTests: false,
child: SlideTransition(
position: _positionAnimation,
child: child,
),
);
}
}
Expand Down
178 changes: 178 additions & 0 deletions packages/flutter/test/cupertino/route_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,184 @@ void main() {
expect(tester.getTopLeft(find.byType(Placeholder)).dy, closeTo(600.0, 0.1));
});

Future<void> testParallax(WidgetTester tester, {@required bool fromFullscreenDialog}) async {
await tester.pumpWidget(
CupertinoApp(
onGenerateRoute: (RouteSettings settings) => CupertinoPageRoute<void>(
fullscreenDialog: fromFullscreenDialog,
settings: settings,
builder: (BuildContext context) {
return Column(
children: <Widget>[
const Placeholder(),
CupertinoButton(
child: const Text('Button'),
onPressed: () {
Navigator.push<void>(context, CupertinoPageRoute<void>(
builder: (BuildContext context) {
return CupertinoButton(
child: const Text('Close'),
onPressed: () {
Navigator.pop<void>(context);
},
);
},
));
},
),
],
);
}
),
),
);

// Enter animation.
await tester.tap(find.text('Button'));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(0.0, 0.1));
await tester.pump();

// We use a higher number of intervals since the animation has to scale the
// entire screen.

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-70.0, 1.0));

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-137.0, 1.0));

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-192.0, 1.0));

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-227.0, 1.0));

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-246.0, 1.0));

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-255.0, 1.0));

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-260.0, 1.0));

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-264.0, 1.0));

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-266.0, 1.0));

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-267.0, 1.0));

// Exit animation
await tester.tap(find.text('Button'));
await tester.pump();

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-198.0, 1.0));

await tester.pump(const Duration(milliseconds: 360));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(-0.0, 1.0));
}

testWidgets('CupertinoPageRoute has parallax when non fullscreenDialog route is pushed on top', (WidgetTester tester) async {
await testParallax(tester, fromFullscreenDialog: false);
});

testWidgets('FullscreenDialog CupertinoPageRoute has parallax when non fullscreenDialog route is pushed on top', (WidgetTester tester) async {
await testParallax(tester, fromFullscreenDialog: true);
});

Future<void> testNoParallax(WidgetTester tester, {@required bool fromFullscreenDialog}) async{
await tester.pumpWidget(
CupertinoApp(
onGenerateRoute: (RouteSettings settings) => CupertinoPageRoute<void>(
fullscreenDialog: fromFullscreenDialog,
builder: (BuildContext context) {
return Column(
children: <Widget>[
const Placeholder(),
CupertinoButton(
child: const Text('Button'),
onPressed: () {
Navigator.push<void>(context, CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (BuildContext context) {
return CupertinoButton(
child: const Text('Close'),
onPressed: () {
Navigator.pop<void>(context);
},
);
},
));
},
),
],
);
}
),
),
);

// Enter animation.
await tester.tap(find.text('Button'));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, closeTo(0.0, 0.1));
await tester.pump();

// We use a higher number of intervals since the animation has to scale the
// entire screen.

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

// Exit animation
await tester.tap(find.text('Button'));
await tester.pump();

await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);

await tester.pump(const Duration(milliseconds: 360));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);
}

testWidgets('CupertinoPageRoute has no parallax when fullscreenDialog route is pushed on top', (WidgetTester tester) async {
await testNoParallax(tester, fromFullscreenDialog: false);
});

testWidgets('FullscreenDialog CupertinoPageRoute has no parallax when fullscreenDialog route is pushed on top', (WidgetTester tester) async {
await testNoParallax(tester, fromFullscreenDialog: true);
});

testWidgets('Animated push/pop is not linear', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
Expand Down

0 comments on commit 6c9071d

Please sign in to comment.