drawer.dart 27.4 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 58 59
/// There is a Material 3 version of this component, [NavigationDrawer],
/// that's preferred for applications that are configured for Material 3
/// (see [ThemeData.useMaterial3]).
///
60 61
/// {@youtube 560 315 https://www.youtube.com/watch?v=WRj86iHihgY}
///
62 63
/// Drawers are typically used with the [Scaffold.drawer] property. The child of
/// the drawer is usually a [ListView] whose first child is a [DrawerHeader]
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].
///
68 69 70 71 72 73
/// 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}
///
74 75 76 77 78 79 80 81
/// ## Updating to [NavigationDrawer]
///
/// There is a Material 3 version of this component, [NavigationDrawer],
/// that's preferred for applications that are configured for Material 3
/// (see [ThemeData.useMaterial3]). The [NavigationDrawer] widget's visual
/// are a little bit different, see the Material 3 spec at
/// <https://m3.material.io/components/navigation-drawer/overview> for
/// more details. While the [Drawer] widget can have only one child, the
82
/// [NavigationDrawer] widget can have a list of widgets, which typically contains
83 84 85
/// [NavigationDrawerDestination] widgets and/or customized widgets like headlines
/// and dividers.
///
86
/// {@tool dartpad}
87 88 89 90 91 92
/// 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.
///
93
/// ** See code in examples/api/lib/material/drawer/drawer.0.dart **
94 95
/// {@end-tool}
///
96
/// {@tool dartpad}
97 98
/// This example shows how to migrate the above [Drawer] to a [NavigationDrawer].
///
99
/// ** See code in examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart **
100 101
/// {@end-tool}
///
102
/// An open drawer may be closed with a swipe to close gesture, pressing the
103
/// escape key, by tapping the scrim, or by calling pop route function such as
104
/// [Navigator.pop]. For example a drawer item might close the drawer when tapped:
105 106
///
/// ```dart
107
/// ListTile(
108 109
///   leading: const Icon(Icons.change_history),
///   title: const Text('Change history'),
110 111 112 113 114 115
///   onTap: () {
///     // change app state...
///     Navigator.pop(context); // close the drawer
///   },
/// );
/// ```
116
///
117
/// See also:
118
///
119 120 121 122 123
///  * [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.
124
///  * <https://material.io/design/components/navigation-drawer.html>
125
class Drawer extends StatelessWidget {
126
  /// Creates a Material Design drawer.
127 128
  ///
  /// Typically used in the [Scaffold.drawer] property.
129 130
  ///
  /// The [elevation] must be non-negative.
131
  const Drawer({
132
    super.key,
133 134
    this.backgroundColor,
    this.elevation,
135 136
    this.shadowColor,
    this.surfaceTintColor,
137
    this.shape,
138
    this.width,
139
    this.child,
140
    this.semanticLabel,
141
    this.clipBehavior,
142
  }) : assert(elevation == null || elevation >= 0.0);
143

144 145 146 147 148 149 150
  /// 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;

151 152 153
  /// The z-coordinate at which to place this drawer relative to its parent.
  ///
  /// This controls the size of the shadow below the drawer.
154
  ///
155 156 157 158
  /// If this is null, then [DrawerThemeData.elevation] is used. If that
  /// is also null, then it defaults to 16.0.
  final double? elevation;

159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
  /// 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].
  ///
178 179 180 181
  /// This is not recommended for use. [Material 3 spec](https://m3.material.io/styles/color/the-color-system/color-roles)
  /// introduced a set of tone-based surfaces and surface containers in its [ColorScheme],
  /// which provide more flexibility. The intention is to eventually remove surface tint color from
  /// the framework.
182 183 184
  ///
  /// To disable this feature, set [surfaceTintColor] to [Colors.transparent].
  ///
185 186
  /// Defaults to [Colors.transparent].
  ///
187 188 189 190 191 192 193 194
  /// 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;

195 196 197 198 199 200 201
  /// 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;
202

203 204 205 206 207 208
  /// 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;

209
  /// The widget below this widget in the tree.
210
  ///
211
  /// Typically a [SliverList].
212
  ///
213
  /// {@macro flutter.widgets.ProxyWidget.child}
214
  final Widget? child;
215

216
  /// The semantic label of the drawer used by accessibility frameworks to
217
  /// announce screen transitions when the drawer is opened and closed.
218
  ///
219 220
  /// If this label is not provided, it will default to
  /// [MaterialLocalizations.drawerLabel].
221
  ///
222
  /// See also:
223
  ///
224 225
  ///  * [SemanticsConfiguration.namesRoute], for a description of how this
  ///    value is used.
226
  final String? semanticLabel;
227

228 229 230 231 232 233 234 235
  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// The [clipBehavior] argument specifies how to clip the drawer's [shape].
  ///
  /// If the drawer has a [shape], it defaults to [Clip.hardEdge]. Otherwise,
  /// defaults to [Clip.none].
  final Clip? clipBehavior;

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

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

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

