diff --git a/packages/flutter/lib/src/cupertino/page.dart b/packages/flutter/lib/src/cupertino/page.dart index e1a16bd5e3..6392a66fb3 100644 --- a/packages/flutter/lib/src/cupertino/page.dart +++ b/packages/flutter/lib/src/cupertino/page.dart @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; const double _kMinFlingVelocity = 1.0; // screen width per second. -const Color _kBackgroundColor = const Color(0xFFEFEFF4); // iOS 10 background color. // Fractional offset from offscreen to the right to fully on screen. final FractionalOffsetTween _kRightMiddleTween = new FractionalOffsetTween( @@ -26,6 +25,20 @@ final FractionalOffsetTween _kBottomUpTween = new FractionalOffsetTween( end: FractionalOffset.topLeft, ); +// BoxDecoration from no shadow to page shadow mimicking iOS page transitions. +final DecorationTween _kShadowTween = new DecorationTween( + begin: BoxDecoration.none, // No shadow initially. + end: const BoxDecoration( + boxShadow: const [ + const BoxShadow( + blurRadius: 10.0, + spreadRadius: 4.0, + color: const Color(0x38000000), + ), + ], + ), +); + /// Provides the native iOS page transition animation. /// /// The page slides in from the right and exits in reverse. It also shifts to the left in @@ -34,51 +47,50 @@ class CupertinoPageTransition extends StatelessWidget { CupertinoPageTransition({ Key key, // Linear route animation from 0.0 to 1.0 when this screen is being pushed. - @required Animation incomingRouteAnimation, + @required Animation primaryRouteAnimation, // Linear route animation from 0.0 to 1.0 when another screen is being pushed on top of this // one. - @required Animation outgoingRouteAnimation, + @required Animation secondaryRouteAnimation, @required this.child, - // Perform incoming transition linearly. Use to precisely track back gesture drags. + // Perform primary transition linearly. Use to precisely track back gesture drags. bool linearTransition, }) : - _incomingPositionAnimation = linearTransition - ? _kRightMiddleTween.animate(incomingRouteAnimation) + _primaryPositionAnimation = linearTransition + ? _kRightMiddleTween.animate(primaryRouteAnimation) : _kRightMiddleTween.animate( new CurvedAnimation( - parent: incomingRouteAnimation, + parent: primaryRouteAnimation, curve: Curves.easeOut, reverseCurve: Curves.easeIn, ) ), - _outgoingPositionAnimation = _kMiddleLeftTween.animate( + _secondaryPositionAnimation = _kMiddleLeftTween.animate( new CurvedAnimation( - parent: outgoingRouteAnimation, + parent: secondaryRouteAnimation, curve: Curves.easeOut, reverseCurve: Curves.easeIn, ) ), + _primaryShadowAnimation = _kShadowTween.animate(primaryRouteAnimation), super(key: key); // When this page is coming in to cover another page. - final Animation _incomingPositionAnimation; + final Animation _primaryPositionAnimation; // When this page is becoming covered by another page. - final Animation _outgoingPositionAnimation; + final Animation _secondaryPositionAnimation; + final Animation _primaryShadowAnimation; final Widget child; - @override Widget build(BuildContext context) { // TODO(ianh): tell the transform to be un-transformed for hit testing // but not while being controlled by a gesture. return new SlideTransition( - position: _outgoingPositionAnimation, + position: _secondaryPositionAnimation, child: new SlideTransition( - position: _incomingPositionAnimation, - child: new PhysicalModel( - shape: BoxShape.rectangle, - color: _kBackgroundColor, - elevation: 32, + position: _primaryPositionAnimation, + child: new DecoratedBoxTransition( + decoration: _primaryShadowAnimation, child: child, ), ), diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index f72a463bf7..18df85fbb1 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -174,8 +174,8 @@ class MaterialPageRoute extends PageRoute { ); else return new CupertinoPageTransition( - incomingRouteAnimation: animation, - outgoingRouteAnimation: secondaryAnimation, + primaryRouteAnimation: animation, + secondaryRouteAnimation: secondaryAnimation, child: child, // In the middle of a back gesture drag, let the transition be linear to match finger // motions. diff --git a/packages/flutter/lib/src/painting/box_painter.dart b/packages/flutter/lib/src/painting/box_painter.dart index b9ccc110ee..1507f22374 100644 --- a/packages/flutter/lib/src/painting/box_painter.dart +++ b/packages/flutter/lib/src/painting/box_painter.dart @@ -1078,6 +1078,9 @@ class BoxDecoration extends Decoration { this.shape: BoxShape.rectangle }); + /// A [BoxDecoration] with no decorating properties. + static const BoxDecoration none = const BoxDecoration(); + @override bool debugAssertIsValid() { assert(shape != BoxShape.circle || diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index 21907f67e3..a231f2badf 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -63,13 +64,17 @@ void main() { }); testWidgets('test iOS page transition', (WidgetTester tester) async { + final Key page2Key = new UniqueKey(); await tester.pumpWidget( new MaterialApp( theme: new ThemeData(platform: TargetPlatform.iOS), home: new Material(child: const Text('Page 1')), routes: { '/next': (BuildContext context) { - return new Material(child: const Text('Page 2')); + return new Material( + key: page2Key, + child: const Text('Page 2'), + ); }, }, ) @@ -79,10 +84,13 @@ void main() { tester.state(find.byType(Navigator)).pushNamed('/next'); await tester.pump(); - await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 150)); Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2')); + DecoratedBox box = tester.element(find.byKey(page2Key)).ancestorWidgetOfExactType(DecoratedBox); + BoxDecoration decoration = box.decoration; + BoxShadow shadow = decoration.boxShadow[0]; // Page 1 is moving to the left. expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true); @@ -92,6 +100,9 @@ void main() { expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); // Page 2 is coming in from the right. expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); + // The shadow should be exactly half its maximum extent. + expect(shadow.blurRadius, 5.0); + expect(shadow.spreadRadius, 2.0); await tester.pumpAndSettle(); @@ -102,6 +113,9 @@ void main() { tester.state(find.byType(Navigator)).pop(); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); + box = tester.element(find.byKey(page2Key)).ancestorWidgetOfExactType(DecoratedBox); + decoration = box.decoration; + shadow = decoration.boxShadow[0]; widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); widget2TopLeft = tester.getTopLeft(find.text('Page 2')); @@ -114,6 +128,9 @@ void main() { expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); // Page 2 is leaving towards the right. expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); + // The shadow should be exactly 2/3 of its maximum extent. + expect(shadow.blurRadius, closeTo(6.6, 0.1)); + expect(shadow.spreadRadius, closeTo(2.6, 0.1)); await tester.pumpAndSettle(); diff --git a/packages/flutter/test/widgets/transitions_test.dart b/packages/flutter/test/widgets/transitions_test.dart index 686173c9f5..5e8f89c098 100644 --- a/packages/flutter/test/widgets/transitions_test.dart +++ b/packages/flutter/test/widgets/transitions_test.dart @@ -15,7 +15,7 @@ void main() { expect(widget.toString, isNot(throwsException)); }); - group('ContainerTransition test', () { + group('DecoratedBoxTransition test', () { final DecorationTween decorationTween = new DecorationTween( begin: new BoxDecoration( backgroundColor: const Color(0xFFFFFFFF),