drawer.dart 27.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/gestures.dart' show DragStartBehavior;
6
import 'package:flutter/widgets.dart';
7

8
import 'colors.dart';
9
import 'debug.dart';
10
import 'drawer_theme.dart';
11
import 'list_tile.dart';
12
import 'list_tile_theme.dart';
13
import 'material.dart';
14
import 'material_localizations.dart';
15
import 'theme.dart';
16

17 18 19
// Examples can assume:
// late BuildContext context;

20
/// The possible alignments of a [Drawer].
21
enum DrawerAlignment {
22 23 24 25
  /// 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.
26 27
  start,

28 29 30 31
  /// 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.
32 33 34
  end,
}

35
// TODO(eseidel): Draw width should vary based on device size:
36
// https://material.io/design/components/navigation-drawer.html#specs
37 38 39 40 41 42 43 44 45 46 47 48

// 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;
49
const double _kEdgeDragWidth = 20.0;
Matt Perry's avatar
Matt Perry committed
50
const double _kMinFlingVelocity = 365.0;
51
const Duration _kBaseSettleDuration = Duration(milliseconds: 246);
52

53
/// A Material Design panel that slides in horizontally from the edge of a
54
/// [Scaffold] to show navigation links in an application.
55
///
56 57
/// {@youtube 560 315 https://www.youtube.com/watch?v=WRj86iHihgY}
///
58 59
/// Drawers are typically used with the [Scaffold.drawer] property. The child of
/// the drawer is usually a [ListView] whose first child is a [DrawerHeader]
60 61 62 63
/// that displays status information about the current user. The remaining
/// drawer children are often constructed with [ListTile]s, often concluding
/// with an [AboutListTile].
///
64 65 66 67 68 69
/// 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.
///
/// {@animation 350 622 https://flutter.github.io/assets-for-api-docs/assets/material/drawer.mp4}
///
70
/// {@tool snippet}
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
/// This example shows how to create a [Scaffold] that contains an [AppBar] and
/// a [Drawer]. A user taps the "menu" icon in the [AppBar] to open the
/// [Drawer]. The [Drawer] displays four items: A header and three menu items.
/// The [Drawer] displays the four items using a [ListView], which allows the
/// user to scroll through the items if need be.
///
/// ```dart
/// Scaffold(
///   appBar: AppBar(
///     title: const Text('Drawer Demo'),
///   ),
///   drawer: Drawer(
///     child: ListView(
///       padding: EdgeInsets.zero,
///       children: const <Widget>[
///         DrawerHeader(
///           decoration: BoxDecoration(
///             color: Colors.blue,
///           ),
///           child: Text(
///             'Drawer Header',
///             style: TextStyle(
///               color: Colors.white,
///               fontSize: 24,
///             ),
///           ),
///         ),
///         ListTile(
///           leading: Icon(Icons.message),
///           title: Text('Messages'),
///         ),
///         ListTile(
///           leading: Icon(Icons.account_circle),
///           title: Text('Profile'),
///         ),
///         ListTile(
///           leading: Icon(Icons.settings),
///           title: Text('Settings'),
///         ),
///       ],
///     ),
///   ),
/// )
/// ```
/// {@end-tool}
///
117 118 119
/// An open drawer may be closed with a swipe to close gesture, pressing the
/// the escape key, by tapping the scrim, or by calling pop route function such as
/// [Navigator.pop]. For example a drawer item might close the drawer when tapped:
120 121
///
/// ```dart
122
/// ListTile(
123 124
///   leading: const Icon(Icons.change_history),
///   title: const Text('Change history'),
125 126 127 128 129 130
///   onTap: () {
///     // change app state...
///     Navigator.pop(context); // close the drawer
///   },
/// );
/// ```
131
///
132
/// See also:
133
///
134 135 136 137 138
///  * [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.
139
///  * <https://material.io/design/components/navigation-drawer.html>
140
class Drawer extends StatelessWidget {
141
  /// Creates a Material Design drawer.
142 143
  ///
  /// Typically used in the [Scaffold.drawer] property.
144 145
  ///
  /// The [elevation] must be non-negative.
146
  const Drawer({
147
    super.key,
148 149
    this.backgroundColor,
    this.elevation,
150 151
    this.shadowColor,
    this.surfaceTintColor,
152
    this.shape,
153
    this.width,
154
    this.child,
155
    this.semanticLabel,
156
  }) : assert(elevation == null || elevation >= 0.0);
157

158 159 160 161 162 163 164
  /// Sets the color of the [Material] that holds all of the [Drawer]'s
  /// contents.
  ///
  /// If this is null, then [DrawerThemeData.backgroundColor] is used. If that
  /// is also null, then it falls back to [Material]'s default.
  final Color? backgroundColor;

165 166 167
  /// The z-coordinate at which to place this drawer relative to its parent.
  ///
  /// This controls the size of the shadow below the drawer.
168
  ///
169 170 171 172
  /// If this is null, then [DrawerThemeData.elevation] is used. If that
  /// is also null, then it defaults to 16.0.
  final double? elevation;

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
  /// The color used to paint a drop shadow under the drawer's [Material],
  /// which reflects the drawer's [elevation].
  ///
  /// If null and [ThemeData.useMaterial3] is true then no drop shadow will
  /// be rendered.
  ///
  /// If null and [ThemeData.useMaterial3] is false then it will default to
  /// [ThemeData.shadowColor].
  ///
  /// See also:
  ///   * [Material.shadowColor], which describes how the drop shadow is painted.
  ///   * [elevation], which affects how the drop shadow is painted.
  ///   * [surfaceTintColor], which can be used to indicate elevation through
  ///     tinting the background color.
  final Color? shadowColor;

