| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'colors.dart'; |
| import 'list_tile.dart'; |
| import 'material.dart'; |
| |
| /// The possible alignments of a [Drawer]. |
| enum DrawerAlignment { |
| /// 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. |
| start, |
| |
| /// 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. |
| end, |
| } |
| |
| // TODO(eseidel): Draw width should vary based on device size: |
| // http://material.google.com/layout/structure.html#structure-side-nav |
| |
| // 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; |
| const double _kEdgeDragWidth = 20.0; |
| const double _kMinFlingVelocity = 365.0; |
| const Duration _kBaseSettleDuration = const Duration(milliseconds: 246); |
| |
| /// A material design panel that slides in horizontally from the edge of a |
| /// [Scaffold] to show navigation links in an application. |
| /// |
| /// Drawers are typically used with the [Scaffold.drawer] property. The child of |
| /// the drawer is usually a [ListView] whose first child is a [DrawerHeader] |
| /// that displays status information about the current user. The remaining |
| /// drawer children are often constructed with [ListTile]s, often concluding |
| /// with an [AboutListTile]. |
| /// |
| /// An open drawer can be closed by calling [Navigator.pop]. For example |
| /// a drawer item might close the drawer when tapped: |
| /// |
| /// ```dart |
| /// new ListTile( |
| /// leading: new Icon(Icons.change_history), |
| /// title: new Text('Change history'), |
| /// onTap: () { |
| /// // change app state... |
| /// Navigator.pop(context); // close the drawer |
| /// }, |
| /// ); |
| /// ``` |
| /// |
| /// 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. |
| /// |
| /// See also: |
| /// |
| /// * [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. |
| /// * <https://material.google.com/patterns/navigation-drawer.html> |
| class Drawer extends StatelessWidget { |
| /// Creates a material design drawer. |
| /// |
| /// Typically used in the [Scaffold.drawer] property. |
| const Drawer({ |
| Key key, |
| this.elevation: 16.0, |
| this.child, |
| }) : super(key: key); |
| |
| /// The z-coordinate at which to place this drawer. This controls the size of |
| /// the shadow below the drawer. |
| /// |
| /// Defaults to 16, the appropriate elevation for drawers. |
| final double elevation; |
| |
| /// The widget below this widget in the tree. |
| /// |
| /// Typically a [SliverList]. |
| final Widget child; |
| |
| @override |
| Widget build(BuildContext context) { |
| return new ConstrainedBox( |
| constraints: const BoxConstraints.expand(width: _kWidth), |
| child: new Material( |
| elevation: elevation, |
| child: child, |
| ), |
| ); |
| } |
| } |
| |
| /// Provides interactive behavior for [Drawer] widgets. |
| /// |
| /// Rarely used directly. Drawer controllers are typically created automatically |
| /// by [Scaffold] widgets. |
| /// |
| /// The draw controller provides the ability to open and close a drawer, either |
| /// via an animation or via user interaction. When closed, the drawer collapses |
| /// to a translucent gesture detector that can be used to listen for edge |
| /// swipes. |
| /// |
| /// See also: |
| /// |
| /// * [Drawer], a container with the default width of a drawer. |
| /// * [Scaffold.drawer], the [Scaffold] slot for showing a drawer. |
| class DrawerController extends StatefulWidget { |
| /// Creates a controller for a [Drawer]. |
| /// |
| /// Rarely used directly. |
| /// |
| /// The [child] argument must not be null and is typically a [Drawer]. |
| const DrawerController({ |
| GlobalKey key, |
| @required this.child, |
| @required this.alignment, |
| }) : assert(child != null), |
| assert(alignment != null), |
| super(key: key); |
| |
| /// The widget below this widget in the tree. |
| /// |
| /// Typically a [Drawer]. |
| final Widget child; |
| |
| /// The alignment of the [Drawer]. |
| /// |
| /// This controls the direction in which the user should swipe to open and |
| /// close the drawer. |
| final DrawerAlignment alignment; |
| |
| @override |
| DrawerControllerState createState() => new DrawerControllerState(); |
| } |
| |
| /// State for a [DrawerController]. |
| /// |
| /// Typically used by a [Scaffold] to [open] and [close] the drawer. |
| class DrawerControllerState extends State<DrawerController> with SingleTickerProviderStateMixin { |
| @override |
| void initState() { |
| super.initState(); |
| _controller = new AnimationController(duration: _kBaseSettleDuration, vsync: this) |
| ..addListener(_animationChanged) |
| ..addStatusListener(_animationStatusChanged); |
| } |
| |
| @override |
| void dispose() { |
| _historyEntry?.remove(); |
| _controller.dispose(); |
| super.dispose(); |
| } |
| |
| void _animationChanged() { |
| setState(() { |
| // The animation controller's state is our build state, and it changed already. |
| }); |
| } |
| |
| LocalHistoryEntry _historyEntry; |
| final FocusScopeNode _focusScopeNode = new FocusScopeNode(); |
| |
| void _ensureHistoryEntry() { |
| if (_historyEntry == null) { |
| final ModalRoute<dynamic> route = ModalRoute.of(context); |
| if (route != null) { |
| _historyEntry = new LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved); |
| route.addLocalHistoryEntry(_historyEntry); |
| FocusScope.of(context).setFirstFocus(_focusScopeNode); |
| } |
| } |
| } |
| |
| void _animationStatusChanged(AnimationStatus status) { |
| switch (status) { |
| case AnimationStatus.forward: |
| _ensureHistoryEntry(); |
| break; |
| case AnimationStatus.reverse: |
| _historyEntry?.remove(); |
| _historyEntry = null; |
| break; |
| case AnimationStatus.dismissed: |
| break; |
| case AnimationStatus.completed: |
| break; |
| } |
| } |
| |
| void _handleHistoryEntryRemoved() { |
| _historyEntry = null; |
| close(); |
| } |
| |
| AnimationController _controller; |
| |
| void _handleDragDown(DragDownDetails details) { |
| _controller.stop(); |
| _ensureHistoryEntry(); |
| } |
| |
| void _handleDragCancel() { |
| if (_controller.isDismissed || _controller.isAnimating) |
| return; |
| if (_controller.value < 0.5) { |
| close(); |
| } else { |
| open(); |
| } |
| } |
| |
| final GlobalKey _drawerKey = new GlobalKey(); |
| |
| double get _width { |
| final RenderBox box = _drawerKey.currentContext?.findRenderObject(); |
| if (box != null) |
| return box.size.width; |
| return _kWidth; // drawer not being shown currently |
| } |
| |
| void _move(DragUpdateDetails details) { |
| double delta = details.primaryDelta / _width; |
| switch (widget.alignment) { |
| case DrawerAlignment.start: |
| break; |
| case DrawerAlignment.end: |
| delta = -delta; |
| break; |
| } |
| switch (Directionality.of(context)) { |
| case TextDirection.rtl: |
| _controller.value -= delta; |
| break; |
| case TextDirection.ltr: |
| _controller.value += delta; |
| break; |
| } |
| } |
| |
| void _settle(DragEndDetails details) { |
| if (_controller.isDismissed) |
| return; |
| if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) { |
| double visualVelocity = details.velocity.pixelsPerSecond.dx / _width; |
| switch (widget.alignment) { |
| case DrawerAlignment.start: |
| break; |
| case DrawerAlignment.end: |
| visualVelocity = -visualVelocity; |
| break; |
| } |
| switch (Directionality.of(context)) { |
| case TextDirection.rtl: |
| _controller.fling(velocity: -visualVelocity); |
| break; |
| case TextDirection.ltr: |
| _controller.fling(velocity: visualVelocity); |
| break; |
| } |
| } else if (_controller.value < 0.5) { |
| close(); |
| } else { |
| open(); |
| } |
| } |
| |
| /// Starts an animation to open the drawer. |
| /// |
| /// Typically called by [ScaffoldState.openDrawer]. |
| void open() { |
| _controller.fling(velocity: 1.0); |
| } |
| |
| /// Starts an animation to close the drawer. |
| void close() { |
| _controller.fling(velocity: -1.0); |
| } |
| |
| final ColorTween _color = new ColorTween(begin: Colors.transparent, end: Colors.black54); |
| final GlobalKey _gestureDetectorKey = new GlobalKey(); |
| |
| AlignmentDirectional get _drawerOuterAlignment { |
| assert(widget.alignment != null); |
| switch (widget.alignment) { |
| case DrawerAlignment.start: |
| return AlignmentDirectional.centerStart; |
| case DrawerAlignment.end: |
| return AlignmentDirectional.centerEnd; |
| } |
| return null; |
| } |
| |
| AlignmentDirectional get _drawerInnerAlignment { |
| assert(widget.alignment != null); |
| switch (widget.alignment) { |
| case DrawerAlignment.start: |
| return AlignmentDirectional.centerEnd; |
| case DrawerAlignment.end: |
| return AlignmentDirectional.centerStart; |
| } |
| return null; |
| } |
| |
| Widget _buildDrawer(BuildContext context) { |
| if (_controller.status == AnimationStatus.dismissed) { |
| return new Align( |
| alignment: _drawerOuterAlignment, |
| child: new GestureDetector( |
| key: _gestureDetectorKey, |
| onHorizontalDragUpdate: _move, |
| onHorizontalDragEnd: _settle, |
| behavior: HitTestBehavior.translucent, |
| excludeFromSemantics: true, |
| child: new Container(width: _kEdgeDragWidth) |
| ), |
| ); |
| } else { |
| return new GestureDetector( |
| key: _gestureDetectorKey, |
| onHorizontalDragDown: _handleDragDown, |
| onHorizontalDragUpdate: _move, |
| onHorizontalDragEnd: _settle, |
| onHorizontalDragCancel: _handleDragCancel, |
| excludeFromSemantics: true, |
| child: new RepaintBoundary( |
| child: new Stack( |
| children: <Widget>[ |
| new BlockSemantics( |
| child: new GestureDetector( |
| // On Android, the back button is used to dismiss a modal. |
| excludeFromSemantics: defaultTargetPlatform == TargetPlatform.android, |
| onTap: close, |
| child: new Container( |
| color: _color.evaluate(_controller) |
| ), |
| ), |
| ), |
| new Align( |
| alignment: _drawerOuterAlignment, |
| child: new Align( |
| alignment: _drawerInnerAlignment, |
| widthFactor: _controller.value, |
| child: new RepaintBoundary( |
| child: new FocusScope( |
| key: _drawerKey, |
| node: _focusScopeNode, |
| child: widget.child |
| ), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| } |
| @override |
| Widget build(BuildContext context) { |
| return new ListTileTheme( |
| style: ListTileStyle.drawer, |
| child: _buildDrawer(context), |
| ); |
| } |
| } |