drawer.dart 27.5 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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
  /// 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;

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

201 202 203 204 205 206
  /// 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;

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

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

226 227 228 229 230 231 232 233
  /// {@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;

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

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

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

  final DrawerController controller;

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

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

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

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

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

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

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

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

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

385 386 387 388 389 390 391 392
  /// 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
393 394 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
  /// 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!;
  }

449
  @override
450
  DrawerControllerState createState() => DrawerControllerState();
451
}
452

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

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

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

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

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

508
  LocalHistoryEntry? _historyEntry;
509
  final FocusScopeNode _focusScopeNode = FocusScopeNode();
510 511 512

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

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

536 537 538 539
  void _handleHistoryEntryRemoved() {
    _historyEntry = null;
    close();
  }
540

541
  late AnimationController _controller;
Hixie's avatar
Hixie committed
542

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

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

559
  final GlobalKey _drawerKey = GlobalKey();
560

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

jslavitz's avatar
jslavitz committed
569 570
  bool _previouslyOpened = false;

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

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

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

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

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

634
  late ColorTween _scrimColorTween;
635
  final GlobalKey _gestureDetectorKey = GlobalKey();
636

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

646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
  AlignmentDirectional get _drawerOuterAlignment {
    switch (widget.alignment) {
      case DrawerAlignment.start:
        return AlignmentDirectional.centerStart;
      case DrawerAlignment.end:
        return AlignmentDirectional.centerEnd;
    }
  }

  AlignmentDirectional get _drawerInnerAlignment {
    switch (widget.alignment) {
      case DrawerAlignment.start:
        return AlignmentDirectional.centerEnd;
      case DrawerAlignment.end:
        return AlignmentDirectional.centerStart;
    }
  }

664
  Widget _buildDrawer(BuildContext context) {
665
    final bool drawerIsStart = widget.alignment == DrawerAlignment.start;
666
    final TextDirection textDirection = Directionality.of(context);
667 668 669 670 671 672 673 674 675 676 677
    final bool isDesktop;
    switch (Theme.of(context).platform) {
      case TargetPlatform.android:
      case TargetPlatform.iOS:
      case TargetPlatform.fuchsia:
        isDesktop = false;
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        isDesktop = true;
    }
678

679
    double? dragAreaWidth = widget.edgeDragWidth;
680
    if (widget.edgeDragWidth == null) {
681
      final EdgeInsets padding = MediaQuery.paddingOf(context);
682
      switch (textDirection) {
683
        case TextDirection.ltr:
684 685
          dragAreaWidth = _kEdgeDragWidth +
            (drawerIsStart ? padding.left : padding.right);
686
        case TextDirection.rtl:
687 688 689 690
          dragAreaWidth = _kEdgeDragWidth +
            (drawerIsStart ? padding.right : padding.left);
      }
    }
691

692
    if (_controller.status == AnimationStatus.dismissed) {
693
      if (widget.enableOpenDragGesture && !isDesktop) {
694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
        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();
      }
709
    } else {
710
      final bool platformHasBackButton;
711
      switch (Theme.of(context).platform) {
712 713 714
        case TargetPlatform.android:
          platformHasBackButton = true;
        case TargetPlatform.iOS:
715
        case TargetPlatform.macOS:
716
        case TargetPlatform.fuchsia:
717 718
        case TargetPlatform.linux:
        case TargetPlatform.windows:
719 720
          platformHasBackButton = false;
      }
721

hangyu's avatar
hangyu committed
722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
      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),
                      ),
738
                    ),
739
                  ),
740
                ),
741
              ),
hangyu's avatar
hangyu committed
742 743 744 745 746 747 748 749 750 751 752
              Align(
                alignment: _drawerOuterAlignment,
                child: Align(
                  alignment: _drawerInnerAlignment,
                  widthFactor: _controller.value,
                  child: RepaintBoundary(
                    child: FocusScope(
                      key: _drawerKey,
                      node: _focusScopeNode,
                      child: widget.child,
                    ),
753 754 755
                  ),
                ),
              ),
hangyu's avatar
hangyu committed
756 757
            ],
          ),
758
        ),
759
      );
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774

      if (isDesktop) {
        return child;
      }

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

778 779
  @override
  Widget build(BuildContext context) {
780
    assert(debugCheckHasMaterialLocalizations(context));
781
    return ListTileTheme.merge(
782 783 784 785
      style: ListTileStyle.drawer,
      child: _buildDrawer(context),
    );
  }
786
}
hangyu's avatar
hangyu committed
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806

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 {
807
  _DrawerDefaultsM3(this.context)
hangyu's avatar
hangyu committed
808 809 810
      : super(elevation: 1.0);

  final BuildContext context;
811
  late final TextDirection direction = Directionality.of(context);
hangyu's avatar
hangyu committed
812 813 814 815 816 817 818 819 820 821

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

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

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

822 823
  // 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
824
  @override
825 826 827 828
  ShapeBorder? get shape => RoundedRectangleBorder(
    borderRadius: const BorderRadiusDirectional.horizontal(
      end: Radius.circular(16.0),
    ).resolve(direction),
hangyu's avatar
hangyu committed
829 830
  );

831 832
  // 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
833
  @override
834 835 836 837
  ShapeBorder? get endShape => RoundedRectangleBorder(
    borderRadius: const BorderRadiusDirectional.horizontal(
      start: Radius.circular(16.0),
    ).resolve(direction),
hangyu's avatar
hangyu committed
838 839 840 841
  );
}

// END GENERATED TOKEN PROPERTIES - Drawer