  /// The color used as a surface tint overlay on the drawer's background color,
  /// which reflects the drawer's [elevation].
  ///
  /// If [ThemeData.useMaterial3] is false property has no effect.
  ///
  /// If null and [ThemeData.useMaterial3] is true then [ThemeData]'s
  /// [ColorScheme.surfaceTint] will be used.
  ///
  /// To disable this feature, set [surfaceTintColor] to [Colors.transparent].
  ///
  /// See also:
  ///   * [Material.surfaceTintColor], which describes how the surface tint will
  ///     be applied to the background color of the drawer.
  ///   * [elevation], which affects the opacity of the surface tint.
  ///   * [shadowColor], which can be used to indicate elevation through
  ///     a drop shadow.
  final Color? surfaceTintColor;

207 208 209 210 211 212 213
  /// The shape of the drawer.
  ///
  /// Defines the drawer's [Material.shape].
  ///
  /// If this is null, then [DrawerThemeData.shape] is used. If that
  /// is also null, then it falls back to [Material]'s default.
  final ShapeBorder? shape;
214

215 216 217 218 219 220
  /// The width of the drawer.
  ///
  /// If this is null, then [DrawerThemeData.width] is used. If that is also
  /// null, then it falls back to the Material spec's default (304.0).
  final double? width;

221
  /// The widget below this widget in the tree.
222
  ///
223
  /// Typically a [SliverList].
224
  ///
225
  /// {@macro flutter.widgets.ProxyWidget.child}
226
  final Widget? child;
227

228
  /// The semantic label of the drawer used by accessibility frameworks to
229
  /// announce screen transitions when the drawer is opened and closed.
230
  ///
231 232
  /// If this label is not provided, it will default to
  /// [MaterialLocalizations.drawerLabel].
233
  ///
234
  /// See also:
235
  ///
236 237
  ///  * [SemanticsConfiguration.namesRoute], for a description of how this
  ///    value is used.
238
  final String? semanticLabel;
239

240
  @override
241
  Widget build(BuildContext context) {
242
    assert(debugCheckHasMaterialLocalizations(context));
243
    final DrawerThemeData drawerTheme = DrawerTheme.of(context);
244
    String? label = semanticLabel;
245
    switch (Theme.of(context).platform) {
246
      case TargetPlatform.iOS:
247
      case TargetPlatform.macOS:
248 249 250
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
251 252
      case TargetPlatform.linux:
      case TargetPlatform.windows:
253
        label = semanticLabel ?? MaterialLocalizations.of(context).drawerLabel;
254
    }
255
    final bool useMaterial3 = Theme.of(context).useMaterial3;
hangyu's avatar
hangyu committed
256 257
    final bool isDrawerStart = DrawerController.maybeOf(context)?.alignment != DrawerAlignment.end;
    final DrawerThemeData defaults= useMaterial3 ? _DrawerDefaultsM3(context): _DrawerDefaultsM2(context);
258
    return Semantics(
259 260 261 262
      scopesRoute: true,
      namesRoute: true,
      explicitChildNodes: true,
      label: label,
263
      child: ConstrainedBox(
264
        constraints: BoxConstraints.expand(width: width ?? drawerTheme.width ?? _kWidth),
265
        child: Material(
hangyu's avatar
hangyu committed
266 267 268 269 270 271 272
          color: backgroundColor ?? drawerTheme.backgroundColor ?? defaults.backgroundColor,
          elevation: elevation ?? drawerTheme.elevation ?? defaults.elevation!,
          shadowColor: shadowColor ?? drawerTheme.shadowColor ?? defaults.shadowColor,
          surfaceTintColor: surfaceTintColor ?? drawerTheme.surfaceTintColor ?? defaults.surfaceTintColor,
          shape: shape ?? (isDrawerStart
            ? (drawerTheme.shape ?? defaults.shape)
            : (drawerTheme.endShape ?? defaults.endShape)),
273 274
          child: child,
        ),
275
      ),
276 277 278 279
    );
  }
}

