drawer.dart 13.1 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/foundation.dart';
6
import 'package:flutter/widgets.dart';
7

8
import 'colors.dart';
9
import 'list_tile.dart';
10
import 'material.dart';
11
import 'material_localizations.dart';
12

13
/// The possible alignments of a [Drawer].
14
enum DrawerAlignment {
15 16 17 18
  /// Denotes that the [Drawer] is at the start side of the [Scaffold].
  ///
  /// This corresponds to the left side when the text direction is left-to-right
  /// and the right side when the text direction is right-to-left.
19 20
  start,

21 22 23 24
  /// Denotes that the [Drawer] is at the end side of the [Scaffold].
  ///
  /// This corresponds to the right side when the text direction is left-to-right
  /// and the left side when the text direction is right-to-left.
25 26 27
  end,
}

28
// TODO(eseidel): Draw width should vary based on device size:
29
// http://material.google.com/layout/structure.html#structure-side-nav
30 31 32 33 34 35 36 37 38 39 40 41

// 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;
42
const double _kEdgeDragWidth = 20.0;
Matt Perry's avatar
Matt Perry committed
43
const double _kMinFlingVelocity = 365.0;
44
const Duration _kBaseSettleDuration = Duration(milliseconds: 246);
45

46 47
/// A material design panel that slides in horizontally from the edge of a
/// [Scaffold] to show navigation links in an application.
48
///
49 50
/// Drawers are typically used with the [Scaffold.drawer] property. The child of
/// the drawer is usually a [ListView] whose first child is a [DrawerHeader]
51 52 53 54 55 56 57 58
/// that displays status information about the current user. The remaining
/// drawer children are often constructed with [ListTile]s, often concluding
/// with an [AboutListTile].
///
/// An open drawer can be closed by calling [Navigator.pop]. For example
/// a drawer item might close the drawer when tapped:
///
/// ```dart
59 60 61
/// ListTile(
///   leading: Icon(Icons.change_history),
///   title: Text('Change history'),
62 63 64 65 66 67
///   onTap: () {
///     // change app state...
///     Navigator.pop(context); // close the drawer
///   },
/// );
/// ```
68
///
69 70 71
/// The [AppBar] automatically displays an appropriate [IconButton] to show the
/// [Drawer] when a [Drawer] is available in the [Scaffold]. The [Scaffold]
/// automatically handles the edge-swipe gesture to show the drawer.
72
///
73
/// See also:
74
///
75 76 77 78 79
///  * [Scaffold.drawer], where one specifies a [Drawer] so that it can be
///    shown.
///  * [Scaffold.of], to obtain the current [ScaffoldState], which manages the
///    display and animation of the drawer.
///  * [ScaffoldState.openDrawer], which displays its [Drawer], if any.
80
///  * <https://material.google.com/patterns/navigation-drawer.html>
81
class Drawer extends StatelessWidget {
82 83 84
  /// Creates a material design drawer.
  ///
  /// Typically used in the [Scaffold.drawer] property.
85
  const Drawer({
86
    Key key,
87
    this.elevation = 16.0,
88
    this.child,
89
    this.semanticLabel,
90
  }) : super(key: key);
91

92 93
  /// The z-coordinate at which to place this drawer. This controls the size of
  /// the shadow below the drawer.
94
  ///
95
  /// Defaults to 16, the appropriate elevation for drawers.
96
  final double elevation;
97 98

  /// The widget below this widget in the tree.
99
  ///
100
  /// Typically a [SliverList].
101 102
  ///
  /// {@macro flutter.widgets.child}
103
  final Widget child;
104

105
  /// The semantic label of the dialog used by accessibility frameworks to
106
  /// announce screen transitions when the drawer is opened and closed.
107
  ///
108 109
  /// If this label is not provided, it will default to
  /// [MaterialLocalizations.drawerLabel].
110
  ///
111
  /// See also:
112
  ///
113 114 115 116
  ///  * [SemanticsConfiguration.namesRoute], for a description of how this
  ///    value is used.
  final String semanticLabel;

117
  @override
118
  Widget build(BuildContext context) {
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
    String label = semanticLabel;
    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
        label = semanticLabel;
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        label = semanticLabel ?? MaterialLocalizations.of(context)?.drawerLabel;
    }
    return new Semantics(
      scopesRoute: true,
      namesRoute: true,
      explicitChildNodes: true,
      label: label,
      child: new ConstrainedBox(
        constraints: const BoxConstraints.expand(width: _kWidth),
        child: new Material(
          elevation: elevation,
          child: child,
        ),
139
      ),
140 141 142 143
    );
  }
}

jslavitz's avatar
jslavitz committed
144 145 146 147
/// Signature for the callback that's called when a [DrawerController] is
/// opened or closed.
typedef void DrawerCallback(bool isOpened);

