drawer.dart 12.3 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 45
const Duration _kBaseSettleDuration = const Duration(milliseconds: 246);

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 59 60 61 62 63 64 65 66 67
/// 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
/// new ListTile(
///   leading: new Icon(Icons.change_history),
///   title: new Text('Change history'),
///   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 106 107 108 109 110 111 112 113 114 115 116
  /// The semantic label of the dialog used by accessibility frameworks to 
  /// announce screen transitions when the drawer is opened and closed.
  /// 
  /// If this label is not provided, it will default to
  /// [MaterialLocalizations.drawerLabel].
  /// 
  /// See also:
  /// 
  ///  * [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
    );
  }
}

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

172
  /// The widget below this widget in the tree.
173 174
  ///
  /// Typically a [Drawer].
175 176
  final Widget child;

177
  /// The alignment of the [Drawer].
178
  ///
179 180
  /// This controls the direction in which the user should swipe to open and
  /// close the drawer.
181 182
  final DrawerAlignment alignment;

183
  @override
184
  DrawerControllerState createState() => new DrawerControllerState();
185
}
186

187 188 189
/// State for a [DrawerController].
///
/// Typically used by a [Scaffold] to [open] and [close] the drawer.
190
class DrawerControllerState extends State<DrawerController> with SingleTickerProviderStateMixin {
191
  @override
192 193
  void initState() {
    super.initState();
194
    _controller = new AnimationController(duration: _kBaseSettleDuration, vsync: this)
195 196
      ..addListener(_animationChanged)
      ..addStatusListener(_animationStatusChanged);
197 198
  }

199
  @override
200
  void dispose() {
201
    _historyEntry?.remove();
202
    _controller.dispose();
203 204 205
    super.dispose();
  }

206
  void _animationChanged() {
207
    setState(() {
208
      // The animation controller's state is our build state, and it changed already.
209 210 211
    });
  }

212
  LocalHistoryEntry _historyEntry;
213
  final FocusScopeNode _focusScopeNode = new FocusScopeNode();
214 215 216

  void _ensureHistoryEntry() {
    if (_historyEntry == null) {
217
      final ModalRoute<dynamic> route = ModalRoute.of(context);
218 219 220
      if (route != null) {
        _historyEntry = new LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
        route.addLocalHistoryEntry(_historyEntry);
221
        FocusScope.of(context).setFirstFocus(_focusScopeNode);
222 223 224 225
      }
    }
  }

226
  void _animationStatusChanged(AnimationStatus status) {
227
    switch (status) {
228
      case AnimationStatus.forward:
229 230
        _ensureHistoryEntry();
        break;
231
      case AnimationStatus.reverse:
232 233 234
        _historyEntry?.remove();
        _historyEntry = null;
        break;
235
      case AnimationStatus.dismissed:
236
        break;
237
      case AnimationStatus.completed:
238 239
        break;
    }
240 241
  }

242 243 244 245
  void _handleHistoryEntryRemoved() {
    _historyEntry = null;
    close();
  }
246

247
  AnimationController _controller;
Hixie's avatar
Hixie committed
248

249
  void _handleDragDown(DragDownDetails details) {
250
    _controller.stop();
251
    _ensureHistoryEntry();
Hixie's avatar
Hixie committed
252 253
  }

254 255 256 257 258 259 260 261 262 263
  void _handleDragCancel() {
    if (_controller.isDismissed || _controller.isAnimating)
      return;
    if (_controller.value < 0.5) {
      close();
    } else {
      open();
    }
  }

264 265
  final GlobalKey _drawerKey = new GlobalKey();

Hixie's avatar
Hixie committed
266
  double get _width {
267 268 269
    final RenderBox box = _drawerKey.currentContext?.findRenderObject();
    if (box != null)
      return box.size.width;
Hixie's avatar
Hixie committed
270 271 272
    return _kWidth; // drawer not being shown currently
  }

273
  void _move(DragUpdateDetails details) {
274 275 276 277 278 279 280 281
    double delta = details.primaryDelta / _width;
    switch (widget.alignment) {
      case DrawerAlignment.start:
        break;
      case DrawerAlignment.end:
        delta = -delta;
        break;
    }
282 283 284 285 286 287 288 289
    switch (Directionality.of(context)) {
      case TextDirection.rtl:
        _controller.value -= delta;
        break;
      case TextDirection.ltr:
        _controller.value += delta;
        break;
    }
290 291
  }

292
  void _settle(DragEndDetails details) {
293
    if (_controller.isDismissed)
294
      return;
295
    if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
296 297 298 299 300 301 302 303
      double visualVelocity = details.velocity.pixelsPerSecond.dx / _width;
      switch (widget.alignment) {
        case DrawerAlignment.start:
          break;
        case DrawerAlignment.end:
          visualVelocity = -visualVelocity;
          break;
      }
304 305 306 307 308 309 310
      switch (Directionality.of(context)) {
      case TextDirection.rtl:
        _controller.fling(velocity: -visualVelocity);
        break;
      case TextDirection.ltr:
        _controller.fling(velocity: visualVelocity);
        break;
311
      }
312
    } else if (_controller.value < 0.5) {
313
      close();
314
    } else {
315
      open();
316 317 318
    }
  }

