diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 5460d9942..7ce611ab8 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -41,6 +41,15 @@ jobs: channel: stable flutter-version-file: pubspec.yaml + - name: apply bottom sheet patch + working-directory: ${{ env.FLUTTER_ROOT }} + run: | + git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add . + git commit -m "bottom sheet patch" + - name: 下载项目依赖 run: flutter pub get diff --git a/lib/common/widgets/scaffold/bottom_sheet.dart b/lib/common/widgets/scaffold/bottom_sheet.dart deleted file mode 100644 index 37a600f94..000000000 --- a/lib/common/widgets/scaffold/bottom_sheet.dart +++ /dev/null @@ -1,1557 +0,0 @@ -// 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 . -/// * The Material 3 spec at . -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 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 { - 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 dragHandleStates = {}; - - 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( - onNotification: extentChanged, - child: !showDragHandle - ? widget.builder(context) - : Stack( - alignment: Alignment.topCenter, - children: [ - 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 handleHover; - final Set 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( - dragHandleColor, - states, - ) ?? - WidgetStateProperty.resolveAs( - 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 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 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 get onChildSizeChanged => _onChildSizeChanged; - ValueChanged _onChildSizeChanged; - set onChildSizeChanged(ValueChanged 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 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 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 createState() => _ModalBottomSheetState(); -} - -class _ModalBottomSheetState extends State<_ModalBottomSheet> { - ParametricCurve 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 . -/// * The Material 3 spec at . -class ModalBottomSheetRoute extends PopupRoute { - /// 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 _clipDetailsNotifier = - ValueNotifier(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 animation, - Animation 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( - 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 = 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 . -/// * The Material 3 spec at . -/// * [AnimationStyle], which is used to override the modal bottom sheet -/// animation duration and reverse animation duration. -Future showModalBottomSheet({ - 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( - 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 . -/// * The Material 3 spec at . -/// * [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() == null) { - throw FlutterError.fromParts([ - 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: >{ - VerticalDragGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => 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 diff --git a/lib/common/widgets/scaffold/scaffold.dart b/lib/common/widgets/scaffold/scaffold.dart deleted file mode 100644 index 95157bfdf..000000000 --- a/lib/common/widgets/scaffold/scaffold.dart +++ /dev/null @@ -1,3556 +0,0 @@ -// 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. - -// ignore_for_file: uri_does_not_exist_in_doc_import - -/// @docImport 'package:flutter/services.dart'; -/// -/// @docImport 'app.dart'; -/// @docImport 'bottom_app_bar.dart'; -/// @docImport 'bottom_navigation_bar.dart'; -/// @docImport 'bottom_sheet_theme.dart'; -/// @docImport 'drawer_theme.dart'; -/// @docImport 'icon_button.dart'; -/// @docImport 'tab_controller.dart'; -/// @docImport 'tabs.dart'; -/// @docImport 'text_button.dart'; -library; - -import 'dart:async'; -import 'dart:collection'; -import 'dart:math' as math; -import 'dart:ui'; - -import 'package:PiliPlus/common/widgets/scaffold/bottom_sheet.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart' show DragStartBehavior; -import 'package:flutter/material.dart' - hide showBottomSheet, showModalBottomSheet, BottomSheet; -import 'package:flutter/material.dart' as material; - -// Examples can assume: -// late TabController tabController; -// void setState(VoidCallback fn) { } -// late String appBarTitle; -// late int tabCount; -// late TickerProvider tickerProvider; - -const FloatingActionButtonLocation _kDefaultFloatingActionButtonLocation = - FloatingActionButtonLocation.endFloat; -const FloatingActionButtonAnimator _kDefaultFloatingActionButtonAnimator = - FloatingActionButtonAnimator.scaling; - -const Curve _standardBottomSheetCurve = standardEasing; -// When the top of the BottomSheet crosses this threshold, it will start to -// shrink the FAB and show a scrim. -const double _kBottomSheetDominatesPercentage = 0.3; -const double _kMinBottomSheetScrimOpacity = 0.1; -const double _kMaxBottomSheetScrimOpacity = 0.6; - -enum _ScaffoldSlot { - body, - appBar, - bodyScrim, - bottomSheet, - snackBar, - materialBanner, - persistentFooter, - bottomNavigationBar, - floatingActionButton, - drawer, - endDrawer, - statusBar, -} - -/// Manages [SnackBar]s and [MaterialBanner]s for descendant [Scaffold]s. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=lytQi-slT5Y} -/// -/// This class provides APIs for showing snack bars and material banners at the -/// bottom and top of the screen, respectively. -/// -/// To display one of these notifications, obtain the [ScaffoldMessengerState] -/// for the current [BuildContext] via [ScaffoldMessenger.of] and use the -/// [ScaffoldMessengerState.showSnackBar] or the -/// [ScaffoldMessengerState.showMaterialBanner] functions. -/// -/// When the [ScaffoldMessenger] has nested [Scaffold] descendants, the -/// ScaffoldMessenger will only present the notification to the root Scaffold of -/// the subtree of Scaffolds. In order to show notifications for the inner, nested -/// Scaffolds, set a new scope by instantiating a new ScaffoldMessenger in -/// between the levels of nesting. -/// -/// {@tool dartpad} -/// Here is an example of showing a [SnackBar] when the user presses a button. -/// -/// ** See code in examples/api/lib/material/scaffold/scaffold_messenger.0.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [SnackBar], which is a temporary notification typically shown near the -/// bottom of the app using the [ScaffoldMessengerState.showSnackBar] method. -/// * [MaterialBanner], which is a temporary notification typically shown at the -/// top of the app using the [ScaffoldMessengerState.showMaterialBanner] method. -/// * [debugCheckHasScaffoldMessenger], which asserts that the given context -/// has a [ScaffoldMessenger] ancestor. -/// * Cookbook: [Display a SnackBar](https://docs.flutter.dev/cookbook/design/snackbars) -class ScaffoldMessenger extends StatefulWidget { - /// Creates a widget that manages [SnackBar]s for [Scaffold] descendants. - const ScaffoldMessenger({super.key, required this.child}); - - /// The widget below this widget in the tree. - /// - /// {@macro flutter.widgets.ProxyWidget.child} - final Widget child; - - /// The state from the closest instance of this class that encloses the given - /// context. - /// - /// {@tool dartpad} - /// Typical usage of the [ScaffoldMessenger.of] function is to call it in - /// response to a user gesture or an application state change. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold_messenger.of.0.dart ** - /// {@end-tool} - /// - /// A less elegant but more expedient solution is to assign a [GlobalKey] to the - /// [ScaffoldMessenger], then use the `key.currentState` property to obtain the - /// [ScaffoldMessengerState] rather than using the [ScaffoldMessenger.of] - /// function. The [MaterialApp.scaffoldMessengerKey] refers to the root - /// ScaffoldMessenger that is provided by default. - /// - /// {@tool dartpad} - /// Sometimes [SnackBar]s are produced by code that doesn't have ready access - /// to a valid [BuildContext]. One such example of this is when you show a - /// SnackBar from a method outside of the `build` function. In these - /// cases, you can assign a [GlobalKey] to the [ScaffoldMessenger]. This - /// example shows a key being used to obtain the [ScaffoldMessengerState] - /// provided by the [MaterialApp]. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold_messenger.of.1.dart ** - /// {@end-tool} - /// - /// If there is no [ScaffoldMessenger] in scope, then this will assert in - /// debug mode, and throw an exception in release mode. - /// - /// See also: - /// - /// * [maybeOf], which is a similar function but will return null instead of - /// throwing if there is no [ScaffoldMessenger] ancestor. - /// * [debugCheckHasScaffoldMessenger], which asserts that the given context - /// has a [ScaffoldMessenger] ancestor. - static ScaffoldMessengerState of(BuildContext context) { - assert(debugCheckHasScaffoldMessenger(context)); - - final _ScaffoldMessengerScope scope = context - .dependOnInheritedWidgetOfExactType<_ScaffoldMessengerScope>()!; - return scope._scaffoldMessengerState; - } - - /// The state from the closest instance of this class that encloses the given - /// context, if any. - /// - /// Will return null if a [ScaffoldMessenger] is not found in the given context. - /// - /// See also: - /// - /// * [of], which is a similar function, except that it will throw an - /// exception if a [ScaffoldMessenger] is not found in the given context. - static ScaffoldMessengerState? maybeOf(BuildContext context) { - final _ScaffoldMessengerScope? scope = context - .dependOnInheritedWidgetOfExactType<_ScaffoldMessengerScope>(); - return scope?._scaffoldMessengerState; - } - - @override - ScaffoldMessengerState createState() => ScaffoldMessengerState(); -} - -/// State for a [ScaffoldMessenger]. -/// -/// A [ScaffoldMessengerState] object can be used to [showSnackBar] or -/// [showMaterialBanner] for every registered [Scaffold] that is a descendant of -/// the associated [ScaffoldMessenger]. Scaffolds will register to receive -/// [SnackBar]s and [MaterialBanner]s from their closest ScaffoldMessenger -/// ancestor. -/// -/// Typically obtained via [ScaffoldMessenger.of]. -class ScaffoldMessengerState extends State - with TickerProviderStateMixin { - final LinkedHashSet _scaffolds = - LinkedHashSet(); - final Queue< - ScaffoldFeatureController - > - _materialBanners = - Queue< - ScaffoldFeatureController - >(); - AnimationController? _materialBannerController; - final Queue> - _snackBars = - Queue>(); - AnimationController? _snackBarController; - Timer? _snackBarTimer; - bool? _accessibleNavigation; - - @protected - @override - void didChangeDependencies() { - final bool accessibleNavigation = MediaQuery.accessibleNavigationOf( - context, - ); - // If we transition from accessible navigation to non-accessible navigation - // and there is a SnackBar that would have timed out that has already - // completed its timer, dismiss that SnackBar. If the timer hasn't finished - // yet, let it timeout as normal. - if ((_accessibleNavigation ?? false) && - !accessibleNavigation && - _snackBarTimer != null && - !_snackBarTimer!.isActive) { - hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); - } - _accessibleNavigation = accessibleNavigation; - super.didChangeDependencies(); - } - - void _register(ScaffoldState scaffold) { - _scaffolds.add(scaffold); - - if (_isRoot(scaffold)) { - if (_snackBars.isNotEmpty) { - scaffold._updateSnackBar(); - } - - if (_materialBanners.isNotEmpty) { - scaffold._updateMaterialBanner(); - } - } - } - - void _unregister(ScaffoldState scaffold) { - final bool removed = _scaffolds.remove(scaffold); - // ScaffoldStates should only be removed once. - assert(removed); - } - - void _updateScaffolds() { - for (final ScaffoldState scaffold in _scaffolds) { - if (_isRoot(scaffold)) { - scaffold._updateSnackBar(); - scaffold._updateMaterialBanner(); - } - } - } - - // Nested Scaffolds are handled by the ScaffoldMessenger by only presenting a - // MaterialBanner or SnackBar in the root Scaffold of the nested set. - bool _isRoot(ScaffoldState scaffold) { - final ScaffoldState? parent = scaffold.context - .findAncestorStateOfType(); - return parent == null || !_scaffolds.contains(parent); - } - - // SNACKBAR API - - /// Shows a [SnackBar] across all registered [Scaffold]s. Scaffolds register - /// to receive snack bars from their closest [ScaffoldMessenger] ancestor. - /// If there are several registered scaffolds the snack bar is shown - /// simultaneously on all of them. - /// - /// A scaffold can show at most one snack bar at a time. If this function is - /// called while another snack bar is already visible, the given snack bar - /// will be added to a queue and displayed after the earlier snack bars have - /// closed. - /// - /// To control how long a [SnackBar] remains visible, use [SnackBar.duration]. - /// - /// To remove the [SnackBar] with an exit animation, use [hideCurrentSnackBar] - /// or call [ScaffoldFeatureController.close] on the returned - /// [ScaffoldFeatureController]. To remove a [SnackBar] suddenly (without an - /// animation), use [removeCurrentSnackBar]. - /// - /// See [ScaffoldMessenger.of] for information about how to obtain the - /// [ScaffoldMessengerState]. - /// - /// {@tool dartpad} - /// Here is an example of showing a [SnackBar] when the user presses a button. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.0.dart ** - /// {@end-tool} - /// - /// ## Relative positioning of floating SnackBars - /// - /// A [SnackBar] with [SnackBar.behavior] set to [SnackBarBehavior.floating] is - /// positioned above the widgets provided to [Scaffold.floatingActionButton], - /// [Scaffold.persistentFooterButtons], and [Scaffold.bottomNavigationBar]. - /// If some or all of these widgets take up enough space such that the SnackBar - /// would not be visible when positioned above them, an error will be thrown. - /// In this case, consider constraining the size of these widgets to allow room for - /// the SnackBar to be visible. - /// - /// {@tool dartpad} - /// Here is an example showing how to display a [SnackBar] with [showSnackBar] - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.0.dart ** - /// {@end-tool} - /// - /// {@tool dartpad} - /// Here is an example showing that a floating [SnackBar] appears above [Scaffold.floatingActionButton]. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart ** - /// {@end-tool} - /// - /// If [AnimationStyle.duration] is provided in the [snackBarAnimationStyle] - /// parameter, it will be used to override the snackbar show animation duration. - /// Otherwise, defaults to 250ms. - /// - /// If [AnimationStyle.reverseDuration] is provided in the [snackBarAnimationStyle] - /// parameter, it will be used to override the snackbar hide animation duration. - /// Otherwise, defaults to 250ms. - /// - /// To disable the snackbar animation, use [AnimationStyle.noAnimation]. - /// - /// {@tool dartpad} - /// This sample showcases how to override [SnackBar] show and hide animation - /// duration using [AnimationStyle] in [ScaffoldMessengerState.showSnackBar]. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart ** - /// {@end-tool} - /// - ScaffoldFeatureController showSnackBar( - SnackBar snackBar, { - AnimationStyle? snackBarAnimationStyle, - }) { - assert( - _scaffolds.isNotEmpty, - 'ScaffoldMessenger.showSnackBar was called, but there are currently no ' - 'descendant Scaffolds to present to.', - ); - _didUpdateAnimationStyle(snackBarAnimationStyle); - _snackBarController ??= SnackBar.createAnimationController( - duration: snackBarAnimationStyle?.duration, - reverseDuration: snackBarAnimationStyle?.reverseDuration, - vsync: this, - )..addStatusListener(_handleSnackBarStatusChanged); - if (_snackBars.isEmpty) { - assert(_snackBarController!.isDismissed); - _snackBarController!.forward(); - } - late ScaffoldFeatureController controller; - controller = ScaffoldFeatureController._( - // We provide a fallback key so that if back-to-back snackbars happen to - // match in structure, material ink splashes and highlights don't survive - // from one to the next. - snackBar.withAnimation(_snackBarController!, fallbackKey: UniqueKey()), - Completer(), - () { - assert(_snackBars.first == controller); - hideCurrentSnackBar(); - }, - null, // SnackBar doesn't use a builder function so setState() wouldn't rebuild it - ); - try { - setState(() { - _snackBars.addLast(controller); - }); - _updateScaffolds(); - } catch (exception) { - assert(() { - if (exception is FlutterError) { - final String summary = exception.diagnostics.first.toDescription(); - if (summary == - 'setState() or markNeedsBuild() called during build.') { - final List information = [ - ErrorSummary( - 'The showSnackBar() method cannot be called during build.', - ), - ErrorDescription( - 'The showSnackBar() method was called during build, which is ' - 'prohibited as showing snack bars requires updating state. Updating ' - 'state is not possible during build.', - ), - ErrorHint( - 'Instead of calling showSnackBar() during build, call it directly ' - 'in your on tap (and related) callbacks. If you need to immediately ' - 'show a snack bar, make the call in initState() or ' - 'didChangeDependencies() instead. Otherwise, you can also schedule a ' - 'post-frame callback using SchedulerBinding.addPostFrameCallback to ' - 'show the snack bar after the current frame.', - ), - context.describeOwnershipChain( - 'The ownership chain for the particular ScaffoldMessenger is', - ), - ]; - throw FlutterError.fromParts(information); - } - } - return true; - }()); - rethrow; - } - - return controller; - } - - void _didUpdateAnimationStyle(AnimationStyle? snackBarAnimationStyle) { - if (snackBarAnimationStyle != null) { - if (_snackBarController?.duration != snackBarAnimationStyle.duration || - _snackBarController?.reverseDuration != - snackBarAnimationStyle.reverseDuration) { - _snackBarController?.dispose(); - _snackBarController = null; - } - } - } - - void _handleSnackBarStatusChanged(AnimationStatus status) { - switch (status) { - case AnimationStatus.dismissed: - assert(_snackBars.isNotEmpty); - setState(() { - _snackBars.removeFirst(); - }); - _updateScaffolds(); - if (_snackBars.isNotEmpty) { - _snackBarController!.forward(); - } - case AnimationStatus.completed: - setState(() { - assert(_snackBarTimer == null); - // build will create a new timer if necessary to dismiss the snackBar. - }); - _updateScaffolds(); - case AnimationStatus.forward: - case AnimationStatus.reverse: - break; - } - } - - /// Removes the current [SnackBar] (if any) immediately from registered - /// [Scaffold]s. - /// - /// The removed snack bar does not run its normal exit animation. If there are - /// any queued snack bars, they begin their entrance animation immediately. - void removeCurrentSnackBar({ - SnackBarClosedReason reason = SnackBarClosedReason.remove, - }) { - if (_snackBars.isEmpty) { - return; - } - final Completer completer = - _snackBars.first._completer; - if (!completer.isCompleted) { - completer.complete(reason); - } - _snackBarTimer?.cancel(); - _snackBarTimer = null; - // This will trigger the animation's status callback. - _snackBarController!.value = 0.0; - } - - /// Removes the current [SnackBar] by running its normal exit animation. - /// - /// The closed completer is called after the animation is complete. - void hideCurrentSnackBar({ - SnackBarClosedReason reason = SnackBarClosedReason.hide, - }) { - if (_snackBars.isEmpty || _snackBarController!.isDismissed) { - return; - } - final Completer completer = - _snackBars.first._completer; - if (_accessibleNavigation!) { - _snackBarController!.value = 0.0; - completer.complete(reason); - } else { - _snackBarController!.reverse().then((void value) { - assert(mounted); - if (!completer.isCompleted) { - completer.complete(reason); - } - }); - } - _snackBarTimer?.cancel(); - _snackBarTimer = null; - } - - /// Removes all the snackBars currently in queue by clearing the queue - /// and running normal exit animation on the current snackBar. - void clearSnackBars() { - if (_snackBars.isEmpty || _snackBarController!.isDismissed) { - return; - } - final ScaffoldFeatureController - currentSnackbar = _snackBars.first; - _snackBars.clear(); - _snackBars.add(currentSnackbar); - hideCurrentSnackBar(); - } - - // MATERIAL BANNER API - - /// Shows a [MaterialBanner] across all registered [Scaffold]s. Scaffolds register - /// to receive material banners from their closest [ScaffoldMessenger] ancestor. - /// If there are several registered scaffolds the material banner is shown - /// simultaneously on all of them. - /// - /// A scaffold can show at most one material banner at a time. If this function is - /// called while another material banner is already visible, the given material banner - /// will be added to a queue and displayed after the earlier material banners have - /// closed. - /// - /// To remove the [MaterialBanner] with an exit animation, use [hideCurrentMaterialBanner] - /// or call [ScaffoldFeatureController.close] on the returned - /// [ScaffoldFeatureController]. To remove a [MaterialBanner] suddenly (without an - /// animation), use [removeCurrentMaterialBanner]. - /// - /// See [ScaffoldMessenger.of] for information about how to obtain the - /// [ScaffoldMessengerState]. - /// - /// {@tool dartpad} - /// Here is an example of showing a [MaterialBanner] when the user presses a button. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_material_banner.0.dart ** - /// {@end-tool} - ScaffoldFeatureController - showMaterialBanner( - MaterialBanner materialBanner, - ) { - assert( - _scaffolds.isNotEmpty, - 'ScaffoldMessenger.showMaterialBanner was called, but there are currently no ' - 'descendant Scaffolds to present to.', - ); - _materialBannerController ??= MaterialBanner.createAnimationController( - vsync: this, - )..addStatusListener(_handleMaterialBannerStatusChanged); - if (_materialBanners.isEmpty) { - assert(_materialBannerController!.isDismissed); - _materialBannerController!.forward(); - } - late ScaffoldFeatureController - controller; - controller = ScaffoldFeatureController._( - // We provide a fallback key so that if back-to-back material banners happen to - // match in structure, material ink splashes and highlights don't survive - // from one to the next. - materialBanner.withAnimation( - _materialBannerController!, - fallbackKey: UniqueKey(), - ), - Completer(), - () { - assert(_materialBanners.first == controller); - hideCurrentMaterialBanner(); - }, - null, // MaterialBanner doesn't use a builder function so setState() wouldn't rebuild it - ); - setState(() { - _materialBanners.addLast(controller); - }); - _updateScaffolds(); - return controller; - } - - void _handleMaterialBannerStatusChanged(AnimationStatus status) { - switch (status) { - case AnimationStatus.dismissed: - assert(_materialBanners.isNotEmpty); - setState(() { - _materialBanners.removeFirst(); - }); - _updateScaffolds(); - if (_materialBanners.isNotEmpty) { - _materialBannerController!.forward(); - } - case AnimationStatus.completed: - _updateScaffolds(); - case AnimationStatus.forward: - case AnimationStatus.reverse: - break; - } - } - - /// Removes the current [MaterialBanner] (if any) immediately from registered - /// [Scaffold]s. - /// - /// The removed material banner does not run its normal exit animation. If there are - /// any queued material banners, they begin their entrance animation immediately. - void removeCurrentMaterialBanner({ - MaterialBannerClosedReason reason = MaterialBannerClosedReason.remove, - }) { - if (_materialBanners.isEmpty) { - return; - } - final Completer completer = - _materialBanners.first._completer; - if (!completer.isCompleted) { - completer.complete(reason); - } - - // This will trigger the animation's status callback. - _materialBannerController!.value = 0.0; - } - - /// Removes the current [MaterialBanner] by running its normal exit animation. - /// - /// The closed completer is called after the animation is complete. - void hideCurrentMaterialBanner({ - MaterialBannerClosedReason reason = MaterialBannerClosedReason.hide, - }) { - if (_materialBanners.isEmpty || _materialBannerController!.isDismissed) { - return; - } - final Completer completer = - _materialBanners.first._completer; - if (_accessibleNavigation!) { - _materialBannerController!.value = 0.0; - completer.complete(reason); - } else { - _materialBannerController!.reverse().then((void value) { - assert(mounted); - if (!completer.isCompleted) { - completer.complete(reason); - } - }); - } - } - - /// Removes all the [MaterialBanner]s currently in queue by clearing the queue - /// and running normal exit animation on the current [MaterialBanner]. - void clearMaterialBanners() { - if (_materialBanners.isEmpty || _materialBannerController!.isDismissed) { - return; - } - final ScaffoldFeatureController - currentMaterialBanner = _materialBanners.first; - _materialBanners.clear(); - _materialBanners.add(currentMaterialBanner); - hideCurrentMaterialBanner(); - } - - @protected - @override - Widget build(BuildContext context) { - assert(debugCheckHasMediaQuery(context)); - _accessibleNavigation = MediaQuery.accessibleNavigationOf(context); - - if (_snackBars.isNotEmpty) { - final ModalRoute? route = ModalRoute.of(context); - if (route == null || route.isCurrent) { - if (_snackBarController!.isCompleted && _snackBarTimer == null) { - final SnackBar snackBar = _snackBars.first._widget; - _snackBarTimer = Timer(snackBar.duration, () { - assert(_snackBarController!.isForwardOrCompleted); - // Look up MediaQuery again in case the setting changed. - if (snackBar.action != null && - MediaQuery.accessibleNavigationOf(context)) { - return; - } - hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); - }); - } - } - } - - return _ScaffoldMessengerScope( - scaffoldMessengerState: this, - child: widget.child, - ); - } - - @protected - @override - void dispose() { - _materialBannerController?.dispose(); - _snackBarController?.dispose(); - _snackBarTimer?.cancel(); - _snackBarTimer = null; - super.dispose(); - } -} - -class _ScaffoldMessengerScope extends InheritedWidget { - const _ScaffoldMessengerScope({ - required super.child, - required ScaffoldMessengerState scaffoldMessengerState, - }) : _scaffoldMessengerState = scaffoldMessengerState; - - final ScaffoldMessengerState _scaffoldMessengerState; - - @override - bool updateShouldNotify(_ScaffoldMessengerScope old) => - _scaffoldMessengerState != old._scaffoldMessengerState; -} - -/// A snapshot of a transition between two [FloatingActionButtonLocation]s. -/// -/// [ScaffoldState] uses this to seamlessly change transition animations -/// when a running [FloatingActionButtonLocation] transition is interrupted by a new transition. -@immutable -class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation { - const _TransitionSnapshotFabLocation( - this.begin, - this.end, - this.animator, - this.progress, - ); - - final FloatingActionButtonLocation begin; - final FloatingActionButtonLocation end; - final FloatingActionButtonAnimator animator; - final double progress; - - @override - Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { - return animator.getOffset( - begin: begin.getOffset(scaffoldGeometry), - end: end.getOffset(scaffoldGeometry), - progress: progress, - ); - } - - @override - String toString() { - return '${objectRuntimeType(this, '_TransitionSnapshotFabLocation')}(begin: $begin, end: $end, progress: $progress)'; - } -} - -/// Geometry information for [Scaffold] components after layout is finished. -/// -/// To get a [ValueNotifier] for the scaffold geometry of a given -/// [BuildContext], use [Scaffold.geometryOf]. -/// -/// The ScaffoldGeometry is only available during the paint phase, because -/// its value is computed during the animation and layout phases prior to painting. -/// -/// For an example of using the [ScaffoldGeometry], see the [BottomAppBar], -/// which uses the [ScaffoldGeometry] to paint a notch around the -/// [FloatingActionButton]. -/// -/// For information about the [Scaffold]'s geometry that is used while laying -/// out the [FloatingActionButton], see [ScaffoldPrelayoutGeometry]. -@immutable -class ScaffoldGeometry { - /// Create an object that describes the geometry of a [Scaffold]. - const ScaffoldGeometry({ - this.bottomNavigationBarTop, - this.floatingActionButtonArea, - }); - - /// The distance from the [Scaffold]'s top edge to the top edge of the - /// rectangle in which the [Scaffold.bottomNavigationBar] bar is laid out. - /// - /// Null if [Scaffold.bottomNavigationBar] is null. - final double? bottomNavigationBarTop; - - /// The [Scaffold.floatingActionButton]'s bounding rectangle. - /// - /// This is null when there is no floating action button showing. - final Rect? floatingActionButtonArea; - - ScaffoldGeometry _scaleFloatingActionButton(double scaleFactor) { - if (scaleFactor == 1.0) { - return this; - } - - if (scaleFactor == 0.0) { - return ScaffoldGeometry(bottomNavigationBarTop: bottomNavigationBarTop); - } - - final Rect scaledButton = Rect.lerp( - floatingActionButtonArea!.center & Size.zero, - floatingActionButtonArea, - scaleFactor, - )!; - return copyWith(floatingActionButtonArea: scaledButton); - } - - /// Creates a copy of this [ScaffoldGeometry] but with the given fields replaced with - /// the new values. - ScaffoldGeometry copyWith({ - double? bottomNavigationBarTop, - Rect? floatingActionButtonArea, - }) { - return ScaffoldGeometry( - bottomNavigationBarTop: - bottomNavigationBarTop ?? this.bottomNavigationBarTop, - floatingActionButtonArea: - floatingActionButtonArea ?? this.floatingActionButtonArea, - ); - } -} - -class _ScaffoldGeometryNotifier extends ChangeNotifier - implements ValueListenable { - _ScaffoldGeometryNotifier(this.geometry, this.context); - - final BuildContext context; - double? floatingActionButtonScale; - ScaffoldGeometry geometry; - - @override - ScaffoldGeometry get value { - assert(() { - final RenderObject? renderObject = context.findRenderObject(); - if (renderObject == null || !renderObject.owner!.debugDoingPaint) { - throw FlutterError( - 'Scaffold.geometryOf() must only be accessed during the paint phase.\n' - 'The ScaffoldGeometry is only available during the paint phase, because ' - 'its value is computed during the animation and layout phases prior to painting.', - ); - } - return true; - }()); - return geometry._scaleFloatingActionButton(floatingActionButtonScale!); - } - - void _updateWith({ - double? bottomNavigationBarTop, - Rect? floatingActionButtonArea, - double? floatingActionButtonScale, - }) { - this.floatingActionButtonScale = - floatingActionButtonScale ?? this.floatingActionButtonScale; - geometry = geometry.copyWith( - bottomNavigationBarTop: bottomNavigationBarTop, - floatingActionButtonArea: floatingActionButtonArea, - ); - notifyListeners(); - } -} - -// Used to communicate the height of the Scaffold's bottomNavigationBar and -// persistentFooterButtons to the LayoutBuilder which builds the Scaffold's body. -// -// Scaffold expects a _BodyBoxConstraints to be passed to the _BodyBuilder -// widget's LayoutBuilder, see _ScaffoldLayout.performLayout(). The BoxConstraints -// methods that construct new BoxConstraints objects, like copyWith() have not -// been overridden here because we expect the _BodyBoxConstraintsObject to be -// passed along unmodified to the LayoutBuilder. If that changes in the future -// then _BodyBuilder will assert. -class _BodyBoxConstraints extends BoxConstraints { - const _BodyBoxConstraints({ - super.maxWidth, - super.maxHeight, - required this.bottomWidgetsHeight, - required this.appBarHeight, - required this.materialBannerHeight, - }) : assert(bottomWidgetsHeight >= 0), - assert(appBarHeight >= 0), - assert(materialBannerHeight >= 0); - - final double bottomWidgetsHeight; - final double appBarHeight; - final double materialBannerHeight; - - // RenderObject.layout() will only short-circuit its call to its performLayout - // method if the new layout constraints are not == to the current constraints. - // If the height of the bottom widgets has changed, even though the constraints' - // min and max values have not, we still want performLayout to happen. - @override - bool operator ==(Object other) { - if (super != other) { - return false; - } - return other is _BodyBoxConstraints && - other.materialBannerHeight == materialBannerHeight && - other.bottomWidgetsHeight == bottomWidgetsHeight && - other.appBarHeight == appBarHeight; - } - - @override - int get hashCode => Object.hash( - super.hashCode, - materialBannerHeight, - bottomWidgetsHeight, - appBarHeight, - ); -} - -// Used when Scaffold.extendBody is true to wrap the scaffold's body in a MediaQuery -// whose padding accounts for the height of the bottomNavigationBar and/or the -// persistentFooterButtons. -// -// The bottom widgets' height is passed along via the _BodyBoxConstraints parameter. -// The constraints parameter is constructed in_ScaffoldLayout.performLayout(). -class _BodyBuilder extends StatelessWidget { - const _BodyBuilder({ - required this.extendBody, - required this.extendBodyBehindAppBar, - required this.body, - }); - - final Widget body; - final bool extendBody; - final bool extendBodyBehindAppBar; - - @override - Widget build(BuildContext context) { - if (!extendBody && !extendBodyBehindAppBar) { - return body; - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - final _BodyBoxConstraints bodyConstraints = - constraints as _BodyBoxConstraints; - final MediaQueryData metrics = MediaQuery.of(context); - - final double bottom = extendBody - ? math.max( - metrics.padding.bottom, - bodyConstraints.bottomWidgetsHeight, - ) - : metrics.padding.bottom; - - final double top = extendBodyBehindAppBar - ? math.max( - metrics.padding.top, - bodyConstraints.appBarHeight + - bodyConstraints.materialBannerHeight, - ) - : metrics.padding.top; - - return MediaQuery( - data: metrics.copyWith( - padding: metrics.padding.copyWith(top: top, bottom: bottom), - ), - child: body, - ); - }, - ); - } -} - -class _ScaffoldLayout extends MultiChildLayoutDelegate { - _ScaffoldLayout({ - required this.minInsets, - required this.minViewPadding, - required this.textDirection, - required this.geometryNotifier, - // for floating action button - required this.previousFloatingActionButtonLocation, - required this.currentFloatingActionButtonLocation, - required this.floatingActionButtonMoveAnimationProgress, - required this.floatingActionButtonMotionAnimator, - required this.isSnackBarFloating, - required this.snackBarWidth, - required this.extendBody, - required this.extendBodyBehindAppBar, - required this.extendBodyBehindMaterialBanner, - }); - - final bool extendBody; - final bool extendBodyBehindAppBar; - final EdgeInsets minInsets; - final EdgeInsets minViewPadding; - final TextDirection textDirection; - final _ScaffoldGeometryNotifier geometryNotifier; - - final FloatingActionButtonLocation previousFloatingActionButtonLocation; - final FloatingActionButtonLocation currentFloatingActionButtonLocation; - final double floatingActionButtonMoveAnimationProgress; - final FloatingActionButtonAnimator floatingActionButtonMotionAnimator; - - final bool isSnackBarFloating; - final double? snackBarWidth; - - final bool extendBodyBehindMaterialBanner; - - @override - void performLayout(Size size) { - final BoxConstraints looseConstraints = BoxConstraints.loose(size); - - // This part of the layout has the same effect as putting the app bar and - // body in a column and making the body flexible. What's different is that - // in this case the app bar appears _after_ the body in the stacking order, - // so the app bar's shadow is drawn on top of the body. - - final BoxConstraints fullWidthConstraints = looseConstraints.tighten( - width: size.width, - ); - final double bottom = size.height; - double contentTop = 0.0; - double bottomWidgetsHeight = 0.0; - double appBarHeight = 0.0; - - if (hasChild(_ScaffoldSlot.appBar)) { - appBarHeight = layoutChild( - _ScaffoldSlot.appBar, - fullWidthConstraints, - ).height; - contentTop = extendBodyBehindAppBar ? 0.0 : appBarHeight; - positionChild(_ScaffoldSlot.appBar, Offset.zero); - } - - double? bottomNavigationBarTop; - if (hasChild(_ScaffoldSlot.bottomNavigationBar)) { - final double bottomNavigationBarHeight = layoutChild( - _ScaffoldSlot.bottomNavigationBar, - fullWidthConstraints, - ).height; - bottomWidgetsHeight += bottomNavigationBarHeight; - bottomNavigationBarTop = math.max(0.0, bottom - bottomWidgetsHeight); - positionChild( - _ScaffoldSlot.bottomNavigationBar, - Offset(0.0, bottomNavigationBarTop), - ); - } - - if (hasChild(_ScaffoldSlot.persistentFooter)) { - final BoxConstraints footerConstraints = BoxConstraints( - maxWidth: fullWidthConstraints.maxWidth, - maxHeight: math.max(0.0, bottom - bottomWidgetsHeight - contentTop), - ); - final double persistentFooterHeight = layoutChild( - _ScaffoldSlot.persistentFooter, - footerConstraints, - ).height; - bottomWidgetsHeight += persistentFooterHeight; - positionChild( - _ScaffoldSlot.persistentFooter, - Offset(0.0, math.max(0.0, bottom - bottomWidgetsHeight)), - ); - } - - Size materialBannerSize = Size.zero; - if (hasChild(_ScaffoldSlot.materialBanner)) { - materialBannerSize = layoutChild( - _ScaffoldSlot.materialBanner, - fullWidthConstraints, - ); - positionChild(_ScaffoldSlot.materialBanner, Offset(0.0, appBarHeight)); - - // Push content down only if elevation is 0. - if (!extendBodyBehindMaterialBanner) { - contentTop += materialBannerSize.height; - } - } - - // Set the content bottom to account for the greater of the height of any - // bottom-anchored material widgets or of the keyboard or other - // bottom-anchored system UI. - final double contentBottom = math.max( - 0.0, - bottom - math.max(minInsets.bottom, bottomWidgetsHeight), - ); - - if (hasChild(_ScaffoldSlot.body)) { - double bodyMaxHeight = math.max(0.0, contentBottom - contentTop); - - // When extendBody is true, the body is visible underneath the bottom widgets. - // This does not apply when the area is obscured by the device keyboard. - if (extendBody && minInsets.bottom <= bottomWidgetsHeight) { - bodyMaxHeight += bottomWidgetsHeight; - bodyMaxHeight = clampDouble( - bodyMaxHeight, - 0.0, - looseConstraints.maxHeight - contentTop, - ); - assert( - bodyMaxHeight <= - math.max(0.0, looseConstraints.maxHeight - contentTop), - ); - } else { - bottomWidgetsHeight = 0.0; - } - - final BoxConstraints bodyConstraints = _BodyBoxConstraints( - maxWidth: fullWidthConstraints.maxWidth, - maxHeight: bodyMaxHeight, - materialBannerHeight: materialBannerSize.height, - bottomWidgetsHeight: bottomWidgetsHeight, - appBarHeight: appBarHeight, - ); - layoutChild(_ScaffoldSlot.body, bodyConstraints); - positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop)); - } - - // The BottomSheet and the SnackBar are anchored to the bottom of the parent, - // they're as wide as the parent and are given their intrinsic height. The - // only difference is that SnackBar appears on the top side of the - // BottomNavigationBar while the BottomSheet is stacked on top of it. - // - // If all three elements are present then either the center of the FAB straddles - // the top edge of the BottomSheet or the bottom of the FAB is - // kFloatingActionButtonMargin above the SnackBar, whichever puts the FAB - // the farthest above the bottom of the parent. If only the FAB is has a - // non-zero height then it's inset from the parent's right and bottom edges - // by kFloatingActionButtonMargin. - - Size bottomSheetSize = Size.zero; - Size snackBarSize = Size.zero; - if (hasChild(_ScaffoldSlot.bodyScrim)) { - final BoxConstraints bottomSheetScrimConstraints = BoxConstraints( - maxWidth: fullWidthConstraints.maxWidth, - maxHeight: contentBottom, - ); - layoutChild(_ScaffoldSlot.bodyScrim, bottomSheetScrimConstraints); - positionChild(_ScaffoldSlot.bodyScrim, Offset.zero); - } - - // Set the size of the SnackBar early if the behavior is fixed so - // the FAB can be positioned correctly. - if (hasChild(_ScaffoldSlot.snackBar) && !isSnackBarFloating) { - snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints); - } - - if (hasChild(_ScaffoldSlot.bottomSheet)) { - final BoxConstraints bottomSheetConstraints = BoxConstraints( - maxWidth: fullWidthConstraints.maxWidth, - maxHeight: math.max(0.0, contentBottom - contentTop), - ); - bottomSheetSize = layoutChild( - _ScaffoldSlot.bottomSheet, - bottomSheetConstraints, - ); - positionChild( - _ScaffoldSlot.bottomSheet, - Offset( - (size.width - bottomSheetSize.width) / 2.0, - contentBottom - bottomSheetSize.height, - ), - ); - } - - late Rect floatingActionButtonRect; - if (hasChild(_ScaffoldSlot.floatingActionButton)) { - final Size fabSize = layoutChild( - _ScaffoldSlot.floatingActionButton, - looseConstraints, - ); - - // To account for the FAB position being changed, we'll animate between - // the old and new positions. - final ScaffoldPrelayoutGeometry - currentGeometry = ScaffoldPrelayoutGeometry( - bottomSheetSize: bottomSheetSize, - contentBottom: contentBottom, - - /// [appBarHeight] should be used instead of [contentTop] because - /// ScaffoldPrelayoutGeometry.contentTop must not be affected by [extendBodyBehindAppBar]. - contentTop: appBarHeight, - floatingActionButtonSize: fabSize, - minInsets: minInsets, - scaffoldSize: size, - snackBarSize: snackBarSize, - materialBannerSize: materialBannerSize, - textDirection: textDirection, - minViewPadding: minViewPadding, - ); - final Offset currentFabOffset = currentFloatingActionButtonLocation - .getOffset( - currentGeometry, - ); - final Offset previousFabOffset = previousFloatingActionButtonLocation - .getOffset( - currentGeometry, - ); - final Offset fabOffset = floatingActionButtonMotionAnimator.getOffset( - begin: previousFabOffset, - end: currentFabOffset, - progress: floatingActionButtonMoveAnimationProgress, - ); - positionChild(_ScaffoldSlot.floatingActionButton, fabOffset); - floatingActionButtonRect = fabOffset & fabSize; - } - - if (hasChild(_ScaffoldSlot.snackBar)) { - final bool hasCustomWidth = - snackBarWidth != null && snackBarWidth! < size.width; - if (snackBarSize == Size.zero) { - snackBarSize = layoutChild( - _ScaffoldSlot.snackBar, - hasCustomWidth ? looseConstraints : fullWidthConstraints, - ); - } - - final double snackBarYOffsetBase; - final bool showAboveFab = switch (currentFloatingActionButtonLocation) { - FloatingActionButtonLocation.startTop || - FloatingActionButtonLocation.centerTop || - FloatingActionButtonLocation.endTop || - FloatingActionButtonLocation.miniStartTop || - FloatingActionButtonLocation.miniCenterTop || - FloatingActionButtonLocation.miniEndTop => false, - FloatingActionButtonLocation.startDocked || - FloatingActionButtonLocation.startFloat || - FloatingActionButtonLocation.centerDocked || - FloatingActionButtonLocation.centerFloat || - FloatingActionButtonLocation.endContained || - FloatingActionButtonLocation.endDocked || - FloatingActionButtonLocation.endFloat || - FloatingActionButtonLocation.miniStartDocked || - FloatingActionButtonLocation.miniStartFloat || - FloatingActionButtonLocation.miniCenterDocked || - FloatingActionButtonLocation.miniCenterFloat || - FloatingActionButtonLocation.miniEndDocked || - FloatingActionButtonLocation.miniEndFloat => true, - FloatingActionButtonLocation() => true, - }; - if (floatingActionButtonRect.size != Size.zero && - isSnackBarFloating && - showAboveFab) { - if (bottomNavigationBarTop != null) { - snackBarYOffsetBase = math.min( - bottomNavigationBarTop, - floatingActionButtonRect.top, - ); - } else { - snackBarYOffsetBase = floatingActionButtonRect.top; - } - } else { - // SnackBarBehavior.fixed applies a SafeArea automatically. - // SnackBarBehavior.floating does not since the positioning is affected - // if there is a FloatingActionButton (see condition above). If there is - // no FAB, make sure we account for safe space when the SnackBar is - // floating. - final double safeYOffsetBase = size.height - minViewPadding.bottom; - snackBarYOffsetBase = isSnackBarFloating - ? math.min(contentBottom, safeYOffsetBase) - : contentBottom; - } - - final double xOffset = hasCustomWidth - ? (size.width - snackBarWidth!) / 2 - : 0.0; - positionChild( - _ScaffoldSlot.snackBar, - Offset(xOffset, snackBarYOffsetBase - snackBarSize.height), - ); - - assert(() { - // Whether a floating SnackBar has been offset too high. - // - // To improve the developer experience, this assert is done after the call to positionChild. - // if we assert sooner the SnackBar is visible because its defaults position is (0,0) and - // it can cause confusion to the user as the error message states that the SnackBar is off screen. - if (isSnackBarFloating) { - final bool snackBarVisible = - (snackBarYOffsetBase - snackBarSize.height) >= 0; - if (!snackBarVisible) { - throw FlutterError.fromParts([ - ErrorSummary('Floating SnackBar presented off screen.'), - ErrorDescription( - 'A SnackBar with behavior property set to SnackBarBehavior.floating is fully ' - 'or partially off screen because some or all the widgets provided to ' - 'Scaffold.floatingActionButton, Scaffold.persistentFooterButtons and ' - 'Scaffold.bottomNavigationBar take up too much vertical space.\n', - ), - ErrorHint( - 'Consider constraining the size of these widgets to allow room for the SnackBar to be visible.', - ), - ]); - } - } - return true; - }()); - } - - if (hasChild(_ScaffoldSlot.statusBar)) { - layoutChild( - _ScaffoldSlot.statusBar, - fullWidthConstraints.tighten(height: minInsets.top), - ); - positionChild(_ScaffoldSlot.statusBar, Offset.zero); - } - - if (hasChild(_ScaffoldSlot.drawer)) { - layoutChild(_ScaffoldSlot.drawer, BoxConstraints.tight(size)); - positionChild(_ScaffoldSlot.drawer, Offset.zero); - } - - if (hasChild(_ScaffoldSlot.endDrawer)) { - layoutChild(_ScaffoldSlot.endDrawer, BoxConstraints.tight(size)); - positionChild(_ScaffoldSlot.endDrawer, Offset.zero); - } - - geometryNotifier._updateWith( - bottomNavigationBarTop: bottomNavigationBarTop, - floatingActionButtonArea: floatingActionButtonRect, - ); - } - - @override - bool shouldRelayout(_ScaffoldLayout oldDelegate) { - return oldDelegate.minInsets != minInsets || - oldDelegate.minViewPadding != minViewPadding || - oldDelegate.textDirection != textDirection || - oldDelegate.floatingActionButtonMoveAnimationProgress != - floatingActionButtonMoveAnimationProgress || - oldDelegate.previousFloatingActionButtonLocation != - previousFloatingActionButtonLocation || - oldDelegate.currentFloatingActionButtonLocation != - currentFloatingActionButtonLocation || - oldDelegate.extendBody != extendBody || - oldDelegate.extendBodyBehindAppBar != extendBodyBehindAppBar; - } -} - -/// Handler for scale and rotation animations in the [FloatingActionButton]. -/// -/// Currently, there are two types of [FloatingActionButton] animations: -/// -/// * Entrance/Exit animations, which this widget triggers -/// when the [FloatingActionButton] is added, updated, or removed. -/// * Motion animations, which are triggered by the [Scaffold] -/// when its [FloatingActionButtonLocation] is updated. -class _FloatingActionButtonTransition extends StatefulWidget { - const _FloatingActionButtonTransition({ - required this.child, - required this.fabMoveAnimation, - required this.fabMotionAnimator, - required this.geometryNotifier, - required this.currentController, - }); - - final Widget? child; - final Animation fabMoveAnimation; - final FloatingActionButtonAnimator fabMotionAnimator; - final _ScaffoldGeometryNotifier geometryNotifier; - - /// Controls the current child widget.child as it exits. - final AnimationController currentController; - - @override - _FloatingActionButtonTransitionState createState() => - _FloatingActionButtonTransitionState(); -} - -class _FloatingActionButtonTransitionState - extends State<_FloatingActionButtonTransition> - with TickerProviderStateMixin { - // The animations applied to the Floating Action Button when it is entering or exiting. - // Controls the previous widget.child as it exits. - late AnimationController _previousController; - CurvedAnimation? _previousExitScaleAnimation; - CurvedAnimation? _previousExitRotationCurvedAnimation; - CurvedAnimation? _currentEntranceScaleAnimation; - late Animation _previousScaleAnimation; - late TrainHoppingAnimation _previousRotationAnimation; - // The animations to run, considering the widget's fabMoveAnimation and the current/previous entrance/exit animations. - late Animation _currentScaleAnimation; - late Animation _extendedCurrentScaleAnimation; - late TrainHoppingAnimation _currentRotationAnimation; - Widget? _previousChild; - - @override - void initState() { - super.initState(); - - _previousController = AnimationController( - duration: kFloatingActionButtonSegue, - vsync: this, - )..addStatusListener(_handlePreviousAnimationStatusChanged); - _updateAnimations(); - - if (widget.child != null) { - // If we start out with a child, have the child appear fully visible instead - // of animating in. - widget.currentController.value = 1.0; - } else { - // If we start without a child we update the geometry object with a - // floating action button scale of 0, as it is not showing on the screen. - _updateGeometryScale(0.0); - } - } - - @override - void dispose() { - _previousController.dispose(); - _previousExitScaleAnimation?.dispose(); - _previousExitRotationCurvedAnimation?.dispose(); - _currentEntranceScaleAnimation?.dispose(); - _disposeAnimations(); - super.dispose(); - } - - @override - void didUpdateWidget(_FloatingActionButtonTransition oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.fabMotionAnimator != widget.fabMotionAnimator || - oldWidget.fabMoveAnimation != widget.fabMoveAnimation) { - _disposeAnimations(); - // Get the right scale and rotation animations to use for this widget. - _updateAnimations(); - } - final bool oldChildIsNull = oldWidget.child == null; - final bool newChildIsNull = widget.child == null; - if (oldChildIsNull == newChildIsNull && - oldWidget.child?.key == widget.child?.key) { - return; - } - if (_previousController.isDismissed) { - final double currentValue = widget.currentController.value; - if (currentValue == 0.0 || oldWidget.child == null) { - // The current child hasn't started its entrance animation yet. We can - // just skip directly to the new child's entrance. - _previousChild = null; - if (widget.child != null) { - widget.currentController.forward(); - } - } else { - // Otherwise, we need to copy the state from the current controller to - // the previous controller and run an exit animation for the previous - // widget before running the entrance animation for the new child. - _previousChild = oldWidget.child; - _previousController - ..value = currentValue - ..reverse(); - widget.currentController.value = 0.0; - } - } - } - - static final Animatable _entranceTurnTween = Tween( - begin: 1.0 - kFloatingActionButtonTurnInterval, - end: 1.0, - ).chain(CurveTween(curve: Curves.easeIn)); - - void _disposeAnimations() { - _previousRotationAnimation.dispose(); - _currentRotationAnimation.dispose(); - } - - void _updateAnimations() { - _previousExitScaleAnimation?.dispose(); - // Get the animations for exit and entrance. - _previousExitScaleAnimation = CurvedAnimation( - parent: _previousController, - curve: Curves.easeIn, - ); - _previousExitRotationCurvedAnimation?.dispose(); - _previousExitRotationCurvedAnimation = CurvedAnimation( - parent: _previousController, - curve: Curves.easeIn, - ); - - final Animation previousExitRotationAnimation = Tween( - begin: 1.0, - end: 1.0, - ).animate(_previousExitRotationCurvedAnimation!); - - _currentEntranceScaleAnimation?.dispose(); - _currentEntranceScaleAnimation = CurvedAnimation( - parent: widget.currentController, - curve: Curves.easeIn, - ); - final Animation currentEntranceRotationAnimation = widget - .currentController - .drive( - _entranceTurnTween, - ); - - // Get the animations for when the FAB is moving. - final Animation moveScaleAnimation = widget.fabMotionAnimator - .getScaleAnimation( - parent: widget.fabMoveAnimation, - ); - final Animation moveRotationAnimation = widget.fabMotionAnimator - .getRotationAnimation( - parent: widget.fabMoveAnimation, - ); - - // Aggregate the animations. - if (widget.fabMotionAnimator == FloatingActionButtonAnimator.noAnimation) { - _previousScaleAnimation = moveScaleAnimation; - _currentScaleAnimation = moveScaleAnimation; - _previousRotationAnimation = TrainHoppingAnimation( - moveRotationAnimation, - null, - ); - _currentRotationAnimation = TrainHoppingAnimation( - moveRotationAnimation, - null, - ); - } else { - _previousScaleAnimation = AnimationMin( - moveScaleAnimation, - _previousExitScaleAnimation!, - ); - _currentScaleAnimation = AnimationMin( - moveScaleAnimation, - _currentEntranceScaleAnimation!, - ); - _previousRotationAnimation = TrainHoppingAnimation( - previousExitRotationAnimation, - moveRotationAnimation, - ); - _currentRotationAnimation = TrainHoppingAnimation( - currentEntranceRotationAnimation, - moveRotationAnimation, - ); - } - - _extendedCurrentScaleAnimation = _currentScaleAnimation.drive( - CurveTween(curve: const Interval(0.0, 0.1)), - ); - _currentScaleAnimation.addListener(_onProgressChanged); - _previousScaleAnimation.addListener(_onProgressChanged); - } - - void _handlePreviousAnimationStatusChanged(AnimationStatus status) { - setState(() { - if (widget.child != null && status.isDismissed) { - assert(widget.currentController.isDismissed); - widget.currentController.forward(); - } - }); - } - - bool _isExtendedFloatingActionButton(Widget? widget) { - return widget is FloatingActionButton && widget.isExtended; - } - - @override - Widget build(BuildContext context) { - return Stack( - alignment: Alignment.centerRight, - children: [ - if (!_previousController.isDismissed) - if (_isExtendedFloatingActionButton(_previousChild)) - FadeTransition( - opacity: _previousScaleAnimation, - child: _previousChild, - ) - else - ScaleTransition( - scale: _previousScaleAnimation, - child: RotationTransition( - turns: _previousRotationAnimation, - child: _previousChild, - ), - ), - if (_isExtendedFloatingActionButton(widget.child)) - ScaleTransition( - scale: _extendedCurrentScaleAnimation, - child: FadeTransition( - opacity: _currentScaleAnimation, - child: widget.child, - ), - ) - else - ScaleTransition( - scale: _currentScaleAnimation, - child: RotationTransition( - turns: _currentRotationAnimation, - child: widget.child, - ), - ), - ], - ); - } - - void _onProgressChanged() { - _updateGeometryScale( - math.max(_previousScaleAnimation.value, _currentScaleAnimation.value), - ); - } - - void _updateGeometryScale(double scale) { - widget.geometryNotifier._updateWith(floatingActionButtonScale: scale); - } -} - -/// Implements the basic Material Design visual layout structure. -/// -/// This class provides APIs for showing drawers and bottom sheets. -/// -/// To display a persistent bottom sheet, obtain the -/// [ScaffoldState] for the current [BuildContext] via [Scaffold.of] and use the -/// [ScaffoldState.showBottomSheet] function. -/// -/// {@tool dartpad} -/// This example shows a [Scaffold] with a [body] and [FloatingActionButton]. -/// The [body] is a [Text] placed in a [Center] in order to center the text -/// within the [Scaffold]. The [FloatingActionButton] is connected to a -/// callback that increments a counter. -/// -/// ** See code in examples/api/lib/material/scaffold/scaffold.0.dart ** -/// {@end-tool} -/// -/// {@tool dartpad} -/// This example shows a [Scaffold] with a blueGrey [backgroundColor], [body] -/// and [FloatingActionButton]. The [body] is a [Text] placed in a [Center] in -/// order to center the text within the [Scaffold]. The [FloatingActionButton] -/// is connected to a callback that increments a counter. -/// -/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/scaffold_background_color.png) -/// -/// ** See code in examples/api/lib/material/scaffold/scaffold.1.dart ** -/// {@end-tool} -/// -/// {@tool dartpad} -/// This example shows a [Scaffold] with an [AppBar], a [BottomAppBar] and a -/// [FloatingActionButton]. The [body] is a [Text] placed in a [Center] in order -/// to center the text within the [Scaffold]. The [FloatingActionButton] is -/// centered and docked within the [BottomAppBar] using -/// [FloatingActionButtonLocation.centerDocked]. The [FloatingActionButton] is -/// connected to a callback that increments a counter. -/// -/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/scaffold_bottom_app_bar.png) -/// -/// ** See code in examples/api/lib/material/scaffold/scaffold.2.dart ** -/// {@end-tool} -/// -/// ## Scaffold layout, the keyboard, and display "notches" -/// -/// The scaffold will expand to fill the available space. That usually -/// means that it will occupy its entire window or device screen. When -/// the device's keyboard appears the Scaffold's ancestor [MediaQuery] -/// widget's [MediaQueryData.viewInsets] changes and the Scaffold will -/// be rebuilt. By default the scaffold's [body] is resized to make -/// room for the keyboard. To prevent the resize set -/// [resizeToAvoidBottomInset] to false. In either case the focused -/// widget will be scrolled into view if it's within a scrollable -/// container. -/// -/// The [MediaQueryData.padding] value defines areas that might -/// not be completely visible, like the display "notch" on the iPhone -/// X. The scaffold's [body] is not inset by this padding value -/// although an [appBar] or [bottomNavigationBar] will typically -/// cause the body to avoid the padding. The [SafeArea] -/// widget can be used within the scaffold's body to avoid areas -/// like display notches. -/// -/// ## Floating action button with a draggable scrollable bottom sheet -/// -/// If [Scaffold.bottomSheet] is a [DraggableScrollableSheet], -/// [Scaffold.floatingActionButton] is set, and the bottom sheet is dragged to -/// cover greater than 70% of the Scaffold's height, two things happen in parallel: -/// -/// * Scaffold starts to show scrim (see [ScaffoldState.showBodyScrim]), and -/// * [Scaffold.floatingActionButton] is scaled down through an animation with a [Curves.easeIn], and -/// disappears when the bottom sheet covers the entire Scaffold. -/// -/// And as soon as the bottom sheet is dragged down to cover less than 70% of the [Scaffold], the scrim -/// disappears and [Scaffold.floatingActionButton] animates back to its normal size. -/// -/// ## Troubleshooting -/// -/// ### Nested Scaffolds -/// -/// The Scaffold is designed to be a top level container for -/// a [MaterialApp]. This means that adding a Scaffold -/// to each route on a Material app will provide the app with -/// Material's basic visual layout structure. -/// -/// It is typically not necessary to nest Scaffolds. For example, in a -/// tabbed UI, where the [bottomNavigationBar] is a [TabBar] -/// and the body is a [TabBarView], you might be tempted to make each tab bar -/// view a scaffold with a differently titled AppBar. Rather, it would be -/// better to add a listener to the [TabController] that updates the -/// AppBar -/// -/// {@tool snippet} -/// Add a listener to the app's tab controller so that the [AppBar] title of the -/// app's one and only scaffold is reset each time a new tab is selected. -/// -/// ```dart -/// TabController(vsync: tickerProvider, length: tabCount)..addListener(() { -/// if (!tabController.indexIsChanging) { -/// setState(() { -/// // Rebuild the enclosing scaffold with a new AppBar title -/// appBarTitle = 'Tab ${tabController.index}'; -/// }); -/// } -/// }) -/// ``` -/// {@end-tool} -/// -/// Although there are some use cases, like a presentation app that -/// shows embedded flutter content, where nested scaffolds are -/// appropriate, it's best to avoid nesting scaffolds. -/// -/// See also: -/// -/// * [AppBar], which is a horizontal bar typically shown at the top of an app -/// using the [appBar] property. -/// * [BottomAppBar], which is a horizontal bar typically shown at the bottom -/// of an app using the [bottomNavigationBar] property. -/// * [FloatingActionButton], which is a circular button typically shown in the -/// bottom right corner of the app using the [floatingActionButton] property. -/// * [Drawer], which is a vertical panel that is typically displayed to the -/// left of the body (and often hidden on phones) using the [drawer] -/// property. -/// * [BottomNavigationBar], which is a horizontal array of buttons typically -/// shown along the bottom of the app using the [bottomNavigationBar] -/// property. -/// * [BottomSheet], which is an overlay typically shown near the bottom of the -/// app. A bottom sheet can either be persistent, in which case it is shown -/// using the [ScaffoldState.showBottomSheet] method, or modal, in which case -/// it is shown using the [showModalBottomSheet] function. -/// * [SnackBar], which is a lightweight message with an optional action which -/// briefly displays at the bottom of the screen. Use the -/// [ScaffoldMessengerState.showSnackBar] method to show snack bars. -/// * [MaterialBanner], which displays an important, succinct message, at the -/// top of the screen, below the app bar. Use the -/// [ScaffoldMessengerState.showMaterialBanner] method to show material banners. -/// * [ScaffoldState], which is the state associated with this widget. -/// * -/// * Cookbook: [Add a Drawer to a screen](https://docs.flutter.dev/cookbook/design/drawer) -class Scaffold extends StatefulWidget implements material.Scaffold { - /// Creates a visual scaffold for Material Design widgets. - const Scaffold({ - super.key, - this.appBar, - this.body, - this.floatingActionButton, - this.floatingActionButtonLocation, - this.floatingActionButtonAnimator, - this.persistentFooterButtons, - this.persistentFooterAlignment = AlignmentDirectional.centerEnd, - this.persistentFooterDecoration, - this.drawer, - this.onDrawerChanged, - this.endDrawer, - this.onEndDrawerChanged, - this.bottomNavigationBar, - this.bottomSheet, - this.backgroundColor, - this.resizeToAvoidBottomInset, - this.primary = true, - this.drawerDragStartBehavior = DragStartBehavior.start, - this.extendBody = false, - this.drawerBarrierDismissible = true, - this.extendBodyBehindAppBar = false, - this.drawerScrimColor, - this.bottomSheetScrimBuilder = _defaultBottomSheetScrimBuilder, - this.drawerEdgeDragWidth, - this.drawerEnableOpenDragGesture = true, - this.endDrawerEnableOpenDragGesture = true, - this.restorationId, - }); - - /// If true, and [bottomNavigationBar] or [persistentFooterButtons] - /// is specified, then the [body] extends to the bottom of the Scaffold, - /// instead of only extending to the top of the [bottomNavigationBar] - /// or the [persistentFooterButtons]. - /// - /// If true, a [MediaQuery] widget whose bottom padding matches the height - /// of the [bottomNavigationBar] will be added above the scaffold's [body]. - /// - /// This property is often useful when the [bottomNavigationBar] has - /// a non-rectangular shape, like [CircularNotchedRectangle], which - /// adds a [FloatingActionButton] sized notch to the top edge of the bar. - /// In this case specifying `extendBody: true` ensures that scaffold's - /// body will be visible through the bottom navigation bar's notch. - /// - /// See also: - /// - /// * [extendBodyBehindAppBar], which extends the height of the body - /// to the top of the scaffold. - final bool extendBody; - - /// Whether the drawer can be dismissed by tapping on the barrier. - /// - /// If false, and a [drawer] is specified, then the barrier behind the drawer - /// will not respond to a tap event and thus remains open. - /// - /// Defaults to true, in which case the drawer will close upon the user tapping on the barrier. - final bool drawerBarrierDismissible; - - /// If true, and an [appBar] is specified, then the height of the [body] is - /// extended to include the height of the app bar and the top of the body - /// is aligned with the top of the app bar. - /// - /// This is useful if the app bar's [AppBar.backgroundColor] is not - /// completely opaque. - /// - /// This property is false by default. - /// - /// See also: - /// - /// * [extendBody], which extends the height of the body to the bottom - /// of the scaffold. - final bool extendBodyBehindAppBar; - - /// An app bar to display at the top of the scaffold. - final PreferredSizeWidget? appBar; - - /// The primary content of the scaffold. - /// - /// Displayed below the [appBar], above the bottom of the ambient - /// [MediaQuery]'s [MediaQueryData.viewInsets], and behind the - /// [floatingActionButton] and [drawer]. If [resizeToAvoidBottomInset] is - /// false then the body is not resized when the onscreen keyboard appears, - /// i.e. it is not inset by `viewInsets.bottom`. - /// - /// The widget in the body of the scaffold is positioned at the top-left of - /// the available space between the app bar and the bottom of the scaffold. To - /// center this widget instead, consider putting it in a [Center] widget and - /// having that be the body. To expand this widget instead, consider - /// putting it in a [SizedBox.expand]. - /// - /// If you have a column of widgets that should normally fit on the screen, - /// but may overflow and would in such cases need to scroll, consider using a - /// [ListView] as the body of the scaffold. This is also a good choice for - /// the case where your body is a scrollable list. - final Widget? body; - - /// A button displayed floating above [body], in the bottom right corner. - /// - /// Typically a [FloatingActionButton]. - final Widget? floatingActionButton; - - /// Responsible for determining where the [floatingActionButton] should go. - /// - /// If null, the [ScaffoldState] will use the default location, [FloatingActionButtonLocation.endFloat]. - final FloatingActionButtonLocation? floatingActionButtonLocation; - - /// Animator to move the [floatingActionButton] to a new [floatingActionButtonLocation]. - /// - /// If null, the [ScaffoldState] will use the default animator, [FloatingActionButtonAnimator.scaling]. - final FloatingActionButtonAnimator? floatingActionButtonAnimator; - - /// A set of buttons that are displayed at the bottom of the scaffold. - /// - /// Typically this is a list of [TextButton] widgets. These buttons are - /// persistently visible, even if the [body] of the scaffold scrolls. - /// - /// These widgets will be wrapped in an [OverflowBar]. - /// - /// The [persistentFooterButtons] are rendered above the - /// [bottomNavigationBar] but below the [body]. - final List? persistentFooterButtons; - - /// The alignment of the [persistentFooterButtons] inside the [OverflowBar]. - /// - /// Defaults to [AlignmentDirectional.centerEnd]. - final AlignmentDirectional persistentFooterAlignment; - - /// Decoration for the container that holds the [persistentFooterButtons]. - /// - /// By default, this container has a top border with a width of 1.0, created by - /// [Divider.createBorderSide]. - /// - /// See also: - /// - /// * [persistentFooterButtons], which defines the buttons to show in the footer. - /// * [persistentFooterAlignment], which defines the alignment of the footer buttons. - final BoxDecoration? persistentFooterDecoration; - - /// A panel displayed to the side of the [body], often hidden on mobile - /// devices. Swipes in from either left-to-right ([TextDirection.ltr]) or - /// right-to-left ([TextDirection.rtl]) - /// - /// Typically a [Drawer]. - /// - /// To open the drawer, use the [ScaffoldState.openDrawer] function. - /// - /// To close the drawer, use either [ScaffoldState.closeDrawer], [Navigator.pop] - /// or press the escape key on the keyboard. - /// - /// {@tool dartpad} - /// To disable the drawer edge swipe on mobile, set the - /// [Scaffold.drawerEnableOpenDragGesture] to false. Then, use - /// [ScaffoldState.openDrawer] to open the drawer and [Navigator.pop] to close - /// it. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold.drawer.0.dart ** - /// {@end-tool} - final Widget? drawer; - - /// Optional callback that is called when the [Scaffold.drawer] is opened or closed. - final DrawerCallback? onDrawerChanged; - - /// A panel displayed to the side of the [body], often hidden on mobile - /// devices. Swipes in from right-to-left ([TextDirection.ltr]) or - /// left-to-right ([TextDirection.rtl]) - /// - /// Typically a [Drawer]. - /// - /// To open the drawer, use the [ScaffoldState.openEndDrawer] function. - /// - /// To close the drawer, use either [ScaffoldState.closeEndDrawer], [Navigator.pop] - /// or press the escape key on the keyboard. - /// - /// {@tool dartpad} - /// To disable the drawer edge swipe, set the - /// [Scaffold.endDrawerEnableOpenDragGesture] to false. Then, use - /// [ScaffoldState.openEndDrawer] to open the drawer and [Navigator.pop] to - /// close it. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold.end_drawer.0.dart ** - /// {@end-tool} - final Widget? endDrawer; - - /// Optional callback that is called when the [Scaffold.endDrawer] is opened or closed. - final DrawerCallback? onEndDrawerChanged; - - /// The color to use for the scrim that obscures primary content while a drawer is open. - /// - /// If this is null, then [DrawerThemeData.scrimColor] is used. If that - /// is also null, then it defaults to [Colors.black54]. - final Color? drawerScrimColor; - - /// A builder for the widget that obscures primary content while a bottom sheet is open. - /// - /// The builder receives the current [BuildContext] and an [Animation] as parameters. - /// The [Animation] ranges from 0.0 to 1.0 based on how much the bottom sheet covers the screen. - /// A value of 0.0 represents when the bottom sheet covers 70% of the screen, - /// and 1.0 represents when the bottom sheet fully covers the screen. - /// - /// If this is null, then a non-dismissable [ModalBarrier] with [Colors.black] is used. The - /// barrier is animated to fade in and out as the bottom sheet is opened and closed. - /// - /// If the builder returns null, then no scrim is shown. - final Widget? Function(BuildContext, Animation) - bottomSheetScrimBuilder; - - /// The color of the [Material] widget that underlies the entire Scaffold. - /// - /// The theme's [ThemeData.scaffoldBackgroundColor] by default. - final Color? backgroundColor; - - /// A bottom navigation bar to display at the bottom of the scaffold. - /// - /// Snack bars slide from underneath the bottom navigation bar while bottom - /// sheets are stacked on top. - /// - /// The [bottomNavigationBar] is rendered below the [persistentFooterButtons] - /// and the [body]. - final Widget? bottomNavigationBar; - - /// The persistent bottom sheet to display. - /// - /// 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. - /// - /// 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. - /// - /// Unlike the persistent bottom sheet displayed by [showBottomSheet] - /// this bottom sheet is not a [LocalHistoryEntry] and cannot be dismissed - /// with the scaffold appbar's back button. - /// - /// If a persistent bottom sheet created with [showBottomSheet] is already - /// visible, it must be closed before building the Scaffold with a new - /// [bottomSheet]. - /// - /// The value of [bottomSheet] can be any widget at all. It's unlikely to - /// actually be a [BottomSheet], which is used by the implementations of - /// [showBottomSheet] and [showModalBottomSheet]. Typically it's a widget - /// that includes [Material]. - /// - /// See also: - /// - /// * [showBottomSheet], which displays a bottom sheet as a route that can - /// be dismissed with the scaffold's back button. - /// * [showModalBottomSheet], which displays a modal bottom sheet. - /// * [BottomSheetThemeData], which can be used to customize the default - /// bottom sheet property values when using a [BottomSheet]. - final Widget? bottomSheet; - - /// If true the [body] and the scaffold's floating widgets should size - /// themselves to avoid the onscreen keyboard whose height is defined by the - /// ambient [MediaQuery]'s [MediaQueryData.viewInsets] `bottom` property. - /// - /// For example, if there is an onscreen keyboard displayed above the - /// scaffold, the body can be resized to avoid overlapping the keyboard, which - /// prevents widgets inside the body from being obscured by the keyboard. - /// - /// Defaults to true. - final bool? resizeToAvoidBottomInset; - - /// Whether this scaffold is being displayed at the top of the screen. - /// - /// If true then the height of the [appBar] will be extended by the height - /// of the screen's status bar, i.e. the top padding for [MediaQuery]. - /// - /// The default value of this property, like the default value of - /// [AppBar.primary], is true. - final bool primary; - - /// {@macro flutter.material.DrawerController.dragStartBehavior} - final DragStartBehavior drawerDragStartBehavior; - - /// The width of the area within which a horizontal swipe will open the - /// drawer. - /// - /// By default, the value used is 20.0 added to the padding edge of - /// `MediaQuery.paddingOf(context)` that corresponds to the surrounding - /// [TextDirection]. This ensures that the drag area for notched devices is - /// not obscured. For example, if `TextDirection.of(context)` is set to - /// [TextDirection.ltr], 20.0 will be added to - /// `MediaQuery.paddingOf(context).left`. - final double? drawerEdgeDragWidth; - - /// Determines if the [Scaffold.drawer] can be opened with a drag - /// gesture on mobile. - /// - /// On desktop platforms, the drawer is not draggable. - /// - /// By default, the drag gesture is enabled on mobile. - final bool drawerEnableOpenDragGesture; - - /// Determines if the [Scaffold.endDrawer] can be opened with a - /// gesture on mobile. - /// - /// On desktop platforms, the drawer is not draggable. - /// - /// By default, the drag gesture is enabled on mobile. - final bool endDrawerEnableOpenDragGesture; - - /// Restoration ID to save and restore the state of the [Scaffold]. - /// - /// If it is non-null, the scaffold will persist and restore whether the - /// [drawer] and [endDrawer] was open or closed. - /// - /// The state of this widget is persisted in a [RestorationBucket] claimed - /// from the surrounding [RestorationScope] using the provided restoration ID. - /// - /// See also: - /// - /// * [RestorationManager], which explains how state restoration works in - /// Flutter. - final String? restorationId; - - /// Finds the [ScaffoldState] from the closest instance of this class that - /// encloses the given context. - /// - /// If no instance of this class encloses the given context, will cause an - /// assert in debug mode, and throw an exception in release mode. - /// - /// This method can be expensive (it walks the element tree). - /// - /// {@tool dartpad} - /// Typical usage of the [Scaffold.of] function is to call it from within the - /// `build` method of a child of a [Scaffold]. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold.of.0.dart ** - /// {@end-tool} - /// - /// {@tool dartpad} - /// When the [Scaffold] is actually created in the same `build` function, the - /// `context` argument to the `build` function can't be used to find the - /// [Scaffold] (since it's "above" the widget being returned in the widget - /// tree). In such cases, the following technique with a [Builder] can be used - /// to provide a new scope with a [BuildContext] that is "under" the - /// [Scaffold]: - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold.of.1.dart ** - /// {@end-tool} - /// - /// A more efficient solution is to split your build function into several - /// widgets. This introduces a new context from which you can obtain the - /// [Scaffold]. In this solution, you would have an outer widget that creates - /// the [Scaffold] populated by instances of your new inner widgets, and then - /// in these inner widgets you would use [Scaffold.of]. - /// - /// A less elegant but more expedient solution is assign a [GlobalKey] to the - /// [Scaffold], then use the `key.currentState` property to obtain the - /// [ScaffoldState] rather than using the [Scaffold.of] function. - /// - /// If there is no [Scaffold] in scope, then this will throw an exception. - /// To return null if there is no [Scaffold], use [maybeOf] instead. - static ScaffoldState of(BuildContext context) { - final ScaffoldState? result = context - .findAncestorStateOfType(); - if (result != null) { - return result; - } - throw FlutterError.fromParts([ - ErrorSummary( - 'Scaffold.of() called with a context that does not contain a Scaffold.', - ), - ErrorDescription( - 'No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). ' - 'This usually happens when the context provided is from the same StatefulWidget as that ' - 'whose build function actually creates the Scaffold widget being sought.', - ), - ErrorHint( - 'There are several ways to avoid this problem. The simplest is to use a Builder to get a ' - 'context that is "under" the Scaffold. For an example of this, please see the ' - 'documentation for Scaffold.of():\n' - ' https://api.flutter.dev/flutter/material/Scaffold/of.html', - ), - ErrorHint( - 'A more efficient solution is to split your build function into several widgets. This ' - 'introduces a new context from which you can obtain the Scaffold. In this solution, ' - 'you would have an outer widget that creates the Scaffold populated by instances of ' - 'your new inner widgets, and then in these inner widgets you would use Scaffold.of().\n' - 'A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, ' - 'then use the key.currentState property to obtain the ScaffoldState rather than ' - 'using the Scaffold.of() function.', - ), - context.describeElement('The context used was'), - ]); - } - - /// Finds the [ScaffoldState] from the closest instance of this class that - /// encloses the given context. - /// - /// If no instance of this class encloses the given context, will return null. - /// To throw an exception instead, use [of] instead of this function. - /// - /// This method can be expensive (it walks the element tree). - /// - /// See also: - /// - /// * [of], a similar function to this one that throws if no instance - /// encloses the given context. Also includes some sample code in its - /// documentation. - static ScaffoldState? maybeOf(BuildContext context) { - return context.findAncestorStateOfType(); - } - - /// Returns a [ValueListenable] for the [ScaffoldGeometry] for the closest - /// [Scaffold] ancestor of the given context. - /// - /// The [ValueListenable.value] is only available at paint time. - /// - /// Notifications are guaranteed to be sent before the first paint pass - /// with the new geometry, but there is no guarantee whether a build or - /// layout passes are going to happen between the notification and the next - /// paint pass. - /// - /// The closest [Scaffold] ancestor for the context might change, e.g when - /// an element is moved from one scaffold to another. For [StatefulWidget]s - /// using this listenable, a change of the [Scaffold] ancestor will - /// trigger a [State.didChangeDependencies]. - /// - /// A typical pattern for listening to the scaffold geometry would be to - /// call [Scaffold.geometryOf] in [State.didChangeDependencies], compare the - /// return value with the previous listenable, if it has changed, unregister - /// the listener, and register a listener to the new [ScaffoldGeometry] - /// listenable. - static ValueListenable geometryOf(BuildContext context) { - final _ScaffoldScope? scaffoldScope = context - .dependOnInheritedWidgetOfExactType<_ScaffoldScope>(); - if (scaffoldScope == null) { - throw FlutterError.fromParts([ - ErrorSummary( - 'Scaffold.geometryOf() called with a context that does not contain a Scaffold.', - ), - ErrorDescription( - 'This usually happens when the context provided is from the same StatefulWidget as that ' - 'whose build function actually creates the Scaffold widget being sought.', - ), - ErrorHint( - 'There are several ways to avoid this problem. The simplest is to use a Builder to get a ' - 'context that is "under" the Scaffold. For an example of this, please see the ' - 'documentation for Scaffold.of():\n' - ' https://api.flutter.dev/flutter/material/Scaffold/of.html', - ), - ErrorHint( - 'A more efficient solution is to split your build function into several widgets. This ' - 'introduces a new context from which you can obtain the Scaffold. In this solution, ' - 'you would have an outer widget that creates the Scaffold populated by instances of ' - 'your new inner widgets, and then in these inner widgets you would use Scaffold.geometryOf().', - ), - context.describeElement('The context used was'), - ]); - } - return scaffoldScope.geometryNotifier; - } - - /// Whether the Scaffold that most tightly encloses the given context has a - /// drawer. - /// - /// If this is being used during a build (for example to decide whether to - /// show an "open drawer" button), set the `registerForUpdates` argument to - /// true. This will then set up an [InheritedWidget] relationship with the - /// [Scaffold] so that the client widget gets rebuilt whenever the [hasDrawer] - /// value changes. - /// - /// This method can be expensive (it walks the element tree). - /// - /// See also: - /// - /// * [Scaffold.of], which provides access to the [ScaffoldState] object as a - /// whole, from which you can show bottom sheets, and so forth. - static bool hasDrawer( - BuildContext context, { - bool registerForUpdates = true, - }) { - if (registerForUpdates) { - final _ScaffoldScope? scaffold = context - .dependOnInheritedWidgetOfExactType<_ScaffoldScope>(); - return scaffold?.hasDrawer ?? false; - } else { - final ScaffoldState? scaffold = context - .findAncestorStateOfType(); - return scaffold?.hasDrawer ?? false; - } - } - - static Widget _defaultBottomSheetScrimBuilder( - BuildContext context, - Animation animation, - ) { - return AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget? child) { - final double extentRemaining = - _kBottomSheetDominatesPercentage * (1.0 - animation.value); - final double floatingButtonVisibilityValue = - extentRemaining * _kBottomSheetDominatesPercentage * 10; - - final double opacity = math.max( - _kMinBottomSheetScrimOpacity, - _kMaxBottomSheetScrimOpacity - floatingButtonVisibilityValue, - ); - - return ModalBarrier( - dismissible: false, - color: Colors.black.withOpacity(opacity), - ); - }, - ); - } - - @override - ScaffoldState createState() => ScaffoldState(); -} - -/// State for a [Scaffold]. -/// -/// Can display [BottomSheet]s. Retrieve a [ScaffoldState] from the current -/// [BuildContext] using [Scaffold.of]. -class ScaffoldState extends State - with TickerProviderStateMixin, RestorationMixin - implements material.ScaffoldState { - @override - String? get restorationId => widget.restorationId; - - @protected - @override - void restoreState(RestorationBucket? oldBucket, bool initialRestore) { - registerForRestoration(_drawerOpened, 'drawer_open'); - registerForRestoration(_endDrawerOpened, 'end_drawer_open'); - } - - // DRAWER API - - final GlobalKey _drawerKey = - GlobalKey(); - final GlobalKey _endDrawerKey = - GlobalKey(); - - final GlobalKey _bodyKey = GlobalKey(); - - /// Whether this scaffold has a non-null [Scaffold.appBar]. - bool get hasAppBar => widget.appBar != null; - - /// Whether this scaffold has a non-null [Scaffold.drawer]. - bool get hasDrawer => widget.drawer != null; - - /// Whether this scaffold has a non-null [Scaffold.endDrawer]. - bool get hasEndDrawer => widget.endDrawer != null; - - /// Whether this scaffold has a non-null [Scaffold.floatingActionButton]. - bool get hasFloatingActionButton => widget.floatingActionButton != null; - - double? _appBarMaxHeight; - - /// The max height the [Scaffold.appBar] uses. - /// - /// This is based on the appBar preferred height plus the top padding. - double? get appBarMaxHeight => _appBarMaxHeight; - final RestorableBool _drawerOpened = RestorableBool(false); - final RestorableBool _endDrawerOpened = RestorableBool(false); - - /// Whether the [Scaffold.drawer] is opened. - /// - /// See also: - /// - /// * [ScaffoldState.openDrawer], which opens the [Scaffold.drawer] of a - /// [Scaffold]. - bool get isDrawerOpen => _drawerOpened.value; - - /// Whether the [Scaffold.endDrawer] is opened. - /// - /// See also: - /// - /// * [ScaffoldState.openEndDrawer], which opens the [Scaffold.endDrawer] of - /// a [Scaffold]. - bool get isEndDrawerOpen => _endDrawerOpened.value; - - void _drawerOpenedCallback(bool isOpened) { - if (_drawerOpened.value != isOpened && _drawerKey.currentState != null) { - setState(() { - _drawerOpened.value = isOpened; - }); - widget.onDrawerChanged?.call(isOpened); - } - } - - void _endDrawerOpenedCallback(bool isOpened) { - if (_endDrawerOpened.value != isOpened && - _endDrawerKey.currentState != null) { - setState(() { - _endDrawerOpened.value = isOpened; - }); - widget.onEndDrawerChanged?.call(isOpened); - } - } - - /// Opens the [Drawer] (if any). - /// - /// If the scaffold has a non-null [Scaffold.drawer], this function will cause - /// the drawer to begin its entrance animation. - /// - /// Normally this is not needed since the [Scaffold] automatically shows an - /// appropriate [IconButton], and handles the edge-swipe gesture, to show the - /// drawer. - /// - /// To close the drawer, use either [ScaffoldState.closeDrawer] or - /// [Navigator.pop]. - /// - /// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. - void openDrawer() { - if (_endDrawerKey.currentState != null && _endDrawerOpened.value) { - _endDrawerKey.currentState!.close(); - } - _drawerKey.currentState?.open(); - } - - /// Opens the end side [Drawer] (if any). - /// - /// If the scaffold has a non-null [Scaffold.endDrawer], this function will cause - /// the end side drawer to begin its entrance animation. - /// - /// Normally this is not needed since the [Scaffold] automatically shows an - /// appropriate [IconButton], and handles the edge-swipe gesture, to show the - /// drawer. - /// - /// To close the drawer, use either [ScaffoldState.closeEndDrawer] or - /// [Navigator.pop]. - /// - /// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. - void openEndDrawer() { - if (_drawerKey.currentState != null && _drawerOpened.value) { - _drawerKey.currentState!.close(); - } - _endDrawerKey.currentState?.open(); - } - - // Used for both the snackbar and material banner APIs - ScaffoldMessengerState? _scaffoldMessenger; - - // SNACKBAR API - ScaffoldFeatureController? _messengerSnackBar; - - // This is used to update the _messengerSnackBar by the ScaffoldMessenger. - void _updateSnackBar() { - final ScaffoldFeatureController? - messengerSnackBar = _scaffoldMessenger!._snackBars.isNotEmpty - ? _scaffoldMessenger!._snackBars.first - : null; - - if (_messengerSnackBar != messengerSnackBar) { - setState(() { - _messengerSnackBar = messengerSnackBar; - }); - } - } - - // MATERIAL BANNER API - - // The _messengerMaterialBanner represents the current MaterialBanner being managed by - // the ScaffoldMessenger, instead of the Scaffold. - ScaffoldFeatureController? - _messengerMaterialBanner; - - // This is used to update the _messengerMaterialBanner by the ScaffoldMessenger. - void _updateMaterialBanner() { - final ScaffoldFeatureController? - messengerMaterialBanner = _scaffoldMessenger!._materialBanners.isNotEmpty - ? _scaffoldMessenger!._materialBanners.first - : null; - - if (_messengerMaterialBanner != messengerMaterialBanner) { - setState(() { - _messengerMaterialBanner = messengerMaterialBanner; - }); - } - } - - // PERSISTENT BOTTOM SHEET API - - // Contains bottom sheets that may still be animating out of view. - // Important if the app/user takes an action that could repeatedly show a - // bottom sheet. - final List<_StandardBottomSheet> _dismissedBottomSheets = - <_StandardBottomSheet>[]; - PersistentBottomSheetController? _currentBottomSheet; - final GlobalKey _currentBottomSheetKey = GlobalKey(); - LocalHistoryEntry? _persistentSheetHistoryEntry; - - void _maybeBuildPersistentBottomSheet() { - if (widget.bottomSheet != null && _currentBottomSheet == null) { - // The new _currentBottomSheet is not a local history entry so a "back" button - // will not be added to the Scaffold's appbar and the bottom sheet will not - // support drag or swipe to dismiss. - final AnimationController animationController = - BottomSheet.createAnimationController(this)..value = 1.0; - bool persistentBottomSheetExtentChanged( - DraggableScrollableNotification notification, - ) { - if (notification.extent - notification.initialExtent > - precisionErrorTolerance) { - if (_persistentSheetHistoryEntry == null) { - _persistentSheetHistoryEntry = LocalHistoryEntry( - onRemove: () { - DraggableScrollableActuator.reset(notification.context); - showBodyScrim(false, 0.0); - _floatingActionButtonVisibilityController.value = 1.0; - _persistentSheetHistoryEntry = null; - }, - ); - ModalRoute.of( - context, - )!.addLocalHistoryEntry(_persistentSheetHistoryEntry!); - } - } else if (_persistentSheetHistoryEntry != null) { - _persistentSheetHistoryEntry!.remove(); - } - return false; - } - - // Stop the animation and unmount the dismissed sheets from the tree immediately, - // otherwise may cause duplicate GlobalKey assertion if the sheet sub-tree contains - // GlobalKey widgets. - if (_dismissedBottomSheets.isNotEmpty) { - final List<_StandardBottomSheet> sheets = List<_StandardBottomSheet>.of( - _dismissedBottomSheets, - growable: false, - ); - for (final _StandardBottomSheet sheet in sheets) { - sheet.animationController.reset(); - } - assert(_dismissedBottomSheets.isEmpty); - } - - _currentBottomSheet = _buildBottomSheet( - (BuildContext context) { - return NotificationListener( - onNotification: persistentBottomSheetExtentChanged, - child: DraggableScrollableActuator( - child: StatefulBuilder( - key: _currentBottomSheetKey, - builder: (BuildContext context, StateSetter setState) { - return widget.bottomSheet ?? const SizedBox.shrink(); - }, - ), - ), - ); - }, - isPersistent: true, - animationController: animationController, - ); - } - } - - void _closeCurrentBottomSheet() { - if (_currentBottomSheet != null) { - if (!_currentBottomSheet!._isLocalHistoryEntry) { - _currentBottomSheet!.close(); - } - assert(() { - _currentBottomSheet?._completer.future.whenComplete(() { - assert(_currentBottomSheet == null); - }); - return true; - }()); - } - } - - /// Closes [Scaffold.drawer] if it is currently opened. - /// - /// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. - void closeDrawer() { - if (hasDrawer && isDrawerOpen) { - _drawerKey.currentState!.close(); - } - } - - /// Closes [Scaffold.endDrawer] if it is currently opened. - /// - /// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. - void closeEndDrawer() { - if (hasEndDrawer && isEndDrawerOpen) { - _endDrawerKey.currentState!.close(); - } - } - - void _updatePersistentBottomSheet() { - _currentBottomSheetKey.currentState!.setState(() {}); - } - - PersistentBottomSheetController _buildBottomSheet( - WidgetBuilder builder, { - required bool isPersistent, - required AnimationController animationController, - Color? backgroundColor, - double? elevation, - ShapeBorder? shape, - Clip? clipBehavior, - BoxConstraints? constraints, - bool? enableDrag, - bool? showDragHandle, - bool shouldDisposeAnimationController = true, - }) { - assert(() { - if (widget.bottomSheet != null && - isPersistent && - _currentBottomSheet != null) { - throw FlutterError( - 'Scaffold.bottomSheet cannot be specified while a bottom sheet ' - 'displayed with showBottomSheet() is still visible.\n' - 'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().', - ); - } - return true; - }()); - - final Completer completer = Completer(); - final GlobalKey<_StandardBottomSheetState> bottomSheetKey = - GlobalKey<_StandardBottomSheetState>(); - late _StandardBottomSheet bottomSheet; - - bool removedEntry = false; - bool doingDispose = false; - - void removePersistentSheetHistoryEntryIfNeeded() { - assert(isPersistent); - if (_persistentSheetHistoryEntry != null) { - _persistentSheetHistoryEntry!.remove(); - _persistentSheetHistoryEntry = null; - } - } - - void removeCurrentBottomSheet() { - removedEntry = true; - if (_currentBottomSheet == null) { - return; - } - assert(_currentBottomSheet!._widget == bottomSheet); - assert(bottomSheetKey.currentState != null); - _showFloatingActionButton(); - - if (isPersistent) { - removePersistentSheetHistoryEntryIfNeeded(); - } - - bottomSheetKey.currentState!.close(); - setState(() { - _showBodyScrim = false; - _bottomSheetScrimAnimationController.value = 0.0; - _currentBottomSheet = null; - }); - - if (!animationController.isDismissed) { - _dismissedBottomSheets.add(bottomSheet); - } - completer.complete(); - } - - final LocalHistoryEntry? entry = isPersistent - ? null - : LocalHistoryEntry( - onRemove: () { - if (!removedEntry && - _currentBottomSheet?._widget == bottomSheet && - !doingDispose) { - removeCurrentBottomSheet(); - } - }, - ); - - void removeEntryIfNeeded() { - if (!isPersistent && !removedEntry) { - assert(entry != null); - entry!.remove(); - removedEntry = true; - } - } - - bottomSheet = _StandardBottomSheet( - key: bottomSheetKey, - animationController: animationController, - enableDrag: enableDrag ?? !isPersistent, - showDragHandle: showDragHandle, - onClosing: () { - if (_currentBottomSheet == null) { - return; - } - assert(_currentBottomSheet!._widget == bottomSheet); - removeEntryIfNeeded(); - }, - onDismissed: () { - if (_dismissedBottomSheets.contains(bottomSheet)) { - setState(() { - _dismissedBottomSheets.remove(bottomSheet); - }); - } - }, - onDispose: () { - doingDispose = true; - removeEntryIfNeeded(); - if (shouldDisposeAnimationController) { - animationController.dispose(); - } - }, - builder: builder, - isPersistent: isPersistent, - backgroundColor: backgroundColor, - elevation: elevation, - shape: shape, - clipBehavior: clipBehavior, - constraints: constraints, - ); - - if (!isPersistent) { - ModalRoute.of(context)!.addLocalHistoryEntry(entry!); - } - - return PersistentBottomSheetController._( - bottomSheet, - completer, - entry != null ? entry.remove : removeCurrentBottomSheet, - (VoidCallback fn) { - bottomSheetKey.currentState?.setState(fn); - }, - !isPersistent, - ); - } - - /// Shows a Material Design bottom sheet in the nearest [Scaffold]. 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. - /// - /// 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. - /// - /// 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. - /// - /// 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 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. - /// - /// 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. - /// - /// {@tool dartpad} - /// This example demonstrates how to use [showBottomSheet] to display a - /// bottom sheet when a user taps a button. It also demonstrates how to - /// close a bottom sheet using the Navigator. - /// - /// ** See code in examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.0.dart ** - /// {@end-tool} - /// - /// 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/scaffold/scaffold_state.show_bottom_sheet.1.dart ** - /// {@end-tool} - /// See also: - /// - /// * [BottomSheet], which becomes the parent of the widget returned by the - /// `builder`. - /// * [showBottomSheet], which calls this method given a [BuildContext]. - /// * [showModalBottomSheet], which can be used to display a modal bottom - /// sheet. - /// * [Scaffold.of], for information about how to obtain the [ScaffoldState]. - /// * The Material 2 spec at . - /// * The Material 3 spec at . - /// * [AnimationStyle], which is used to override the modal bottom sheet - /// animation duration and reverse animation duration. - PersistentBottomSheetController showBottomSheet( - WidgetBuilder builder, { - Color? backgroundColor, - double? elevation, - ShapeBorder? shape, - Clip? clipBehavior, - BoxConstraints? constraints, - bool? enableDrag, - bool? showDragHandle, - AnimationController? transitionAnimationController, - AnimationStyle? sheetAnimationStyle, - }) { - assert(() { - if (widget.bottomSheet != null) { - throw FlutterError( - 'Scaffold.bottomSheet cannot be specified while a bottom sheet ' - 'displayed with showBottomSheet() is still visible.\n' - 'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().', - ); - } - return true; - }()); - assert(debugCheckHasMediaQuery(context)); - - _closeCurrentBottomSheet(); - final AnimationController controller = - (transitionAnimationController ?? - BottomSheet.createAnimationController( - this, - sheetAnimationStyle: sheetAnimationStyle, - )) - ..forward(); - setState(() { - _currentBottomSheet = _buildBottomSheet( - builder, - isPersistent: false, - animationController: controller, - backgroundColor: backgroundColor, - elevation: elevation, - shape: shape, - clipBehavior: clipBehavior, - constraints: constraints, - enableDrag: enableDrag, - showDragHandle: showDragHandle, - shouldDisposeAnimationController: transitionAnimationController == null, - ); - }); - return _currentBottomSheet!; - } - - // Floating Action Button API - late AnimationController _floatingActionButtonMoveController; - late FloatingActionButtonAnimator _floatingActionButtonAnimator; - FloatingActionButtonLocation? _previousFloatingActionButtonLocation; - FloatingActionButtonLocation? _floatingActionButtonLocation; - - late AnimationController _floatingActionButtonVisibilityController; - - /// Shows the [Scaffold.floatingActionButton]. - TickerFuture _showFloatingActionButton() { - return _floatingActionButtonVisibilityController.forward(); - } - - // Moves the Floating Action Button to the new Floating Action Button Location. - void _moveFloatingActionButton( - final FloatingActionButtonLocation newLocation, - ) { - FloatingActionButtonLocation? previousLocation = - _floatingActionButtonLocation; - double restartAnimationFrom = 0.0; - // If the Floating Action Button is moving right now, we need to start from a snapshot of the current transition. - if (_floatingActionButtonMoveController.isAnimating) { - previousLocation = _TransitionSnapshotFabLocation( - _previousFloatingActionButtonLocation!, - _floatingActionButtonLocation!, - _floatingActionButtonAnimator, - _floatingActionButtonMoveController.value, - ); - restartAnimationFrom = _floatingActionButtonAnimator.getAnimationRestart( - _floatingActionButtonMoveController.value, - ); - } - - setState(() { - _previousFloatingActionButtonLocation = previousLocation; - _floatingActionButtonLocation = newLocation; - }); - - // Animate the motion even when the fab is null so that if the exit animation is running, - // the old fab will start the motion transition while it exits instead of jumping to the - // new position. - _floatingActionButtonMoveController.forward(from: restartAnimationFrom); - } - - // iOS FEATURES - status bar tap, back gesture - - // On iOS, tapping the status bar scrolls the app's primary scrollable to the - // top. We implement this by looking up the primary scroll controller and - // scrolling it to the top when tapped. - void _handleStatusBarTap() { - final ScrollController? primaryScrollController = - PrimaryScrollController.maybeOf(context); - if (primaryScrollController != null && primaryScrollController.hasClients) { - primaryScrollController.animateTo( - 0.0, - duration: const Duration(milliseconds: 1000), - curve: Curves.easeOutCirc, - ); - } - } - - // INTERNALS - - late _ScaffoldGeometryNotifier _geometryNotifier; - - bool get _resizeToAvoidBottomInset { - return widget.resizeToAvoidBottomInset ?? true; - } - - @protected - @override - void initState() { - super.initState(); - _geometryNotifier = _ScaffoldGeometryNotifier( - const ScaffoldGeometry(), - context, - ); - _floatingActionButtonLocation = - widget.floatingActionButtonLocation ?? - _kDefaultFloatingActionButtonLocation; - _floatingActionButtonAnimator = - widget.floatingActionButtonAnimator ?? - _kDefaultFloatingActionButtonAnimator; - _previousFloatingActionButtonLocation = _floatingActionButtonLocation; - _floatingActionButtonMoveController = AnimationController( - vsync: this, - value: 1.0, - duration: kFloatingActionButtonSegue * 2, - ); - - _floatingActionButtonVisibilityController = AnimationController( - duration: kFloatingActionButtonSegue, - vsync: this, - ); - - _bottomSheetScrimAnimationController = AnimationController(vsync: this); - } - - @protected - @override - void didUpdateWidget(Scaffold oldWidget) { - super.didUpdateWidget(oldWidget); - // Update the Floating Action Button Animator, and then schedule the Floating Action Button for repositioning. - if (widget.floatingActionButtonAnimator != - oldWidget.floatingActionButtonAnimator) { - _floatingActionButtonAnimator = - widget.floatingActionButtonAnimator ?? - _kDefaultFloatingActionButtonAnimator; - } - if (widget.floatingActionButtonLocation != - oldWidget.floatingActionButtonLocation) { - _moveFloatingActionButton( - widget.floatingActionButtonLocation ?? - _kDefaultFloatingActionButtonLocation, - ); - } - if (widget.bottomSheet != oldWidget.bottomSheet) { - assert(() { - if (widget.bottomSheet != null && - (_currentBottomSheet?._isLocalHistoryEntry ?? false)) { - throw FlutterError.fromParts([ - ErrorSummary( - 'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed ' - 'with showBottomSheet() is still visible.', - ), - ErrorHint( - 'Use the PersistentBottomSheetController ' - 'returned by showBottomSheet() to close the old bottom sheet before creating ' - 'a Scaffold with a (non null) bottomSheet.', - ), - ]); - } - return true; - }()); - if (widget.bottomSheet == null) { - _closeCurrentBottomSheet(); - } else if (widget.bottomSheet != null && oldWidget.bottomSheet == null) { - _maybeBuildPersistentBottomSheet(); - } else { - _updatePersistentBottomSheet(); - } - } - } - - @protected - @override - void didChangeDependencies() { - // Using maybeOf is valid here since both the Scaffold and ScaffoldMessenger - // are currently available for managing SnackBars. - final ScaffoldMessengerState? currentScaffoldMessenger = - ScaffoldMessenger.maybeOf(context); - // If our ScaffoldMessenger has changed, unregister with the old one first. - if (_scaffoldMessenger != null && - (currentScaffoldMessenger == null || - _scaffoldMessenger != currentScaffoldMessenger)) { - _scaffoldMessenger?._unregister(this); - } - // Register with the current ScaffoldMessenger, if there is one. - _scaffoldMessenger = currentScaffoldMessenger; - _scaffoldMessenger?._register(this); - - _maybeBuildPersistentBottomSheet(); - super.didChangeDependencies(); - } - - @protected - @override - void dispose() { - _geometryNotifier.dispose(); - _floatingActionButtonMoveController.dispose(); - _floatingActionButtonVisibilityController.dispose(); - _scaffoldMessenger?._unregister(this); - _drawerOpened.dispose(); - _endDrawerOpened.dispose(); - _bottomSheetScrimAnimationController.dispose(); - super.dispose(); - } - - void _addIfNonNull( - List children, - Widget? child, - Object childId, { - required bool removeLeftPadding, - required bool removeTopPadding, - required bool removeRightPadding, - required bool removeBottomPadding, - bool removeBottomInset = false, - bool maintainBottomViewPadding = false, - }) { - MediaQueryData data = MediaQuery.of(context).removePadding( - removeLeft: removeLeftPadding, - removeTop: removeTopPadding, - removeRight: removeRightPadding, - removeBottom: removeBottomPadding, - ); - if (removeBottomInset) { - data = data.removeViewInsets(removeBottom: true); - } - - if (maintainBottomViewPadding && data.viewInsets.bottom != 0.0) { - data = data.copyWith( - padding: data.padding.copyWith(bottom: data.viewPadding.bottom), - ); - } - - if (child != null) { - children.add( - LayoutId( - id: childId, - child: MediaQuery(data: data, child: child), - ), - ); - } - } - - void _buildEndDrawer(List children, TextDirection textDirection) { - if (widget.endDrawer != null) { - assert(hasEndDrawer); - _addIfNonNull( - children, - DrawerController( - key: _endDrawerKey, - alignment: DrawerAlignment.end, - drawerCallback: _endDrawerOpenedCallback, - dragStartBehavior: widget.drawerDragStartBehavior, - scrimColor: widget.drawerScrimColor, - edgeDragWidth: widget.drawerEdgeDragWidth, - enableOpenDragGesture: widget.endDrawerEnableOpenDragGesture, - isDrawerOpen: _endDrawerOpened.value, - drawerBarrierDismissible: widget.drawerBarrierDismissible, - child: widget.endDrawer!, - ), - _ScaffoldSlot.endDrawer, - // remove the side padding from the side we're not touching - removeLeftPadding: textDirection == TextDirection.ltr, - removeTopPadding: false, - removeRightPadding: textDirection == TextDirection.rtl, - removeBottomPadding: false, - ); - } - } - - void _buildDrawer(List children, TextDirection textDirection) { - if (widget.drawer != null) { - assert(hasDrawer); - _addIfNonNull( - children, - DrawerController( - key: _drawerKey, - alignment: DrawerAlignment.start, - drawerCallback: _drawerOpenedCallback, - dragStartBehavior: widget.drawerDragStartBehavior, - scrimColor: widget.drawerScrimColor, - edgeDragWidth: widget.drawerEdgeDragWidth, - enableOpenDragGesture: widget.drawerEnableOpenDragGesture, - isDrawerOpen: _drawerOpened.value, - drawerBarrierDismissible: widget.drawerBarrierDismissible, - child: widget.drawer!, - ), - _ScaffoldSlot.drawer, - // remove the side padding from the side we're not touching - removeLeftPadding: textDirection == TextDirection.rtl, - removeTopPadding: false, - removeRightPadding: textDirection == TextDirection.ltr, - removeBottomPadding: false, - ); - } - } - - late AnimationController _bottomSheetScrimAnimationController; - bool _showBodyScrim = false; - - /// Updates the state of the body scrim. - /// - /// This method is used to show or hide the body scrim and to set the animation value. - void showBodyScrim(bool value, double animationValue) { - if (_showBodyScrim != value) { - setState(() { - _showBodyScrim = value; - }); - } - if (_bottomSheetScrimAnimationController.value != animationValue) { - _bottomSheetScrimAnimationController.value = animationValue; - } - } - - @protected - @override - Widget build(BuildContext context) { - assert(debugCheckHasMediaQuery(context)); - assert(debugCheckHasDirectionality(context)); - final ThemeData themeData = Theme.of(context); - final TextDirection textDirection = Directionality.of(context); - - final List children = []; - _addIfNonNull( - children, - widget.body == null - ? null - : _BodyBuilder( - extendBody: widget.extendBody, - extendBodyBehindAppBar: widget.extendBodyBehindAppBar, - body: KeyedSubtree(key: _bodyKey, child: widget.body!), - ), - _ScaffoldSlot.body, - removeLeftPadding: false, - removeTopPadding: widget.appBar != null, - removeRightPadding: false, - removeBottomPadding: - widget.bottomNavigationBar != null || - widget.persistentFooterButtons != null, - removeBottomInset: _resizeToAvoidBottomInset, - ); - if (_showBodyScrim) { - _addIfNonNull( - children, - widget.bottomSheetScrimBuilder( - context, - _bottomSheetScrimAnimationController.view, - ), - _ScaffoldSlot.bodyScrim, - removeLeftPadding: true, - removeTopPadding: true, - removeRightPadding: true, - removeBottomPadding: true, - ); - } - - if (widget.appBar != null) { - final double topPadding = widget.primary - ? MediaQuery.paddingOf(context).top - : 0.0; - _appBarMaxHeight = - AppBar.preferredHeightFor(context, widget.appBar!.preferredSize) + - topPadding; - assert(_appBarMaxHeight! >= 0.0 && _appBarMaxHeight!.isFinite); - _addIfNonNull( - children, - ConstrainedBox( - constraints: BoxConstraints(maxHeight: _appBarMaxHeight!), - child: FlexibleSpaceBar.createSettings( - currentExtent: _appBarMaxHeight!, - child: widget.appBar!, - ), - ), - _ScaffoldSlot.appBar, - removeLeftPadding: false, - removeTopPadding: false, - removeRightPadding: false, - removeBottomPadding: true, - ); - } - - bool isSnackBarFloating = false; - double? snackBarWidth; - - if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) { - final Widget stack = Stack( - alignment: Alignment.bottomCenter, - children: [ - ..._dismissedBottomSheets, - if (_currentBottomSheet != null) _currentBottomSheet!._widget, - ], - ); - _addIfNonNull( - children, - stack, - _ScaffoldSlot.bottomSheet, - removeLeftPadding: false, - removeTopPadding: true, - removeRightPadding: false, - removeBottomPadding: _resizeToAvoidBottomInset, - ); - } - - // SnackBar set by ScaffoldMessenger - if (_messengerSnackBar != null) { - final SnackBarBehavior snackBarBehavior = - _messengerSnackBar?._widget.behavior ?? - themeData.snackBarTheme.behavior ?? - SnackBarBehavior.fixed; - isSnackBarFloating = snackBarBehavior == SnackBarBehavior.floating; - snackBarWidth = - _messengerSnackBar?._widget.width ?? themeData.snackBarTheme.width; - - _addIfNonNull( - children, - _messengerSnackBar?._widget, - _ScaffoldSlot.snackBar, - removeLeftPadding: false, - removeTopPadding: true, - removeRightPadding: false, - removeBottomPadding: - widget.bottomNavigationBar != null || - widget.persistentFooterButtons != null, - maintainBottomViewPadding: !_resizeToAvoidBottomInset, - ); - } - - bool extendBodyBehindMaterialBanner = false; - // MaterialBanner set by ScaffoldMessenger - if (_messengerMaterialBanner != null) { - final MaterialBannerThemeData bannerTheme = MaterialBannerTheme.of( - context, - ); - final double elevation = - _messengerMaterialBanner?._widget.elevation ?? - bannerTheme.elevation ?? - 0.0; - extendBodyBehindMaterialBanner = elevation != 0.0; - - _addIfNonNull( - children, - _messengerMaterialBanner?._widget, - _ScaffoldSlot.materialBanner, - removeLeftPadding: false, - removeTopPadding: widget.appBar != null, - removeRightPadding: false, - removeBottomPadding: true, - maintainBottomViewPadding: !_resizeToAvoidBottomInset, - ); - } - - if (widget.persistentFooterButtons != null) { - _addIfNonNull( - children, - Container( - decoration: - widget.persistentFooterDecoration ?? - BoxDecoration( - border: Border( - top: Divider.createBorderSide(context, width: 1.0), - ), - ), - child: SafeArea( - top: false, - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(8), - child: Align( - alignment: widget.persistentFooterAlignment, - child: OverflowBar( - spacing: 8, - overflowAlignment: OverflowBarAlignment.end, - children: widget.persistentFooterButtons!, - ), - ), - ), - ), - ), - ), - _ScaffoldSlot.persistentFooter, - removeLeftPadding: false, - removeTopPadding: true, - removeRightPadding: false, - removeBottomPadding: widget.bottomNavigationBar != null, - maintainBottomViewPadding: !_resizeToAvoidBottomInset, - ); - } - - if (widget.bottomNavigationBar != null) { - _addIfNonNull( - children, - widget.bottomNavigationBar, - _ScaffoldSlot.bottomNavigationBar, - removeLeftPadding: false, - removeTopPadding: true, - removeRightPadding: false, - removeBottomPadding: false, - maintainBottomViewPadding: !_resizeToAvoidBottomInset, - ); - } - - _addIfNonNull( - children, - _FloatingActionButtonTransition( - fabMoveAnimation: _floatingActionButtonMoveController, - fabMotionAnimator: _floatingActionButtonAnimator, - geometryNotifier: _geometryNotifier, - currentController: _floatingActionButtonVisibilityController, - child: widget.floatingActionButton, - ), - _ScaffoldSlot.floatingActionButton, - removeLeftPadding: true, - removeTopPadding: true, - removeRightPadding: true, - removeBottomPadding: true, - ); - - switch (themeData.platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - _addIfNonNull( - children, - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _handleStatusBarTap, - // iOS accessibility automatically adds scroll-to-top to the clock in the status bar - excludeFromSemantics: true, - ), - _ScaffoldSlot.statusBar, - removeLeftPadding: false, - removeTopPadding: true, - removeRightPadding: false, - removeBottomPadding: true, - ); - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - break; - } - - if (_endDrawerOpened.value) { - _buildDrawer(children, textDirection); - _buildEndDrawer(children, textDirection); - } else { - _buildEndDrawer(children, textDirection); - _buildDrawer(children, textDirection); - } - - // The minimum insets for contents of the Scaffold to keep visible. - final EdgeInsets minInsets = - MediaQuery.paddingOf( - context, - ).copyWith( - bottom: _resizeToAvoidBottomInset - ? MediaQuery.viewInsetsOf(context).bottom - : 0.0, - ); - - // The minimum viewPadding for interactive elements positioned by the - // Scaffold to keep within safe interactive areas. - final EdgeInsets minViewPadding = MediaQuery.viewPaddingOf(context) - .copyWith( - bottom: - _resizeToAvoidBottomInset && - MediaQuery.viewInsetsOf(context).bottom != 0.0 - ? 0.0 - : null, - ); - - return _ScaffoldScope( - hasDrawer: hasDrawer, - geometryNotifier: _geometryNotifier, - child: ScrollNotificationObserver( - child: Material( - color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor, - child: AnimatedBuilder( - animation: _floatingActionButtonMoveController, - builder: (BuildContext context, Widget? child) { - return Actions( - actions: >{ - DismissIntent: _DismissDrawerAction(context), - }, - child: CustomMultiChildLayout( - delegate: _ScaffoldLayout( - extendBody: widget.extendBody, - extendBodyBehindAppBar: widget.extendBodyBehindAppBar, - minInsets: minInsets, - minViewPadding: minViewPadding, - currentFloatingActionButtonLocation: - _floatingActionButtonLocation!, - floatingActionButtonMoveAnimationProgress: - _floatingActionButtonMoveController.value, - floatingActionButtonMotionAnimator: - _floatingActionButtonAnimator, - geometryNotifier: _geometryNotifier, - previousFloatingActionButtonLocation: - _previousFloatingActionButtonLocation!, - textDirection: textDirection, - isSnackBarFloating: isSnackBarFloating, - extendBodyBehindMaterialBanner: - extendBodyBehindMaterialBanner, - snackBarWidth: snackBarWidth, - ), - children: children, - ), - ); - }, - ), - ), - ), - ); - } -} - -class _DismissDrawerAction extends DismissAction { - _DismissDrawerAction(this.context); - - final BuildContext context; - - @override - bool isEnabled(DismissIntent intent) { - return Scaffold.of(context).isDrawerOpen || - Scaffold.of(context).isEndDrawerOpen; - } - - @override - void invoke(DismissIntent intent) { - Scaffold.of(context).closeDrawer(); - Scaffold.of(context).closeEndDrawer(); - } -} - -/// An interface for controlling a feature of a [Scaffold]. -/// -/// Commonly obtained from [ScaffoldMessengerState.showSnackBar] or -/// [ScaffoldState.showBottomSheet]. -class ScaffoldFeatureController { - const ScaffoldFeatureController._( - this._widget, - this._completer, - this.close, - this.setState, - ); - final T _widget; - final Completer _completer; - - /// Completes when the feature controlled by this object is no longer visible. - Future get closed => _completer.future; - - /// Remove the feature (e.g., bottom sheet, snack bar, or material banner) from the scaffold. - final VoidCallback close; - - /// Mark the feature (e.g., bottom sheet or snack bar) as needing to rebuild. - final StateSetter? setState; -} - -class _StandardBottomSheet extends StatefulWidget { - const _StandardBottomSheet({ - super.key, - required this.animationController, - this.enableDrag = true, - this.showDragHandle, - required this.onClosing, - required this.onDismissed, - required this.builder, - this.isPersistent = false, - this.backgroundColor, - this.elevation, - this.shape, - this.clipBehavior, - this.constraints, - this.onDispose, - }); - - final AnimationController - animationController; // we control it, but it must be disposed by whoever created it. - final bool enableDrag; - final bool? showDragHandle; - final VoidCallback? onClosing; - final VoidCallback? onDismissed; - final VoidCallback? onDispose; - final WidgetBuilder builder; - final bool isPersistent; - final Color? backgroundColor; - final double? elevation; - final ShapeBorder? shape; - final Clip? clipBehavior; - final BoxConstraints? constraints; - - @override - _StandardBottomSheetState createState() => _StandardBottomSheetState(); -} - -class _StandardBottomSheetState extends State<_StandardBottomSheet> { - ParametricCurve animationCurve = _standardBottomSheetCurve; - - @override - void initState() { - super.initState(); - assert(widget.animationController.isForwardOrCompleted); - widget.animationController.addStatusListener(_handleStatusChange); - } - - @override - void dispose() { - widget.animationController.removeStatusListener(_handleStatusChange); - widget.onDispose?.call(); - super.dispose(); - } - - @override - void didUpdateWidget(_StandardBottomSheet oldWidget) { - super.didUpdateWidget(oldWidget); - assert(widget.animationController == oldWidget.animationController); - } - - void close() { - widget.animationController.reverse(); - widget.onClosing?.call(); - } - - 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.animationController.value, - endCurve: _standardBottomSheetCurve, - ); - } - - void _handleStatusChange(AnimationStatus status) { - if (status.isDismissed) { - widget.onDismissed?.call(); - } - } - - bool extentChanged(DraggableScrollableNotification notification) { - final double extentRemaining = 1.0 - notification.extent; - final ScaffoldState scaffold = Scaffold.of(context); - if (extentRemaining < _kBottomSheetDominatesPercentage) { - scaffold._floatingActionButtonVisibilityController.value = - extentRemaining * _kBottomSheetDominatesPercentage * 10; - - final double scrimAnimationValue = - 1 - extentRemaining / _kBottomSheetDominatesPercentage; - scaffold.showBodyScrim(true, scrimAnimationValue); - } else { - scaffold._floatingActionButtonVisibilityController.value = 1.0; - scaffold.showBodyScrim(false, 0.0); - } - // If the Scaffold.bottomSheet != null, we're a persistent bottom sheet. - if (notification.extent == notification.minExtent && - scaffold.widget.bottomSheet == null && - notification.shouldCloseOnMinExtent) { - close(); - } - return false; - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: widget.animationController, - builder: (BuildContext context, Widget? child) { - return Align( - alignment: AlignmentDirectional.topStart, - heightFactor: animationCurve.transform( - widget.animationController.value, - ), - child: child, - ); - }, - child: Semantics( - container: true, - onDismiss: !widget.isPersistent ? close : null, - child: NotificationListener( - onNotification: extentChanged, - child: BottomSheet( - animationController: widget.animationController, - enableDrag: widget.enableDrag, - showDragHandle: widget.showDragHandle, - onDragStart: _handleDragStart, - onDragEnd: _handleDragEnd, - onClosing: widget.onClosing!, - builder: widget.builder, - backgroundColor: widget.backgroundColor, - elevation: widget.elevation, - shape: widget.shape, - clipBehavior: widget.clipBehavior, - constraints: widget.constraints, - ), - ), - ), - ); - } -} - -/// A [ScaffoldFeatureController] for standard bottom sheets. -/// -/// This is the type of objects returned by [ScaffoldState.showBottomSheet]. -/// -/// This controller is used to display both standard and persistent bottom -/// sheets. A bottom sheet is only persistent if it is set as the -/// [Scaffold.bottomSheet]. -class PersistentBottomSheetController - extends ScaffoldFeatureController<_StandardBottomSheet, void> - implements material.PersistentBottomSheetController { - const PersistentBottomSheetController._( - super.widget, - super.completer, - super.close, - StateSetter super.setState, - this._isLocalHistoryEntry, - ) : super._(); - - final bool _isLocalHistoryEntry; -} - -class _ScaffoldScope extends InheritedWidget { - const _ScaffoldScope({ - required this.hasDrawer, - required this.geometryNotifier, - required super.child, - }); - - final bool hasDrawer; - final _ScaffoldGeometryNotifier geometryNotifier; - - @override - bool updateShouldNotify(_ScaffoldScope oldWidget) { - return hasDrawer != oldWidget.hasDrawer; - } -} diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index f84499bd5..08d55395b 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; -import 'package:PiliPlus/common/widgets/scaffold/scaffold.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/init.dart'; @@ -54,7 +53,7 @@ import 'package:dio/dio.dart' show Options; import 'package:easy_debounce/easy_throttle.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/foundation.dart' show kDebugMode; -import 'package:flutter/material.dart' hide Scaffold, ScaffoldState; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:get/get.dart' hide ContextExtensionss; diff --git a/lib/pages/video/reply/view.dart b/lib/pages/video/reply/view.dart index da23d05ae..f62813433 100644 --- a/lib/pages/video/reply/view.dart +++ b/lib/pages/video/reply/view.dart @@ -2,7 +2,6 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; -import 'package:PiliPlus/common/widgets/scaffold/bottom_sheet.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/http/loading_state.dart'; @@ -11,7 +10,7 @@ import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:easy_debounce/easy_throttle.dart'; -import 'package:flutter/material.dart' hide showBottomSheet; +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; diff --git a/lib/pages/video/reply_reply/view.dart b/lib/pages/video/reply_reply/view.dart index 8ace63c06..03ef34977 100644 --- a/lib/pages/video/reply_reply/view.dart +++ b/lib/pages/video/reply_reply/view.dart @@ -2,7 +2,6 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; -import 'package:PiliPlus/common/widgets/scaffold/scaffold.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo, Mode; import 'package:PiliPlus/http/loading_state.dart'; @@ -12,7 +11,7 @@ import 'package:PiliPlus/pages/video/reply_reply/controller.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; -import 'package:flutter/material.dart' hide Scaffold, ScaffoldState; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart' hide ContextExtensionss; import 'package:super_sliver_list/super_sliver_list.dart'; diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index d94c8546b..e62b9ae62 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -7,7 +7,6 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; -import 'package:PiliPlus/common/widgets/scaffold/scaffold.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/main.dart'; @@ -56,7 +55,7 @@ import 'package:auto_orientation/auto_orientation.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart' show kDebugMode; -import 'package:flutter/material.dart' hide Scaffold; +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart' show SystemUiOverlayStyle; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; diff --git a/lib/scripts/bottom_sheet_patch.diff b/lib/scripts/bottom_sheet_patch.diff new file mode 100644 index 000000000..d0325aecf --- /dev/null +++ b/lib/scripts/bottom_sheet_patch.diff @@ -0,0 +1,12 @@ +diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart +index ec9e4270863..c850912afe1 100644 +--- a/packages/flutter/lib/src/material/bottom_sheet.dart ++++ b/packages/flutter/lib/src/material/bottom_sheet.dart +@@ -1412,6 +1412,7 @@ class _BottomSheetGestureDetector extends StatelessWidget { + ..onStart = onVerticalDragStart + ..onUpdate = onVerticalDragUpdate + ..onEnd = onVerticalDragEnd ++ ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context) + ..onlyAcceptDragOnThreshold = true; + }, + ),