bottom_sheet.dart 12.9 KB
Newer Older
1 2 3 4 5 6
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7
import 'package:flutter/foundation.dart';
8 9 10
import 'package:flutter/widgets.dart';

import 'colors.dart';
11
import 'debug.dart';
12
import 'material.dart';
13
import 'material_localizations.dart';
14
import 'scaffold.dart';
15
import 'theme.dart';
16

17
const Duration _kBottomSheetDuration = Duration(milliseconds: 200);
18
const double _kMinFlingVelocity = 700.0;
19 20
const double _kCloseProgressThreshold = 0.5;

21 22 23 24 25 26 27 28
/// 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
29 30
///    [ScaffoldState.showBottomSheet] function or by specifying the
///    [Scaffold.bottomSheet] constructor parameter.
31 32 33 34 35 36 37
///
///  * _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
38 39
/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] or
/// [Scaffold.bottomSheet], and a modal bottom sheet with [showModalBottomSheet].
40 41 42
///
/// See also:
///
Ian Hickson's avatar
Ian Hickson committed
43
///  * [ScaffoldState.showBottomSheet]
44
///  * [showModalBottomSheet]
45
///  * <https://material.google.com/components/bottom-sheets.html>
46
class BottomSheet extends StatefulWidget {
47 48 49
  /// Creates a bottom sheet.
  ///
  /// Typically, bottom sheets are created implicitly by
50
  /// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by
51
  /// [showModalBottomSheet], for modal bottom sheets.
52
  const BottomSheet({
53
    Key key,
54
    this.animationController,
55
    this.enableDrag = true,
56
    this.elevation = 0.0,
57 58
    @required this.onClosing,
    @required this.builder
59 60
  }) : assert(enableDrag != null),
       assert(onClosing != null),
61
       assert(builder != null),
62
       assert(elevation != null),
63
       super(key: key);
64

65 66 67 68
  /// The animation that controls the bottom sheet's position.
  ///
  /// The BottomSheet widget will manipulate the position of this animation, it
  /// is not just a passive observer.
69
  final AnimationController animationController;
70 71 72

  /// Called when the bottom sheet begins to close.
  ///
73
  /// A bottom sheet might be prevented from closing (e.g., by user
74 75
  /// interaction) even after this callback is called. For this reason, this
  /// callback might be call multiple times for a given bottom sheet.
76
  final VoidCallback onClosing;
77 78 79 80 81

  /// A builder for the contents of the sheet.
  ///
  /// The bottom sheet will wrap the widget produced by this builder in a
  /// [Material] widget.
82 83
  final WidgetBuilder builder;

84 85 86 87 88 89
  /// If true, the bottom sheet can dragged up and down and dismissed by swiping
  /// downards.
  ///
  /// Default is true.
  final bool enableDrag;

90 91 92 93 94 95
  /// The z-coordinate at which to place this material. This controls the size
  /// of the shadow below the material.
  ///
  /// Defaults to 0.
  final double elevation;

96
  @override
97
  _BottomSheetState createState() => _BottomSheetState();
98

99
  /// Creates an animation controller suitable for controlling a [BottomSheet].
100
  static AnimationController createAnimationController(TickerProvider vsync) {
101
    return AnimationController(
102
      duration: _kBottomSheetDuration,
103 104
      debugLabel: 'BottomSheet',
      vsync: vsync,
105 106
    );
  }
107 108 109 110
}

class _BottomSheetState extends State<BottomSheet> {

111
  final GlobalKey _childKey = GlobalKey(debugLabel: 'BottomSheet child');
112 113 114 115 116

  double get _childHeight {
    final RenderBox renderBox = _childKey.currentContext.findRenderObject();
    return renderBox.size.height;
  }
117

118
  bool get _dismissUnderway => widget.animationController.status == AnimationStatus.reverse;
119

120
  void _handleDragUpdate(DragUpdateDetails details) {
121 122
    if (_dismissUnderway)
      return;
123
    widget.animationController.value -= details.primaryDelta / (_childHeight ?? details.primaryDelta);
124 125
  }

126
  void _handleDragEnd(DragEndDetails details) {
127 128
    if (_dismissUnderway)
      return;
129
    if (details.velocity.pixelsPerSecond.dy > _kMinFlingVelocity) {
130
      final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
131 132
      if (widget.animationController.value > 0.0)
        widget.animationController.fling(velocity: flingVelocity);
133
      if (flingVelocity < 0.0)
134 135 136 137 138
        widget.onClosing();
    } else if (widget.animationController.value < _kCloseProgressThreshold) {
      if (widget.animationController.value > 0.0)
        widget.animationController.fling(velocity: -1.0);
      widget.onClosing();
139
    } else {
140
      widget.animationController.forward();
141
    }
142 143
  }

144
  @override
145
  Widget build(BuildContext context) {
146
    final Widget bottomSheet = Material(
147
      key: _childKey,
148
      elevation: widget.elevation,
149 150
      child: widget.builder(context),
    );
151
    return !widget.enableDrag ? bottomSheet : GestureDetector(
152
      onVerticalDragUpdate: _handleDragUpdate,
153
      onVerticalDragEnd: _handleDragEnd,
154
      child: bottomSheet,
155
      excludeFromSemantics: true,
156 157 158 159
    );
  }
}

160
// PERSISTENT BOTTOM SHEETS
161

162
// See scaffold.dart
163 164


165
// MODAL BOTTOM SHEETS
166

167
class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
168 169 170
  _ModalBottomSheetLayout(this.progress);

  final double progress;
171

172
  @override