319 320
  /// Starts an animation to open the drawer.
  ///
321
  /// Typically called by [ScaffoldState.openDrawer].
322
  void open() {
323
    _controller.fling(velocity: 1.0);
324 325
  }

326
  /// Starts an animation to close the drawer.
327
  void close() {
328
    _controller.fling(velocity: -1.0);
329
  }
330

331
  final ColorTween _color = new ColorTween(begin: Colors.transparent, end: Colors.black54);
332
  final GlobalKey _gestureDetectorKey = new GlobalKey();
333

334
  AlignmentDirectional get _drawerOuterAlignment {
335
    assert(widget.alignment != null);
336 337 338 339 340 341
    switch (widget.alignment) {
      case DrawerAlignment.start:
        return AlignmentDirectional.centerStart;
      case DrawerAlignment.end:
        return AlignmentDirectional.centerEnd;
    }
342
    return null;
343 344 345
  }

  AlignmentDirectional get _drawerInnerAlignment {
346
    assert(widget.alignment != null);
347 348 349 350 351 352
    switch (widget.alignment) {
      case DrawerAlignment.start:
        return AlignmentDirectional.centerEnd;
      case DrawerAlignment.end:
        return AlignmentDirectional.centerStart;
    }
353
    return null;
354 355
  }

356
  Widget _buildDrawer(BuildContext context) {
357
    if (_controller.status == AnimationStatus.dismissed) {
358
      return new Align(
359
        alignment: _drawerOuterAlignment,
360 361 362 363 364
        child: new GestureDetector(
          key: _gestureDetectorKey,
          onHorizontalDragUpdate: _move,
          onHorizontalDragEnd: _settle,
          behavior: HitTestBehavior.translucent,
Hixie's avatar
Hixie committed
365
          excludeFromSemantics: true,
366
          child: new Container(width: _kEdgeDragWidth)
367
        ),
368 369
      );
    } else {
370 371
      return new GestureDetector(
        key: _gestureDetectorKey,
372
        onHorizontalDragDown: _handleDragDown,
373 374
        onHorizontalDragUpdate: _move,
        onHorizontalDragEnd: _settle,
375
        onHorizontalDragCancel: _handleDragCancel,
376
        excludeFromSemantics: true,
377
        child: new RepaintBoundary(
378 379
          child: new Stack(
            children: <Widget>[
380 381
              new BlockSemantics(
                child: new GestureDetector(
382 383
                  // On Android, the back button is used to dismiss a modal.
                  excludeFromSemantics: defaultTargetPlatform == TargetPlatform.android,
384 385 386 387
                  onTap: close,
                  child: new Container(
                    color: _color.evaluate(_controller)
                  ),
388
                ),
389 390
              ),
              new Align(
391
                alignment: _drawerOuterAlignment,
392
                child: new Align(
393
                  alignment: _drawerInnerAlignment,
394 395
                  widthFactor: _controller.value,
                  child: new RepaintBoundary(
396
                    child: new FocusScope(
397
                      key: _drawerKey,
398
                      node: _focusScopeNode,
399
                      child: widget.child
400 401 402 403 404 405 406
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
407 408
      );
    }
409
  }
410 411 412 413 414 415 416
  @override
  Widget build(BuildContext context) {
    return new ListTileTheme(
      style: ListTileStyle.drawer,
      child: _buildDrawer(context),
    );
  }
417
}