jslavitz's avatar
jslavitz committed
280 281
/// Signature for the callback that's called when a [DrawerController] is
/// opened or closed.
282
typedef DrawerCallback = void Function(bool isOpened);
jslavitz's avatar
jslavitz committed
283

hangyu's avatar
hangyu committed
284 285 286 287 288 289 290 291 292 293 294 295 296 297
class _DrawerControllerScope extends InheritedWidget {
  const _DrawerControllerScope({
    required this.controller,
    required super.child,
  });

  final DrawerController controller;

  @override
  bool updateShouldNotify(_DrawerControllerScope old) {
    return controller != old.controller;
  }
}

298 299
/// Provides interactive behavior for [Drawer] widgets.
///
300 301 302
/// Rarely used directly. Drawer controllers are typically created automatically
/// by [Scaffold] widgets.
///
Ian Hickson's avatar
Ian Hickson committed
303
/// The drawer controller provides the ability to open and close a drawer, either
304 305 306
/// 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.
307 308
///
/// See also:
309
///
310 311
///  * [Drawer], a container with the default width of a drawer.
///  * [Scaffold.drawer], the [Scaffold] slot for showing a drawer.
312
class DrawerController extends StatefulWidget {
313 314 315 316 317
  /// Creates a controller for a [Drawer].
  ///
  /// Rarely used directly.
  ///
  /// The [child] argument must not be null and is typically a [Drawer].
318
  const DrawerController({
319 320 321
    GlobalKey? key,
    required this.child,
    required this.alignment,
322
    this.isDrawerOpen = false,
jslavitz's avatar
jslavitz committed
323
    this.drawerCallback,
324
    this.dragStartBehavior = DragStartBehavior.start,
325
    this.scrimColor,
326
    this.edgeDragWidth,
327
    this.enableOpenDragGesture = true,
328
  }) : assert(child != null),
329
       assert(dragStartBehavior != null),
330
       assert(alignment != null),
331
       super(key: key);
332

333
  /// The widget below this widget in the tree.
334 335
  ///
  /// Typically a [Drawer].
336 337
  final Widget child;

338
  /// The alignment of the [Drawer].
339
  ///
340 341
  /// This controls the direction in which the user should swipe to open and
  /// close the drawer.
342 343
  final DrawerAlignment alignment;

jslavitz's avatar
jslavitz committed
344
  /// Optional callback that is called when a [Drawer] is opened or closed.
345
  final DrawerCallback? drawerCallback;
jslavitz's avatar
jslavitz committed
346

347
  /// {@template flutter.material.DrawerController.dragStartBehavior}
348 349 350
  /// Determines the way that drag start behavior is handled.
  ///
  /// If set to [DragStartBehavior.start], the drag behavior used for opening
351 352 353
  /// and closing a drawer will begin at the position where the drag gesture won
  /// the arena. If set to [DragStartBehavior.down] it will begin at the position
  /// where a down event is first detected.
354 355 356 357 358
  ///
  /// In general, setting this to [DragStartBehavior.start] will make drag
  /// animation smoother and setting it to [DragStartBehavior.down] will make
  /// drag behavior feel slightly more reactive.
  ///
359
  /// By default, the drag start behavior is [DragStartBehavior.start].
360 361 362
  ///
  /// See also:
  ///
363 364 365
  ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for
  ///    the different behaviors.
  ///
366 367 368
  /// {@endtemplate}
  final DragStartBehavior dragStartBehavior;

369 370
  /// The color to use for the scrim that obscures the underlying content while
  /// a drawer is open.
371
  ///
372 373
  /// If this is null, then [DrawerThemeData.scrimColor] is used. If that
  /// is also null, then it defaults to [Colors.black54].
374
  final Color? scrimColor;
375

376 377 378 379 380
  /// Determines if the [Drawer] can be opened with a drag gesture.
  ///
  /// By default, the drag gesture is enabled.
  final bool enableOpenDragGesture;

381 382 383 384
  /// The width of the area within which a horizontal swipe will open the
  /// drawer.
  ///
  /// By default, the value used is 20.0 added to the padding edge of
385
  /// `MediaQuery.paddingOf(context)` that corresponds to [alignment].
386 387 388
  /// This ensures that the drag area for notched devices is not obscured. For
  /// example, if [alignment] is set to [DrawerAlignment.start] and
  /// `TextDirection.of(context)` is set to [TextDirection.ltr],
389
  /// 20.0 will be added to `MediaQuery.paddingOf(context).left`.
390
  final double? edgeDragWidth;
391

392 393 394 395 396 397 398 399
  /// Whether or not the drawer is opened or closed.
  ///
  /// This parameter is primarily used by the state restoration framework
  /// to restore the drawer's animation controller to the open or closed state
  /// depending on what was last saved to the target platform before the
  /// application was killed.
  final bool isDrawerOpen;

hangyu's avatar
hangyu committed
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
  /// The closest instance of [DrawerController] that encloses the given
  /// context, or null if none is found.
  ///
  /// {@tool snippet} Typical usage is as follows:
  ///
  /// ```dart
  /// DrawerController? controller = DrawerController.maybeOf(context);
  /// ```
  /// {@end-tool}
  ///
  /// Calling this method will create a dependency on the closest
  /// [DrawerController] in the [context], if there is one.
  ///
  /// See also:
  ///
  /// * [DrawerController.of], which is similar to this method, but asserts
  ///   if no [DrawerController] ancestor is found.
  static DrawerController? maybeOf(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<_DrawerControllerScope>()?.controller;
  }

