mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Allow mixing route transitions in one app. (#150031)
Fixes #33799 Allows for a route to inform the route below it in the navigation stack how to animate when the topmost route enters are leaves the stack. It does this by making a `DelegatedTransition` available for the previous route to look up and use. If available, the route lower in the stack will wrap it's transition builders with that delegated transition and use it instead of it's default secondary transition. This is what the sample code in this PR shows an app that is able to use both a Material zoom transition and a Cupertino slide transition in one app. It also includes a custom vertical transition. Every page animates off the screen in a way to match up with the incoming page's transition. When popped, the correct transitions play in reverse. https://github.com/user-attachments/assets/1fc910fa-8cde-4e05-898e-daad8ff4a697 The below video shows this logic making a pseudo iOS styled sheet transition. https://github.com/flutter/flutter/assets/58190796/207163d8-d87f-48b1-aad9-7e770d1d96c5 All existing page transitions in Flutter will be overwritten by the incoming route if a `delegatedTransition` is provided. This can be opted out of through `canTransitionTo` for a new route widget. Of Flutter's existing page transitions, this PR only adds a `DelegatedTransition` for the Zoom and Cupertino transitions. The other transitions possible in Material will get delegated transitions in a later PR.
This commit is contained in:
parent
b6bfa6a376
commit
d877d2875e
@ -0,0 +1,223 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// This sample demonstrates creating a custom page transition that is able to
|
||||
/// override the outgoing transition of the route behind it in the navigation
|
||||
/// stack using [DelegatedTransitionBuilder].
|
||||
|
||||
void main() {
|
||||
runApp(const FlexibleRouteTransitionsApp());
|
||||
}
|
||||
|
||||
class FlexibleRouteTransitionsApp extends StatelessWidget {
|
||||
const FlexibleRouteTransitionsApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Mixing Routes',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
// By default the zoom builder is used on all platforms but iOS. Normally
|
||||
// on iOS the default is the Cupertino sliding transition. Setting
|
||||
// it to use zoom on all platforms allows the example to show multiple
|
||||
// transitions in one app for all platforms.
|
||||
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
),
|
||||
home: const _MyHomePage(title: 'Zoom Page'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MyHomePage extends StatelessWidget {
|
||||
const _MyHomePage({required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(title),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
_VerticalTransitionPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const _MyHomePage(title: 'Crazy Vertical Page');
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Crazy Vertical Transition'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const _MyHomePage(title: 'Zoom Page');
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Zoom Transition'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final CupertinoPageRoute<void> route = CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const _MyHomePage(title: 'Cupertino Page');
|
||||
}
|
||||
);
|
||||
Navigator.of(context).push(route);
|
||||
},
|
||||
child: const Text('Cupertino Transition'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A PageRoute that applies a _VerticalPageTransition.
|
||||
class _VerticalTransitionPageRoute<T> extends PageRoute<T> {
|
||||
|
||||
_VerticalTransitionPageRoute({
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final WidgetBuilder builder;
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition => _VerticalPageTransition._delegatedTransitionBuilder;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => const Color(0x00000000);
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => false;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => 'Should be no visible barrier...';
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
@override
|
||||
bool get opaque => false;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 2000);
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||
return builder(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||
return _VerticalPageTransition(
|
||||
primaryRouteAnimation: animation,
|
||||
secondaryRouteAnimation: secondaryAnimation,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A page transition that slides off the screen vertically, and uses
|
||||
// delegatedTransition to ensure that the outgoing route slides with it.
|
||||
class _VerticalPageTransition extends StatelessWidget {
|
||||
_VerticalPageTransition({
|
||||
required Animation<double> primaryRouteAnimation,
|
||||
required this.secondaryRouteAnimation,
|
||||
required this.child,
|
||||
}) : _primaryPositionAnimation =
|
||||
CurvedAnimation(
|
||||
parent: primaryRouteAnimation,
|
||||
curve: _curve,
|
||||
reverseCurve: _curve,
|
||||
).drive(_kBottomUpTween),
|
||||
_secondaryPositionAnimation =
|
||||
CurvedAnimation(
|
||||
parent: secondaryRouteAnimation,
|
||||
curve: _curve,
|
||||
reverseCurve: _curve,
|
||||
)
|
||||
.drive(_kTopDownTween);
|
||||
|
||||
final Animation<Offset> _primaryPositionAnimation;
|
||||
|
||||
final Animation<Offset> _secondaryPositionAnimation;
|
||||
|
||||
final Animation<double> secondaryRouteAnimation;
|
||||
|
||||
final Widget child;
|
||||
|
||||
static const Curve _curve = Curves.decelerate;
|
||||
|
||||
static final Animatable<Offset> _kBottomUpTween = Tween<Offset>(
|
||||
begin: const Offset(0.0, 1.0),
|
||||
end: Offset.zero,
|
||||
);
|
||||
|
||||
static final Animatable<Offset> _kTopDownTween = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.0, -1.0),
|
||||
);
|
||||
|
||||
// When the _VerticalTransitionPageRoute animates onto or off of the navigation
|
||||
// stack, this transition is given to the route below it so that they animate in
|
||||
// sync.
|
||||
static Widget _delegatedTransitionBuilder(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
bool allowSnapshotting,
|
||||
Widget? child
|
||||
) {
|
||||
final Animatable<Offset> tween = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.0, -1.0),
|
||||
).chain(CurveTween(curve: _curve));
|
||||
|
||||
return SlideTransition(
|
||||
position: secondaryAnimation.drive(tween),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
return SlideTransition(
|
||||
position: _secondaryPositionAnimation,
|
||||
textDirection: textDirection,
|
||||
transformHitTests: false,
|
||||
child: SlideTransition(
|
||||
position: _primaryPositionAnimation,
|
||||
textDirection: textDirection,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: child,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,424 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// This sample demonstrates creating a custom page transition that is able to
|
||||
/// override the outgoing transition of the route behind it in the navigation
|
||||
/// stack using [DelegatedTransitionBuilder], using a [MaterialApp.router]
|
||||
/// pattern of navigation.
|
||||
|
||||
void main() {
|
||||
runApp(FlexibleRouteTransitionsApp());
|
||||
}
|
||||
|
||||
class FlexibleRouteTransitionsApp extends StatelessWidget {
|
||||
FlexibleRouteTransitionsApp({super.key});
|
||||
|
||||
final _MyRouteInformationParser _routeInformationParser = _MyRouteInformationParser();
|
||||
final MyRouterDelegate _routerDelegate = MyRouterDelegate();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'Mixing Routes',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
// By default the zoom builder is used on all platforms but iOS. Normally
|
||||
// on iOS the default is the Cupertino sliding transition. Setting
|
||||
// it to use zoom on all platforms allows the example to show multiple
|
||||
// transitions in one app for all platforms.
|
||||
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
),
|
||||
routeInformationParser: _routeInformationParser,
|
||||
routerDelegate: _routerDelegate,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MyRouteInformationParser extends RouteInformationParser<MyPageConfiguration> {
|
||||
@override
|
||||
SynchronousFuture<MyPageConfiguration> parseRouteInformation(RouteInformation routeInformation) {
|
||||
return SynchronousFuture<MyPageConfiguration>(MyPageConfiguration.values.firstWhere((MyPageConfiguration pageConfiguration) {
|
||||
return pageConfiguration.uriString == routeInformation.uri.toString();
|
||||
},
|
||||
orElse: () => MyPageConfiguration.unknown,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
RouteInformation? restoreRouteInformation(MyPageConfiguration configuration) {
|
||||
return RouteInformation(uri: configuration.uri);
|
||||
}
|
||||
}
|
||||
|
||||
class MyRouterDelegate extends RouterDelegate<MyPageConfiguration> {
|
||||
final Set<VoidCallback> _listeners = <VoidCallback>{};
|
||||
final List<MyPageConfiguration> _pages = <MyPageConfiguration>[];
|
||||
|
||||
void _notifyListeners() {
|
||||
for (final VoidCallback listener in _listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
void onNavigateToHome() {
|
||||
_pages.clear();
|
||||
_pages.add(MyPageConfiguration.home);
|
||||
_notifyListeners();
|
||||
}
|
||||
|
||||
void onNavigateToZoom() {
|
||||
_pages.add(MyPageConfiguration.zoom);
|
||||
_notifyListeners();
|
||||
}
|
||||
|
||||
void onNavigateToIOS() {
|
||||
_pages.add(MyPageConfiguration.iOS);
|
||||
_notifyListeners();
|
||||
}
|
||||
|
||||
void onNavigateToVertical() {
|
||||
_pages.add(MyPageConfiguration.vertical);
|
||||
_notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(VoidCallback listener) {
|
||||
_listeners.remove(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> popRoute() {
|
||||
if (_pages.isEmpty) {
|
||||
return SynchronousFuture<bool>(false);
|
||||
}
|
||||
_pages.removeLast();
|
||||
_notifyListeners();
|
||||
return SynchronousFuture<bool>(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(MyPageConfiguration configuration) {
|
||||
_pages.add(configuration);
|
||||
_notifyListeners();
|
||||
return SynchronousFuture<void>(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
restorationScopeId: 'root',
|
||||
onDidRemovePage: (Page<dynamic> page) {
|
||||
_pages.remove(MyPageConfiguration.fromName(page.name!));
|
||||
},
|
||||
pages: _pages.map((MyPageConfiguration page) => switch (page) {
|
||||
MyPageConfiguration.unknown => _MyUnknownPage<void>(),
|
||||
MyPageConfiguration.home => _MyHomePage<void>(routerDelegate: this),
|
||||
MyPageConfiguration.zoom => _ZoomPage<void>(routerDelegate: this),
|
||||
MyPageConfiguration.iOS => _IOSPage<void>(routerDelegate: this),
|
||||
MyPageConfiguration.vertical => _VerticalPage<void>(routerDelegate: this),
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MyUnknownPage<T> extends MaterialPage<T> {
|
||||
_MyUnknownPage() : super(
|
||||
restorationId: 'unknown-page',
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: const Text('404')),
|
||||
body: const Center(
|
||||
child: Text('404'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
String get name => MyPageConfiguration.unknown.name;
|
||||
}
|
||||
|
||||
class _MyHomePage<T> extends MaterialPage<T> {
|
||||
_MyHomePage({required this.routerDelegate}) : super(
|
||||
restorationId: 'home-page',
|
||||
child: _MyPageScaffold(title: 'Home', routerDelegate: routerDelegate),
|
||||
);
|
||||
|
||||
final MyRouterDelegate routerDelegate;
|
||||
|
||||
@override
|
||||
String get name => MyPageConfiguration.home.name;
|
||||
}
|
||||
|
||||
class _ZoomPage<T> extends MaterialPage<T> {
|
||||
_ZoomPage({required this.routerDelegate}) : super(
|
||||
restorationId: 'zoom-page',
|
||||
child: _MyPageScaffold(title: 'Zoom Route', routerDelegate: routerDelegate),
|
||||
);
|
||||
|
||||
final MyRouterDelegate routerDelegate;
|
||||
|
||||
@override
|
||||
String get name => MyPageConfiguration.zoom.name;
|
||||
}
|
||||
|
||||
class _IOSPage<T> extends CupertinoPage<T> {
|
||||
_IOSPage({required this.routerDelegate}) : super(
|
||||
restorationId: 'ios-page',
|
||||
child: _MyPageScaffold(title: 'Cupertino Route', routerDelegate: routerDelegate),
|
||||
);
|
||||
|
||||
final MyRouterDelegate routerDelegate;
|
||||
|
||||
@override
|
||||
String get name => MyPageConfiguration.iOS.name;
|
||||
}
|
||||
|
||||
class _VerticalPage<T> extends _VerticalTransitionPage<T> {
|
||||
_VerticalPage({required this.routerDelegate}) : super(
|
||||
restorationId: 'vertical-page',
|
||||
child: _MyPageScaffold(title: 'Vertical Route', routerDelegate: routerDelegate),
|
||||
);
|
||||
|
||||
final MyRouterDelegate routerDelegate;
|
||||
|
||||
@override
|
||||
String get name => MyPageConfiguration.vertical.name;
|
||||
}
|
||||
|
||||
class _MyPageScaffold extends StatelessWidget {
|
||||
const _MyPageScaffold({required this.title, required this.routerDelegate});
|
||||
|
||||
final String title;
|
||||
|
||||
final MyRouterDelegate routerDelegate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(title),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
routerDelegate.onNavigateToVertical();
|
||||
},
|
||||
child: const Text('Crazy Vertical Transition'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
routerDelegate.onNavigateToZoom();
|
||||
},
|
||||
child: const Text('Zoom Transition'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
routerDelegate.onNavigateToIOS();
|
||||
},
|
||||
child: const Text('Cupertino Transition'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A Page that applies a _VerticalPageTransition.
|
||||
class _VerticalTransitionPage<T> extends Page<T> {
|
||||
|
||||
const _VerticalTransitionPage({
|
||||
required this.child,
|
||||
this.maintainState = true,
|
||||
this.fullscreenDialog = false,
|
||||
this.allowSnapshotting = true,
|
||||
super.key,
|
||||
super.canPop,
|
||||
super.onPopInvoked,
|
||||
super.name,
|
||||
super.arguments,
|
||||
super.restorationId,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final bool maintainState;
|
||||
|
||||
final bool fullscreenDialog;
|
||||
|
||||
final bool allowSnapshotting;
|
||||
|
||||
@override
|
||||
Route<T> createRoute(BuildContext context) {
|
||||
return _PageBasedVerticalPageRoute<T>(page: this);
|
||||
}
|
||||
}
|
||||
|
||||
class _PageBasedVerticalPageRoute<T> extends PageRoute<T> {
|
||||
_PageBasedVerticalPageRoute({
|
||||
required _VerticalTransitionPage<T> page,
|
||||
super.allowSnapshotting,
|
||||
}) : super(settings: page);
|
||||
|
||||
_VerticalTransitionPage<T> get _page => settings as _VerticalTransitionPage<T>;
|
||||
|
||||
@override
|
||||
bool get maintainState => _page.maintainState;
|
||||
|
||||
@override
|
||||
bool get fullscreenDialog => _page.fullscreenDialog;
|
||||
|
||||
@override
|
||||
String get debugLabel => '${super.debugLabel}(${_page.name})';
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition => _VerticalPageTransition._delegatedTransitionBuilder;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => const Color(0x00000000);
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => false;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => 'Should be no visible barrier...';
|
||||
|
||||
@override
|
||||
bool get opaque => false;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 2000);
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||
return _page.child;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||
return _VerticalPageTransition(
|
||||
primaryRouteAnimation: animation,
|
||||
secondaryRouteAnimation: secondaryAnimation,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A page transition that slides off the screen vertically, and uses
|
||||
// delegatedTransition to ensure that the outgoing route slides with it.
|
||||
class _VerticalPageTransition extends StatelessWidget {
|
||||
_VerticalPageTransition({
|
||||
required Animation<double> primaryRouteAnimation,
|
||||
required this.secondaryRouteAnimation,
|
||||
required this.child,
|
||||
}) : _primaryPositionAnimation =
|
||||
CurvedAnimation(
|
||||
parent: primaryRouteAnimation,
|
||||
curve: _curve,
|
||||
reverseCurve: _curve,
|
||||
).drive(_kBottomUpTween),
|
||||
_secondaryPositionAnimation =
|
||||
CurvedAnimation(
|
||||
parent: secondaryRouteAnimation,
|
||||
curve: _curve,
|
||||
reverseCurve: _curve,
|
||||
)
|
||||
.drive(_kTopDownTween);
|
||||
|
||||
final Animation<Offset> _primaryPositionAnimation;
|
||||
|
||||
final Animation<Offset> _secondaryPositionAnimation;
|
||||
|
||||
final Animation<double> secondaryRouteAnimation;
|
||||
|
||||
final Widget child;
|
||||
|
||||
static const Curve _curve = Curves.decelerate;
|
||||
|
||||
static final Animatable<Offset> _kBottomUpTween = Tween<Offset>(
|
||||
begin: const Offset(0.0, 1.0),
|
||||
end: Offset.zero,
|
||||
);
|
||||
|
||||
static final Animatable<Offset> _kTopDownTween = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.0, -1.0),
|
||||
);
|
||||
|
||||
// When the _VerticalTransitionPageRoute animates onto or off of the navigation
|
||||
// stack, this transition is given to the route below it so that they animate in
|
||||
// sync.
|
||||
static Widget _delegatedTransitionBuilder(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
bool allowSnapshotting,
|
||||
Widget? child
|
||||
) {
|
||||
final Animatable<Offset> tween = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.0, -1.0),
|
||||
).chain(CurveTween(curve: _curve));
|
||||
|
||||
return SlideTransition(
|
||||
position: secondaryAnimation.drive(tween),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
return SlideTransition(
|
||||
position: _secondaryPositionAnimation,
|
||||
textDirection: textDirection,
|
||||
transformHitTests: false,
|
||||
child: SlideTransition(
|
||||
position: _primaryPositionAnimation,
|
||||
textDirection: textDirection,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: child,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum MyPageConfiguration {
|
||||
home(uriString: '/'),
|
||||
zoom(uriString: '/zoom'),
|
||||
iOS(uriString: '/iOS'),
|
||||
vertical(uriString: '/vertical'),
|
||||
unknown(uriString: '/404');
|
||||
|
||||
const MyPageConfiguration({
|
||||
required this.uriString,
|
||||
});
|
||||
|
||||
final String uriString;
|
||||
|
||||
static MyPageConfiguration fromName(String testName) {
|
||||
return values.firstWhere((MyPageConfiguration page) => page.name == testName);
|
||||
}
|
||||
|
||||
Uri get uri => Uri.parse(uriString);
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_api_samples/widgets/routes/flexible_route_transitions.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Flexible Transitions App is able to build', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.FlexibleRouteTransitionsApp(),
|
||||
);
|
||||
|
||||
expect(find.text('Zoom Page'), findsOneWidget);
|
||||
expect(find.text('Zoom Transition'), findsOneWidget);
|
||||
expect(find.text('Crazy Vertical Transition'), findsOneWidget);
|
||||
expect(find.text('Cupertino Transition'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Cupertino Transition'));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Zoom Page'), findsNothing);
|
||||
expect(find.text('Cupertino Page'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('A vertical slide animation is passed to the previous route', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.FlexibleRouteTransitionsApp(),
|
||||
);
|
||||
|
||||
expect(find.text('Zoom Page'), findsOneWidget);
|
||||
|
||||
// Save the Y coordinate of the page title.
|
||||
double lastYPosition = tester.getTopLeft(find.text('Zoom Page')).dy;
|
||||
|
||||
await tester.tap(find.text('Crazy Vertical Transition'));
|
||||
|
||||
await tester.pump();
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
// The current Y coordinate of the page title should be lower than it was
|
||||
// before as the page slides upwards.
|
||||
expect(tester.getTopLeft(find.text('Zoom Page')).dy, lessThan(lastYPosition));
|
||||
lastYPosition = tester.getTopLeft(find.text('Zoom Page')).dy;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(tester.getTopLeft(find.text('Zoom Page')).dy, lessThan(lastYPosition));
|
||||
lastYPosition = tester.getTopLeft(find.text('Zoom Page')).dy;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(tester.getTopLeft(find.text('Zoom Page')).dy, lessThan(lastYPosition));
|
||||
lastYPosition = tester.getTopLeft(find.text('Zoom Page')).dy;
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Crazy Vertical Page'), findsOneWidget);
|
||||
});
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_api_samples/widgets/routes/flexible_route_transitions.1.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../navigator_utils.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Flexible Transitions App is able to build', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
FlexibleRouteTransitionsApp(),
|
||||
);
|
||||
|
||||
expect(find.text('Zoom Transition'), findsOneWidget);
|
||||
expect(find.text('Crazy Vertical Transition'), findsOneWidget);
|
||||
expect(find.text('Cupertino Transition'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('on Pop the correct page shows', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
FlexibleRouteTransitionsApp(),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Home'), findsOneWidget);
|
||||
expect(find.text('Cupertino Route'), findsNothing);
|
||||
expect(find.text('Zoom Route'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Zoom Transition'));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Home'), findsNothing);
|
||||
expect(find.text('Cupertino Route'), findsNothing);
|
||||
expect(find.text('Zoom Route'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Cupertino Transition'));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Home'), findsNothing);
|
||||
expect(find.text('Cupertino Route'), findsOneWidget);
|
||||
expect(find.text('Zoom Route'), findsNothing);
|
||||
|
||||
await simulateSystemBack();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Home'), findsNothing);
|
||||
expect(find.text('Cupertino Route'), findsNothing);
|
||||
expect(find.text('Zoom Route'), findsOneWidget);
|
||||
|
||||
await simulateSystemBack();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Home'), findsOneWidget);
|
||||
expect(find.text('Cupertino Route'), findsNothing);
|
||||
expect(find.text('Zoom Route'), findsNothing);
|
||||
});
|
||||
}
|
@ -161,7 +161,18 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
|
||||
@override
|
||||
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
|
||||
// Don't perform outgoing animation if the next route is a fullscreen dialog.
|
||||
return nextRoute is CupertinoRouteTransitionMixin && !nextRoute.fullscreenDialog;
|
||||
final bool nextRouteIsNotFullscreen = (nextRoute is! PageRoute<T>) || !nextRoute.fullscreenDialog;
|
||||
|
||||
// If the next route has a delegated transition, then this route is able to
|
||||
// use that delegated transition to smoothly sync with the next route's
|
||||
// transition.
|
||||
final bool nextRouteHasDelegatedTransition = nextRoute is ModalRoute<T>
|
||||
&& nextRoute.delegatedTransition != null;
|
||||
|
||||
// Otherwise if the next route has the same route transition mixin as this
|
||||
// one, then this route will already be synced with its transition.
|
||||
return nextRouteIsNotFullscreen &&
|
||||
((nextRoute is CupertinoRouteTransitionMixin) || nextRouteHasDelegatedTransition);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -286,6 +297,9 @@ class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMi
|
||||
assert(opaque);
|
||||
}
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition => CupertinoPageTransition.delegatedTransition;
|
||||
|
||||
/// Builds the primary contents of the route.
|
||||
final WidgetBuilder builder;
|
||||
|
||||
@ -314,6 +328,9 @@ class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTr
|
||||
assert(opaque);
|
||||
}
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition => this.fullscreenDialog ? null : CupertinoPageTransition.delegatedTransition;
|
||||
|
||||
CupertinoPage<T> get _page => settings as CupertinoPage<T>;
|
||||
|
||||
@override
|
||||
@ -415,6 +432,27 @@ class CupertinoPageTransition extends StatefulWidget {
|
||||
/// Used to precisely track back gesture drags.
|
||||
final bool linearTransition;
|
||||
|
||||
/// The Cupertino styled [DelegatedTransitionBuilder] provided to the previous
|
||||
/// route.
|
||||
///
|
||||
/// {@macro flutter.widgets.delegatedTransition}
|
||||
static Widget? delegatedTransition(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, bool allowSnapshotting, Widget? child) {
|
||||
final Animation<Offset> delegatedPositionAnimation =
|
||||
CurvedAnimation(
|
||||
parent: secondaryAnimation,
|
||||
curve: Curves.linearToEaseOut,
|
||||
reverseCurve: Curves.easeInToLinear,
|
||||
).drive(_kMiddleLeftTween);
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
return SlideTransition(
|
||||
position: delegatedPositionAnimation,
|
||||
textDirection: textDirection,
|
||||
transformHitTests: false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<CupertinoPageTransition> createState() => _CupertinoPageTransitionState();
|
||||
}
|
||||
|
@ -95,11 +95,33 @@ mixin MaterialRouteTransitionMixin<T> on PageRoute<T> {
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition => _delegatedTransition;
|
||||
|
||||
static Widget? _delegatedTransition(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, bool allowSnapshotting, Widget? child) {
|
||||
final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
|
||||
final TargetPlatform platform = Theme.of(context).platform;
|
||||
final DelegatedTransitionBuilder? themeDelegatedTransition = theme.delegatedTransition(platform);
|
||||
return themeDelegatedTransition != null ? themeDelegatedTransition(context, animation, secondaryAnimation, allowSnapshotting, child) : null;
|
||||
}
|
||||
|
||||
@override
|
||||
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
|
||||
// Don't perform outgoing animation if the next route is a fullscreen dialog,
|
||||
// or there is no matching transition to use.
|
||||
// Don't perform outgoing animation if the next route is a fullscreen dialog.
|
||||
return (nextRoute is MaterialRouteTransitionMixin && !nextRoute.fullscreenDialog)
|
||||
|| (nextRoute is CupertinoRouteTransitionMixin && !nextRoute.fullscreenDialog);
|
||||
final bool nextRouteIsNotFullscreen = (nextRoute is! PageRoute<T>) || !nextRoute.fullscreenDialog;
|
||||
|
||||
// If the next route has a delegated transition, then this route is able to
|
||||
// use that delegated transition to smoothly sync with the next route's
|
||||
// transition.
|
||||
final bool nextRouteHasDelegatedTransition = nextRoute is ModalRoute<T>
|
||||
&& nextRoute.delegatedTransition != null;
|
||||
|
||||
// Otherwise if the next route has the same route transition mixin as this
|
||||
// one, then this route will already be synced with its transition.
|
||||
return nextRouteIsNotFullscreen &&
|
||||
((nextRoute is MaterialRouteTransitionMixin) || nextRouteHasDelegatedTransition);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -305,33 +305,14 @@ class _ZoomPageTransition extends StatelessWidget {
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: DualTransitionBuilder(
|
||||
animation: ReverseAnimation(secondaryAnimation),
|
||||
forwardBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget? child,
|
||||
) {
|
||||
return _ZoomEnterTransition(
|
||||
animation: animation,
|
||||
allowSnapshotting: allowSnapshotting && allowEnterRouteSnapshotting ,
|
||||
reverse: true,
|
||||
backgroundColor: enterTransitionBackgroundColor,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
reverseBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget? child,
|
||||
) {
|
||||
return _ZoomExitTransition(
|
||||
animation: animation,
|
||||
allowSnapshotting: allowSnapshotting,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
child: ZoomPageTransitionsBuilder._snapshotAwareDelegatedTransition(
|
||||
context,
|
||||
animation,
|
||||
secondaryAnimation,
|
||||
child,
|
||||
allowSnapshotting,
|
||||
allowEnterRouteSnapshotting,
|
||||
enterTransitionBackgroundColor
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -575,6 +556,11 @@ abstract class PageTransitionsBuilder {
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
const PageTransitionsBuilder();
|
||||
|
||||
/// Provideds a secondary transition to the previous route.
|
||||
///
|
||||
/// {@macro flutter.widgets.delegatedTransition}
|
||||
DelegatedTransitionBuilder? get delegatedTransition => null;
|
||||
|
||||
/// Wraps the child with one or more transition widgets which define how [route]
|
||||
/// arrives on and leaves the screen.
|
||||
///
|
||||
@ -728,6 +714,44 @@ class ZoomPageTransitionsBuilder extends PageTransitionsBuilder {
|
||||
// for the Impeller backend.
|
||||
static const bool _kProfileForceDisableSnapshotting = bool.fromEnvironment('flutter.benchmarks.force_disable_snapshot');
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition => (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, bool allowSnapshotting, Widget? child)
|
||||
=> _snapshotAwareDelegatedTransition(context, animation, secondaryAnimation, child, allowSnapshotting && this.allowSnapshotting, allowEnterRouteSnapshotting, backgroundColor);
|
||||
|
||||
// A transition builder that takes into account the snapshotting properties of
|
||||
// ZoomPageTransitionsBuilder.
|
||||
static Widget _snapshotAwareDelegatedTransition(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget? child, bool allowSnapshotting, bool allowEnterRouteSnapshotting, Color? backgroundColor) {
|
||||
final Color enterTransitionBackgroundColor = backgroundColor ?? Theme.of(context).colorScheme.surface;
|
||||
return DualTransitionBuilder(
|
||||
animation: ReverseAnimation(secondaryAnimation),
|
||||
forwardBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget? child,
|
||||
) {
|
||||
return _ZoomEnterTransition(
|
||||
animation: animation,
|
||||
allowSnapshotting: allowSnapshotting && allowEnterRouteSnapshotting,
|
||||
reverse: true,
|
||||
backgroundColor: enterTransitionBackgroundColor,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
reverseBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget? child,
|
||||
) {
|
||||
return _ZoomExitTransition(
|
||||
animation: animation,
|
||||
allowSnapshotting: allowSnapshotting,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTransitions<T>(
|
||||
PageRoute<T> route,
|
||||
@ -771,6 +795,9 @@ class CupertinoPageTransitionsBuilder extends PageTransitionsBuilder {
|
||||
/// Constructs a page transition animation that matches the iOS transition.
|
||||
const CupertinoPageTransitionsBuilder();
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition => CupertinoPageTransition.delegatedTransition;
|
||||
|
||||
@override
|
||||
Widget buildTransitions<T>(
|
||||
PageRoute<T> route,
|
||||
@ -853,6 +880,16 @@ class PageTransitionsTheme with Diagnosticable {
|
||||
);
|
||||
}
|
||||
|
||||
/// Provides the delegate transition for the target platform.
|
||||
///
|
||||
/// {@macro flutter.widgets.delegatedTransition}
|
||||
DelegatedTransitionBuilder? delegatedTransition(TargetPlatform platform) {
|
||||
final PageTransitionsBuilder matchingBuilder =
|
||||
builders[platform] ?? const ZoomPageTransitionsBuilder();
|
||||
|
||||
return matchingBuilder.delegatedTransition;
|
||||
}
|
||||
|
||||
// Map the builders to a list with one PageTransitionsBuilder per platform for
|
||||
// the operator == overload.
|
||||
List<PageTransitionsBuilder?> _all(Map<TargetPlatform, PageTransitionsBuilder> builders) {
|
||||
|
@ -1078,7 +1078,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
|
||||
child: ListenableBuilder(
|
||||
listenable: _listenable, // immutable
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return widget.route.buildTransitions(
|
||||
return widget.route._buildFlexibleTransitions(
|
||||
context,
|
||||
widget.route.animation!,
|
||||
widget.route.secondaryAnimation!,
|
||||
@ -1417,6 +1417,77 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
return child;
|
||||
}
|
||||
|
||||
/// The [DelegatedTransitionBuilder] provided to the route below this one in the
|
||||
/// navigation stack.
|
||||
///
|
||||
/// {@template flutter.widgets.delegatedTransition}
|
||||
/// Used for the purposes of coordinating transitions between two routes with
|
||||
/// different route transitions. When a route is added to the stack, the original
|
||||
/// topmost route will look for this transition, and if available, it will use
|
||||
/// the `delegatedTransition` from the incoming transition to animate off the
|
||||
/// screen.
|
||||
///
|
||||
/// If the return of the [DelegatedTransitionBuilder] is null, then by default
|
||||
/// the original transition of the routes will be used. This is useful if a
|
||||
/// route can conditionally provide a transition based on the [BuildContext].
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// The [ModalRoute] receiving this transition will set it to their
|
||||
/// [receivedTransition] property.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample shows an app that uses three different page transitions, a
|
||||
/// Material Zoom transition, the standard Cupertino sliding transition, and a
|
||||
/// custom vertical transition. All of the page routes are able to inform the
|
||||
/// previous page how to transition off the screen to sync with the new page.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/routes/flexible_route_transitions.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample shows an app that uses the same transitions as the previous
|
||||
/// sample, this time in a [MaterialApp.router].
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/routes/flexible_route_transitions.1.dart **
|
||||
/// {@end-tool}
|
||||
DelegatedTransitionBuilder? get delegatedTransition => null;
|
||||
|
||||
/// The [DelegatedTransitionBuilder] received from the route above this one in
|
||||
/// the navigation stack.
|
||||
///
|
||||
/// {@macro flutter.widgets.delegatedTransition}
|
||||
///
|
||||
/// The `receivedTransition` will use the above route's [delegatedTransition] in
|
||||
/// order to show the right route transition when the above route either enters
|
||||
/// or leaves the navigation stack. If not null, the `receivedTransition` will
|
||||
/// wrap the route content.
|
||||
@visibleForTesting
|
||||
DelegatedTransitionBuilder? receivedTransition;
|
||||
|
||||
// Wraps the transitions of this route with a DelegatedTransitionBuilder, when
|
||||
// _receivedTransition is not null.
|
||||
Widget _buildFlexibleTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
if (receivedTransition == null) {
|
||||
return buildTransitions(context, animation, secondaryAnimation, child);
|
||||
}
|
||||
|
||||
// Create a static proxy animation to supress the original secondary transition.
|
||||
final ProxyAnimation proxyAnimation = ProxyAnimation();
|
||||
|
||||
final Widget proxiedOriginalTransitions = buildTransitions(context, animation, proxyAnimation, child);
|
||||
|
||||
// If recievedTransitions return null, then we want to return the original transitions,
|
||||
// but with the secondary animation still proxied. This keeps a desynched
|
||||
// animation from playing.
|
||||
return receivedTransition!(context, animation, secondaryAnimation, allowSnapshotting, proxiedOriginalTransitions) ??
|
||||
proxiedOriginalTransitions;
|
||||
}
|
||||
|
||||
@override
|
||||
void install() {
|
||||
super.install();
|
||||
@ -1933,12 +2004,22 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
|
||||
@override
|
||||
void didChangeNext(Route<dynamic>? nextRoute) {
|
||||
if (nextRoute is ModalRoute<T> && canTransitionTo(nextRoute) && nextRoute.delegatedTransition != this.delegatedTransition) {
|
||||
receivedTransition = nextRoute.delegatedTransition;
|
||||
} else {
|
||||
receivedTransition = null;
|
||||
}
|
||||
super.didChangeNext(nextRoute);
|
||||
changedInternalState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext(Route<dynamic> nextRoute) {
|
||||
if (nextRoute is ModalRoute<T> && canTransitionTo(nextRoute) && nextRoute.delegatedTransition != this.delegatedTransition) {
|
||||
receivedTransition = nextRoute.delegatedTransition;
|
||||
} else {
|
||||
receivedTransition = null;
|
||||
}
|
||||
super.didPopNext(nextRoute);
|
||||
changedInternalState();
|
||||
_maybeDispatchNavigationNotification();
|
||||
|
@ -142,6 +142,17 @@ class _AnimatedState extends State<AnimatedWidget> {
|
||||
Widget build(BuildContext context) => widget.build(context);
|
||||
}
|
||||
|
||||
/// Signature for a builder used to control a page's exit transition.
|
||||
///
|
||||
/// When a new route enters the stack, the `animation` argument is typically
|
||||
/// used to control the entery and exit transition of the topmost route. The exit
|
||||
/// transition of the route just below the new route is controlled with the
|
||||
/// `secondaryAnimation`, which also controls the transition of the old route
|
||||
/// when the topmost route is popped off the stack.
|
||||
///
|
||||
/// Typically used as the argument for [ModalRoute.delegatedTransition].
|
||||
typedef DelegatedTransitionBuilder = Widget? Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, bool allowSnapshotting, Widget? child);
|
||||
|
||||
/// Animates the position of a widget relative to its normal position.
|
||||
///
|
||||
/// The translation is expressed as an [Offset] scaled to the child's size. For
|
||||
|
@ -5,6 +5,7 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -1918,6 +1919,336 @@ void main() {
|
||||
expect(modalRoute, isNotNull);
|
||||
expect(modalRoute!.requestFocus, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('outgoing route receives a delegated transition from the new route', (WidgetTester tester) async {
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
final MaterialPageRoute<void> materialPageRoute = MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () {
|
||||
final CupertinoPageRoute<void> route = CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const Text('Cupertino Transition');
|
||||
}
|
||||
);
|
||||
Navigator.of(context).push(route);
|
||||
},
|
||||
child: const Text('Cupertino Transition'),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
theme: ThemeData(
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
),
|
||||
home: Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () {
|
||||
navigatorKey.currentState!.push<void>(materialPageRoute);
|
||||
},
|
||||
child: const Text('Material Route Transition'),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(materialPageRoute.receivedTransition, null);
|
||||
|
||||
await tester.tap(find.text('Material Route Transition'));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Cupertino Transition'), findsOneWidget);
|
||||
expect(find.text('Material Route Transition'), findsNothing);
|
||||
|
||||
expect(materialPageRoute.receivedTransition, null);
|
||||
|
||||
await tester.tap(find.text('Cupertino Transition'));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(materialPageRoute.receivedTransition, isNotNull);
|
||||
expect(materialPageRoute.receivedTransition, CupertinoPageTransition.delegatedTransition);
|
||||
});
|
||||
|
||||
testWidgets('outgoing route does not receive a delegated transition from a route with the same transition', (WidgetTester tester) async {
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
final MaterialPageRoute<void> materialPageRoute = MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () {
|
||||
final MaterialPageRoute<void> route = MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const Text('Page 3');
|
||||
}
|
||||
);
|
||||
Navigator.of(context).push(route);
|
||||
},
|
||||
child: const Text('Second Material Transition'),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
theme: ThemeData(
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
),
|
||||
home: Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () {
|
||||
navigatorKey.currentState!.push<void>(materialPageRoute);
|
||||
},
|
||||
child: const Text('Material Route Transition'),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(materialPageRoute.receivedTransition, null);
|
||||
|
||||
await tester.tap(find.text('Material Route Transition'));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(materialPageRoute.receivedTransition, null);
|
||||
|
||||
await tester.tap(find.text('Second Material Transition'));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(materialPageRoute.receivedTransition, null);
|
||||
});
|
||||
|
||||
testWidgets('outgoing route does not receive a delegated transition from a route with the same un-snapshotted transition', (WidgetTester tester) async {
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
final MaterialPageRoute<void> materialPageRoute = MaterialPageRoute<void>(
|
||||
allowSnapshotting: false,
|
||||
builder: (BuildContext context) {
|
||||
return Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () {
|
||||
final MaterialPageRoute<void> route = MaterialPageRoute<void>(
|
||||
allowSnapshotting: false,
|
||||
builder: (BuildContext context) {
|
||||
return const Text('Page 3');
|
||||
}
|
||||
);
|
||||
Navigator.of(context).push(route);
|
||||
},
|
||||
child: const Text('Second Material Transition'),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
theme: ThemeData(
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
),
|
||||
home: Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () {
|
||||
navigatorKey.currentState!.push<void>(materialPageRoute);
|
||||
},
|
||||
child: const Text('Material Route Transition'),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(materialPageRoute.receivedTransition, null);
|
||||
|
||||
await tester.tap(find.text('Material Route Transition'));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(materialPageRoute.receivedTransition, null);
|
||||
|
||||
await tester.tap(find.text('Second Material Transition'));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(materialPageRoute.receivedTransition, null);
|
||||
|
||||
navigatorKey.currentState!.pop();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(materialPageRoute.receivedTransition, null);
|
||||
});
|
||||
|
||||
testWidgets('a received transition animates the same as a non-received transition', (WidgetTester tester) async {
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
const Key firstPlaceholderKey = Key('First Placeholder');
|
||||
const Key secondPlaceholderKey = Key('Second Placeholder');
|
||||
|
||||
final CupertinoPageRoute<void> cupertinoPageRoute = CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
const Placeholder(key: secondPlaceholderKey),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final CupertinoPageRoute<void> route = CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Page 3')
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
Navigator.of(context).push(route);
|
||||
},
|
||||
child: const Text('Second Cupertino Transition'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
theme: ThemeData(
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
),
|
||||
home: Column(
|
||||
children: <Widget>[
|
||||
const Placeholder(key: firstPlaceholderKey),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
navigatorKey.currentState!.push<void>(cupertinoPageRoute);
|
||||
},
|
||||
child: const Text('First Cupertino Transition'),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Start first page transition. This one will be playing the delegated transition
|
||||
// received from Cupertino page route.
|
||||
await tester.tap(find.text('First Cupertino Transition'));
|
||||
|
||||
await tester.pump();
|
||||
|
||||
// Save the position of element on the screen at certain intervals
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalOne = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalTwo = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalThree = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalFour = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalFive = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalSix = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalSeven = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalEight = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalNine = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final double xLocationIntervalTen = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
final double xLocationIntervalEleven = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
final double xLocationIntervalTwelve = tester.getTopLeft(find.byKey(firstPlaceholderKey)).dx;
|
||||
|
||||
// Give time to the animation to finish
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 1));
|
||||
|
||||
// Start the second page transition. This time it's the default secondary
|
||||
// transition of a Cupertino page, with no delegation.
|
||||
await tester.tap(find.text('Second Cupertino Transition'));
|
||||
|
||||
await tester.pump();
|
||||
|
||||
// Compare against the values from before.
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalOne, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalTwo, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalThree, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalFour, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalFive, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalSix, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalSeven, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalEight, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalNine, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalTen, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalEleven, epsilon: 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(tester.getTopLeft(find.byKey(secondPlaceholderKey)).dx, moreOrLessEquals(xLocationIntervalTwelve, epsilon: 0.1));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('can be dismissed with escape keyboard shortcut', (WidgetTester tester) async {
|
||||
|
Loading…
Reference in New Issue
Block a user