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 { ...@@ -231,6 +231,7 @@ class DrawerController extends StatefulWidget {
GlobalKey? key, GlobalKey? key,
required this.child, required this.child,
required this.alignment, required this.alignment,
this.isDrawerOpen = false,
this.drawerCallback, this.drawerCallback,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.scrimColor, this.scrimColor,
...@@ -298,6 +299,14 @@ class DrawerController extends StatefulWidget { ...@@ -298,6 +299,14 @@ class DrawerController extends StatefulWidget {
/// 20.0 will be added to `MediaQuery.of(context).padding.left`. /// 20.0 will be added to `MediaQuery.of(context).padding.left`.
final double? edgeDragWidth; 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 @override
DrawerControllerState createState() => DrawerControllerState(); DrawerControllerState createState() => DrawerControllerState();
} }
...@@ -310,7 +319,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -310,7 +319,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
void initState() { void initState() {
super.initState(); super.initState();
_scrimColorTween = _buildScrimColorTween(); _scrimColorTween = _buildScrimColorTween();
_controller = AnimationController(duration: _kBaseSettleDuration, vsync: this) _controller = AnimationController(
value: widget.isDrawerOpen ? 1.0 : 0.0,
duration: _kBaseSettleDuration,
vsync: this,
);
_controller
..addListener(_animationChanged) ..addListener(_animationChanged)
..addStatusListener(_animationStatusChanged); ..addStatusListener(_animationStatusChanged);
} }
...@@ -327,6 +341,16 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -327,6 +341,16 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.scrimColor != oldWidget.scrimColor) if (widget.scrimColor != oldWidget.scrimColor)
_scrimColorTween = _buildScrimColorTween(); _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() { void _animationChanged() {
......
...@@ -1459,6 +1459,7 @@ class Scaffold extends StatefulWidget { ...@@ -1459,6 +1459,7 @@ class Scaffold extends StatefulWidget {
this.drawerEdgeDragWidth, this.drawerEdgeDragWidth,
this.drawerEnableOpenDragGesture = true, this.drawerEnableOpenDragGesture = true,
this.endDrawerEnableOpenDragGesture = true, this.endDrawerEnableOpenDragGesture = true,
this.restorationId,
}) : assert(primary != null), }) : assert(primary != null),
assert(extendBody != null), assert(extendBody != null),
assert(extendBodyBehindAppBar != null), assert(extendBodyBehindAppBar != null),
...@@ -1783,6 +1784,20 @@ class Scaffold extends StatefulWidget { ...@@ -1783,6 +1784,20 @@ class Scaffold extends StatefulWidget {
/// By default, the drag gesture is enabled. /// By default, the drag gesture is enabled.
final bool endDrawerEnableOpenDragGesture; 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 /// Finds the [ScaffoldState] from the closest instance of this class that
/// encloses the given context. /// encloses the given context.
/// ///
...@@ -2055,7 +2070,15 @@ class Scaffold extends StatefulWidget { ...@@ -2055,7 +2070,15 @@ class Scaffold extends StatefulWidget {
/// ///
/// Can display [BottomSheet]s. Retrieve a [ScaffoldState] from the current /// Can display [BottomSheet]s. Retrieve a [ScaffoldState] from the current
/// [BuildContext] using [Scaffold.of]. /// [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 // DRAWER API
...@@ -2076,8 +2099,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2076,8 +2099,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// ///
/// This is based on the appBar preferred height plus the top padding. /// This is based on the appBar preferred height plus the top padding.
double? get appBarMaxHeight => _appBarMaxHeight; double? get appBarMaxHeight => _appBarMaxHeight;
bool _drawerOpened = false; final RestorableBool _drawerOpened = RestorableBool(false);
bool _endDrawerOpened = false; final RestorableBool _endDrawerOpened = RestorableBool(false);
/// Whether the [Scaffold.drawer] is opened. /// Whether the [Scaffold.drawer] is opened.
/// ///
...@@ -2085,7 +2108,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2085,7 +2108,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// ///
/// * [ScaffoldState.openDrawer], which opens the [Scaffold.drawer] of a /// * [ScaffoldState.openDrawer], which opens the [Scaffold.drawer] of a
/// [Scaffold]. /// [Scaffold].
bool get isDrawerOpen => _drawerOpened; bool get isDrawerOpen => _drawerOpened.value;
/// Whether the [Scaffold.endDrawer] is opened. /// Whether the [Scaffold.endDrawer] is opened.
/// ///
...@@ -2093,18 +2116,18 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2093,18 +2116,18 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// ///
/// * [ScaffoldState.openEndDrawer], which opens the [Scaffold.endDrawer] of /// * [ScaffoldState.openEndDrawer], which opens the [Scaffold.endDrawer] of
/// a [Scaffold]. /// a [Scaffold].
bool get isEndDrawerOpen => _endDrawerOpened; bool get isEndDrawerOpen => _endDrawerOpened.value;
void _drawerOpenedCallback(bool isOpened) { void _drawerOpenedCallback(bool isOpened) {
setState(() { setState(() {
_drawerOpened = isOpened; _drawerOpened.value = isOpened;
}); });
widget.onDrawerChanged?.call(isOpened); widget.onDrawerChanged?.call(isOpened);
} }
void _endDrawerOpenedCallback(bool isOpened) { void _endDrawerOpenedCallback(bool isOpened) {
setState(() { setState(() {
_endDrawerOpened = isOpened; _endDrawerOpened.value = isOpened;
}); });
widget.onEndDrawerChanged?.call(isOpened); widget.onEndDrawerChanged?.call(isOpened);
} }
...@@ -2122,7 +2145,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2122,7 +2145,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// ///
/// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. /// See [Scaffold.of] for information about how to obtain the [ScaffoldState].
void openDrawer() { void openDrawer() {
if (_endDrawerKey.currentState != null && _endDrawerOpened) if (_endDrawerKey.currentState != null && _endDrawerOpened.value)
_endDrawerKey.currentState!.close(); _endDrawerKey.currentState!.close();
_drawerKey.currentState?.open(); _drawerKey.currentState?.open();
} }
...@@ -2140,7 +2163,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2140,7 +2163,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// ///
/// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. /// See [Scaffold.of] for information about how to obtain the [ScaffoldState].
void openEndDrawer() { void openEndDrawer() {
if (_drawerKey.currentState != null && _drawerOpened) if (_drawerKey.currentState != null && _drawerOpened.value)
_drawerKey.currentState!.close(); _drawerKey.currentState!.close();
_endDrawerKey.currentState?.open(); _endDrawerKey.currentState?.open();
} }
...@@ -2859,6 +2882,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2859,6 +2882,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
scrimColor: widget.drawerScrimColor, scrimColor: widget.drawerScrimColor,
edgeDragWidth: widget.drawerEdgeDragWidth, edgeDragWidth: widget.drawerEdgeDragWidth,
enableOpenDragGesture: widget.endDrawerEnableOpenDragGesture, enableOpenDragGesture: widget.endDrawerEnableOpenDragGesture,
isDrawerOpen: _endDrawerOpened.value,
), ),
_ScaffoldSlot.endDrawer, _ScaffoldSlot.endDrawer,
// remove the side padding from the side we're not touching // remove the side padding from the side we're not touching
...@@ -2884,6 +2908,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -2884,6 +2908,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
scrimColor: widget.drawerScrimColor, scrimColor: widget.drawerScrimColor,
edgeDragWidth: widget.drawerEdgeDragWidth, edgeDragWidth: widget.drawerEdgeDragWidth,
enableOpenDragGesture: widget.drawerEnableOpenDragGesture, enableOpenDragGesture: widget.drawerEnableOpenDragGesture,
isDrawerOpen: _drawerOpened.value,
), ),
_ScaffoldSlot.drawer, _ScaffoldSlot.drawer,
// remove the side padding from the side we're not touching // remove the side padding from the side we're not touching
...@@ -3145,7 +3170,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -3145,7 +3170,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
break; break;
} }
if (_endDrawerOpened) { if (_endDrawerOpened.value) {
_buildDrawer(children, textDirection); _buildDrawer(children, textDirection);
_buildEndDrawer(children, textDirection); _buildEndDrawer(children, textDirection);
} else { } else {
......
...@@ -215,4 +215,173 @@ void main() { ...@@ -215,4 +215,173 @@ void main() {
expect(state.isDrawerOpen, equals(false)); expect(state.isDrawerOpen, equals(false));
expect(state.isEndDrawerOpen, 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