drawer.dart 7.82 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:flutter/widgets.dart';
6

7
import 'colors.dart';
8
import 'material.dart';
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// TODO(eseidel): Draw width should vary based on device size:
// http://www.google.com/design/spec/layout/structure.html#structure-side-nav

// Mobile:
// Width = Screen width − 56 dp
// Maximum width: 320dp
// Maximum width applies only when using a left nav. When using a right nav,
// the panel can cover the full width of the screen.

// Desktop/Tablet:
// Maximum width for a left nav is 400dp.
// The right nav can vary depending on content.

const double _kWidth = 304.0;
24
const double _kEdgeDragWidth = 20.0;
Matt Perry's avatar
Matt Perry committed
25
const double _kMinFlingVelocity = 365.0;
26 27
const Duration _kBaseSettleDuration = const Duration(milliseconds: 246);

28 29 30 31
/// A material design drawer.
///
/// Typically used in the [Scaffold.drawer] property, a drawer slides in from
/// the side of the screen and displays a list of items that the user can
32 33 34 35
/// interact with.
///
/// Typically, the child of the drawer is a [Block] whose first child is a
/// [DrawerHeader] that displays status information about the current user.
36 37
///
/// See also:
38
///
39 40 41 42
///  * [Scaffold.drawer]
///  * [DrawerItem]
///  * [DrawerHeader]
///  * <https://www.google.com/design/spec/patterns/navigation-drawer.html>
43
class Drawer extends StatelessWidget {
44 45 46
  /// Creates a material design drawer.
  ///
  /// Typically used in the [Scaffold.drawer] property.
47 48 49 50 51
  Drawer({
    Key key,
    this.elevation: 16,
    this.child
  }) : super(key: key);
52

53
  /// The z-coordinate at which to place this drawer.
54 55
  ///
  /// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
Hans Muller's avatar
Hans Muller committed
56
  final int elevation;
57 58

  /// The widget below this widget in the tree.
59 60
  ///
  /// Typically a [Block].
61
  final Widget child;
62

63
  @override
64 65 66 67 68 69
  Widget build(BuildContext context) {
    return new ConstrainedBox(
      constraints: const BoxConstraints.expand(width: _kWidth),
      child: new Material(
        elevation: elevation,
        child: child
70
      )
71 72 73 74
    );
  }
}

75 76
/// Provides interactive behavior for [Drawer] widgets.
///
77 78 79 80 81 82 83
/// Rarely used directly. Drawer controllers are typically created automatically
/// by [Scaffold] widgets.
///
/// The draw controller provides the ability to open and close a drawer, either
/// via an animation or via user interaction. When closed, the drawer collapses
/// to a translucent gesture detector that can be used to listen for edge
/// swipes.
84 85
///
/// See also:
86
///
87 88
///  * [Drawer]
///  * [Scaffold.drawer]
89
class DrawerController extends StatefulWidget {
90 91 92 93 94
  /// Creates a controller for a [Drawer].
  ///
  /// Rarely used directly.
  ///
  /// The [child] argument must not be null and is typically a [Drawer].
95 96
  DrawerController({
    GlobalKey key,
97
    this.child
98 99 100
  }) : super(key: key) {
    assert(child != null);
  }
101

102
  /// The widget below this widget in the tree.
103 104
  ///
  /// Typically a [Drawer].
105 106
  final Widget child;

107
  @override
108
  DrawerControllerState createState() => new DrawerControllerState();
109
}
110

111 112 113
/// State for a [DrawerController].
///
/// Typically used by a [Scaffold] to [open] and [close] the drawer.
114
class DrawerControllerState extends State<DrawerController> {
115
  @override
116 117
  void initState() {
    super.initState();
118 119 120
    _controller = new AnimationController(duration: _kBaseSettleDuration)
      ..addListener(_animationChanged)
      ..addStatusListener(_animationStatusChanged);
121 122
  }

123
  @override
124
  void dispose() {
125
    _historyEntry?.remove();
126 127 128
    _controller
      ..removeListener(_animationChanged)
      ..removeStatusListener(_animationStatusChanged)
129 130 131 132
      ..stop();
    super.dispose();
  }

133
  void _animationChanged() {
134
    setState(() {
135
      // The animation controller's state is our build state, and it changed already.
136 137 138
    });
  }

139
  LocalHistoryEntry _historyEntry;
140
  // TODO(abarth): This should be a GlobalValueKey when those exist.
Hixie's avatar
Hixie committed
141
  GlobalKey get _drawerKey => new GlobalObjectKey(config.key);
142 143 144

