Drawer fix (#20015)
* Drawer fix * fixed nits * fixed nits * fixed nits * final change * Drawer fix final
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user