Commit 8ed17541 authored by xster's avatar xster Committed by GitHub

Make Cupertino page transition elevation animated too (#9166)

* Make Cupertino page transition elevation animated too

* Rename and change physical model to a decorated box

* Tests

* Add a comment

* still need to handle null in the tween somewhere

* nits

* Tweens evaluate to the actual begin/end instances. Let them be non-null

* Rename no decoration to none
parent 8bf97cc4
...@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; ...@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
const double _kMinFlingVelocity = 1.0; // screen width per second. 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. // Fractional offset from offscreen to the right to fully on screen.
final FractionalOffsetTween _kRightMiddleTween = new FractionalOffsetTween( final FractionalOffsetTween _kRightMiddleTween = new FractionalOffsetTween(
...@@ -26,6 +25,20 @@ final FractionalOffsetTween _kBottomUpTween = new FractionalOffsetTween( ...@@ -26,6 +25,20 @@ final FractionalOffsetTween _kBottomUpTween = new FractionalOffsetTween(
end: FractionalOffset.topLeft, 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 <BoxShadow>[
const BoxShadow(
blurRadius: 10.0,
spreadRadius: 4.0,
color: const Color(0x38000000),
),
],
),
);
/// Provides the native iOS page transition animation. /// 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 /// 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 { ...@@ -34,51 +47,50 @@ class CupertinoPageTransition extends StatelessWidget {
CupertinoPageTransition({ CupertinoPageTransition({
Key key, Key key,
// Linear route animation from 0.0 to 1.0 when this screen is being pushed. // Linear route animation from 0.0 to 1.0 when this screen is being pushed.
@required Animation<double> incomingRouteAnimation, @required Animation<double> primaryRouteAnimation,
// Linear route animation from 0.0 to 1.0 when another screen is being pushed on top of this // Linear route animation from 0.0 to 1.0 when another screen is being pushed on top of this
// one. // one.
@required Animation<double> outgoingRouteAnimation, @required Animation<double> secondaryRouteAnimation,
@required this.child, @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, bool linearTransition,
}) : }) :
_incomingPositionAnimation = linearTransition _primaryPositionAnimation = linearTransition
? _kRightMiddleTween.animate(incomingRouteAnimation) ? _kRightMiddleTween.animate(primaryRouteAnimation)
: _kRightMiddleTween.animate( : _kRightMiddleTween.animate(
new CurvedAnimation( new CurvedAnimation(
parent: incomingRouteAnimation, parent: primaryRouteAnimation,
curve: Curves.easeOut, curve: Curves.easeOut,
reverseCurve: Curves.easeIn, reverseCurve: Curves.easeIn,
) )
), ),
_outgoingPositionAnimation = _kMiddleLeftTween.animate( _secondaryPositionAnimation = _kMiddleLeftTween.animate(
new CurvedAnimation( new CurvedAnimation(
parent: outgoingRouteAnimation, parent: secondaryRouteAnimation,
curve: Curves.easeOut, curve: Curves.easeOut,
reverseCurve: Curves.easeIn, reverseCurve: Curves.easeIn,
) )
), ),
_primaryShadowAnimation = _kShadowTween.animate(primaryRouteAnimation),
super(key: key); super(key: key);
// When this page is coming in to cover another page. // When this page is coming in to cover another page.
final Animation<FractionalOffset> _incomingPositionAnimation; final Animation<FractionalOffset> _primaryPositionAnimation;
// When this page is becoming covered by another page. // When this page is becoming covered by another page.
final Animation<FractionalOffset> _outgoingPositionAnimation; final Animation<FractionalOffset> _secondaryPositionAnimation;
final Animation<Decoration> _primaryShadowAnimation;
final Widget child; final Widget child;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// TODO(ianh): tell the transform to be un-transformed for hit testing // TODO(ianh): tell the transform to be un-transformed for hit testing
// but not while being controlled by a gesture. // but not while being controlled by a gesture.
return new SlideTransition( return new SlideTransition(
position: _outgoingPositionAnimation, position: _secondaryPositionAnimation,
child: new SlideTransition( child: new SlideTransition(
position: _incomingPositionAnimation, position: _primaryPositionAnimation,
child: new PhysicalModel( child: new DecoratedBoxTransition(
shape: BoxShape.rectangle, decoration: _primaryShadowAnimation,
color: _kBackgroundColor,
elevation: 32,
child: child, child: child,
), ),
), ),
......
...@@ -174,8 +174,8 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -174,8 +174,8 @@ class MaterialPageRoute<T> extends PageRoute<T> {
); );
else else
return new CupertinoPageTransition( return new CupertinoPageTransition(
incomingRouteAnimation: animation, primaryRouteAnimation: animation,
outgoingRouteAnimation: secondaryAnimation, secondaryRouteAnimation: secondaryAnimation,
child: child, child: child,
// In the middle of a back gesture drag, let the transition be linear to match finger // In the middle of a back gesture drag, let the transition be linear to match finger
// motions. // motions.
......
...@@ -1078,6 +1078,9 @@ class BoxDecoration extends Decoration { ...@@ -1078,6 +1078,9 @@ class BoxDecoration extends Decoration {
this.shape: BoxShape.rectangle this.shape: BoxShape.rectangle
}); });
/// A [BoxDecoration] with no decorating properties.
static const BoxDecoration none = const BoxDecoration();
@override @override
bool debugAssertIsValid() { bool debugAssertIsValid() {
assert(shape != BoxShape.circle || assert(shape != BoxShape.circle ||
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
...@@ -63,13 +64,17 @@ void main() { ...@@ -63,13 +64,17 @@ void main() {
}); });
testWidgets('test iOS page transition', (WidgetTester tester) async { testWidgets('test iOS page transition', (WidgetTester tester) async {
final Key page2Key = new UniqueKey();
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS), theme: new ThemeData(platform: TargetPlatform.iOS),
home: new Material(child: const Text('Page 1')), home: new Material(child: const Text('Page 1')),
routes: <String, WidgetBuilder>{ routes: <String, WidgetBuilder>{
'/next': (BuildContext context) { '/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() { ...@@ -79,10 +84,13 @@ void main() {
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next'); tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump(); 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 widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2')); 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. // Page 1 is moving to the left.
expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true); expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true);
...@@ -92,6 +100,9 @@ void main() { ...@@ -92,6 +100,9 @@ void main() {
expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
// Page 2 is coming in from the right. // Page 2 is coming in from the right.
expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); 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(); await tester.pumpAndSettle();
...@@ -102,6 +113,9 @@ void main() { ...@@ -102,6 +113,9 @@ void main() {
tester.state<NavigatorState>(find.byType(Navigator)).pop(); tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 100)); 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')); widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
widget2TopLeft = tester.getTopLeft(find.text('Page 2')); widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
...@@ -114,6 +128,9 @@ void main() { ...@@ -114,6 +128,9 @@ void main() {
expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
// Page 2 is leaving towards the right. // Page 2 is leaving towards the right.
expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); 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(); await tester.pumpAndSettle();
......
...@@ -15,7 +15,7 @@ void main() { ...@@ -15,7 +15,7 @@ void main() {
expect(widget.toString, isNot(throwsException)); expect(widget.toString, isNot(throwsException));
}); });
group('ContainerTransition test', () { group('DecoratedBoxTransition test', () {
final DecorationTween decorationTween = new DecorationTween( final DecorationTween decorationTween = new DecorationTween(
begin: new BoxDecoration( begin: new BoxDecoration(
backgroundColor: const Color(0xFFFFFFFF), backgroundColor: const Color(0xFFFFFFFF),
......
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