173
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
174
    return BoxConstraints(
175 176 177 178 179 180 181
      minWidth: constraints.maxWidth,
      maxWidth: constraints.maxWidth,
      minHeight: 0.0,
      maxHeight: constraints.maxHeight * 9.0 / 16.0
    );
  }

182
  @override
183
  Offset getPositionForChild(Size size, Size childSize) {
184
    return Offset(0.0, size.height - childSize.height * progress);
185 186
  }

187
  @override
188 189
  bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
    return progress != oldDelegate.progress;
190 191 192
  }
}

193
class _ModalBottomSheet<T> extends StatefulWidget {
194
  const _ModalBottomSheet({ Key key, this.route }) : super(key: key);
195

196
  final _ModalBottomSheetRoute<T> route;
197

198
  @override
199
  _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
200 201
}

202
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
203
  @override
204
  Widget build(BuildContext context) {
205 206 207 208 209 210 211 212 213 214 215 216 217
    final MediaQueryData mediaQuery = MediaQuery.of(context);
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
    String routeLabel;
    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
        routeLabel = '';
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        routeLabel = localizations.dialogLabel;
        break;
    }

218
    return GestureDetector(
219
      excludeFromSemantics: true,
220
      onTap: () => Navigator.pop(context),
221
      child: AnimatedBuilder(
222
        animation: widget.route.animation,
223
        builder: (BuildContext context, Widget child) {
224 225 226
          // Disable the initial animation when accessible navigation is on so
          // that the semantics are added to the tree at the correct time.
          final double animationValue = mediaQuery.accessibleNavigation ? 1.0 : widget.route.animation.value;
227
          return Semantics(
228 229 230 231
            scopesRoute: true,
            namesRoute: true,
            label: routeLabel,
            explicitChildNodes: true,
232 233 234 235
            child: ClipRect(
              child: CustomSingleChildLayout(
                delegate: _ModalBottomSheetLayout(animationValue),
                child: BottomSheet(
236 237 238 239 240 241
                  animationController: widget.route._animationController,
                  onClosing: () => Navigator.pop(context),
                  builder: widget.route.builder,
                ),
              ),
            ),
242 243 244
          );
        }
      )
245 246 247 248
    );
  }
}

Hixie's avatar
Hixie committed
249 250
class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
  _ModalBottomSheetRoute({
251 252
    this.builder,
    this.theme,
253
    this.barrierLabel,
254 255
    RouteSettings settings,
  }) : super(settings: settings);
256 257

  final WidgetBuilder builder;
258
  final ThemeData theme;
259

260
  @override
Hixie's avatar
Hixie committed
261
  Duration get transitionDuration => _kBottomSheetDuration;
262 263

  @override
264
  bool get barrierDismissible => true;
265

266 267 268
  @override
  final String barrierLabel;

269
  @override
Hixie's avatar
Hixie committed
270
  Color get barrierColor => Colors.black54;
271

272 273
  AnimationController _animationController;

274
  @override
275
  AnimationController createAnimationController() {
276
    assert(_animationController == null);
277
    _animationController = BottomSheet.createAnimationController(navigator.overlay);
278
    return _animationController;
279 280
  }

281
  @override
282
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
283 284
    // By definition, the bottom sheet is aligned to the bottom of the page
    // and isn't exposed to the top padding of the MediaQuery.
285
    Widget bottomSheet = MediaQuery.removePadding(
286 287
      context: context,
      removeTop: true,
288
      child: _ModalBottomSheet<T>(route: this),
289
    );
290
    if (theme != null)
291
      bottomSheet = Theme(data: theme, child: bottomSheet);
292
    return bottomSheet;
293 294 295
  }
}

296 297 298 299 300 301 302 303
/// Shows a modal material design bottom sheet.
///
/// 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 use from interacting with the app. Persistent bottom sheets
304 305
/// can be created and displayed with the [showBottomSheet] function or the
/// [ScaffoldState.showBottomSheet] method.
306
///
Ian Hickson's avatar
Ian Hickson committed
307 308 309 310 311
/// 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.
///
312
/// Returns a `Future` that resolves to the value (if any) that was passed to
313
/// [Navigator.pop] when the modal bottom sheet was closed.
314 315 316
///
/// See also:
///
317 318
///  * [BottomSheet], which is the widget normally returned by the function
///    passed as the `builder` argument to [showModalBottomSheet].
319 320
///  * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
///    non-modal bottom sheets.
321
///  * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets>
322 323 324 325
Future<T> showModalBottomSheet<T>({
  @required BuildContext context,
  @required WidgetBuilder builder,
}) {
326 327
  assert(context != null);
  assert(builder != null);
328
  assert(debugCheckHasMaterialLocalizations(context));
329
  return Navigator.push(context, _ModalBottomSheetRoute<T>(
330 331
    builder: builder,
    theme: Theme.of(context, shadowThemeOnly: true),
332
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
333 334
  ));
}
335 336 337

/// Shows a persistent material design bottom sheet in the nearest [Scaffold].
///
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
/// 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 appbar 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 appbar, use the
/// [Scaffold.bottomSheet] constructor parameter.
///
353
/// A persistent bottom sheet shows information that supplements the primary
354 355
/// content of the app. A persistent bottom sheet remains visible even when
/// the user interacts with other parts of the app.
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
///
/// 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 is the widget typically 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].
///  * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets>
PersistentBottomSheetController<T> showBottomSheet<T>({
  @required BuildContext context,
  @required WidgetBuilder builder,
}) {
  assert(context != null);
  assert(builder != null);
  return Scaffold.of(context).showBottomSheet<T>(builder);
}