  void _ensureHistoryEntry() {
    if (_historyEntry == null) {
145
      ModalRoute<dynamic> route = ModalRoute.of(context);
146 147 148
      if (route != null) {
        _historyEntry = new LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
        route.addLocalHistoryEntry(_historyEntry);
Hixie's avatar
Hixie committed
149
        Focus.moveScopeTo(_drawerKey, context: context);
150 151 152 153
      }
    }
  }

154
  void _animationStatusChanged(AnimationStatus status) {
155
    switch (status) {
156
      case AnimationStatus.forward:
157 158
        _ensureHistoryEntry();
        break;
159
      case AnimationStatus.reverse:
160 161 162
        _historyEntry?.remove();
        _historyEntry = null;
        break;
163
      case AnimationStatus.dismissed:
164
        break;
165
      case AnimationStatus.completed:
166 167
        break;
    }
168 169
  }

170 171 172 173
  void _handleHistoryEntryRemoved() {
    _historyEntry = null;
    close();
  }
174

175
  AnimationController _controller;
Hixie's avatar
Hixie committed
176

177
  void _handleDragDown(DragDownDetails details) {
178
    _controller.stop();
179
    _ensureHistoryEntry();
Hixie's avatar
Hixie committed
180 181
  }

182 183 184 185 186 187 188 189 190 191
  void _handleDragCancel() {
    if (_controller.isDismissed || _controller.isAnimating)
      return;
    if (_controller.value < 0.5) {
      close();
    } else {
      open();
    }
  }

Hixie's avatar
Hixie committed
192 193 194 195 196 197 198
  double get _width {
    RenderBox drawerBox = _drawerKey.currentContext?.findRenderObject();
    if (drawerBox != null)
      return drawerBox.size.width;
    return _kWidth; // drawer not being shown currently
  }

199 200
  void _move(DragUpdateDetails details) {
    _controller.value += details.primaryDelta / _width;
201 202
  }

203
  void _settle(DragEndDetails details) {
204
    if (_controller.isDismissed)
205
      return;
206 207
    if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
      _controller.fling(velocity: details.velocity.pixelsPerSecond.dx / _width);
208
    } else if (_controller.value < 0.5) {
209
      close();
210
    } else {
211
      open();
212 213 214
    }
  }

215 216 217
  /// Starts an animation to open the drawer.
  ///
  /// Typically called by [Scaffold.openDrawer].
218
  void open() {
219
    _controller.fling(velocity: 1.0);
220 221
  }

222
  /// Starts an animation to close the drawer.
223
  void close() {
224
    _controller.fling(velocity: -1.0);
225
  }
226

227
  final ColorTween _color = new ColorTween(begin: Colors.transparent, end: Colors.black54);
228
  final GlobalKey _gestureDetectorKey = new GlobalKey();
229

230
  @override
231
  Widget build(BuildContext context) {
232
    if (_controller.status == AnimationStatus.dismissed) {
233
      return new Align(
234
        alignment: FractionalOffset.centerLeft,
235 236 237 238 239
        child: new GestureDetector(
          key: _gestureDetectorKey,
          onHorizontalDragUpdate: _move,
          onHorizontalDragEnd: _settle,
          behavior: HitTestBehavior.translucent,
Hixie's avatar
Hixie committed
240
          excludeFromSemantics: true,
241 242
          child: new Container(width: _kEdgeDragWidth)
        )
243 244
      );
    } else {
245 246
      return new GestureDetector(
        key: _gestureDetectorKey,
247
        onHorizontalDragDown: _handleDragDown,
248 249
        onHorizontalDragUpdate: _move,
        onHorizontalDragEnd: _settle,
250
        onHorizontalDragCancel: _handleDragCancel,
251
        child: new RepaintBoundary(
252 253 254 255 256 257
          child: new Stack(
            children: <Widget>[
              new GestureDetector(
                onTap: close,
                child: new DecoratedBox(
                  decoration: new BoxDecoration(
258
                    backgroundColor: _color.evaluate(_controller)
259 260 261 262 263
                  ),
                  child: new Container()
                )
              ),
              new Align(
264
                alignment: FractionalOffset.centerLeft,
265
                child: new Align(
266
                  alignment: FractionalOffset.centerRight,
267 268 269 270 271
                  widthFactor: _controller.value,
                  child: new RepaintBoundary(
                    child: new Focus(
                      key: _drawerKey,
                      child: config.child
272 273
                    )
                  )
274
                )
275
              )
276 277
            ]
          )
278
        )
279 280
      );
    }
281
  }
282
}