  /// The closest instance of [DrawerController] that encloses the given
  /// context.
  ///
  /// If no instance is found, this method will assert in debug mode and throw
  /// an exception in release mode.
  ///
  /// Calling this method will create a dependency on the closest
  /// [DrawerController] in the [context].
  ///
  /// {@tool snippet} Typical usage is as follows:
  ///
  /// ```dart
  /// DrawerController controller = DrawerController.of(context);
  /// ```
  /// {@end-tool}
  static DrawerController of(BuildContext context) {
    final DrawerController? controller = maybeOf(context);
    assert(() {
      if (controller == null) {
        throw FlutterError(
          'DrawerController.of() was called with a context that does not '
          'contain a DrawerController widget.\n'
          'No DrawerController widget ancestor could be found starting from '
          'the context that was passed to DrawerController.of(). This can '
          'happen because you are using a widget that looks for a DrawerController '
          'ancestor, but no such ancestor exists.\n'
          'The context used was:\n'
          '  $context',
        );
      }
      return true;
    }());
    return controller!;
  }

456
  @override
457
  DrawerControllerState createState() => DrawerControllerState();
458
}
459

460 461 462
/// State for a [DrawerController].
///
/// Typically used by a [Scaffold] to [open] and [close] the drawer.
463
class DrawerControllerState extends State<DrawerController> with SingleTickerProviderStateMixin {
464
  @override
465 466
  void initState() {
    super.initState();
467 468 469 470 471 472
    _controller = AnimationController(
      value: widget.isDrawerOpen ? 1.0 : 0.0,
      duration: _kBaseSettleDuration,
      vsync: this,
    );
    _controller
473 474
      ..addListener(_animationChanged)
      ..addStatusListener(_animationStatusChanged);
475 476
  }

477
  @override
478
  void dispose() {
479
    _historyEntry?.remove();
480
    _controller.dispose();
481 482 483
    super.dispose();
  }

484 485 486 487 488 489
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _scrimColorTween = _buildScrimColorTween();
  }