148 149
/// Provides interactive behavior for [Drawer] widgets.
///
150 151 152 153 154 155 156
/// 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.
157 158
///
/// See also:
159
///
160 161
///  * [Drawer], a container with the default width of a drawer.
///  * [Scaffold.drawer], the [Scaffold] slot for showing a drawer.
162
class DrawerController extends StatefulWidget {
163 164 165 166 167
  /// Creates a controller for a [Drawer].
  ///
  /// Rarely used directly.
  ///
  /// The [child] argument must not be null and is typically a [Drawer].
168
  const DrawerController({
169
    GlobalKey key,
170
    @required this.child,
171
    @required this.alignment,
jslavitz's avatar
jslavitz committed
172
    this.drawerCallback,
173
  }) : assert(child != null),
174
       assert(alignment != null),
175
       super(key: key);
176

177
  /// The widget below this widget in the tree.
178 179
  ///
  /// Typically a [Drawer].
180 181
  final Widget child;

182
  /// The alignment of the [Drawer].
183
  ///
184 185
  /// This controls the direction in which the user should swipe to open and
  /// close the drawer.
186 187
  final DrawerAlignment alignment;

jslavitz's avatar
jslavitz committed
188 189 190
  /// Optional callback that is called when a [Drawer] is opened or closed.
  final DrawerCallback drawerCallback;

191
  @override
192
  DrawerControllerState createState() => new DrawerControllerState();
193
}
194

195 196 197
/// State for a [DrawerController].
///
/// Typically used by a [Scaffold] to [open] and [close] the drawer.
198
class DrawerControllerState extends State<DrawerController> with SingleTickerProviderStateMixin {
199
  @override
200 201
  void initState() {
    super.initState();
202
    _controller = new AnimationController(duration: _kBaseSettleDuration, vsync: this)
203 204
      ..addListener(_animationChanged)
      ..addStatusListener(_animationStatusChanged);
205 206
  }

207
  @override
208
  void dispose() {
209
    _historyEntry?.remove();
210
    _controller.dispose();
211 212 213
    super.dispose();
  }

214
  void _animationChanged() {
215
    setState(() {
216
      // The animation controller's state is our build state, and it changed already.
217 218 219
    });
  }

220
  LocalHistoryEntry _historyEntry;
221
  final FocusScopeNode _focusScopeNode = new FocusScopeNode();
222 223 224

  void _ensureHistoryEntry() {
    if (_historyEntry == null) {
225
      final ModalRoute<dynamic> route = ModalRoute.of(context);
226 227 228
      if (route != null) {
        _historyEntry = new LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
        route.addLocalHistoryEntry(_historyEntry);
229
        FocusScope.of(context).setFirstFocus(_focusScopeNode);
230 231 232 233
      }
    }
  }

234
  void _animationStatusChanged(AnimationStatus status) {
235
    switch (status) {
236
      case AnimationStatus.forward:
237 238
        _ensureHistoryEntry();
        break;
239
      case AnimationStatus.reverse:
240 241 242
        _historyEntry?.remove();
        _historyEntry = null;
        break;
243
      case AnimationStatus.dismissed:
244
        break;
245
      case AnimationStatus.completed:
246 247
        break;
    }
248 249
  }

250 251 252 253
  void _handleHistoryEntryRemoved() {
    _historyEntry = null;
    close();
  }
254

255
  AnimationController _controller;
Hixie's avatar
Hixie committed
256

257
  void _handleDragDown(DragDownDetails details) {
258
    _controller.stop();
259
    _ensureHistoryEntry();
Hixie's avatar
Hixie committed
260 261
  }

262 263 264 265 266 267 268 269 270 271
  void _handleDragCancel() {
    if (_controller.isDismissed || _controller.isAnimating)
      return;
    if (_controller.value < 0.5) {
      close();
    } else {
      open();
    }
  }

272 273
  final GlobalKey _drawerKey = new GlobalKey();

Hixie's avatar
Hixie committed
274
  double get _width {
275 276 277
    final RenderBox box = _drawerKey.currentContext?.findRenderObject();
    if (box != null)
      return box.size.width;
Hixie's avatar
Hixie committed
278 279 280
    return _kWidth; // drawer not being shown currently
  }

jslavitz's avatar
jslavitz committed
281 282
  bool _previouslyOpened = false;

283
  void _move(DragUpdateDetails details) {
284 285 286 287 288 289 290 291
    double delta = details.primaryDelta / _width;
    switch (widget.alignment) {
      case DrawerAlignment.start:
        break;
      case DrawerAlignment.end:
        delta = -delta;
        break;
    }
292 293 294 295 296 297 298 299
    switch (Directionality.of(context)) {
      case TextDirection.rtl:
        _controller.value -= delta;
        break;
      case TextDirection.ltr:
        _controller.value += delta;
        break;
    }
jslavitz's avatar
jslavitz committed
300 301 302 303 304

    final bool opened = _controller.value > 0.5 ? true : false;
    if (opened != _previouslyOpened && widget.drawerCallback != null)
      widget.drawerCallback(opened);
    _previouslyOpened = opened;
305 306
  }

307
  void _settle(DragEndDetails details) {
308
    if (_controller.isDismissed)
309
      return;
310
    if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
311 312 313 314 315 316 317 318
      double visualVelocity = details.velocity.pixelsPerSecond.dx / _width;
      switch (widget.alignment) {
        case DrawerAlignment.start:
          break;
        case DrawerAlignment.end:
          visualVelocity = -visualVelocity;
          break;
      }
319 320 321 322 323 324 325
      switch (Directionality.of(context)) {
      case TextDirection.rtl:
        _controller.fling(velocity: -visualVelocity);
        break;
      case TextDirection.ltr:
        _controller.fling(velocity: visualVelocity);
        break;
326
      }
327
    } else if (_controller.value < 0.5) {
328
      close();
329
    } else {
330
      open();
331 332 333
    }
  }