  final DrawerController controller;

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

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

328
  /// The widget below this widget in the tree.
329 330
  ///
  /// Typically a [Drawer].
331 332
  final Widget child;

333
  /// The alignment of the [Drawer].
334
  ///
335 336
  /// This controls the direction in which the user should swipe to open and
  /// close the drawer.
337 338
  final DrawerAlignment alignment;

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

342
  /// {@template flutter.material.DrawerController.dragStartBehavior}
343 344 345
  /// Determines the way that drag start behavior is handled.
  ///
  /// If set to [DragStartBehavior.start], the drag behavior used for opening
346 347 348
  /// 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.
349 350 351 352 353
  ///
  /// 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.
  ///
354
  /// By default, the drag start behavior is [DragStartBehavior.start].
355 356 357
  ///
  /// See also:
  ///
358 359 360
  ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for
  ///    the different behaviors.
  ///
361 362 363
  /// {@endtemplate}
  final DragStartBehavior dragStartBehavior;

364 365
  /// The color to use for the scrim that obscures the underlying content while
  /// a drawer is open.
366
  ///
367 368
  /// If this is null, then [DrawerThemeData.scrimColor] is used. If that
  /// is also null, then it defaults to [Colors.black54].
369
  final Color? scrimColor;
370

371 372 373 374 375
  /// Determines if the [Drawer] can be opened with a drag gesture.
  ///
  /// By default, the drag gesture is enabled.
  final bool enableOpenDragGesture;

376 377 378 379
  /// 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
380
  /// `MediaQuery.paddingOf(context)` that corresponds to [alignment].
381 382 383
  /// 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],
384
  /// 20.0 will be added to `MediaQuery.paddingOf(context).left`.
385
  final double? edgeDragWidth;
386

387 388 389 390 391 392 393 394
  /// 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
395 396 397 398 399 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
  /// 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!;
  }

451
  @override
452
  DrawerControllerState createState() => DrawerControllerState();
453
}
454

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

472
  @override
473
  void dispose() {
474
    _historyEntry?.remove();
475
    _controller.dispose();
476
    _focusScopeNode.dispose();
477 478 479
    super.dispose();
  }

480 481 482 483 484 485
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _scrimColorTween = _buildScrimColorTween();
  }

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

504
  void _animationChanged() {
505
    setState(() {
506
      // The animation controller's state is our build state, and it changed already.
507 508 509
    });
  }

510
  LocalHistoryEntry? _historyEntry;
511
  final FocusScopeNode _focusScopeNode = FocusScopeNode();
512 513 514

  void _ensureHistoryEntry() {
    if (_historyEntry == null) {
515
      final ModalRoute<dynamic>? route = ModalRoute.of(context);
516
      if (route != null) {
517
        _historyEntry = LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved, impliesAppBarDismissal: false);
518
        route.addLocalHistoryEntry(_historyEntry!);
519
        FocusScope.of(context).setFirstFocus(_focusScopeNode);
520 521 522 523
      }
    }
  }

524
  void _animationStatusChanged(AnimationStatus status) {
525
    switch (status) {
526
      case AnimationStatus.forward:
527
        _ensureHistoryEntry();
528
      case AnimationStatus.reverse:
529 530
        _historyEntry?.remove();
        _historyEntry = null;
531
      case AnimationStatus.dismissed:
532
        break;
533
      case AnimationStatus.completed:
534 535
        break;
    }
536 537
  }

538 539 540 541
  void _handleHistoryEntryRemoved() {
    _historyEntry = null;
    close();
  }