490 491 492
  @override
  void didUpdateWidget(DrawerController oldWidget) {
    super.didUpdateWidget(oldWidget);
493
    if (widget.scrimColor != oldWidget.scrimColor) {
494
      _scrimColorTween = _buildScrimColorTween();
495
    }
496 497 498 499 500 501
    if (widget.isDrawerOpen != oldWidget.isDrawerOpen) {
      switch(_controller.status) {
        case AnimationStatus.completed:
        case AnimationStatus.dismissed:
          _controller.value = widget.isDrawerOpen ? 1.0 : 0.0;
          break;
502 503
        case AnimationStatus.forward:
        case AnimationStatus.reverse:
504 505 506
          break;
      }
    }
507 508
  }

509
  void _animationChanged() {
510
    setState(() {
511
      // The animation controller's state is our build state, and it changed already.
512 513 514
    });
  }

515
  LocalHistoryEntry? _historyEntry;
516
  final FocusScopeNode _focusScopeNode = FocusScopeNode();
517 518 519

  void _ensureHistoryEntry() {
    if (_historyEntry == null) {
520
      final ModalRoute<dynamic>? route = ModalRoute.of(context);
521
      if (route != null) {
522
        _historyEntry = LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved, impliesAppBarDismissal: false);
523
        route.addLocalHistoryEntry(_historyEntry!);
524
        FocusScope.of(context).setFirstFocus(_focusScopeNode);
525 526 527 528
      }
    }
  }

529
  void _animationStatusChanged(AnimationStatus status) {
530
    switch (status) {
531
      case AnimationStatus.forward:
532 533
        _ensureHistoryEntry();
        break;
534
      case AnimationStatus.reverse:
535 536 537
        _historyEntry?.remove();
        _historyEntry = null;
        break;
538
      case AnimationStatus.dismissed:
539
        break;
540
      case AnimationStatus.completed:
541 542
        break;
    }
543 544
  }

545 546 547 548
  void _handleHistoryEntryRemoved() {
    _historyEntry = null;
    close();
  }
549

550
  late AnimationController _controller;
Hixie's avatar
Hixie committed
551

552
  void _handleDragDown(DragDownDetails details) {
553
    _controller.stop();
554
    _ensureHistoryEntry();
Hixie's avatar
Hixie committed
555 556
  }

557
  void _handleDragCancel() {
558
    if (_controller.isDismissed || _controller.isAnimating) {
559
      return;
560
    }
561 562 563 564 565 566 567
    if (_controller.value < 0.5) {
      close();
    } else {
      open();
    }
  }

568
  final GlobalKey _drawerKey = GlobalKey();
569

Hixie's avatar
Hixie committed
570
  double get _width {
571
    final RenderBox? box = _drawerKey.currentContext?.findRenderObject() as RenderBox?;
572
    if (box != null) {
573
      return box.size.width;
574
    }
Hixie's avatar
Hixie committed
575 576 577
    return _kWidth; // drawer not being shown currently
  }

jslavitz's avatar
jslavitz committed
578 579
  bool _previouslyOpened = false;

580
  void _move(DragUpdateDetails details) {
581
    double delta = details.primaryDelta! / _width;
582 583 584 585 586 587 588
    switch (widget.alignment) {
      case DrawerAlignment.start:
        break;
      case DrawerAlignment.end:
        delta = -delta;
        break;
    }
589
    switch (Directionality.of(context)) {
590 591 592 593 594 595 596
      case TextDirection.rtl:
        _controller.value -= delta;
        break;
      case TextDirection.ltr:
        _controller.value += delta;
        break;
    }
jslavitz's avatar
jslavitz committed
597

598
    final bool opened = _controller.value > 0.5;
599
    if (opened != _previouslyOpened && widget.drawerCallback != null) {
600
      widget.drawerCallback!(opened);
601
    }
jslavitz's avatar
jslavitz committed
602
    _previouslyOpened = opened;
603 604
  }

