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:
parent
8bf97cc42a
commit
8ed175411b
@ -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(
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
);
|
);
|
||||||
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 {
|
|||||||
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 @@
|
|||||||
// 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() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user