542

543
  late AnimationController _controller;
Hixie's avatar
Hixie committed
544

545
  void _handleDragDown(DragDownDetails details) {
546
    _controller.stop();
547
    _ensureHistoryEntry();
Hixie's avatar
Hixie committed
548 549
  }

550
  void _handleDragCancel() {
551
    if (_controller.isDismissed || _controller.isAnimating) {
552
      return;
553
    }
554 555 556 557 558 559 560
    if (_controller.value < 0.5) {
      close();
    } else {
      open();
    }
  }

561
  final GlobalKey _drawerKey = GlobalKey();
562

Hixie's avatar
Hixie committed
563
  double get _width {
564
    final RenderBox? box = _drawerKey.currentContext?.findRenderObject() as RenderBox?;
565
    if (box != null) {
566
      return box.size.width;
567
    }
Hixie's avatar
Hixie committed
568 569 570
    return _kWidth; // drawer not being shown currently
  }

jslavitz's avatar
jslavitz committed
571 572
  bool _previouslyOpened = false;

573
  void _move(DragUpdateDetails details) {
574
    double delta = details.primaryDelta! / _width;
575 576 577 578 579 580
    switch (widget.alignment) {
      case DrawerAlignment.start:
        break;
      case DrawerAlignment.end:
        delta = -delta;
    }
581
    switch (Directionality.of(context)) {
582 583 584 585 586
      case TextDirection.rtl:
        _controller.value -= delta;
      case TextDirection.ltr:
        _controller.value += delta;
    }
jslavitz's avatar
jslavitz committed
587

588
    final bool opened = _controller.value > 0.5;
589
    if (opened != _previouslyOpened && widget.drawerCallback != null) {
590
      widget.drawerCallback!(opened);
591
    }
jslavitz's avatar
jslavitz committed
592
    _previouslyOpened = opened;
593 594
  }

595
  void _settle(DragEndDetails details) {
596
    if (_controller.isDismissed) {
597
      return;
598
    }
599
    if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
600 601 602 603 604 605 606
      double visualVelocity = details.velocity.pixelsPerSecond.dx / _width;
      switch (widget.alignment) {
        case DrawerAlignment.start:
          break;
        case DrawerAlignment.end:
          visualVelocity = -visualVelocity;
      }
607
      switch (Directionality.of(context)) {
608 609
        case TextDirection.rtl:
          _controller.fling(velocity: -visualVelocity);
610
          widget.drawerCallback?.call(visualVelocity < 0.0);
611 612
        case TextDirection.ltr:
          _controller.fling(velocity: visualVelocity);
613
          widget.drawerCallback?.call(visualVelocity > 0.0);
614
      }
615
    } else if (_controller.value < 0.5) {
616
      close();
617
    } else {
618
      open();
619 620 621
    }
  }

622 623
  /// Starts an animation to open the drawer.
  ///
624
  /// Typically called by [ScaffoldState.openDrawer].
625
  void open() {
626
    _controller.fling();
627
    widget.drawerCallback?.call(true);
628 629
  }

630
  /// Starts an animation to close the drawer.
631
  void close() {
632
    _controller.fling(velocity: -1.0);
633
    widget.drawerCallback?.call(false);
634
  }
635

636
  late ColorTween _scrimColorTween;
637
  final GlobalKey _gestureDetectorKey = GlobalKey();
638

639
  ColorTween _buildScrimColorTween() {
640 641 642 643 644 645
    return ColorTween(
      begin: Colors.transparent,
      end: widget.scrimColor
          ?? DrawerTheme.of(context).scrimColor
          ?? Colors.black54,
    );
646 647
  }

648
  AlignmentDirectional get _drawerOuterAlignment {
649 650 651 652
    return switch (widget.alignment) {
      DrawerAlignment.start => AlignmentDirectional.centerStart,
      DrawerAlignment.end   => AlignmentDirectional.centerEnd,
    };
653 654 655
  }

  AlignmentDirectional get _drawerInnerAlignment {
656 657 658 659
    return switch (widget.alignment) {
      DrawerAlignment.start => AlignmentDirectional.centerEnd,
      DrawerAlignment.end => AlignmentDirectional.centerStart,
    };
660 661
  }