605
  void _settle(DragEndDetails details) {
606
    if (_controller.isDismissed) {
607
      return;
608
    }
609
    if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
610 611 612 613 614 615 616 617
      double visualVelocity = details.velocity.pixelsPerSecond.dx / _width;
      switch (widget.alignment) {
        case DrawerAlignment.start:
          break;
        case DrawerAlignment.end:
          visualVelocity = -visualVelocity;
          break;
      }
618
      switch (Directionality.of(context)) {
619 620
        case TextDirection.rtl:
          _controller.fling(velocity: -visualVelocity);
621
          widget.drawerCallback?.call(visualVelocity < 0.0);
622 623 624
          break;
        case TextDirection.ltr:
          _controller.fling(velocity: visualVelocity);
625
          widget.drawerCallback?.call(visualVelocity > 0.0);
626
          break;
627
      }
628
    } else if (_controller.value < 0.5) {
629
      close();
630
    } else {
631
      open();
632 633 634
    }
  }

635 636
  /// Starts an animation to open the drawer.
  ///
637
  /// Typically called by [ScaffoldState.openDrawer].
638
  void open() {
639
    _controller.fling();
640
    widget.drawerCallback?.call(true);
641 642
  }

643
  /// Starts an animation to close the drawer.
644
  void close() {
645
    _controller.fling(velocity: -1.0);
646
    widget.drawerCallback?.call(false);
647
  }
648

649
  late ColorTween _scrimColorTween;
650
  final GlobalKey _gestureDetectorKey = GlobalKey();
651

652
  ColorTween _buildScrimColorTween() {
653 654 655 656 657 658
    return ColorTween(
      begin: Colors.transparent,
      end: widget.scrimColor
          ?? DrawerTheme.of(context).scrimColor
          ?? Colors.black54,
    );
659 660
  }

661
  AlignmentDirectional get _drawerOuterAlignment {
662
    assert(widget.alignment != null);
663 664 665 666 667 668 669 670 671
    switch (widget.alignment) {
      case DrawerAlignment.start:
        return AlignmentDirectional.centerStart;
      case DrawerAlignment.end:
        return AlignmentDirectional.centerEnd;
    }
  }

  AlignmentDirectional get _drawerInnerAlignment {
672
    assert(widget.alignment != null);
673 674 675 676 677 678 679 680
    switch (widget.alignment) {
      case DrawerAlignment.start:
        return AlignmentDirectional.centerEnd;
      case DrawerAlignment.end:
        return AlignmentDirectional.centerStart;
    }
  }

