drawer.dart 7 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 77 78 79 80
/// Provides interactive behavior for [Drawer] widgets.
///
/// Drawer controllers are typically created automatically by [Scaffold]
/// widgets.
///
/// See also:
81
///
82 83
/// * [Drawer]
/// * [Scaffold.drawer]
84
class DrawerController extends StatefulWidget {
85 86
  DrawerController({
    GlobalKey key,
87 88 89
    this.child
  }) : super(key: key);

90
  /// The widget below this widget in the tree.
91 92
  final Widget child;

93
  @override
94
  DrawerControllerState createState() => new DrawerControllerState();
95
}
96

97
class DrawerControllerState extends State<DrawerController> {
98
  @override
99 100
  void initState() {
    super.initState();
101 102 103
    _controller = new AnimationController(duration: _kBaseSettleDuration)
      ..addListener(_animationChanged)
      ..addStatusListener(_animationStatusChanged);
104 105
  }

106
  @override
107
  void dispose() {
108 109 110
    _controller
      ..removeListener(_animationChanged)
      ..removeStatusListener(_animationStatusChanged)
111 112 113 114
      ..stop();
    super.dispose();
  }

115
  void _animationChanged() {
116
    setState(() {
117
      // The animation controller's state is our build state, and it changed already.
118 119 120
    });
  }

121
  LocalHistoryEntry _historyEntry;
122
  // TODO(abarth): This should be a GlobalValueKey when those exist.
Hixie's avatar
Hixie committed
123
  GlobalKey get _drawerKey => new GlobalObjectKey(config.key);
124 125 126

  void _ensureHistoryEntry() {
    if (_historyEntry == null) {
127
      ModalRoute<dynamic> route = ModalRoute.of(context);
128 129 130
      if (route != null) {
        _historyEntry = new LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
        route.addLocalHistoryEntry(_historyEntry);
Hixie's avatar
Hixie committed
131
        Focus.moveScopeTo(_drawerKey, context: context);
132 133 134 135
      }
    }
  }

136
  void _animationStatusChanged(AnimationStatus status) {
137
    switch (status) {
138
      case AnimationStatus.forward:
139 140
        _ensureHistoryEntry();
        break;
141
      case AnimationStatus.reverse:
142 143 144
        _historyEntry?.remove();
        _historyEntry = null;
        break;
145
      case AnimationStatus.dismissed:
146
        break;
147
      case AnimationStatus.completed:
148 149
        break;
    }
150 151
  }

152 153 154 155
  void _handleHistoryEntryRemoved() {
    _historyEntry = null;
    close();
  }
156

157
  AnimationController _controller;
Hixie's avatar
Hixie committed
158

159
  void _handleDragDown(Point position) {
160
    _controller.stop();
161
    _ensureHistoryEntry();
Hixie's avatar
Hixie committed
162 163
  }

164 165 166 167 168 169 170 171 172 173
  void _handleDragCancel() {
    if (_controller.isDismissed || _controller.isAnimating)
      return;
    if (_controller.value < 0.5) {
      close();
    } else {
      open();
    }
  }

Hixie's avatar
Hixie committed
174 175 176 177 178 179 180
  double get _width {
    RenderBox drawerBox = _drawerKey.currentContext?.findRenderObject();
    if (drawerBox != null)
      return drawerBox.size.width;
    return _kWidth; // drawer not being shown currently
  }

181
  void _move(double delta) {
182
    _controller.value += delta / _width;
183 184
  }

185
  void _settle(Velocity velocity) {
186
    if (_controller.isDismissed)
187
      return;
188 189
    if (velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
      _controller.fling(velocity: velocity.pixelsPerSecond.dx / _width);
190
    } else if (_controller.value < 0.5) {
191
      close();
192
    } else {
193
      open();
194 195 196
    }
  }

197
  void open() {
198
    _controller.fling(velocity: 1.0);
199 200 201
  }

  void close() {
202
    _controller.fling(velocity: -1.0);
203
  }
204

205
  final ColorTween _color = new ColorTween(begin: Colors.transparent, end: Colors.black54);
206
  final GlobalKey _gestureDetectorKey = new GlobalKey();
207

208
  @override
209
  Widget build(BuildContext context) {
210
    if (_controller.status == AnimationStatus.dismissed) {
211
      return new Align(
212
        alignment: FractionalOffset.centerLeft,
213 214 215 216 217
        child: new GestureDetector(
          key: _gestureDetectorKey,
          onHorizontalDragUpdate: _move,
          onHorizontalDragEnd: _settle,
          behavior: HitTestBehavior.translucent,
Hixie's avatar
Hixie committed
218
          excludeFromSemantics: true,
219 220
          child: new Container(width: _kEdgeDragWidth)
        )
221 222
      );
    } else {
223 224
      return new GestureDetector(
        key: _gestureDetectorKey,
225
        onHorizontalDragDown: _handleDragDown,
226 227
        onHorizontalDragUpdate: _move,
        onHorizontalDragEnd: _settle,
228
        onHorizontalDragCancel: _handleDragCancel,
229
        child: new RepaintBoundary(
230 231 232 233 234 235
          child: new Stack(
            children: <Widget>[
              new GestureDetector(
                onTap: close,
                child: new DecoratedBox(
                  decoration: new BoxDecoration(
236
                    backgroundColor: _color.evaluate(_controller)
237 238 239 240 241
                  ),
                  child: new Container()
                )
              ),
              new Align(
242
                alignment: FractionalOffset.centerLeft,
243
                child: new Align(
244
                  alignment: FractionalOffset.centerRight,
245 246 247 248 249
                  widthFactor: _controller.value,
                  child: new RepaintBoundary(
                    child: new Focus(
                      key: _drawerKey,
                      child: config.child
250 251
                    )
                  )
252
                )
253
              )
254 255
            ]
          )
256
        )
257 258
      );
    }
259
  }
260
}