Unverified Commit f6a49913 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

[State Restoration] Scaffold.drawer and Scaffold.endDrawer (#72788)

parent 5d1306e7
......@@ -231,6 +231,7 @@ class DrawerController extends StatefulWidget {
GlobalKey? key,
required this.child,
required this.alignment,
this.isDrawerOpen = false,
this.drawerCallback,
this.dragStartBehavior = DragStartBehavior.start,
this.scrimColor,
......@@ -298,6 +299,14 @@ class DrawerController extends StatefulWidget {
/// 20.0 will be added to `MediaQuery.of(context).padding.left`.
final double? edgeDragWidth;
/// Whether or not the drawer is opened or closed.
///
/// This parameter is primarily used by the state restoration framework
/// to restore the drawer's animation controller to the open or closed state
/// depending on what was last saved to the target platform before the
/// application was killed.
final bool isDrawerOpen;
@override
DrawerControllerState createState() => DrawerControllerState();
}
......@@ -310,7 +319,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
void initState() {
super.initState();
_scrimColorTween = _buildScrimColorTween();
_controller = AnimationController(duration: _kBaseSettleDuration, vsync: this)
_controller = AnimationController(
value: widget.isDrawerOpen ? 1.0 : 0.0,
duration: _kBaseSettleDuration,
vsync: this,
);
_controller
..addListener(_animationChanged)
..addStatusListener(_animationStatusChanged);
}
......@@ -327,6 +341,16 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
super.didUpdateWidget(oldWidget);
if (widget.scrimColor != oldWidget.scrimColor)
_scrimColorTween = _buildScrimColorTween();
if (widget.isDrawerOpen != oldWidget.isDrawerOpen) {
switch(_controller.status) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
_controller.value = widget.isDrawerOpen ? 1.0 : 0.0;
break;
default:
break;
}
}
}
void _animationChanged() {
......
......@@ -1459,6 +1459,7 @@ class Scaffold extends StatefulWidget {
this.drawerEdgeDragWidth,
this.drawerEnableOpenDragGesture = true,
this.endDrawerEnableOpenDragGesture = true,
this.restorationId,
}) : assert(primary != null),
assert(extendBody != null),
assert(extendBodyBehindAppBar != null),
......@@ -1783,6 +1784,20 @@ class Scaffold extends StatefulWidget {
/// By default, the drag gesture is enabled.
final bool endDrawerEnableOpenDragGesture;
/// Restoration ID to save and restore the state of the [Scaffold].
///
/// If it is non-null, the scaffold will persist and restore whether the
/// [drawer] and [endDrawer] was open or closed.
///
/// The state of this widget is persisted in a [RestorationBucket] claimed
/// from the surrounding [RestorationScope] using the provided restoration ID.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String? restorationId;
/// Finds the [ScaffoldState] from the closest instance of this class that
/// encloses the given context.
///
......@@ -2055,7 +2070,15 @@ class Scaffold extends StatefulWidget {
///
/// Can display [BottomSheet]s. Retrieve a [ScaffoldState] from the current
/// [BuildContext] using [Scaffold.of].
class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, RestorationMixin {
@override
String? get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_drawerOpened, 'drawer_open');
registerForRestoration(_endDrawerOpened, 'end_drawer_open');
}
// DRAWER API
......@@ -2076,8 +2099,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
///
/// This is based on the appBar preferred height plus the top padding.
double? get appBarMaxHeight => _appBarMaxHeight;
bool _drawerOpened = false;
bool _endDrawerOpened = false;
final RestorableBool _drawerOpened = RestorableBool(false);
final RestorableBool _endDrawerOpened = RestorableBool(false);
/// Whether the [Scaffold.drawer] is opened.
///
......@@ -2085,7 +2108,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
///
/// * [ScaffoldState.openDrawer], which opens the [Scaffold.drawer] of a
/// [Scaffold].
bool get isDrawerOpen => _drawerOpened;
bool get isDrawerOpen => _drawerOpened.value;
/// Whether the [Scaffold.endDrawer] is opened.
///
......@@ -2093,18 +2116,18 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
///
/// * [ScaffoldState.openEndDrawer], which opens the [Scaffold.endDrawer] of
/// a [Scaffold].
bool get isEndDrawerOpen => _endDrawerOpened;
bool get isEndDrawerOpen => _endDrawerOpened.value;
void _drawerOpenedCallback(bool isOpened) {
setState(() {
_drawerOpened = isOpened;
_drawerOpened.value = isOpened;
});
widget.onDrawerChanged?.call(isOpened);
}
void _endDrawerOpenedCallback(bool isOpened) {
setState(() {
_endDrawerOpened = isOpened;
_endDrawerOpened.value = isOpened;
});
widget.onEndDrawerChanged?.call(isOpened);
}
......@@ -2122,7 +2145,7 @@ 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)
if (_endDrawerKey.currentState != null && _endDrawerOpened.value)
_endDrawerKey.currentState!.close();
_drawerKey.currentState?.open();
}
......@@ -2140,7 +2163,7 @@ 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)
if (_drawerKey.currentState != null && _drawerOpened.value)
_drawerKey.currentState!.close();
_endDrawerKey.currentState?.open();
}
......@@ -2859,6 +2882,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
scrimColor: widget.drawerScrimColor,
edgeDragWidth: widget.drawerEdgeDragWidth,
enableOpenDragGesture: widget.endDrawerEnableOpenDragGesture,
isDrawerOpen: _endDrawerOpened.value,
),
_ScaffoldSlot.endDrawer,
// remove the side padding from the side we're not touching
......@@ -2884,6 +2908,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
scrimColor: widget.drawerScrimColor,
edgeDragWidth: widget.drawerEdgeDragWidth,
enableOpenDragGesture: widget.drawerEnableOpenDragGesture,
isDrawerOpen: _drawerOpened.value,
),
_ScaffoldSlot.drawer,
// remove the side padding from the side we're not touching
......@@ -3145,7 +3170,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
break;
}
if (_endDrawerOpened) {
if (_endDrawerOpened.value) {
_buildDrawer(children, textDirection);
_buildEndDrawer(children, textDirection);
} else {
......
......@@ -215,4 +215,173 @@ void main() {
expect(state.isDrawerOpen, equals(false));
expect(state.isEndDrawerOpen, equals(false));
});
testWidgets('Scaffold.drawer - null restorationId ', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(
MaterialApp(
restorationScopeId: 'app',
home: Scaffold(
key: scaffoldKey,
drawer: const Text('drawer'),
body: Container(),
),
),
);
await tester.pump(); // no effect
expect(find.text('drawer'), findsNothing);
scaffoldKey.currentState!.openDrawer();
await tester.pumpAndSettle();
expect(find.text('drawer'), findsOneWidget);
await tester.restartAndRestore();
// Drawer state should not have been saved.
expect(find.text('drawer'), findsNothing);
});
testWidgets('Scaffold.endDrawer - null restorationId ', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(
MaterialApp(
restorationScopeId: 'app',
home: Scaffold(
key: scaffoldKey,
drawer: const Text('endDrawer'),
body: Container(),
),
),
);
await tester.pump(); // no effect
expect(find.text('endDrawer'), findsNothing);
scaffoldKey.currentState!.openDrawer();
await tester.pumpAndSettle();
expect(find.text('endDrawer'), findsOneWidget);
await tester.restartAndRestore();
// Drawer state should not have been saved.
expect(find.text('endDrawer'), findsNothing);
});
testWidgets('Scaffold.drawer state restoration test', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(
MaterialApp(
restorationScopeId: 'app',
home: Scaffold(
key: scaffoldKey,
restorationId: 'scaffold',
drawer: const Text('drawer'),
body: Container(),
),
),
);
await tester.pump(); // no effect
expect(find.text('drawer'), findsNothing);
scaffoldKey.currentState!.openDrawer();
await tester.pumpAndSettle();
expect(find.text('drawer'), findsOneWidget);
await tester.restartAndRestore();
expect(find.text('drawer'), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tester.tapAt(const Offset(750.0, 100.0)); // on the mask
await tester.pumpAndSettle();
expect(find.text('drawer'), findsNothing);
await tester.restoreFrom(data);
expect(find.text('drawer'), findsOneWidget);
});
testWidgets('Scaffold.endDrawer state restoration test', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(
MaterialApp(
restorationScopeId: 'app',
home: Scaffold(
key: scaffoldKey,
restorationId: 'scaffold',
endDrawer: const Text('endDrawer'),
body: Container(),
),
),
);
await tester.pump(); // no effect
expect(find.text('endDrawer'), findsNothing);
scaffoldKey.currentState!.openEndDrawer();
await tester.pumpAndSettle();
expect(find.text('endDrawer'), findsOneWidget);
await tester.restartAndRestore();
expect(find.text('endDrawer'), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tester.tapAt(const Offset(750.0, 100.0)); // on the mask
await tester.pumpAndSettle();
expect(find.text('endDrawer'), findsNothing);
await tester.restoreFrom(data);
expect(find.text('endDrawer'), findsOneWidget);
});
testWidgets('Both drawer and endDrawer state restoration test', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(
MaterialApp(
restorationScopeId: 'app',
home: Scaffold(
restorationId: 'scaffold',
key: scaffoldKey,
drawer: const Text('drawer'),
endDrawer: const Text('endDrawer'),
body: Container(),
),
),
);
await tester.pump(); // no effect
expect(find.text('drawer'), findsNothing);
expect(find.text('endDrawer'), findsNothing);
scaffoldKey.currentState!.openDrawer();
await tester.pumpAndSettle();
expect(find.text('drawer'), findsOneWidget);
expect(find.text('endDrawer'), findsNothing);
await tester.restartAndRestore();
expect(find.text('drawer'), findsOneWidget);
expect(find.text('endDrawer'), findsNothing);
TestRestorationData data = await tester.getRestorationData();
await tester.tapAt(const Offset(750.0, 100.0)); // on the mask
await tester.pumpAndSettle();
expect(find.text('drawer'), findsNothing);
expect(find.text('endDrawer'), findsNothing);
await tester.restoreFrom(data);
expect(find.text('drawer'), findsOneWidget);
expect(find.text('endDrawer'), findsNothing);
await tester.tapAt(const Offset(750.0, 100.0)); // on the mask
await tester.pumpAndSettle();
expect(find.text('drawer'), findsNothing);
expect(find.text('endDrawer'), findsNothing);
scaffoldKey.currentState!.openEndDrawer();
await tester.pumpAndSettle();
expect(find.text('drawer'), findsNothing);
expect(find.text('endDrawer'), findsOneWidget);
await tester.restartAndRestore();
expect(find.text('drawer'), findsNothing);
expect(find.text('endDrawer'), findsOneWidget);
data = await tester.getRestorationData();
await tester.tapAt(const Offset(750.0, 100.0)); // on the mask
await tester.pumpAndSettle();
expect(find.text('drawer'), findsNothing);
expect(find.text('endDrawer'), findsNothing);
await tester.restoreFrom(data);
expect(find.text('drawer'), findsNothing);
expect(find.text('endDrawer'), 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