681
  Widget _buildDrawer(BuildContext context) {
682
    final bool drawerIsStart = widget.alignment == DrawerAlignment.start;
683
    final TextDirection textDirection = Directionality.of(context);
684 685 686 687 688 689 690 691 692 693 694 695 696
    final bool isDesktop;
    switch (Theme.of(context).platform) {
      case TargetPlatform.android:
      case TargetPlatform.iOS:
      case TargetPlatform.fuchsia:
        isDesktop = false;
        break;
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        isDesktop = true;
        break;
    }
697

698
    double? dragAreaWidth = widget.edgeDragWidth;
699
    if (widget.edgeDragWidth == null) {
700
      final EdgeInsets padding = MediaQuery.paddingOf(context);
701
      switch (textDirection) {
702
        case TextDirection.ltr:
703 704
          dragAreaWidth = _kEdgeDragWidth +
            (drawerIsStart ? padding.left : padding.right);
705 706
          break;
        case TextDirection.rtl:
707 708
          dragAreaWidth = _kEdgeDragWidth +
            (drawerIsStart ? padding.right : padding.left);
709
          break;
710 711
      }
    }
712

713
    if (_controller.status == AnimationStatus.dismissed) {
714
      if (widget.enableOpenDragGesture && !isDesktop) {
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
        return Align(
          alignment: _drawerOuterAlignment,
          child: GestureDetector(
            key: _gestureDetectorKey,
            onHorizontalDragUpdate: _move,
            onHorizontalDragEnd: _settle,
            behavior: HitTestBehavior.translucent,
            excludeFromSemantics: true,
            dragStartBehavior: widget.dragStartBehavior,
            child: Container(width: dragAreaWidth),
          ),
        );
      } else {
        return const SizedBox.shrink();
      }
730
    } else {
731
      final bool platformHasBackButton;
732
      switch (Theme.of(context).platform) {
733 734 735 736
        case TargetPlatform.android:
          platformHasBackButton = true;
          break;
        case TargetPlatform.iOS:
737
        case TargetPlatform.macOS:
738
        case TargetPlatform.fuchsia:
739 740
        case TargetPlatform.linux:
        case TargetPlatform.windows:
741 742 743 744
          platformHasBackButton = false;
          break;
      }
      assert(platformHasBackButton != null);
745

hangyu's avatar
hangyu committed
746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
      final Widget child = _DrawerControllerScope(
        controller: widget,
        child: RepaintBoundary(
          child: Stack(
            children: <Widget>[
              BlockSemantics(
                child: ExcludeSemantics(
                  // On Android, the back button is used to dismiss a modal.
                  excluding: platformHasBackButton,
                  child: GestureDetector(
                    onTap: close,
                    child: Semantics(
                      label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
                      child: Container( // The drawer's "scrim"
                        color: _scrimColorTween.evaluate(_controller),
                      ),
762
                    ),
763
                  ),
764
                ),
765
              ),
hangyu's avatar
hangyu committed
766 767 768 769 770 771 772 773 774 775 776
              Align(
                alignment: _drawerOuterAlignment,
                child: Align(
                  alignment: _drawerInnerAlignment,
                  widthFactor: _controller.value,
                  child: RepaintBoundary(
                    child: FocusScope(
                      key: _drawerKey,
                      node: _focusScopeNode,
                      child: widget.child,
                    ),
777 778 779
                  ),
                ),
              ),
hangyu's avatar
hangyu committed
780 781
            ],
          ),
782
        ),
783
      );
784 785 786 787 788 789 790 791 792 793 794 795 796 797 798

      if (isDesktop) {
        return child;
      }

      return GestureDetector(
        key: _gestureDetectorKey,
        onHorizontalDragDown: _handleDragDown,
        onHorizontalDragUpdate: _move,
        onHorizontalDragEnd: _settle,
        onHorizontalDragCancel: _handleDragCancel,
        excludeFromSemantics: true,
        dragStartBehavior: widget.dragStartBehavior,
        child: child,
      );
799
    }
800
  }
Ian Hickson's avatar
Ian Hickson committed
801

802 803
  @override
  Widget build(BuildContext context) {
804
    assert(debugCheckHasMaterialLocalizations(context));
805
    return ListTileTheme.merge(
806 807 808 809
      style: ListTileStyle.drawer,
      child: _buildDrawer(context),
    );
  }
810
}
hangyu's avatar
hangyu committed
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829

class _DrawerDefaultsM2 extends DrawerThemeData {
  const _DrawerDefaultsM2(this.context)
      : super(elevation: 16.0);

  final BuildContext context;

  @override
  Color? get shadowColor => Theme.of(context).shadowColor;

}

// BEGIN GENERATED TOKEN PROPERTIES - Drawer

// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
//   dev/tools/gen_defaults/bin/gen_defaults.dart.

830
// Token database version: v0_150
hangyu's avatar
hangyu committed
831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862

class _DrawerDefaultsM3 extends DrawerThemeData {
  const _DrawerDefaultsM3(this.context)
      : super(elevation: 1.0);

  final BuildContext context;

  @override
  Color? get backgroundColor => Theme.of(context).colorScheme.surface;

  @override
  Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;

  @override
  Color? get shadowColor => Colors.transparent;

  // This don't appear to be tokens for this value, but it is
  // shown in the spec.
  @override
  ShapeBorder? get shape => const RoundedRectangleBorder(
    borderRadius: BorderRadius.horizontal(right: Radius.circular(16.0)),
  );

  // This don't appear to be tokens for this value, but it is
  // shown in the spec.
  @override
  ShapeBorder? get endShape => const RoundedRectangleBorder(
    borderRadius: BorderRadius.horizontal(left: Radius.circular(16.0)),
  );
}

// END GENERATED TOKEN PROPERTIES - Drawer