-
Notifications
You must be signed in to change notification settings - Fork 30.1k
Description
By default, Navigator does not support navigating to a modal bottom sheet Page.
Use case
I want to navigate to a modal bottom sheet. 'Navigating' means using navigator - as opposed to 'show', using showModalBottomSheet.
Details
Say you have a Navigator 2.0 that receive a list of page on Navigator.pages.
void main() {
runApp(MaterialApp(title: 'Navigation', home: MyW()));
}
class MyW extends StatefulWidget {
@override
State<MyW> createState() => _MyWState();
}
class _MyWState extends State<MyW> {
bool showModal = false;
@override
Widget build(BuildContext context) {
return Navigator(
onPopPage: (route, dynamic result) {
return route.didPop(result);
},
pages: [
MaterialPage(
child: Container(
color: Colors.blue,
child: Center(
child: ElevatedButton(
child: Text('show'),
onPressed: () {
setState(() {
showModal = !showModal;
});
},
)),
),
),
if (showModal) SecondPage() // how to create a Modal Bottom Sheet Page
]);
}
}In the example above a Modal Page must be subclassed from Page<T>, which receives a subclassed PopupRoute<T> for it to work.
Solution
Check this solution lulupointu/vrouter#63 (comment)
So ultimately we have this:
Click to expand!
import 'package:flutter/material.dart';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(title: 'Navigation', home: MyW()));
}
class MyW extends StatefulWidget {
@override
State<MyW> createState() => _MyWState();
}
class _MyWState extends State<MyW> {
bool showModal = false;
@override
Widget build(BuildContext context) {
return Navigator(
onPopPage: (route, dynamic result) {
return route.didPop(result);
},
pages: [
MaterialPage(
child: Container(
color: Colors.blue,
child: Center(
child: ElevatedButton(
child: Text('show'),
onPressed: () {
setState(() {
showModal = !showModal;
});
},
)),
),
),
if (showModal)
ModalBottomSheetPage(
builder: (BuildContext context) {
showModal = !showModal;
return Container();
},
key: ValueKey('xx'),
name: 'kjh')
]);
}
}
const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250);
const Duration _bottomSheetExitDuration = Duration(milliseconds: 200);
const Curve _modalBottomSheetCurve = decelerateEasing;
class ModalBottomSheetRoute<T> extends PopupRoute<T> {
ModalBottomSheetRoute({
this.builder,
required this.capturedThemes,
this.barrierLabel,
this.backgroundColor,
this.elevation,
this.shape,
this.clipBehavior,
this.modalBarrierColor,
this.isDismissible = true,
this.enableDrag = true,
required this.isScrollControlled,
RouteSettings? settings,
this.transitionAnimationController,
}) : super(settings: settings);
final WidgetBuilder? builder;
final CapturedThemes capturedThemes;
final bool isScrollControlled;
final Color? backgroundColor;
final double? elevation;
final ShapeBorder? shape;
final Clip? clipBehavior;
final Color? modalBarrierColor;
final bool isDismissible;
final bool enableDrag;
final AnimationController? transitionAnimationController;
@override
Duration get transitionDuration => _bottomSheetEnterDuration;
@override
Duration get reverseTransitionDuration => _bottomSheetExitDuration;
@override
bool get barrierDismissible => isDismissible;
@override
final String? barrierLabel;
@override
Color get barrierColor => modalBarrierColor ?? Colors.black54;
AnimationController? _animationController;
// @override
// AnimationController createAnimationController() {
// assert(_animationController == null);
// _animationController = transitionAnimationController ??
// BottomSheet.createAnimationController(navigator!.overlay!);
// return _animationController!;
// }
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
// By definition, the bottom sheet is aligned to the bottom of the page
// and isn't exposed to the top padding of the MediaQuery.
final Widget bottomSheet = MediaQuery.removePadding(
context: context,
removeTop: true,
child: Builder(
builder: (BuildContext context) {
final BottomSheetThemeData sheetTheme =
Theme.of(context).bottomSheetTheme;
return _ModalBottomSheet<T>(
route: this,
backgroundColor: backgroundColor ??
sheetTheme.modalBackgroundColor ??
sheetTheme.backgroundColor,
elevation:
elevation ?? sheetTheme.modalElevation ?? sheetTheme.elevation,
shape: shape,
clipBehavior: clipBehavior,
isScrollControlled: isScrollControlled,
enableDrag: enableDrag,
);
},
),
);
return capturedThemes.wrap(bottomSheet);
}
}
class _ModalBottomSheet<T> extends StatefulWidget {
const _ModalBottomSheet({
Key? key,
this.route,
this.backgroundColor,
this.elevation,
this.shape,
this.clipBehavior,
this.isScrollControlled = false,
this.enableDrag = true,
}) : super(key: key);
final ModalBottomSheetRoute<T>? route;
final bool isScrollControlled;
final Color? backgroundColor;
final double? elevation;
final ShapeBorder? shape;
final Clip? clipBehavior;
final bool enableDrag;
@override
_ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
}
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
ParametricCurve<double> animationCurve = _modalBottomSheetCurve;
String _getRouteLabel(MaterialLocalizations localizations) {
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return '';
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return localizations.dialogLabel;
}
}
void handleDragStart(DragStartDetails details) {
// Allow the bottom sheet to track the user's finger accurately.
animationCurve = Curves.linear;
}
void handleDragEnd(DragEndDetails details, {bool? isClosing}) {
// Allow the bottom sheet to animate smoothly from its current position.
animationCurve = _BottomSheetSuspendedCurve(
widget.route!.animation!.value,
curve: _modalBottomSheetCurve,
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
final MediaQueryData mediaQuery = MediaQuery.of(context);
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final String routeLabel = _getRouteLabel(localizations);
return AnimatedBuilder(
animation: widget.route!.animation!,
child: Padding(
padding: MediaQuery.of(context).viewInsets,
child: BottomSheet(
animationController: widget.route!._animationController,
onClosing: () {
if (widget.route!.isCurrent) {
Navigator.pop(context);
}
},
builder: widget.route!.builder!,
backgroundColor: widget.backgroundColor,
elevation: widget.elevation,
shape: widget.shape,
clipBehavior: widget.clipBehavior,
enableDrag: widget.enableDrag,
onDragStart: handleDragStart,
onDragEnd: handleDragEnd,
),
),
builder: (BuildContext context, Widget? child) {
// Disable the initial animation when accessible navigation is on so
// that the semantics are added to the tree at the correct time.
final double animationValue = animationCurve.transform(
mediaQuery.accessibleNavigation
? 1.0
: widget.route!.animation!.value);
return Semantics(
scopesRoute: true,
namesRoute: true,
label: routeLabel,
explicitChildNodes: true,
child: ClipRect(
child: CustomSingleChildLayout(
delegate: _ModalBottomSheetLayout(
animationValue, widget.isScrollControlled),
child: child,
),
),
);
},
);
}
}
class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
/// Creates a suspended curve.
const _BottomSheetSuspendedCurve(
this.startingPoint, {
this.curve = Curves.easeOutCubic,
});
/// The progress value at which [curve] should begin.
///
/// This defaults to [Curves.easeOutCubic].
final double startingPoint;
/// The curve to use when [startingPoint] is reached.
final Curve curve;
@override
double transform(double t) {
assert(t >= 0.0 && t <= 1.0);
assert(startingPoint >= 0.0 && startingPoint <= 1.0);
if (t < startingPoint) {
return t;
}
if (t == 1.0) {
return t;
}
final double curveProgress = (t - startingPoint) / (1 - startingPoint);
final double transformed = curve.transform(curveProgress);
return lerpDouble(startingPoint, 1, transformed)!;
}
@override
String toString() {
return '${describeIdentity(this)}($startingPoint, $curve)';
}
}
class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
_ModalBottomSheetLayout(this.progress, this.isScrollControlled);
final double progress;
final bool isScrollControlled;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return BoxConstraints(
minWidth: constraints.maxWidth,
maxWidth: constraints.maxWidth,
minHeight: 0.0,
maxHeight: isScrollControlled
? constraints.maxHeight
: constraints.maxHeight * 9.0 / 16.0,
);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
return Offset(0.0, size.height - childSize.height * progress);
}
@override
bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
return progress != oldDelegate.progress;
}
}
class ModalBottomSheetPage<T> extends Page<T> {
final WidgetBuilder builder;
final Color? backgroundColor;
final double? elevation;
final ShapeBorder? shape;
final Clip? clipBehavior;
final Color? barrierColor;
final bool isScrollControlled;
final bool useRootNavigator;
final bool isDismissible;
final bool enableDrag;
final AnimationController? transitionAnimationController;
ModalBottomSheetPage({
required LocalKey key,
required String? name,
required this.builder,
this.backgroundColor,
this.elevation,
this.shape,
this.clipBehavior,
this.barrierColor,
this.isScrollControlled = false,
this.useRootNavigator = false,
this.isDismissible = true,
this.enableDrag = true,
this.transitionAnimationController,
}) : super(key: key, name: name);
@override
Route<T> createRoute(BuildContext context) {
return ModalBottomSheetRoute<T>(
builder: builder,
capturedThemes: InheritedTheme.capture(
from: context,
to: Navigator.of(context, rootNavigator: useRootNavigator).context),
isScrollControlled: isScrollControlled,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
clipBehavior: clipBehavior,
isDismissible: isDismissible,
modalBarrierColor: barrierColor,
enableDrag: enableDrag,
settings: this,
// This is important and needs to be like this
transitionAnimationController: transitionAnimationController,
);
}
}
I see that other Nav packages are also having issues slovnicki/beamer#276
@lulupointu @chunhtai @johnpryan @slovnicki
Maybe there is a better way?
I tried to create a Widget that calls showModalBottomSheet. But the parent widget (the screen behind the modal) would need to be provided. Not a good hack.
Click to expand!
class ModalWidget extends StatefulWidget {
final Widget parent;
final Widget child;
const ModalWidget({Key? key,required this.child, required this.parent}) : super(key: key);
@override
State<ModalWidget> createState() => _ModalWidgetState();
}
class _ModalWidgetState extends State<ModalWidget> {
@override
void initState() {
super.initState();
SchedulerBinding.instance!.addPostFrameCallback((timeStamp) {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return widget.child;
},
);
});
}
@override
Widget build(BuildContext context) => widget.parent;
}