662
  Widget _buildDrawer(BuildContext context) {
663 664 665 666
    final bool isDesktop = switch (Theme.of(context).platform) {
      TargetPlatform.android || TargetPlatform.iOS || TargetPlatform.fuchsia => false,
      TargetPlatform.macOS || TargetPlatform.linux || TargetPlatform.windows => true,
    };
667

668 669 670 671 672 673 674
    final double dragAreaWidth = widget.edgeDragWidth
      ?? _kEdgeDragWidth + switch ((widget.alignment, Directionality.of(context))) {
        (DrawerAlignment.start, TextDirection.ltr) => MediaQuery.paddingOf(context).left,
        (DrawerAlignment.start, TextDirection.rtl) => MediaQuery.paddingOf(context).right,
        (DrawerAlignment.end,   TextDirection.rtl) => MediaQuery.paddingOf(context).left,
        (DrawerAlignment.end,   TextDirection.ltr) => MediaQuery.paddingOf(context).right,
      };
675

676
    if (_controller.status == AnimationStatus.dismissed) {
677
      if (widget.enableOpenDragGesture && !isDesktop) {
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692
        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();
      }
693
    } else {
694
      final bool platformHasBackButton;
695
      switch (Theme.of(context).platform) {
696 697 698
        case TargetPlatform.android:
          platformHasBackButton = true;
        case TargetPlatform.iOS:
699
        case TargetPlatform.macOS:
700
        case TargetPlatform.fuchsia:
701 702
        case TargetPlatform.linux:
        case TargetPlatform.windows:
703 704
          platformHasBackButton = false;
      }
705

hangyu's avatar
hangyu committed
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721
      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),
                      ),
722
                    ),
723
                  ),
724
                ),
725
              ),
hangyu's avatar
hangyu committed
726 727 728 729 730 731 732 733 734 735 736
              Align(
                alignment: _drawerOuterAlignment,
                child: Align(
                  alignment: _drawerInnerAlignment,
                  widthFactor: _controller.value,
                  child: RepaintBoundary(
                    child: FocusScope(
                      key: _drawerKey,
                      node: _focusScopeNode,
                      child: widget.child,
                    ),
737 738 739
                  ),
                ),
              ),
hangyu's avatar
hangyu committed
740 741
            ],
          ),
742
        ),
743
      );
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758

      if (isDesktop) {
        return child;
      }

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

762 763
  @override
  Widget build(BuildContext context) {
764
    assert(debugCheckHasMaterialLocalizations(context));
765
    return ListTileTheme.merge(
766 767 768 769
      style: ListTileStyle.drawer,
      child: _buildDrawer(context),
    );
  }
770
}
hangyu's avatar
hangyu committed
771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790

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.

class _DrawerDefaultsM3 extends DrawerThemeData {
791
  _DrawerDefaultsM3(this.context)
hangyu's avatar
hangyu committed
792 793 794
      : super(elevation: 1.0);

  final BuildContext context;
795
  late final TextDirection direction = Directionality.of(context);
hangyu's avatar
hangyu committed
796 797

  @override
798
  Color? get backgroundColor => Theme.of(context).colorScheme.surfaceContainerLow;
hangyu's avatar
hangyu committed
799 800

  @override
801
  Color? get surfaceTintColor => Colors.transparent;
hangyu's avatar
hangyu committed
802 803 804 805

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

806 807
  // There isn't currently a token for this value, but it is shown in the spec,
  // so hard coding here for now.
hangyu's avatar
hangyu committed
808
  @override
809 810 811 812
  ShapeBorder? get shape => RoundedRectangleBorder(
    borderRadius: const BorderRadiusDirectional.horizontal(
      end: Radius.circular(16.0),
    ).resolve(direction),
hangyu's avatar
hangyu committed
813 814
  );

815 816
  // There isn't currently a token for this value, but it is shown in the spec,
  // so hard coding here for now.
hangyu's avatar
hangyu committed
817
  @override
818 819 820 821
  ShapeBorder? get endShape => RoundedRectangleBorder(
    borderRadius: const BorderRadiusDirectional.horizontal(
      start: Radius.circular(16.0),
    ).resolve(direction),
hangyu's avatar
hangyu committed
822 823 824 825
  );
}

// END GENERATED TOKEN PROPERTIES - Drawer