Commit c608e666 authored by 5u3it's avatar 5u3it Committed by Adam Barth

Material scaffold to have simultaneous left-and-right drawers (#12686)

Adds `Scaffold#endDrawer` property to supply a second drawer to a Scaffold.
parent 05e10633
...@@ -488,4 +488,4 @@ ...@@ -488,4 +488,4 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = 97C146E61CF9000F007C117D /* Project object */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
} }
\ No newline at end of file
...@@ -113,4 +113,4 @@ ...@@ -113,4 +113,4 @@
"version" : 1, "version" : 1,
"author" : "xcode" "author" : "xcode"
} }
} }
\ No newline at end of file
...@@ -325,6 +325,10 @@ class _AppBarState extends State<AppBar> { ...@@ -325,6 +325,10 @@ class _AppBarState extends State<AppBar> {
Scaffold.of(context).openDrawer(); Scaffold.of(context).openDrawer();
} }
void _handleDrawerButtonEnd() {
Scaffold.of(context).openEndDrawer();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(!widget.primary || debugCheckHasMediaQuery(context)); assert(!widget.primary || debugCheckHasMediaQuery(context));
...@@ -333,6 +337,7 @@ class _AppBarState extends State<AppBar> { ...@@ -333,6 +337,7 @@ class _AppBarState extends State<AppBar> {
final ModalRoute<dynamic> parentRoute = ModalRoute.of(context); final ModalRoute<dynamic> parentRoute = ModalRoute.of(context);
final bool hasDrawer = scaffold?.hasDrawer ?? false; final bool hasDrawer = scaffold?.hasDrawer ?? false;
final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false;
final bool canPop = parentRoute?.canPop ?? false; final bool canPop = parentRoute?.canPop ?? false;
final bool useCloseButton = parentRoute is MaterialPageRoute<dynamic> && parentRoute.fullscreenDialog; final bool useCloseButton = parentRoute is MaterialPageRoute<dynamic> && parentRoute.fullscreenDialog;
...@@ -393,6 +398,12 @@ class _AppBarState extends State<AppBar> { ...@@ -393,6 +398,12 @@ class _AppBarState extends State<AppBar> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: widget.actions, children: widget.actions,
); );
} else if (hasEndDrawer) {
actions = new IconButton(
icon: const Icon(Icons.menu),
onPressed: _handleDrawerButtonEnd,
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
);
} }
final Widget toolbar = new Padding( final Widget toolbar = new Padding(
......
...@@ -9,6 +9,21 @@ import 'colors.dart'; ...@@ -9,6 +9,21 @@ import 'colors.dart';
import 'list_tile.dart'; import 'list_tile.dart';
import 'material.dart'; import 'material.dart';
/// The alginment of a [Drawer] which is used to identify positioning
/// of the [Drawer]
///
enum DrawerAlignment {
/// Denotes that the [Drawer] is at the start side of the [Scaffold]
/// i.e. left side when Directionality is LTR
/// and right side when Directionality is RTL
start,
/// Denotes that the [Drawer] is at the end side of the [Scaffold]
/// i.e. right side when Directionality is LTR
/// and left side when Directionality is RTL
end,
}
// TODO(eseidel): Draw width should vary based on device size: // TODO(eseidel): Draw width should vary based on device size:
// http://material.google.com/layout/structure.html#structure-side-nav // http://material.google.com/layout/structure.html#structure-side-nav
...@@ -118,7 +133,9 @@ class DrawerController extends StatefulWidget { ...@@ -118,7 +133,9 @@ class DrawerController extends StatefulWidget {
const DrawerController({ const DrawerController({
GlobalKey key, GlobalKey key,
@required this.child, @required this.child,
}) : assert(child != null), @required this.alignment,
}) : assert(child != null),
assert(alignment != null),
super(key: key); super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
...@@ -126,6 +143,11 @@ class DrawerController extends StatefulWidget { ...@@ -126,6 +143,11 @@ class DrawerController extends StatefulWidget {
/// Typically a [Drawer]. /// Typically a [Drawer].
final Widget child; final Widget child;
/// The alginment of a [Drawer] which is used to identify positioning
/// of the [Drawer] i.e. either start-side or end-side
///
final DrawerAlignment alignment;
@override @override
DrawerControllerState createState() => new DrawerControllerState(); DrawerControllerState createState() => new DrawerControllerState();
} }
...@@ -217,7 +239,14 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -217,7 +239,14 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
} }
void _move(DragUpdateDetails details) { void _move(DragUpdateDetails details) {
final double delta = details.primaryDelta / _width; double delta = details.primaryDelta / _width;
switch (widget.alignment) {
case DrawerAlignment.start:
break;
case DrawerAlignment.end:
delta = -delta;
break;
}
switch (Directionality.of(context)) { switch (Directionality.of(context)) {
case TextDirection.rtl: case TextDirection.rtl:
_controller.value -= delta; _controller.value -= delta;
...@@ -232,7 +261,14 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -232,7 +261,14 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
if (_controller.isDismissed) if (_controller.isDismissed)
return; return;
if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) { if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
final double visualVelocity = details.velocity.pixelsPerSecond.dx / _width; double visualVelocity = details.velocity.pixelsPerSecond.dx / _width;
switch (widget.alignment) {
case DrawerAlignment.start:
break;
case DrawerAlignment.end:
visualVelocity = -visualVelocity;
break;
}
switch (Directionality.of(context)) { switch (Directionality.of(context)) {
case TextDirection.rtl: case TextDirection.rtl:
_controller.fling(velocity: -visualVelocity); _controller.fling(velocity: -visualVelocity);
...@@ -240,8 +276,7 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -240,8 +276,7 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
case TextDirection.ltr: case TextDirection.ltr:
_controller.fling(velocity: visualVelocity); _controller.fling(velocity: visualVelocity);
break; break;
} }
} else if (_controller.value < 0.5) { } else if (_controller.value < 0.5) {
close(); close();
} else { } else {
...@@ -264,10 +299,28 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -264,10 +299,28 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
final ColorTween _color = new ColorTween(begin: Colors.transparent, end: Colors.black54); final ColorTween _color = new ColorTween(begin: Colors.transparent, end: Colors.black54);
final GlobalKey _gestureDetectorKey = new GlobalKey(); final GlobalKey _gestureDetectorKey = new GlobalKey();
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;
}
}
Widget _buildDrawer(BuildContext context) { Widget _buildDrawer(BuildContext context) {
if (_controller.status == AnimationStatus.dismissed) { if (_controller.status == AnimationStatus.dismissed) {
return new Align( return new Align(
alignment: AlignmentDirectional.centerStart, alignment: _drawerOuterAlignment,
child: new GestureDetector( child: new GestureDetector(
key: _gestureDetectorKey, key: _gestureDetectorKey,
onHorizontalDragUpdate: _move, onHorizontalDragUpdate: _move,
...@@ -299,9 +352,9 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -299,9 +352,9 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
), ),
), ),
new Align( new Align(
alignment: AlignmentDirectional.centerStart, alignment: _drawerOuterAlignment,
child: new Align( child: new Align(
alignment: AlignmentDirectional.centerEnd, alignment: _drawerInnerAlignment,
widthFactor: _controller.value, widthFactor: _controller.value,
child: new RepaintBoundary( child: new RepaintBoundary(
child: new FocusScope( child: new FocusScope(
...@@ -325,4 +378,4 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -325,4 +378,4 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
child: _buildDrawer(context), child: _buildDrawer(context),
); );
} }
} }
\ No newline at end of file
...@@ -32,6 +32,7 @@ enum _ScaffoldSlot { ...@@ -32,6 +32,7 @@ enum _ScaffoldSlot {
bottomNavigationBar, bottomNavigationBar,
floatingActionButton, floatingActionButton,
drawer, drawer,
endDrawer,
statusBar, statusBar,
} }
...@@ -150,6 +151,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -150,6 +151,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
layoutChild(_ScaffoldSlot.drawer, new BoxConstraints.tight(size)); layoutChild(_ScaffoldSlot.drawer, new BoxConstraints.tight(size));
positionChild(_ScaffoldSlot.drawer, Offset.zero); positionChild(_ScaffoldSlot.drawer, Offset.zero);
} }
if (hasChild(_ScaffoldSlot.endDrawer)) {
layoutChild(_ScaffoldSlot.endDrawer, new BoxConstraints.tight(size));
positionChild(_ScaffoldSlot.endDrawer, Offset.zero);
}
} }
@override @override
...@@ -312,6 +318,7 @@ class Scaffold extends StatefulWidget { ...@@ -312,6 +318,7 @@ class Scaffold extends StatefulWidget {
this.floatingActionButton, this.floatingActionButton,
this.persistentFooterButtons, this.persistentFooterButtons,
this.drawer, this.drawer,
this.endDrawer,
this.bottomNavigationBar, this.bottomNavigationBar,
this.backgroundColor, this.backgroundColor,
this.resizeToAvoidBottomPadding: true, this.resizeToAvoidBottomPadding: true,
...@@ -360,14 +367,25 @@ class Scaffold extends StatefulWidget { ...@@ -360,14 +367,25 @@ class Scaffold extends StatefulWidget {
final List<Widget> persistentFooterButtons; final List<Widget> persistentFooterButtons;
/// A panel displayed to the side of the [body], often hidden on mobile /// A panel displayed to the side of the [body], often hidden on mobile
/// devices. /// devices. Swipes in from either left-to-right ([TextDirection.ltr]) or
/// /// right-to-left ([TextDirection.rtl])
///
/// In the uncommon case that you wish to open the drawer manually, use the /// In the uncommon case that you wish to open the drawer manually, use the
/// [ScaffoldState.openDrawer] function. /// [ScaffoldState.openDrawer] function.
/// ///
/// Typically a [Drawer]. /// Typically a [Drawer].
final Widget drawer; final Widget drawer;
/// A panel displayed to the side of the [body], often hidden on mobile
/// devices. Swipes in from right-to-left ([TextDirection.ltr]) or
/// left-to-right ([TextDirection.rtl])
///
/// In the uncommon case that you wish to open the drawer manually, use the
/// [ScaffoldState.openDrawer] function.
///
/// Typically a [Drawer].
final Widget endDrawer;
/// The color of the [Material] widget that underlies the entire Scaffold. /// The color of the [Material] widget that underlies the entire Scaffold.
/// ///
/// The theme's [ThemeData.scaffoldBackgroundColor] by default. /// The theme's [ThemeData.scaffoldBackgroundColor] by default.
...@@ -528,9 +546,12 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -528,9 +546,12 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
// DRAWER API // DRAWER API
final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>(); final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>();
final GlobalKey<DrawerControllerState> _endDrawerKey = new GlobalKey<DrawerControllerState>();
/// Whether this scaffold has a non-null [Scaffold.drawer]. /// Whether this scaffold has a non-null [Scaffold.drawer].
bool get hasDrawer => widget.drawer != null; bool get hasDrawer => widget.drawer != null;
/// Whether this scaffold has a non-null [Scaffold.endDrawer].
bool get hasEndDrawer => widget.endDrawer != null;
/// Opens the [Drawer] (if any). /// Opens the [Drawer] (if any).
/// ///
...@@ -548,6 +569,22 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -548,6 +569,22 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_drawerKey.currentState?.open(); _drawerKey.currentState?.open();
} }
/// Opens the end side [Drawer] (if any).
///
/// If the scaffold has a non-null [Scaffold.endDrawer], this function will cause
/// the end side drawer to begin its entrance animation.
///
/// Normally this is not needed since the [Scaffold] automatically shows an
/// appropriate [IconButton], and handles the edge-swipe gesture, to show the
/// drawer.
///
/// To close the end side drawer once it is open, use [Navigator.pop].
///
/// See [Scaffold.of] for information about how to obtain the [ScaffoldState].
void openEndDrawer() {
_endDrawerKey.currentState?.open();
}
// SNACKBAR API // SNACKBAR API
final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>(); final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
...@@ -945,6 +982,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -945,6 +982,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
children, children,
new DrawerController( new DrawerController(
key: _drawerKey, key: _drawerKey,
alignment: DrawerAlignment.start,
child: widget.drawer, child: widget.drawer,
), ),
_ScaffoldSlot.drawer, _ScaffoldSlot.drawer,
...@@ -956,6 +994,24 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -956,6 +994,24 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
); );
} }
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,
);
}
double endPadding; double endPadding;
switch (textDirection) { switch (textDirection) {
case TextDirection.rtl: case TextDirection.rtl:
...@@ -1096,4 +1152,4 @@ class _ScaffoldScope extends InheritedWidget { ...@@ -1096,4 +1152,4 @@ class _ScaffoldScope extends InheritedWidget {
bool updateShouldNotify(_ScaffoldScope oldWidget) { bool updateShouldNotify(_ScaffoldScope oldWidget) {
return hasDrawer != oldWidget.hasDrawer; return hasDrawer != oldWidget.hasDrawer;
} }
} }
\ No newline at end of file
...@@ -623,4 +623,39 @@ void main() { ...@@ -623,4 +623,39 @@ void main() {
expect(tester.getRect(find.byKey(insideDrawer)), new Rect.fromLTRB(596.0, 30.0, 750.0, 530.0)); expect(tester.getRect(find.byKey(insideDrawer)), new Rect.fromLTRB(596.0, 30.0, 750.0, 530.0));
expect(tester.getRect(find.byKey(insideBottomNavigationBar)), new Rect.fromLTRB(20.0, 475.0, 750.0, 530.0)); expect(tester.getRect(find.byKey(insideBottomNavigationBar)), new Rect.fromLTRB(20.0, 475.0, 750.0, 530.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: new 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();
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment