flutter/examples/api/lib/widgets/routes/flexible_route_transitions.1.dart
Michael Goderbauer 5491c8c146
Auto-format Framework (#160545)
This auto-formats all *.dart files in the repository outside of the
`engine` subdirectory and enforces that these files stay formatted with
a presubmit check.

**Reviewers:** Please carefully review all the commits except for the
one titled "formatted". The "formatted" commit was auto-generated by
running `dev/tools/format.sh -a -f`. The other commits were hand-crafted
to prepare the repo for the formatting change. I recommend reviewing the
commits one-by-one via the "Commits" tab and avoiding Github's "Files
changed" tab as it will likely slow down your browser because of the
size of this PR.

---------

Co-authored-by: Kate Lovett <katelovett@google.com>
Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
2024-12-19 20:06:21 +00:00

432 lines
12 KiB
Dart

// 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);
}