334 335
  /// Starts an animation to open the drawer.
  ///
336
  /// Typically called by [ScaffoldState.openDrawer].
337
  void open() {
338
    _controller.fling(velocity: 1.0);
jslavitz's avatar
jslavitz committed
339 340
    if (widget.drawerCallback != null)
      widget.drawerCallback(true);
341 342
  }

343
  /// Starts an animation to close the drawer.
344
  void close() {
345
    _controller.fling(velocity: -1.0);
jslavitz's avatar
jslavitz committed
346 347
    if (widget.drawerCallback != null)
      widget.drawerCallback(false);
348
  }
349

350
  final ColorTween _color = new ColorTween(begin: Colors.transparent, end: Colors.black54);
351
  final GlobalKey _gestureDetectorKey = new GlobalKey();
352

353
  AlignmentDirectional get _drawerOuterAlignment {
354
    assert(widget.alignment != null);
355 356 357 358 359 360
    switch (widget.alignment) {
      case DrawerAlignment.start:
        return AlignmentDirectional.centerStart;
      case DrawerAlignment.end:
        return AlignmentDirectional.centerEnd;
    }
361
    return null;
362 363 364
  }

  AlignmentDirectional get _drawerInnerAlignment {
365
    assert(widget.alignment != null);
366 367 368 369 370 371
    switch (widget.alignment) {
      case DrawerAlignment.start:
        return AlignmentDirectional.centerEnd;
      case DrawerAlignment.end:
        return AlignmentDirectional.centerStart;
    }
372
    return null;
373 374
  }

375
  Widget _buildDrawer(BuildContext context) {
376
    if (_controller.status == AnimationStatus.dismissed) {
377
      return new Align(
378
        alignment: _drawerOuterAlignment,
379 380 381 382 383
        child: new GestureDetector(
          key: _gestureDetectorKey,
          onHorizontalDragUpdate: _move,
          onHorizontalDragEnd: _settle,
          behavior: HitTestBehavior.translucent,
Hixie's avatar
Hixie committed
384
          excludeFromSemantics: true,
385
          child: new Container(width: _kEdgeDragWidth)
386
        ),
387 388
      );
    } else {
389 390
      return new GestureDetector(
        key: _gestureDetectorKey,
391
        onHorizontalDragDown: _handleDragDown,
392 393
        onHorizontalDragUpdate: _move,
        onHorizontalDragEnd: _settle,
394
        onHorizontalDragCancel: _handleDragCancel,
395
        excludeFromSemantics: true,
396
        child: new RepaintBoundary(
397 398
          child: new Stack(
            children: <Widget>[
399 400
              new BlockSemantics(
                child: new GestureDetector(
401 402
                  // On Android, the back button is used to dismiss a modal.
                  excludeFromSemantics: defaultTargetPlatform == TargetPlatform.android,
403
                  onTap: close,
404 405 406 407 408
                  child: new Semantics(
                    label: MaterialLocalizations.of(context)?.modalBarrierDismissLabel,
                    child: new Container(
                      color: _color.evaluate(_controller),
                    ),
409
                  ),
410
                ),
411 412
              ),
              new Align(
413
                alignment: _drawerOuterAlignment,
414
                child: new Align(
415
                  alignment: _drawerInnerAlignment,
416 417
                  widthFactor: _controller.value,
                  child: new RepaintBoundary(
418
                    child: new FocusScope(
419
                      key: _drawerKey,
420
                      node: _focusScopeNode,
421
                      child: widget.child
422 423 424 425 426 427 428
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
429 430
      );
    }
431
  }
432 433 434 435 436 437 438
  @override
  Widget build(BuildContext context) {
    return new ListTileTheme(
      style: ListTileStyle.drawer,
      child: _buildDrawer(context),
    );
  }
439
}