mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-24 19:16:44 +08:00
* opt: cache * opt: MediaListPanel * feat: nested replyreply panel * tweaks * opt: abstract class * opt: PageStorageKey * opt: contextExt * opt: EpisodePanel * opt * opt: context instead GlobalKey * feat: jump to reply * refa: reply_reply * fix: jump * fix: index * update Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * opt: keepalive * reapply: nested replyreply * mod: spacing * opt: CommonSlidePageState * fix drag bottomsheet Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * opt reply jump Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * opt reply2reply Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * reapply: jumpToReply * fix: padding * fix: anim * fix some panels Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * opt: implements Scaffold * opt: remove keepalive * revert: GlobalKey * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> --------- Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
1558 lines
55 KiB
Dart
1558 lines
55 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.
|
|
|
|
/// @docImport 'dart:ui';
|
|
library;
|
|
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:PiliPlus/common/widgets/scaffold/scaffold.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart'
|
|
hide Scaffold, ScaffoldState, PersistentBottomSheetController;
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250);
|
|
const Duration _bottomSheetExitDuration = Duration(milliseconds: 200);
|
|
const Curve _modalBottomSheetCurve = Easing.legacyDecelerate;
|
|
const double _minFlingVelocity = 700.0;
|
|
const double _closeProgressThreshold = 0.5;
|
|
const double _defaultScrollControlDisabledMaxHeightRatio = 9.0 / 16.0;
|
|
|
|
/// A callback for when the user begins dragging the bottom sheet.
|
|
///
|
|
/// Used by [BottomSheet.onDragStart].
|
|
typedef BottomSheetDragStartHandler = void Function(DragStartDetails details);
|
|
|
|
/// A callback for when the user stops dragging the bottom sheet.
|
|
///
|
|
/// Used by [BottomSheet.onDragEnd].
|
|
typedef BottomSheetDragEndHandler =
|
|
void Function(DragEndDetails details, {required bool isClosing});
|
|
|
|
/// A Material Design bottom sheet.
|
|
///
|
|
/// There are two kinds of bottom sheets in Material Design:
|
|
///
|
|
/// * _Persistent_. A persistent bottom sheet shows information that
|
|
/// supplements the primary content of the app. A persistent bottom sheet
|
|
/// remains visible even when the user interacts with other parts of the app.
|
|
/// Persistent bottom sheets can be created and displayed with the
|
|
/// [ScaffoldState.showBottomSheet] function or by specifying the
|
|
/// [Scaffold.bottomSheet] constructor parameter.
|
|
///
|
|
/// * _Modal_. A modal bottom sheet is an alternative to a menu or a dialog and
|
|
/// prevents the user from interacting with the rest of the app. Modal bottom
|
|
/// sheets can be created and displayed with the [showModalBottomSheet]
|
|
/// function.
|
|
///
|
|
/// The [BottomSheet] widget itself is rarely used directly. Instead, prefer to
|
|
/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] or
|
|
/// [Scaffold.bottomSheet], and a modal bottom sheet with [showModalBottomSheet].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
|
|
/// non-modal "persistent" bottom sheets.
|
|
/// * [showModalBottomSheet], which can be used to display a modal bottom
|
|
/// sheet.
|
|
/// * [BottomSheetThemeData], which can be used to customize the default
|
|
/// bottom sheet property values.
|
|
/// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
|
|
/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
|
|
class BottomSheet extends StatefulWidget {
|
|
/// Creates a bottom sheet.
|
|
///
|
|
/// Typically, bottom sheets are created implicitly by
|
|
/// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by
|
|
/// [showModalBottomSheet], for modal bottom sheets.
|
|
const BottomSheet({
|
|
super.key,
|
|
this.animationController,
|
|
this.enableDrag = true,
|
|
this.showDragHandle,
|
|
this.dragHandleColor,
|
|
this.dragHandleSize,
|
|
this.onDragStart,
|
|
this.onDragEnd,
|
|
this.backgroundColor,
|
|
this.shadowColor,
|
|
this.elevation,
|
|
this.shape,
|
|
this.clipBehavior,
|
|
this.constraints,
|
|
required this.onClosing,
|
|
required this.builder,
|
|
}) : assert(elevation == null || elevation >= 0.0);
|
|
|
|
/// The animation controller that controls the bottom sheet's entrance and
|
|
/// exit animations.
|
|
///
|
|
/// The BottomSheet widget will manipulate the position of this animation, it
|
|
/// is not just a passive observer.
|
|
final AnimationController? animationController;
|
|
|
|
/// Called when the bottom sheet begins to close.
|
|
///
|
|
/// A bottom sheet might be prevented from closing (e.g., by user
|
|
/// interaction) even after this callback is called. For this reason, this
|
|
/// callback might be call multiple times for a given bottom sheet.
|
|
final VoidCallback onClosing;
|
|
|
|
/// A builder for the contents of the sheet.
|
|
///
|
|
/// The bottom sheet will wrap the widget produced by this builder in a
|
|
/// [Material] widget.
|
|
final WidgetBuilder builder;
|
|
|
|
/// If true, the bottom sheet can be dragged up and down and dismissed by
|
|
/// swiping downwards.
|
|
///
|
|
/// If [showDragHandle] is true, this only applies to the content below the drag handle,
|
|
/// because the drag handle is always draggable.
|
|
///
|
|
/// Default is true.
|
|
///
|
|
/// If this is true, the [animationController] must not be null.
|
|
/// Use [BottomSheet.createAnimationController] to create one, or provide
|
|
/// another AnimationController.
|
|
final bool enableDrag;
|
|
|
|
/// Specifies whether a drag handle is shown.
|
|
///
|
|
/// The drag handle appears at the top of the bottom sheet. The default color is
|
|
/// [ColorScheme.onSurfaceVariant] with an opacity of 0.4 and can be customized
|
|
/// using [dragHandleColor]. The default size is `Size(32,4)` and can be customized
|
|
/// with [dragHandleSize].
|
|
///
|
|
/// If null, then the value of [BottomSheetThemeData.showDragHandle] is used. If
|
|
/// that is also null, defaults to false.
|
|
///
|
|
/// If this is true, the [animationController] must not be null.
|
|
/// Use [BottomSheet.createAnimationController] to create one, or provide
|
|
/// another AnimationController.
|
|
final bool? showDragHandle;
|
|
|
|
/// The bottom sheet drag handle's color.
|
|
///
|
|
/// Defaults to [BottomSheetThemeData.dragHandleColor].
|
|
/// If that is also null, defaults to [ColorScheme.onSurfaceVariant].
|
|
final Color? dragHandleColor;
|
|
|
|
/// Defaults to [BottomSheetThemeData.dragHandleSize].
|
|
/// If that is also null, defaults to Size(32, 4).
|
|
final Size? dragHandleSize;
|
|
|
|
/// Called when the user begins dragging the bottom sheet vertically, if
|
|
/// [enableDrag] is true.
|
|
///
|
|
/// Would typically be used to change the bottom sheet animation curve so
|
|
/// that it tracks the user's finger accurately.
|
|
final BottomSheetDragStartHandler? onDragStart;
|
|
|
|
/// Called when the user stops dragging the bottom sheet, if [enableDrag]
|
|
/// is true.
|
|
///
|
|
/// Would typically be used to reset the bottom sheet animation curve, so
|
|
/// that it animates non-linearly. Called before [onClosing] if the bottom
|
|
/// sheet is closing.
|
|
final BottomSheetDragEndHandler? onDragEnd;
|
|
|
|
/// The bottom sheet's background color.
|
|
///
|
|
/// Defines the bottom sheet's [Material.color].
|
|
///
|
|
/// Defaults to null and falls back to [Material]'s default.
|
|
final Color? backgroundColor;
|
|
|
|
/// The color of the shadow below the sheet.
|
|
///
|
|
/// If this property is null, then [BottomSheetThemeData.shadowColor] of
|
|
/// [ThemeData.bottomSheetTheme] is used. If that is also null, the default value
|
|
/// is transparent.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [elevation], which defines the size of the shadow below the sheet.
|
|
/// * [shape], which defines the shape of the sheet and its shadow.
|
|
final Color? shadowColor;
|
|
|
|
/// The z-coordinate at which to place this material relative to its parent.
|
|
///
|
|
/// This controls the size of the shadow below the material.
|
|
///
|
|
/// Defaults to 0. The value is non-negative.
|
|
final double? elevation;
|
|
|
|
/// The shape of the bottom sheet.
|
|
///
|
|
/// Defines the bottom sheet's [Material.shape].
|
|
///
|
|
/// Defaults to null and falls back to [Material]'s default.
|
|
final ShapeBorder? shape;
|
|
|
|
/// {@macro flutter.material.Material.clipBehavior}
|
|
///
|
|
/// Defines the bottom sheet's [Material.clipBehavior].
|
|
///
|
|
/// Use this property to enable clipping of content when the bottom sheet has
|
|
/// a custom [shape] and the content can extend past this shape. For example,
|
|
/// a bottom sheet with rounded corners and an edge-to-edge [Image] at the
|
|
/// top.
|
|
///
|
|
/// If this property is null then [BottomSheetThemeData.clipBehavior] of
|
|
/// [ThemeData.bottomSheetTheme] is used. If that's null then the behavior
|
|
/// will be [Clip.none].
|
|
final Clip? clipBehavior;
|
|
|
|
/// Defines minimum and maximum sizes for a [BottomSheet].
|
|
///
|
|
/// If null, then the ambient [ThemeData.bottomSheetTheme]'s
|
|
/// [BottomSheetThemeData.constraints] will be used. If that
|
|
/// is null and [ThemeData.useMaterial3] is true, then the bottom sheet
|
|
/// will have a max width of 640dp. If [ThemeData.useMaterial3] is false, then
|
|
/// the bottom sheet's size will be constrained by its parent
|
|
/// (usually a [Scaffold]). In this case, consider limiting the width by
|
|
/// setting smaller constraints for large screens.
|
|
///
|
|
/// If constraints are specified (either in this property or in the
|
|
/// theme), the bottom sheet will be aligned to the bottom-center of
|
|
/// the available space. Otherwise, no alignment is applied.
|
|
final BoxConstraints? constraints;
|
|
|
|
@override
|
|
State<BottomSheet> createState() => _BottomSheetState();
|
|
|
|
/// Creates an [AnimationController] suitable for a
|
|
/// [BottomSheet.animationController].
|
|
///
|
|
/// This API is available as a convenience for a Material compliant bottom sheet
|
|
/// animation. If alternative animation durations are required, a different
|
|
/// animation controller could be provided.
|
|
static AnimationController createAnimationController(
|
|
TickerProvider vsync, {
|
|
AnimationStyle? sheetAnimationStyle,
|
|
}) {
|
|
return AnimationController(
|
|
duration: sheetAnimationStyle?.duration ?? _bottomSheetEnterDuration,
|
|
reverseDuration:
|
|
sheetAnimationStyle?.reverseDuration ?? _bottomSheetExitDuration,
|
|
debugLabel: 'BottomSheet',
|
|
vsync: vsync,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BottomSheetState extends State<BottomSheet> {
|
|
final GlobalKey _childKey = GlobalKey(debugLabel: 'BottomSheet child');
|
|
|
|
double get _childHeight {
|
|
final RenderBox renderBox =
|
|
_childKey.currentContext!.findRenderObject()! as RenderBox;
|
|
return renderBox.size.height;
|
|
}
|
|
|
|
bool get _dismissUnderway =>
|
|
widget.animationController!.status == AnimationStatus.reverse;
|
|
|
|
Set<WidgetState> dragHandleStates = <WidgetState>{};
|
|
|
|
void _handleDragStart(DragStartDetails details) {
|
|
setState(() {
|
|
dragHandleStates.add(WidgetState.dragged);
|
|
});
|
|
widget.onDragStart?.call(details);
|
|
}
|
|
|
|
void _handleDragUpdate(DragUpdateDetails details) {
|
|
assert(
|
|
(widget.enableDrag || (widget.showDragHandle ?? false)) &&
|
|
widget.animationController != null,
|
|
"'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. "
|
|
"Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
|
|
);
|
|
if (_dismissUnderway) {
|
|
return;
|
|
}
|
|
widget.animationController!.value -= details.primaryDelta! / _childHeight;
|
|
}
|
|
|
|
void _handleDragEnd(DragEndDetails details) {
|
|
assert(
|
|
(widget.enableDrag || (widget.showDragHandle ?? false)) &&
|
|
widget.animationController != null,
|
|
"'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. "
|
|
"Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
|
|
);
|
|
if (_dismissUnderway) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
dragHandleStates.remove(WidgetState.dragged);
|
|
});
|
|
bool isClosing = false;
|
|
if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
|
|
final double flingVelocity =
|
|
-details.velocity.pixelsPerSecond.dy / _childHeight;
|
|
if (widget.animationController!.value > 0.0) {
|
|
widget.animationController!.fling(velocity: flingVelocity);
|
|
}
|
|
if (flingVelocity < 0.0) {
|
|
isClosing = true;
|
|
}
|
|
} else if (widget.animationController!.value < _closeProgressThreshold) {
|
|
if (widget.animationController!.value > 0.0) {
|
|
widget.animationController!.fling(velocity: -1.0);
|
|
}
|
|
isClosing = true;
|
|
} else {
|
|
widget.animationController!.forward();
|
|
}
|
|
|
|
widget.onDragEnd?.call(details, isClosing: isClosing);
|
|
|
|
if (isClosing) {
|
|
widget.onClosing();
|
|
}
|
|
}
|
|
|
|
bool extentChanged(DraggableScrollableNotification notification) {
|
|
if (notification.extent == notification.minExtent &&
|
|
notification.shouldCloseOnMinExtent) {
|
|
widget.onClosing();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void _handleDragHandleHover(bool hovering) {
|
|
if (hovering != dragHandleStates.contains(WidgetState.hovered)) {
|
|
setState(() {
|
|
if (hovering) {
|
|
dragHandleStates.add(WidgetState.hovered);
|
|
} else {
|
|
dragHandleStates.remove(WidgetState.hovered);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final BottomSheetThemeData bottomSheetTheme = Theme.of(
|
|
context,
|
|
).bottomSheetTheme;
|
|
final bool useMaterial3 = Theme.of(context).useMaterial3;
|
|
final BottomSheetThemeData defaults = useMaterial3
|
|
? _BottomSheetDefaultsM3(context)
|
|
: const BottomSheetThemeData();
|
|
final BoxConstraints? constraints =
|
|
widget.constraints ??
|
|
bottomSheetTheme.constraints ??
|
|
defaults.constraints;
|
|
final Color? color =
|
|
widget.backgroundColor ??
|
|
bottomSheetTheme.backgroundColor ??
|
|
defaults.backgroundColor;
|
|
final Color? surfaceTintColor =
|
|
bottomSheetTheme.surfaceTintColor ?? defaults.surfaceTintColor;
|
|
final Color? shadowColor =
|
|
widget.shadowColor ??
|
|
bottomSheetTheme.shadowColor ??
|
|
defaults.shadowColor;
|
|
final double elevation =
|
|
widget.elevation ??
|
|
bottomSheetTheme.elevation ??
|
|
defaults.elevation ??
|
|
0;
|
|
final ShapeBorder? shape =
|
|
widget.shape ?? bottomSheetTheme.shape ?? defaults.shape;
|
|
final Clip clipBehavior =
|
|
widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
|
|
final bool showDragHandle =
|
|
widget.showDragHandle ??
|
|
(widget.enableDrag && (bottomSheetTheme.showDragHandle ?? false));
|
|
|
|
Widget? dragHandle;
|
|
if (showDragHandle) {
|
|
dragHandle = _DragHandle(
|
|
onSemanticsTap: widget.onClosing,
|
|
handleHover: _handleDragHandleHover,
|
|
states: dragHandleStates,
|
|
dragHandleColor: widget.dragHandleColor,
|
|
dragHandleSize: widget.dragHandleSize,
|
|
);
|
|
// Only add [_BottomSheetGestureDetector] to the drag handle when the rest of the
|
|
// bottom sheet is not draggable. If the whole bottom sheet is draggable,
|
|
// no need to add it.
|
|
if (!widget.enableDrag) {
|
|
dragHandle = _BottomSheetGestureDetector(
|
|
onVerticalDragStart: _handleDragStart,
|
|
onVerticalDragUpdate: _handleDragUpdate,
|
|
onVerticalDragEnd: _handleDragEnd,
|
|
child: dragHandle,
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget bottomSheet = Material(
|
|
key: _childKey,
|
|
color: color,
|
|
elevation: elevation,
|
|
surfaceTintColor: surfaceTintColor,
|
|
shadowColor: shadowColor,
|
|
shape: shape,
|
|
clipBehavior: clipBehavior,
|
|
child: NotificationListener<DraggableScrollableNotification>(
|
|
onNotification: extentChanged,
|
|
child: !showDragHandle
|
|
? widget.builder(context)
|
|
: Stack(
|
|
alignment: Alignment.topCenter,
|
|
children: <Widget>[
|
|
dragHandle!,
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
top: kMinInteractiveDimension,
|
|
),
|
|
child: widget.builder(context),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
if (constraints != null) {
|
|
bottomSheet = Align(
|
|
alignment: Alignment.bottomCenter,
|
|
heightFactor: 1.0,
|
|
child: ConstrainedBox(constraints: constraints, child: bottomSheet),
|
|
);
|
|
}
|
|
|
|
return !widget.enableDrag
|
|
? bottomSheet
|
|
: _BottomSheetGestureDetector(
|
|
onVerticalDragStart: _handleDragStart,
|
|
onVerticalDragUpdate: _handleDragUpdate,
|
|
onVerticalDragEnd: _handleDragEnd,
|
|
child: bottomSheet,
|
|
);
|
|
}
|
|
}
|
|
|
|
// PERSISTENT BOTTOM SHEETS
|
|
|
|
// See scaffold.dart
|
|
|
|
class _DragHandle extends StatelessWidget {
|
|
const _DragHandle({
|
|
required this.onSemanticsTap,
|
|
required this.handleHover,
|
|
required this.states,
|
|
this.dragHandleColor,
|
|
this.dragHandleSize,
|
|
});
|
|
|
|
final VoidCallback? onSemanticsTap;
|
|
final ValueChanged<bool> handleHover;
|
|
final Set<WidgetState> states;
|
|
final Color? dragHandleColor;
|
|
final Size? dragHandleSize;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final BottomSheetThemeData bottomSheetTheme = Theme.of(
|
|
context,
|
|
).bottomSheetTheme;
|
|
final BottomSheetThemeData m3Defaults = _BottomSheetDefaultsM3(context);
|
|
final Size handleSize =
|
|
dragHandleSize ??
|
|
bottomSheetTheme.dragHandleSize ??
|
|
m3Defaults.dragHandleSize!;
|
|
|
|
return MouseRegion(
|
|
onEnter: (PointerEnterEvent event) => handleHover(true),
|
|
onExit: (PointerExitEvent event) => handleHover(false),
|
|
child: Semantics(
|
|
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
|
container: true,
|
|
button: true,
|
|
onTap: onSemanticsTap,
|
|
child: SizedBox(
|
|
width: math.max(handleSize.width, kMinInteractiveDimension),
|
|
height: math.max(handleSize.height, kMinInteractiveDimension),
|
|
child: Center(
|
|
child: Container(
|
|
height: handleSize.height,
|
|
width: handleSize.width,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(handleSize.height / 2),
|
|
color:
|
|
WidgetStateProperty.resolveAs<Color?>(
|
|
dragHandleColor,
|
|
states,
|
|
) ??
|
|
WidgetStateProperty.resolveAs<Color?>(
|
|
bottomSheetTheme.dragHandleColor,
|
|
states,
|
|
) ??
|
|
m3Defaults.dragHandleColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
|
|
const _BottomSheetLayoutWithSizeListener({
|
|
required this.onChildSizeChanged,
|
|
required this.animationValue,
|
|
required this.isScrollControlled,
|
|
required this.scrollControlDisabledMaxHeightRatio,
|
|
super.child,
|
|
});
|
|
|
|
final ValueChanged<Size> onChildSizeChanged;
|
|
final double animationValue;
|
|
final bool isScrollControlled;
|
|
final double scrollControlDisabledMaxHeightRatio;
|
|
|
|
@override
|
|
_RenderBottomSheetLayoutWithSizeListener createRenderObject(
|
|
BuildContext context,
|
|
) {
|
|
return _RenderBottomSheetLayoutWithSizeListener(
|
|
onChildSizeChanged: onChildSizeChanged,
|
|
animationValue: animationValue,
|
|
isScrollControlled: isScrollControlled,
|
|
scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(
|
|
BuildContext context,
|
|
_RenderBottomSheetLayoutWithSizeListener renderObject,
|
|
) {
|
|
renderObject.onChildSizeChanged = onChildSizeChanged;
|
|
renderObject.animationValue = animationValue;
|
|
renderObject.isScrollControlled = isScrollControlled;
|
|
renderObject.scrollControlDisabledMaxHeightRatio =
|
|
scrollControlDisabledMaxHeightRatio;
|
|
}
|
|
}
|
|
|
|
class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox {
|
|
_RenderBottomSheetLayoutWithSizeListener({
|
|
RenderBox? child,
|
|
required ValueChanged<Size> onChildSizeChanged,
|
|
required double animationValue,
|
|
required bool isScrollControlled,
|
|
required double scrollControlDisabledMaxHeightRatio,
|
|
}) : _onChildSizeChanged = onChildSizeChanged,
|
|
_animationValue = animationValue,
|
|
_isScrollControlled = isScrollControlled,
|
|
_scrollControlDisabledMaxHeightRatio =
|
|
scrollControlDisabledMaxHeightRatio,
|
|
super(child);
|
|
|
|
Size _lastSize = Size.zero;
|
|
|
|
ValueChanged<Size> get onChildSizeChanged => _onChildSizeChanged;
|
|
ValueChanged<Size> _onChildSizeChanged;
|
|
set onChildSizeChanged(ValueChanged<Size> newCallback) {
|
|
if (_onChildSizeChanged == newCallback) {
|
|
return;
|
|
}
|
|
|
|
_onChildSizeChanged = newCallback;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
double get animationValue => _animationValue;
|
|
double _animationValue;
|
|
set animationValue(double newValue) {
|
|
if (_animationValue == newValue) {
|
|
return;
|
|
}
|
|
|
|
_animationValue = newValue;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
bool get isScrollControlled => _isScrollControlled;
|
|
bool _isScrollControlled;
|
|
set isScrollControlled(bool newValue) {
|
|
if (_isScrollControlled == newValue) {
|
|
return;
|
|
}
|
|
|
|
_isScrollControlled = newValue;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
double get scrollControlDisabledMaxHeightRatio =>
|
|
_scrollControlDisabledMaxHeightRatio;
|
|
double _scrollControlDisabledMaxHeightRatio;
|
|
set scrollControlDisabledMaxHeightRatio(double newValue) {
|
|
if (_scrollControlDisabledMaxHeightRatio == newValue) {
|
|
return;
|
|
}
|
|
|
|
_scrollControlDisabledMaxHeightRatio = newValue;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
@override
|
|
double computeMinIntrinsicWidth(double height) => 0.0;
|
|
|
|
@override
|
|
double computeMaxIntrinsicWidth(double height) => 0.0;
|
|
|
|
@override
|
|
double computeMinIntrinsicHeight(double width) => 0.0;
|
|
|
|
@override
|
|
double computeMaxIntrinsicHeight(double width) => 0.0;
|
|
|
|
@override
|
|
Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
|
|
|
|
@override
|
|
double? computeDryBaseline(
|
|
covariant BoxConstraints constraints,
|
|
TextBaseline baseline,
|
|
) {
|
|
final RenderBox? child = this.child;
|
|
if (child == null) {
|
|
return null;
|
|
}
|
|
final BoxConstraints childConstraints = _getConstraintsForChild(
|
|
constraints,
|
|
);
|
|
final double? result = child.getDryBaseline(childConstraints, baseline);
|
|
if (result == null) {
|
|
return null;
|
|
}
|
|
final Size childSize = childConstraints.isTight
|
|
? childConstraints.smallest
|
|
: child.getDryLayout(childConstraints);
|
|
return result + _getPositionForChild(constraints.biggest, childSize).dy;
|
|
}
|
|
|
|
BoxConstraints _getConstraintsForChild(BoxConstraints constraints) {
|
|
return BoxConstraints(
|
|
minWidth: constraints.maxWidth,
|
|
maxWidth: constraints.maxWidth,
|
|
maxHeight: isScrollControlled
|
|
? constraints.maxHeight
|
|
: constraints.maxHeight * scrollControlDisabledMaxHeightRatio,
|
|
);
|
|
}
|
|
|
|
Offset _getPositionForChild(Size size, Size childSize) {
|
|
return Offset(0.0, size.height - childSize.height * animationValue);
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
size = constraints.biggest;
|
|
final RenderBox? child = this.child;
|
|
if (child == null) {
|
|
return;
|
|
}
|
|
|
|
final BoxConstraints childConstraints = _getConstraintsForChild(
|
|
constraints,
|
|
);
|
|
assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
|
|
child.layout(childConstraints, parentUsesSize: !childConstraints.isTight);
|
|
final BoxParentData childParentData = child.parentData! as BoxParentData;
|
|
final Size childSize = childConstraints.isTight
|
|
? childConstraints.smallest
|
|
: child.size;
|
|
childParentData.offset = _getPositionForChild(size, childSize);
|
|
|
|
if (_lastSize != childSize) {
|
|
_lastSize = childSize;
|
|
_onChildSizeChanged.call(_lastSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _ModalBottomSheet<T> extends StatefulWidget {
|
|
const _ModalBottomSheet({
|
|
super.key,
|
|
required this.route,
|
|
this.backgroundColor,
|
|
this.elevation,
|
|
this.shape,
|
|
this.clipBehavior,
|
|
this.constraints,
|
|
this.isScrollControlled = false,
|
|
this.scrollControlDisabledMaxHeightRatio =
|
|
_defaultScrollControlDisabledMaxHeightRatio,
|
|
this.enableDrag = true,
|
|
this.showDragHandle = false,
|
|
});
|
|
|
|
final ModalBottomSheetRoute<T> route;
|
|
final bool isScrollControlled;
|
|
final double scrollControlDisabledMaxHeightRatio;
|
|
final Color? backgroundColor;
|
|
final double? elevation;
|
|
final ShapeBorder? shape;
|
|
final Clip? clipBehavior;
|
|
final BoxConstraints? constraints;
|
|
final bool enableDrag;
|
|
final bool showDragHandle;
|
|
|
|
@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;
|
|
}
|
|
}
|
|
|
|
EdgeInsets _getNewClipDetails(Size topLayerSize) {
|
|
return EdgeInsets.fromLTRB(0, 0, 0, topLayerSize.height);
|
|
}
|
|
|
|
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 = Split(
|
|
widget.route.animation!.value,
|
|
endCurve: _modalBottomSheetCurve,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
assert(debugCheckHasMediaQuery(context));
|
|
assert(debugCheckHasMaterialLocalizations(context));
|
|
final MaterialLocalizations localizations = MaterialLocalizations.of(
|
|
context,
|
|
);
|
|
final String routeLabel = _getRouteLabel(localizations);
|
|
|
|
return AnimatedBuilder(
|
|
animation: widget.route.animation!,
|
|
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,
|
|
constraints: widget.constraints,
|
|
enableDrag: widget.enableDrag,
|
|
showDragHandle: widget.showDragHandle,
|
|
onDragStart: handleDragStart,
|
|
onDragEnd: handleDragEnd,
|
|
),
|
|
builder: (BuildContext context, Widget? child) {
|
|
final double animationValue = animationCurve.transform(
|
|
widget.route.animation!.value,
|
|
);
|
|
return Semantics(
|
|
scopesRoute: true,
|
|
namesRoute: true,
|
|
label: routeLabel,
|
|
explicitChildNodes: true,
|
|
child: ClipRect(
|
|
child: _BottomSheetLayoutWithSizeListener(
|
|
onChildSizeChanged: (Size size) {
|
|
widget.route._didChangeBarrierSemanticsClip(
|
|
_getNewClipDetails(size),
|
|
);
|
|
},
|
|
animationValue: animationValue,
|
|
isScrollControlled: widget.isScrollControlled,
|
|
scrollControlDisabledMaxHeightRatio:
|
|
widget.scrollControlDisabledMaxHeightRatio,
|
|
child: child,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A route that represents a Material Design modal bottom sheet.
|
|
///
|
|
/// {@template flutter.material.ModalBottomSheetRoute}
|
|
/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
|
|
/// the user from interacting with the rest of the app.
|
|
///
|
|
/// A closely related widget is a persistent bottom sheet, which shows
|
|
/// information that supplements the primary content of the app without
|
|
/// preventing the user from interacting with the app. Persistent bottom sheets
|
|
/// can be created and displayed with the [showBottomSheet] function or the
|
|
/// [ScaffoldState.showBottomSheet] method.
|
|
///
|
|
/// The [isScrollControlled] parameter specifies whether this is a route for
|
|
/// a bottom sheet that will utilize [DraggableScrollableSheet]. Consider
|
|
/// setting this parameter to true if this bottom sheet has
|
|
/// a scrollable child, such as a [ListView] or a [GridView],
|
|
/// to have the bottom sheet be draggable.
|
|
///
|
|
/// The [isDismissible] parameter specifies whether the bottom sheet will be
|
|
/// dismissed when user taps on the scrim.
|
|
///
|
|
/// The [enableDrag] parameter specifies whether the bottom sheet can be
|
|
/// dragged up and down and dismissed by swiping downwards.
|
|
///
|
|
/// The [useSafeArea] parameter specifies whether the sheet will avoid system
|
|
/// intrusions on the top, left, and right. If false, no [SafeArea] is added;
|
|
/// and [MediaQuery.removePadding] is applied to the top,
|
|
/// so that system intrusions at the top will not be avoided by a [SafeArea]
|
|
/// inside the bottom sheet either.
|
|
/// Defaults to false.
|
|
///
|
|
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
|
|
/// [constraints] and [transitionAnimationController]
|
|
/// parameters can be passed in to customize the appearance and behavior of
|
|
/// modal bottom sheets (see the documentation for these on [BottomSheet]
|
|
/// for more details).
|
|
///
|
|
/// The [transitionAnimationController] controls the bottom sheet's entrance and
|
|
/// exit animations. It's up to the owner of the controller to call
|
|
/// [AnimationController.dispose] when the controller is no longer needed.
|
|
///
|
|
/// The optional `settings` parameter sets the [RouteSettings] of the modal bottom sheet
|
|
/// sheet. This is particularly useful in the case that a user wants to observe
|
|
/// [PopupRoute]s within a [NavigatorObserver].
|
|
/// {@endtemplate}
|
|
///
|
|
/// {@macro flutter.widgets.RawDialogRoute}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [showModalBottomSheet], which is a way to display a ModalBottomSheetRoute.
|
|
/// * [BottomSheet], which becomes the parent of the widget returned by the
|
|
/// function passed as the `builder` argument to [showModalBottomSheet].
|
|
/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
|
|
/// non-modal bottom sheets.
|
|
/// * [DraggableScrollableSheet], creates a bottom sheet that grows
|
|
/// and then becomes scrollable once it reaches its maximum size.
|
|
/// * [DisplayFeatureSubScreen], which documents the specifics of how
|
|
/// [DisplayFeature]s can split the screen into sub-screens.
|
|
/// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
|
|
/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
|
|
class ModalBottomSheetRoute<T> extends PopupRoute<T> {
|
|
/// A modal bottom sheet route.
|
|
ModalBottomSheetRoute({
|
|
required this.builder,
|
|
this.capturedThemes,
|
|
this.barrierLabel,
|
|
this.barrierOnTapHint,
|
|
this.backgroundColor,
|
|
this.elevation,
|
|
this.shape,
|
|
this.clipBehavior,
|
|
this.constraints,
|
|
this.modalBarrierColor,
|
|
this.isDismissible = true,
|
|
this.enableDrag = true,
|
|
this.showDragHandle,
|
|
required this.isScrollControlled,
|
|
this.scrollControlDisabledMaxHeightRatio =
|
|
_defaultScrollControlDisabledMaxHeightRatio,
|
|
super.settings,
|
|
super.requestFocus,
|
|
this.transitionAnimationController,
|
|
this.anchorPoint,
|
|
this.useSafeArea = false,
|
|
this.sheetAnimationStyle,
|
|
});
|
|
|
|
/// A builder for the contents of the sheet.
|
|
///
|
|
/// The bottom sheet will wrap the widget produced by this builder in a
|
|
/// [Material] widget.
|
|
final WidgetBuilder builder;
|
|
|
|
/// Stores a list of captured [InheritedTheme]s that are wrapped around the
|
|
/// bottom sheet.
|
|
///
|
|
/// Consider setting this attribute when the [ModalBottomSheetRoute]
|
|
/// is created through [Navigator.push] and its friends.
|
|
final CapturedThemes? capturedThemes;
|
|
|
|
/// Specifies whether this is a route for a bottom sheet that will utilize
|
|
/// [DraggableScrollableSheet].
|
|
///
|
|
/// Consider setting this parameter to true if this bottom sheet has
|
|
/// a scrollable child, such as a [ListView] or a [GridView],
|
|
/// to have the bottom sheet be draggable.
|
|
final bool isScrollControlled;
|
|
|
|
/// The max height constraint ratio for the bottom sheet
|
|
/// when [isScrollControlled] is set to false,
|
|
/// no ratio will be applied when [isScrollControlled] is set to true.
|
|
///
|
|
/// Defaults to 9 / 16.
|
|
final double scrollControlDisabledMaxHeightRatio;
|
|
|
|
/// The bottom sheet's background color.
|
|
///
|
|
/// Defines the bottom sheet's [Material.color].
|
|
///
|
|
/// If this property is not provided, it falls back to [Material]'s default.
|
|
final Color? backgroundColor;
|
|
|
|
/// The z-coordinate at which to place this material relative to its parent.
|
|
///
|
|
/// This controls the size of the shadow below the material.
|
|
///
|
|
/// Defaults to 0, must not be negative.
|
|
final double? elevation;
|
|
|
|
/// The shape of the bottom sheet.
|
|
///
|
|
/// Defines the bottom sheet's [Material.shape].
|
|
///
|
|
/// If this property is not provided, it falls back to [Material]'s default.
|
|
final ShapeBorder? shape;
|
|
|
|
/// {@macro flutter.material.Material.clipBehavior}
|
|
///
|
|
/// Defines the bottom sheet's [Material.clipBehavior].
|
|
///
|
|
/// Use this property to enable clipping of content when the bottom sheet has
|
|
/// a custom [shape] and the content can extend past this shape. For example,
|
|
/// a bottom sheet with rounded corners and an edge-to-edge [Image] at the
|
|
/// top.
|
|
///
|
|
/// If this property is null, the [BottomSheetThemeData.clipBehavior] of
|
|
/// [ThemeData.bottomSheetTheme] is used. If that's null, the behavior defaults to [Clip.none]
|
|
/// will be [Clip.none].
|
|
final Clip? clipBehavior;
|
|
|
|
/// Defines minimum and maximum sizes for a [BottomSheet].
|
|
///
|
|
/// If null, the ambient [ThemeData.bottomSheetTheme]'s
|
|
/// [BottomSheetThemeData.constraints] will be used. If that
|
|
/// is null and [ThemeData.useMaterial3] is true, then the bottom sheet
|
|
/// will have a max width of 640dp. If [ThemeData.useMaterial3] is false, then
|
|
/// the bottom sheet's size will be constrained by its parent
|
|
/// (usually a [Scaffold]). In this case, consider limiting the width by
|
|
/// setting smaller constraints for large screens.
|
|
///
|
|
/// If constraints are specified (either in this property or in the
|
|
/// theme), the bottom sheet will be aligned to the bottom-center of
|
|
/// the available space. Otherwise, no alignment is applied.
|
|
final BoxConstraints? constraints;
|
|
|
|
/// Specifies the color of the modal barrier that darkens everything below the
|
|
/// bottom sheet.
|
|
///
|
|
/// Defaults to `Colors.black54` if not provided.
|
|
final Color? modalBarrierColor;
|
|
|
|
/// Specifies whether the bottom sheet will be dismissed
|
|
/// when user taps on the scrim.
|
|
///
|
|
/// If true, the bottom sheet will be dismissed when user taps on the scrim.
|
|
///
|
|
/// Defaults to true.
|
|
final bool isDismissible;
|
|
|
|
/// Specifies whether the bottom sheet can be dragged up and down
|
|
/// and dismissed by swiping downwards.
|
|
///
|
|
/// If true, the bottom sheet can be dragged up and down and dismissed by
|
|
/// swiping downwards.
|
|
///
|
|
/// This applies to the content below the drag handle, if showDragHandle is true.
|
|
///
|
|
/// Defaults is true.
|
|
final bool enableDrag;
|
|
|
|
/// Specifies whether a drag handle is shown.
|
|
///
|
|
/// The drag handle appears at the top of the bottom sheet. The default color is
|
|
/// [ColorScheme.onSurfaceVariant] with an opacity of 0.4 and can be customized
|
|
/// using dragHandleColor. The default size is `Size(32,4)` and can be customized
|
|
/// with dragHandleSize.
|
|
///
|
|
/// If null, then the value of [BottomSheetThemeData.showDragHandle] is used. If
|
|
/// that is also null, defaults to false.
|
|
final bool? showDragHandle;
|
|
|
|
/// The animation controller that controls the bottom sheet's entrance and
|
|
/// exit animations.
|
|
///
|
|
/// The BottomSheet widget will manipulate the position of this animation, it
|
|
/// is not just a passive observer.
|
|
final AnimationController? transitionAnimationController;
|
|
|
|
/// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
|
|
final Offset? anchorPoint;
|
|
|
|
/// Whether to avoid system intrusions on the top, left, and right.
|
|
///
|
|
/// If true, a [SafeArea] is inserted to keep the bottom sheet away from
|
|
/// system intrusions at the top, left, and right sides of the screen.
|
|
///
|
|
/// If false, the bottom sheet will extend through any system intrusions
|
|
/// at the top, left, and right.
|
|
///
|
|
/// If false, then moreover [MediaQuery.removePadding] will be used
|
|
/// to remove top padding, so that a [SafeArea] widget inside the bottom
|
|
/// sheet will have no effect at the top edge. If this is undesired, consider
|
|
/// setting [useSafeArea] to true. Alternatively, wrap the [SafeArea] in a
|
|
/// [MediaQuery] that restates an ambient [MediaQueryData] from outside [builder].
|
|
///
|
|
/// In either case, the bottom sheet extends all the way to the bottom of
|
|
/// the screen, including any system intrusions.
|
|
///
|
|
/// The default is false.
|
|
final bool useSafeArea;
|
|
|
|
/// Used to override the modal bottom sheet animation duration and reverse
|
|
/// animation duration.
|
|
///
|
|
/// If [AnimationStyle.duration] is provided, it will be used to override
|
|
/// the modal bottom sheet animation duration in the underlying
|
|
/// [BottomSheet.createAnimationController].
|
|
///
|
|
/// If [AnimationStyle.reverseDuration] is provided, it will be used to
|
|
/// override the modal bottom sheet reverse animation duration in the
|
|
/// underlying [BottomSheet.createAnimationController].
|
|
///
|
|
/// To disable the modal bottom sheet animation, use [AnimationStyle.noAnimation].
|
|
final AnimationStyle? sheetAnimationStyle;
|
|
|
|
/// {@template flutter.material.ModalBottomSheetRoute.barrierOnTapHint}
|
|
/// The semantic hint text that informs users what will happen if they
|
|
/// tap on the widget. Announced in the format of 'Double tap to ...'.
|
|
///
|
|
/// If the field is null, the default hint will be used, which results in
|
|
/// announcement of 'Double tap to activate'.
|
|
/// {@endtemplate}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [barrierDismissible], which controls the behavior of the barrier when
|
|
/// tapped.
|
|
/// * [ModalBarrier], which uses this field as onTapHint when it has an onTap action.
|
|
final String? barrierOnTapHint;
|
|
|
|
final ValueNotifier<EdgeInsets> _clipDetailsNotifier =
|
|
ValueNotifier<EdgeInsets>(EdgeInsets.zero);
|
|
|
|
@override
|
|
void dispose() {
|
|
_clipDetailsNotifier.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
/// Updates the details regarding how the [SemanticsNode.rect] (focus) of
|
|
/// the barrier for this [ModalBottomSheetRoute] should be clipped.
|
|
///
|
|
/// Returns true if the clipDetails did change and false otherwise.
|
|
bool _didChangeBarrierSemanticsClip(EdgeInsets newClipDetails) {
|
|
if (_clipDetailsNotifier.value == newClipDetails) {
|
|
return false;
|
|
}
|
|
_clipDetailsNotifier.value = newClipDetails;
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Duration get transitionDuration =>
|
|
transitionAnimationController?.duration ??
|
|
sheetAnimationStyle?.duration ??
|
|
_bottomSheetEnterDuration;
|
|
|
|
@override
|
|
Duration get reverseTransitionDuration =>
|
|
transitionAnimationController?.reverseDuration ??
|
|
transitionAnimationController?.duration ??
|
|
sheetAnimationStyle?.reverseDuration ??
|
|
_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);
|
|
if (transitionAnimationController != null) {
|
|
_animationController = transitionAnimationController;
|
|
willDisposeAnimationController = false;
|
|
} else {
|
|
_animationController = BottomSheet.createAnimationController(
|
|
navigator!,
|
|
sheetAnimationStyle: sheetAnimationStyle,
|
|
);
|
|
}
|
|
return _animationController!;
|
|
}
|
|
|
|
@override
|
|
Widget buildPage(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
) {
|
|
final Widget content = DisplayFeatureSubScreen(
|
|
anchorPoint: anchorPoint,
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
final BottomSheetThemeData sheetTheme = Theme.of(
|
|
context,
|
|
).bottomSheetTheme;
|
|
final BottomSheetThemeData defaults = Theme.of(context).useMaterial3
|
|
? _BottomSheetDefaultsM3(context)
|
|
: const BottomSheetThemeData();
|
|
return _ModalBottomSheet<T>(
|
|
route: this,
|
|
backgroundColor:
|
|
backgroundColor ??
|
|
sheetTheme.modalBackgroundColor ??
|
|
sheetTheme.backgroundColor ??
|
|
defaults.backgroundColor,
|
|
elevation:
|
|
elevation ??
|
|
sheetTheme.modalElevation ??
|
|
sheetTheme.elevation ??
|
|
defaults.modalElevation,
|
|
shape: shape,
|
|
clipBehavior: clipBehavior,
|
|
constraints: constraints,
|
|
isScrollControlled: isScrollControlled,
|
|
scrollControlDisabledMaxHeightRatio:
|
|
scrollControlDisabledMaxHeightRatio,
|
|
enableDrag: enableDrag,
|
|
showDragHandle:
|
|
showDragHandle ??
|
|
(enableDrag && (sheetTheme.showDragHandle ?? false)),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
final Widget bottomSheet = useSafeArea
|
|
? SafeArea(bottom: false, child: content)
|
|
: MediaQuery.removePadding(
|
|
context: context,
|
|
removeTop: true,
|
|
child: content,
|
|
);
|
|
|
|
return capturedThemes?.wrap(bottomSheet) ?? bottomSheet;
|
|
}
|
|
|
|
@override
|
|
Widget buildModalBarrier() {
|
|
if (barrierColor.a != 0 && !offstage) {
|
|
// changedInternalState is called if barrierColor or offstage updates
|
|
assert(barrierColor != barrierColor.withValues(alpha: 0.0));
|
|
final Animation<Color?> color = animation!.drive(
|
|
ColorTween(
|
|
begin: barrierColor.withValues(alpha: 0.0),
|
|
end:
|
|
barrierColor, // changedInternalState is called if barrierColor updates
|
|
).chain(
|
|
CurveTween(curve: barrierCurve),
|
|
), // changedInternalState is called if barrierCurve updates
|
|
);
|
|
return AnimatedModalBarrier(
|
|
color: color,
|
|
dismissible:
|
|
barrierDismissible, // changedInternalState is called if barrierDismissible updates
|
|
semanticsLabel:
|
|
barrierLabel, // changedInternalState is called if barrierLabel updates
|
|
barrierSemanticsDismissible: semanticsDismissible,
|
|
clipDetailsNotifier: _clipDetailsNotifier,
|
|
semanticsOnTapHint: barrierOnTapHint,
|
|
);
|
|
} else {
|
|
return ModalBarrier(
|
|
dismissible:
|
|
barrierDismissible, // changedInternalState is called if barrierDismissible updates
|
|
semanticsLabel:
|
|
barrierLabel, // changedInternalState is called if barrierLabel updates
|
|
barrierSemanticsDismissible: semanticsDismissible,
|
|
clipDetailsNotifier: _clipDetailsNotifier,
|
|
semanticsOnTapHint: barrierOnTapHint,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Shows a modal Material Design bottom sheet.
|
|
///
|
|
/// {@macro flutter.material.ModalBottomSheetRoute}
|
|
///
|
|
/// {@macro flutter.widgets.RawDialogRoute}
|
|
///
|
|
/// The `context` argument is used to look up the [Navigator] and [Theme] for
|
|
/// the bottom sheet. It is only used when the method is called. Its
|
|
/// corresponding widget can be safely removed from the tree before the bottom
|
|
/// sheet is closed.
|
|
///
|
|
/// The `useRootNavigator` parameter ensures that the root navigator is used to
|
|
/// display the [BottomSheet] when set to `true`. This is useful in the case
|
|
/// that a modal [BottomSheet] needs to be displayed above all other content
|
|
/// but the caller is inside another [Navigator].
|
|
///
|
|
/// Returns a `Future` that resolves to the value (if any) that was passed to
|
|
/// [Navigator.pop] when the modal bottom sheet was closed.
|
|
///
|
|
/// The 'barrierLabel' parameter can be used to set a custom barrier label.
|
|
/// Will default to [MaterialLocalizations.modalBarrierDismissLabel] of context
|
|
/// if not set.
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This example demonstrates how to use [showModalBottomSheet] to display a
|
|
/// bottom sheet that obscures the content behind it when a user taps a button.
|
|
/// It also demonstrates how to close the bottom sheet using the [Navigator]
|
|
/// when a user taps on a button inside the bottom sheet.
|
|
///
|
|
/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This sample shows the creation of [showModalBottomSheet], as described in:
|
|
/// https://m3.material.io/components/bottom-sheets/overview
|
|
///
|
|
/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.1.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// The [sheetAnimationStyle] parameter is used to override the modal bottom sheet
|
|
/// animation duration and reverse animation duration.
|
|
///
|
|
/// The [requestFocus] parameter is used to specify whether the bottom sheet should
|
|
/// request focus when shown.
|
|
/// {@macro flutter.widgets.navigator.Route.requestFocus}
|
|
///
|
|
/// If [AnimationStyle.duration] is provided, it will be used to override
|
|
/// the modal bottom sheet animation duration in the underlying
|
|
/// [BottomSheet.createAnimationController].
|
|
///
|
|
/// If [AnimationStyle.reverseDuration] is provided, it will be used to
|
|
/// override the modal bottom sheet reverse animation duration in the
|
|
/// underlying [BottomSheet.createAnimationController].
|
|
///
|
|
/// To disable the bottom sheet animation, use [AnimationStyle.noAnimation].
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This sample showcases how to override the [showModalBottomSheet] animation
|
|
/// duration and reverse animation duration using [AnimationStyle].
|
|
///
|
|
/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [BottomSheet], which becomes the parent of the widget returned by the
|
|
/// function passed as the `builder` argument to [showModalBottomSheet].
|
|
/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
|
|
/// non-modal bottom sheets.
|
|
/// * [DraggableScrollableSheet], creates a bottom sheet that grows
|
|
/// and then becomes scrollable once it reaches its maximum size.
|
|
/// * [DisplayFeatureSubScreen], which documents the specifics of how
|
|
/// [DisplayFeature]s can split the screen into sub-screens.
|
|
/// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
|
|
/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
|
|
/// * [AnimationStyle], which is used to override the modal bottom sheet
|
|
/// animation duration and reverse animation duration.
|
|
Future<T?> showModalBottomSheet<T>({
|
|
required BuildContext context,
|
|
required WidgetBuilder builder,
|
|
Color? backgroundColor,
|
|
String? barrierLabel,
|
|
double? elevation,
|
|
ShapeBorder? shape,
|
|
Clip? clipBehavior,
|
|
BoxConstraints? constraints,
|
|
Color? barrierColor,
|
|
bool isScrollControlled = false,
|
|
double scrollControlDisabledMaxHeightRatio =
|
|
_defaultScrollControlDisabledMaxHeightRatio,
|
|
bool useRootNavigator = false,
|
|
bool isDismissible = true,
|
|
bool enableDrag = true,
|
|
bool? showDragHandle,
|
|
bool useSafeArea = false,
|
|
RouteSettings? routeSettings,
|
|
AnimationController? transitionAnimationController,
|
|
Offset? anchorPoint,
|
|
AnimationStyle? sheetAnimationStyle,
|
|
bool? requestFocus,
|
|
}) {
|
|
assert(debugCheckHasMediaQuery(context));
|
|
assert(debugCheckHasMaterialLocalizations(context));
|
|
|
|
final NavigatorState navigator = Navigator.of(
|
|
context,
|
|
rootNavigator: useRootNavigator,
|
|
);
|
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
return navigator.push(
|
|
ModalBottomSheetRoute<T>(
|
|
builder: builder,
|
|
capturedThemes: InheritedTheme.capture(
|
|
from: context,
|
|
to: navigator.context,
|
|
),
|
|
isScrollControlled: isScrollControlled,
|
|
scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
|
|
barrierLabel: barrierLabel ?? localizations.scrimLabel,
|
|
barrierOnTapHint: localizations.scrimOnTapHint(
|
|
localizations.bottomSheetLabel,
|
|
),
|
|
backgroundColor: backgroundColor,
|
|
elevation: elevation,
|
|
shape: shape,
|
|
clipBehavior: clipBehavior,
|
|
constraints: constraints,
|
|
isDismissible: isDismissible,
|
|
modalBarrierColor:
|
|
barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor,
|
|
enableDrag: enableDrag,
|
|
showDragHandle: showDragHandle,
|
|
settings: routeSettings,
|
|
transitionAnimationController: transitionAnimationController,
|
|
anchorPoint: anchorPoint,
|
|
useSafeArea: useSafeArea,
|
|
sheetAnimationStyle: sheetAnimationStyle,
|
|
requestFocus: requestFocus,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Shows a Material Design bottom sheet in the nearest [Scaffold] ancestor. To
|
|
/// show a persistent bottom sheet, use the [Scaffold.bottomSheet].
|
|
///
|
|
/// Returns a controller that can be used to close and otherwise manipulate the
|
|
/// bottom sheet.
|
|
///
|
|
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
|
|
/// [constraints] and [transitionAnimationController]
|
|
/// parameters can be passed in to customize the appearance and behavior of
|
|
/// persistent bottom sheets (see the documentation for these on [BottomSheet]
|
|
/// for more details).
|
|
///
|
|
/// The [enableDrag] parameter specifies whether the bottom sheet can be
|
|
/// dragged up and down and dismissed by swiping downwards.
|
|
///
|
|
/// The [sheetAnimationStyle] parameter is used to override the bottom sheet
|
|
/// animation duration and reverse animation duration.
|
|
///
|
|
/// If [AnimationStyle.duration] is provided, it will be used to override
|
|
/// the bottom sheet animation duration in the underlying
|
|
/// [BottomSheet.createAnimationController].
|
|
///
|
|
/// If [AnimationStyle.reverseDuration] is provided, it will be used to
|
|
/// override the bottom sheet reverse animation duration in the underlying
|
|
/// [BottomSheet.createAnimationController].
|
|
///
|
|
/// To disable the bottom sheet animation, use [AnimationStyle.noAnimation].
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This sample showcases how to override the [showBottomSheet] animation
|
|
/// duration and reverse animation duration using [AnimationStyle].
|
|
///
|
|
/// ** See code in examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// To rebuild the bottom sheet (e.g. if it is stateful), call
|
|
/// [PersistentBottomSheetController.setState] on the controller returned by
|
|
/// this method.
|
|
///
|
|
/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing
|
|
/// [ModalRoute] and a back button is added to the app bar of the [Scaffold]
|
|
/// that closes the bottom sheet.
|
|
///
|
|
/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
|
|
/// does not add a back button to the enclosing Scaffold's app bar, use the
|
|
/// [Scaffold.bottomSheet] constructor parameter.
|
|
///
|
|
/// A closely related widget is a modal bottom sheet, which is an alternative
|
|
/// to a menu or a dialog and prevents the user from interacting with the rest
|
|
/// of the app. Modal bottom sheets can be created and displayed with the
|
|
/// [showModalBottomSheet] function.
|
|
///
|
|
/// The `context` argument is used to look up the [Scaffold] for the bottom
|
|
/// sheet. It is only used when the method is called. Its corresponding widget
|
|
/// can be safely removed from the tree before the bottom sheet is closed.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [BottomSheet], which becomes the parent of the widget returned by the
|
|
/// `builder`.
|
|
/// * [showModalBottomSheet], which can be used to display a modal bottom
|
|
/// sheet.
|
|
/// * [Scaffold.of], for information about how to obtain the [BuildContext].
|
|
/// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
|
|
/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
|
|
/// * [AnimationStyle], which is used to override the bottom sheet animation
|
|
/// duration and reverse animation duration.
|
|
bool debugCheckHasScaffold(BuildContext context) {
|
|
assert(() {
|
|
if (context.widget is! Scaffold &&
|
|
context.findAncestorWidgetOfExactType<Scaffold>() == null) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('No Scaffold widget found.'),
|
|
ErrorDescription(
|
|
'${context.widget.runtimeType} widgets require a Scaffold widget ancestor.',
|
|
),
|
|
...context.describeMissingAncestor(expectedAncestorType: Scaffold),
|
|
ErrorHint(
|
|
'Typically, the Scaffold widget is introduced by the MaterialApp or '
|
|
'WidgetsApp widget at the top of your application widget tree.',
|
|
),
|
|
]);
|
|
}
|
|
return true;
|
|
}());
|
|
return true;
|
|
}
|
|
|
|
PersistentBottomSheetController showBottomSheet({
|
|
required BuildContext context,
|
|
required WidgetBuilder builder,
|
|
Color? backgroundColor,
|
|
double? elevation,
|
|
ShapeBorder? shape,
|
|
Clip? clipBehavior,
|
|
BoxConstraints? constraints,
|
|
bool? enableDrag,
|
|
bool? showDragHandle,
|
|
AnimationController? transitionAnimationController,
|
|
AnimationStyle? sheetAnimationStyle,
|
|
}) {
|
|
assert(debugCheckHasScaffold(context));
|
|
|
|
return Scaffold.of(context).showBottomSheet(
|
|
builder,
|
|
backgroundColor: backgroundColor,
|
|
elevation: elevation,
|
|
shape: shape,
|
|
clipBehavior: clipBehavior,
|
|
constraints: constraints,
|
|
enableDrag: enableDrag,
|
|
showDragHandle: showDragHandle,
|
|
transitionAnimationController: transitionAnimationController,
|
|
sheetAnimationStyle: sheetAnimationStyle,
|
|
);
|
|
}
|
|
|
|
class _BottomSheetGestureDetector extends StatelessWidget {
|
|
const _BottomSheetGestureDetector({
|
|
required this.child,
|
|
required this.onVerticalDragStart,
|
|
required this.onVerticalDragUpdate,
|
|
required this.onVerticalDragEnd,
|
|
});
|
|
|
|
final Widget child;
|
|
final GestureDragStartCallback onVerticalDragStart;
|
|
final GestureDragUpdateCallback onVerticalDragUpdate;
|
|
final GestureDragEndCallback onVerticalDragEnd;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return RawGestureDetector(
|
|
excludeFromSemantics: true,
|
|
gestures: <Type, GestureRecognizerFactory<GestureRecognizer>>{
|
|
VerticalDragGestureRecognizer:
|
|
GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
|
|
() => VerticalDragGestureRecognizer(debugOwner: this),
|
|
(VerticalDragGestureRecognizer instance) {
|
|
instance
|
|
..onStart = onVerticalDragStart
|
|
..onUpdate = onVerticalDragUpdate
|
|
..onEnd = onVerticalDragEnd
|
|
..gestureSettings = MediaQuery.maybeGestureSettingsOf(context)
|
|
..onlyAcceptDragOnThreshold = true;
|
|
},
|
|
),
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
}
|
|
|
|
// BEGIN GENERATED TOKEN PROPERTIES - BottomSheet
|
|
|
|
// Do not edit by hand. The code between the "BEGIN GENERATED" and
|
|
// "END GENERATED" comments are generated from data in the Material
|
|
// Design token database by the script:
|
|
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
|
|
|
// dart format off
|
|
class _BottomSheetDefaultsM3 extends BottomSheetThemeData {
|
|
_BottomSheetDefaultsM3(this.context)
|
|
: super(
|
|
elevation: 1.0,
|
|
modalElevation: 1.0,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28.0))),
|
|
constraints: const BoxConstraints(maxWidth: 640),
|
|
);
|
|
|
|
final BuildContext context;
|
|
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
|
|
|
@override
|
|
Color? get backgroundColor => _colors.surfaceContainerLow;
|
|
|
|
@override
|
|
Color? get surfaceTintColor => Colors.transparent;
|
|
|
|
@override
|
|
Color? get shadowColor => Colors.transparent;
|
|
|
|
@override
|
|
Color? get dragHandleColor => _colors.onSurfaceVariant;
|
|
|
|
@override
|
|
Size? get dragHandleSize => const Size(32, 4);
|
|
|
|
@override
|
|
BoxConstraints? get constraints => const BoxConstraints(maxWidth: 640.0);
|
|
}
|
|
// dart format on
|
|
|
|
// END GENERATED TOKEN PROPERTIES - BottomSheet
|