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
This commit is contained in:
xster 2017-04-18 11:27:21 -07:00 committed by GitHub
parent 8bf97cc42a
commit 8ed175411b
5 changed files with 55 additions and 23 deletions

View file

@ -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 <BoxShadow>[
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<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
// one.
@required Animation<double> outgoingRouteAnimation,
@required Animation<double> 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<FractionalOffset> _incomingPositionAnimation;
final Animation<FractionalOffset> _primaryPositionAnimation;
// When this page is becoming covered by another page.
final Animation<FractionalOffset> _outgoingPositionAnimation;
final Animation<FractionalOffset> _secondaryPositionAnimation;
final Animation<Decoration> _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,
),
),

View file

@ -174,8 +174,8 @@ class MaterialPageRoute<T> extends PageRoute<T> {
);
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.

View file

@ -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 ||

View file

@ -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: <String, WidgetBuilder>{
'/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<NavigatorState>(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<NavigatorState>(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();

View file

@ -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),