diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index e77117e47b..31e00f2d4f 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -141,6 +141,10 @@ class Drawer extends StatelessWidget { } } +/// Signature for the callback that's called when a [DrawerController] is +/// opened or closed. +typedef void DrawerCallback(bool isOpened); + /// Provides interactive behavior for [Drawer] widgets. /// /// Rarely used directly. Drawer controllers are typically created automatically @@ -165,6 +169,7 @@ class DrawerController extends StatefulWidget { GlobalKey key, @required this.child, @required this.alignment, + this.drawerCallback, }) : assert(child != null), assert(alignment != null), super(key: key); @@ -180,6 +185,9 @@ class DrawerController extends StatefulWidget { /// close the drawer. final DrawerAlignment alignment; + /// Optional callback that is called when a [Drawer] is opened or closed. + final DrawerCallback drawerCallback; + @override DrawerControllerState createState() => new DrawerControllerState(); } @@ -270,6 +278,8 @@ class DrawerControllerState extends State with SingleTickerPro return _kWidth; // drawer not being shown currently } + bool _previouslyOpened = false; + void _move(DragUpdateDetails details) { double delta = details.primaryDelta / _width; switch (widget.alignment) { @@ -287,6 +297,11 @@ class DrawerControllerState extends State with SingleTickerPro _controller.value += delta; break; } + + final bool opened = _controller.value > 0.5 ? true : false; + if (opened != _previouslyOpened && widget.drawerCallback != null) + widget.drawerCallback(opened); + _previouslyOpened = opened; } void _settle(DragEndDetails details) { @@ -321,11 +336,15 @@ class DrawerControllerState extends State with SingleTickerPro /// Typically called by [ScaffoldState.openDrawer]. void open() { _controller.fling(velocity: 1.0); + if (widget.drawerCallback != null) + widget.drawerCallback(true); } /// Starts an animation to close the drawer. void close() { _controller.fling(velocity: -1.0); + if (widget.drawerCallback != null) + widget.drawerCallback(false); } final ColorTween _color = new ColorTween(begin: Colors.transparent, end: Colors.black54); diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 4c9ce7611c..1364be9d42 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -1019,6 +1019,21 @@ class ScaffoldState extends State with TickerProviderStateMixin { /// Whether this scaffold has a non-null [Scaffold.endDrawer]. bool get hasEndDrawer => widget.endDrawer != null; + bool _drawerOpened = false; + bool _endDrawerOpened = false; + + void _drawerOpenedCallback(bool isOpened) { + setState(() { + _drawerOpened = isOpened; + }); + } + + void _endDrawerOpenedCallback(bool isOpened) { + setState(() { + _endDrawerOpened = isOpened; + }); + } + /// Opens the [Drawer] (if any). /// /// If the scaffold has a non-null [Scaffold.drawer], this function will cause @@ -1032,6 +1047,8 @@ class ScaffoldState extends State with TickerProviderStateMixin { /// /// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. void openDrawer() { + if (_endDrawerKey.currentState != null && _endDrawerOpened) + _endDrawerKey.currentState.close(); _drawerKey.currentState?.open(); } @@ -1048,6 +1065,8 @@ class ScaffoldState extends State with TickerProviderStateMixin { /// /// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. void openEndDrawer() { + if (_drawerKey.currentState != null && _drawerOpened) + _drawerKey.currentState.close(); _endDrawerKey.currentState?.open(); } @@ -1408,6 +1427,48 @@ class ScaffoldState extends State with TickerProviderStateMixin { } } + void _buildEndDrawer(List children, TextDirection textDirection) { + if (widget.endDrawer != null) { + assert(hasEndDrawer); + _addIfNonNull( + children, + new DrawerController( + key: _endDrawerKey, + alignment: DrawerAlignment.end, + child: widget.endDrawer, + drawerCallback: _endDrawerOpenedCallback, + ), + _ScaffoldSlot.endDrawer, + // remove the side padding from the side we're not touching + removeLeftPadding: textDirection == TextDirection.ltr, + removeTopPadding: false, + removeRightPadding: textDirection == TextDirection.rtl, + removeBottomPadding: false, + ); + } + } + + void _buildDrawer(List children, TextDirection textDirection) { + if (widget.drawer != null) { + assert(hasDrawer); + _addIfNonNull( + children, + new DrawerController( + key: _drawerKey, + alignment: DrawerAlignment.start, + child: widget.drawer, + drawerCallback: _drawerOpenedCallback, + ), + _ScaffoldSlot.drawer, + // remove the side padding from the side we're not touching + removeLeftPadding: textDirection == TextDirection.rtl, + removeTopPadding: false, + removeRightPadding: textDirection == TextDirection.ltr, + removeBottomPadding: false, + ); + } + } + @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); @@ -1422,7 +1483,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { if (_snackBarController.isCompleted && _snackBarTimer == null) _snackBarTimer = new Timer(_snackBars.first._widget.duration, () { assert(_snackBarController.status == AnimationStatus.forward || - _snackBarController.status == AnimationStatus.completed); + _snackBarController.status == AnimationStatus.completed); hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); }); } else { @@ -1440,7 +1501,8 @@ class ScaffoldState extends State with TickerProviderStateMixin { removeLeftPadding: false, removeTopPadding: widget.appBar != null, removeRightPadding: false, - removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null, + removeBottomPadding: widget.bottomNavigationBar != null || + widget.persistentFooterButtons != null, ); if (widget.appBar != null) { @@ -1465,7 +1527,8 @@ class ScaffoldState extends State with TickerProviderStateMixin { } if (_snackBars.isNotEmpty) { - final bool removeBottomPadding = widget.persistentFooterButtons != null || widget.bottomNavigationBar != null; + final bool removeBottomPadding = widget.persistentFooterButtons != null || + widget.bottomNavigationBar != null; _addIfNonNull( children, _snackBars.first._widget, @@ -1570,40 +1633,12 @@ class ScaffoldState extends State with TickerProviderStateMixin { ); } - if (widget.drawer != null) { - assert(hasDrawer); - _addIfNonNull( - children, - new DrawerController( - key: _drawerKey, - alignment: DrawerAlignment.start, - child: widget.drawer, - ), - _ScaffoldSlot.drawer, - // remove the side padding from the side we're not touching - removeLeftPadding: textDirection == TextDirection.rtl, - removeTopPadding: false, - removeRightPadding: textDirection == TextDirection.ltr, - removeBottomPadding: false, - ); - } - - if (widget.endDrawer != null) { - assert(hasEndDrawer); - _addIfNonNull( - children, - new DrawerController( - key: _endDrawerKey, - alignment: DrawerAlignment.end, - child: widget.endDrawer, - ), - _ScaffoldSlot.endDrawer, - // remove the side padding from the side we're not touching - removeLeftPadding: textDirection == TextDirection.ltr, - removeTopPadding: false, - removeRightPadding: textDirection == TextDirection.rtl, - removeBottomPadding: false, - ); + if (_endDrawerOpened) { + _buildDrawer(children, textDirection); + _buildEndDrawer(children, textDirection); + } else { + _buildEndDrawer(children, textDirection); + _buildDrawer(children, textDirection); } // The minimum insets for contents of the Scaffold to keep visible. diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index 99951537a2..da080011d6 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -766,39 +766,6 @@ void main() { expect(tester.getRect(find.byKey(insideDrawer)), new Rect.fromLTRB(596.0, 30.0, 750.0, 540.0)); }); - testWidgets('Simultaneous drawers on either side', (WidgetTester tester) async { - const String bodyLabel = 'I am the body'; - const String drawerLabel = 'I am the label on start side'; - const String endDrawerLabel = 'I am the label on end side'; - - final SemanticsTester semantics = new SemanticsTester(tester); - await tester.pumpWidget(new MaterialApp(home: const Scaffold( - body: const Text(bodyLabel), - drawer: const Drawer(child: const Text(drawerLabel)), - endDrawer: const Drawer(child: const Text(endDrawerLabel)), - ))); - - expect(semantics, includesNodeWith(label: bodyLabel)); - expect(semantics, isNot(includesNodeWith(label: drawerLabel))); - expect(semantics, isNot(includesNodeWith(label: endDrawerLabel))); - - final ScaffoldState state = tester.firstState(find.byType(Scaffold)); - state.openDrawer(); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - expect(semantics, isNot(includesNodeWith(label: bodyLabel))); - expect(semantics, includesNodeWith(label: drawerLabel)); - - state.openEndDrawer(); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); - - expect(semantics, isNot(includesNodeWith(label: bodyLabel))); - expect(semantics, includesNodeWith(label: endDrawerLabel)); - - semantics.dispose(); - }); group('ScaffoldGeometry', () { testWidgets('bottomNavigationBar', (WidgetTester tester) async { @@ -969,6 +936,93 @@ void main() { numNotificationsAtLastFrame = listenerState.numNotifications; }); + testWidgets('Simultaneous drawers on either side', (WidgetTester tester) async { + const String bodyLabel = 'I am the body'; + const String drawerLabel = 'I am the label on start side'; + const String endDrawerLabel = 'I am the label on end side'; + + final SemanticsTester semantics = new SemanticsTester(tester); + await tester.pumpWidget(new MaterialApp(home: const Scaffold( + body: const Text(bodyLabel), + drawer: const Drawer(child: const Text(drawerLabel)), + endDrawer: const Drawer(child: const Text(endDrawerLabel)), + ))); + + expect(semantics, includesNodeWith(label: bodyLabel)); + expect(semantics, isNot(includesNodeWith(label: drawerLabel))); + expect(semantics, isNot(includesNodeWith(label: endDrawerLabel))); + + final ScaffoldState state = tester.firstState(find.byType(Scaffold)); + state.openDrawer(); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(semantics, isNot(includesNodeWith(label: bodyLabel))); + expect(semantics, includesNodeWith(label: drawerLabel)); + + state.openEndDrawer(); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(semantics, isNot(includesNodeWith(label: bodyLabel))); + expect(semantics, includesNodeWith(label: endDrawerLabel)); + + semantics.dispose(); + }); + + testWidgets('Dual Drawer Opening', (WidgetTester tester) async { + + await tester.pumpWidget( + new MaterialApp( + home: new SafeArea( + left: false, + top: true, + right: false, + bottom: false, + child: new Scaffold( + endDrawer: const Drawer( + child: const Text('endDrawer'), + ), + drawer: const Drawer( + child: const Text('drawer'), + ), + body: const Text('scaffold body'), + appBar: new AppBar( + centerTitle: true, + title: const Text('Title') + ) + ), + ), + ), + ); + + // Open Drawer, tap on end drawer, which closes the drawer, but does + // not open the drawer. + await tester.tap(find.byType(IconButton).first); + await tester.pumpAndSettle(); + await tester.tap(find.byType(IconButton).last); + await tester.pumpAndSettle(); + + expect(find.text('endDrawer'), findsNothing); + expect(find.text('drawer'), findsNothing); + + // Tapping the first opens the first drawer + await tester.tap(find.byType(IconButton).first); + await tester.pumpAndSettle(); + + expect(find.text('endDrawer'), findsNothing); + expect(find.text('drawer'), findsOneWidget); + + // Tapping on the end drawer and then on the drawer should close the + // drawer and then reopen it. + await tester.tap(find.byType(IconButton).last); + await tester.pumpAndSettle(); + await tester.tap(find.byType(IconButton).first); + await tester.pumpAndSettle(); + + expect(find.text('endDrawer'), findsNothing); + expect(find.text('drawer'), findsOneWidget); + }); }); }