bottom_sheet.dart 8.44 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 11
import 'package:flutter/widgets.dart';

import 'colors.dart';
import 'material.dart';
12
import 'theme.dart';
13 14

const Duration _kBottomSheetDuration = const Duration(milliseconds: 200);
15
const double _kMinFlingVelocity = 700.0;
16
const double _kCloseProgressThreshold = 0.5;
17 18
const Color _kTransparent = const Color(0x00000000);
const Color _kBarrierColor = Colors.black54;
19

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

60 61 62 63
  /// 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.
64
  final AnimationController animationController;
65 66 67 68 69 70

  /// Called when the bottom sheet begins to close.
  ///
  /// A bottom sheet might be 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.
71
  final VoidCallback onClosing;
72 73 74 75 76

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

79
  @override
80 81
  _BottomSheetState createState() => new _BottomSheetState();

82
  /// Creates an animation controller suitable for controlling a [BottomSheet].
83
  static AnimationController createAnimationController(TickerProvider vsync) {
84
    return new AnimationController(
85
      duration: _kBottomSheetDuration,
86 87
      debugLabel: 'BottomSheet',
      vsync: vsync,
88 89
    );
  }
90 91 92 93
}

class _BottomSheetState extends State<BottomSheet> {

94
  final GlobalKey _childKey = new GlobalKey(debugLabel: 'BottomSheet child');
95 96 97 98 99

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

Adam Barth's avatar
Adam Barth committed
101
  bool get _dismissUnderway => config.animationController.status == AnimationStatus.reverse;
102

103
  void _handleDragUpdate(DragUpdateDetails details) {
104 105
    if (_dismissUnderway)
      return;
106
    config.animationController.value -= details.primaryDelta / (_childHeight ?? details.primaryDelta);
107 108
  }

109
  void _handleDragEnd(DragEndDetails details) {
110 111
    if (_dismissUnderway)
      return;
112
    if (details.velocity.pixelsPerSecond.dy > _kMinFlingVelocity) {
113
      final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
114 115
      if (config.animationController.value > 0.0)
        config.animationController.fling(velocity: flingVelocity);
116
      if (flingVelocity < 0.0)
117
        config.onClosing();
118
    } else if (config.animationController.value < _kCloseProgressThreshold) {
119 120
      if (config.animationController.value > 0.0)
        config.animationController.fling(velocity: -1.0);
121
      config.onClosing();
122
    } else {
123
      config.animationController.forward();
124
    }
125 126
  }

127
  @override
128 129 130
  Widget build(BuildContext context) {
    return new GestureDetector(
      onVerticalDragUpdate: _handleDragUpdate,
131
      onVerticalDragEnd: _handleDragEnd,
132
      child: new Material(
133
        key: _childKey,
134
        child: config.builder(context)
135
      )
136 137 138 139
    );
  }
}

140
// PERSISTENT BOTTOM SHEETS
141

142
// See scaffold.dart
143 144


145
// MODAL BOTTOM SHEETS
146

147
class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
148 149 150
  _ModalBottomSheetLayout(this.progress);

  final double progress;
151

152
  @override
153 154 155 156 157 158 159 160 161
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return new BoxConstraints(
      minWidth: constraints.maxWidth,
      maxWidth: constraints.maxWidth,
      minHeight: 0.0,
      maxHeight: constraints.maxHeight * 9.0 / 16.0
    );
  }

162
  @override
163 164
  Offset getPositionForChild(Size size, Size childSize) {
    return new Offset(0.0, size.height - childSize.height * progress);
165 166
  }

167
  @override
168 169
  bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
    return progress != oldDelegate.progress;
170 171 172
  }
}

173
class _ModalBottomSheet<T> extends StatefulWidget {
174 175
  _ModalBottomSheet({ Key key, this.route }) : super(key: key);

176
  final _ModalBottomSheetRoute<T> route;
177

178
  @override
179
  _ModalBottomSheetState<T> createState() => new _ModalBottomSheetState<T>();
180 181
}

182
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
183
  @override
184
  Widget build(BuildContext context) {
185
    return new GestureDetector(
Hixie's avatar
Hixie committed
186
      onTap: () => Navigator.pop(context),
187
      child: new AnimatedBuilder(
188
        animation: config.route.animation,
189
        builder: (BuildContext context, Widget child) {
190
          return new ClipRect(
191
            child: new CustomSingleChildLayout(
192
              delegate: new _ModalBottomSheetLayout(config.route.animation.value),
193
              child: new BottomSheet(
194
                animationController: config.route._animationController,
Hixie's avatar
Hixie committed
195
                onClosing: () => Navigator.pop(context),
196
                builder: config.route.builder
197
              )
198
            )
199 200 201
          );
        }
      )
202 203 204 205
    );
  }
}

Hixie's avatar
Hixie committed
206 207
class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
  _ModalBottomSheetRoute({
208 209
    this.builder,
    this.theme,
210
  });
211 212

  final WidgetBuilder builder;
213
  final ThemeData theme;
214

215
  @override
Hixie's avatar
Hixie committed
216
  Duration get transitionDuration => _kBottomSheetDuration;
217 218

  @override
219
  bool get barrierDismissible => true;
220 221

  @override
Hixie's avatar
Hixie committed
222
  Color get barrierColor => Colors.black54;
223

224 225
  AnimationController _animationController;

226
  @override
227
  AnimationController createAnimationController() {
228
    assert(_animationController == null);
229
    _animationController = BottomSheet.createAnimationController(navigator.overlay);
230
    return _animationController;
231 232
  }

233
  @override
234
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
235 236 237 238
    Widget bottomSheet = new _ModalBottomSheet<T>(route: this);
    if (theme != null)
      bottomSheet = new Theme(data: theme, child: bottomSheet);
    return bottomSheet;
239 240 241
  }
}

242 243 244 245 246 247 248 249 250 251 252
/// 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
/// can be created and displayed with the [Scaffold.showBottomSheet] function.
///
/// Returns a `Future` that resolves to the value (if any) that was passed to
253
/// [Navigator.pop] when the modal bottom sheet was closed.
254 255 256 257 258
///
/// See also:
///
///  * [BottomSheet]
///  * [Scaffold.showBottomSheet]
259
///  * <https://material.google.com/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets>
260
Future<T> showModalBottomSheet<T>({ @required BuildContext context, @required WidgetBuilder builder }) {
261 262
  assert(context != null);
  assert(builder != null);
263
  return Navigator.push(context, new _ModalBottomSheetRoute<T>(
264 265
    builder: builder,
    theme: Theme.of(context, shadowThemeOnly: true),
266 267
  ));
}