Unverified Commit d2ab29d2 authored by jslavitz's avatar jslavitz Committed by GitHub

Drawer fix (#20015)

* Drawer fix

* fixed nits

* fixed nits

* fixed nits

* final change

* Drawer fix final
parent 93705691
......@@ -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<DrawerController> 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<DrawerController> 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<DrawerController> 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);
......
......@@ -1019,6 +1019,21 @@ class ScaffoldState extends State<Scaffold> 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<Scaffold> 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<Scaffold> 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<Scaffold> with TickerProviderStateMixin {
}
}
void _buildEndDrawer(List<LayoutId> 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<LayoutId> 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<Scaffold> 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<Scaffold> 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<Scaffold> 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<Scaffold> 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.
......
......@@ -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);
});
});
}
......
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