Skip to content

Commit f802cf6

Browse files
authored
ScaffoldGeometry plumbing. (flutter#14580)
Adds a ScaffoldGeometry class and ValueNotifier for it. A scaffold's ScaffoldGeometry notifier is held in the _ScaffoldState, and is passed to _ScaffoldScope. New ScaffoldGemometry objects are built and published to the notifier.
1 parent 2aa9bb2 commit f802cf6

File tree

2 files changed

+368
-5
lines changed

2 files changed

+368
-5
lines changed

packages/flutter/lib/src/material/scaffold.dart

+190-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:collection';
77
import 'dart:math' as math;
88

99
import 'package:flutter/foundation.dart';
10+
import 'package:flutter/rendering.dart';
1011
import 'package:flutter/widgets.dart';
1112

1213
import 'app_bar.dart';
@@ -36,18 +37,111 @@ enum _ScaffoldSlot {
3637
statusBar,
3738
}
3839

40+
// Examples can assume:
41+
// ScaffoldGeometry scaffoldGeometry;
42+
43+
/// Geometry information for scaffold components.
44+
///
45+
/// To get a [ValueNotifier] for the scaffold geometry call
46+
/// [Scaffold.geometryOf].
47+
@immutable
48+
class ScaffoldGeometry {
49+
const ScaffoldGeometry({
50+
this.bottomNavigationBarTop,
51+
this.floatingActionButtonArea,
52+
this.floatingActionButtonScale: 1.0,
53+
});
54+
55+
/// The distance from the scaffold's top edge to the top edge of the
56+
/// rectangle in which the [Scaffold.bottomNavigationBar] bar is being laid
57+
/// out.
58+
///
59+
/// When there is no [Scaffold.bottomNavigationBar] set, this will be null.
60+
final double bottomNavigationBarTop;
61+
62+
/// The rectangle in which the scaffold is laying out
63+
/// [Scaffold.floatingActionButton].
64+
///
65+
/// The floating action button might be scaled inside this rectangle, to get
66+
/// the bounding rectangle in which the floating action is painted scale this
67+
/// value by [floatingActionButtonScale].
68+
///
69+
/// ## Sample code
70+
///
71+
/// ```dart
72+
/// final Rect scaledFab = Rect.lerp(
73+
/// scaffoldGeometry.floatingActionButtonArea.center & Size.zero,
74+
/// scaffoldGeometry.floatingActionButtonArea,
75+
/// scaffoldGeometry.floatingActionButtonScale
76+
/// );
77+
/// ```
78+
///
79+
/// This is null when there is no floating action button showing.
80+
final Rect floatingActionButtonArea;
81+
82+
/// The amount by which the [Scaffold.floatingActionButton] is scaled.
83+
///
84+
/// To get the bounding rectangle in which the floating action button is
85+
/// painted scaled [floatingActionPosition] by this proportion.
86+
///
87+
/// This will be 0 when there is no [Scaffold.floatingActionButton] set.
88+
final double floatingActionButtonScale;
89+
}
90+
91+
class _ScaffoldGeometryNotifier extends ValueNotifier<ScaffoldGeometry> {
92+
_ScaffoldGeometryNotifier(ScaffoldGeometry geometry, this.context)
93+
: assert (context != null),
94+
super(geometry);
95+
96+
final BuildContext context;
97+
98+
@override
99+
ScaffoldGeometry get value {
100+
assert(() {
101+
final RenderObject renderObject = context.findRenderObject();
102+
if (renderObject == null || !renderObject.owner.debugDoingPaint)
103+
throw new FlutterError(
104+
'Scaffold.geometryOf() must only be accessed during the paint phase.\n'
105+
'The ScaffoldGeometry is only available during the paint phase, because\n'
106+
'its value is computed during the animation and layout phases prior to painting.'
107+
);
108+
return true;
109+
}());
110+
return super.value;
111+
}
112+
113+
void _updateWith({
114+
double bottomNavigationBarTop,
115+
Rect floatingActionButtonArea,
116+
double floatingActionButtonScale,
117+
}) {
118+
final double newFloatingActionButtonScale = floatingActionButtonScale ?? super.value?.floatingActionButtonScale;
119+
Rect newFloatingActionButtonArea;
120+
if (newFloatingActionButtonScale != 0.0)
121+
newFloatingActionButtonArea = floatingActionButtonArea ?? super.value?.floatingActionButtonArea;
122+
123+
value = new ScaffoldGeometry(
124+
bottomNavigationBarTop: bottomNavigationBarTop ?? super.value?.bottomNavigationBarTop,
125+
floatingActionButtonArea: newFloatingActionButtonArea,
126+
floatingActionButtonScale: newFloatingActionButtonScale,
127+
);
128+
}
129+
}
130+
39131
class _ScaffoldLayout extends MultiChildLayoutDelegate {
40132
_ScaffoldLayout({
41133
@required this.statusBarHeight,
42134
@required this.bottomViewInset,
43135
@required this.endPadding, // for floating action button
44136
@required this.textDirection,
137+
@required this.geometryNotifier,
45138
});
46139

47140
final double statusBarHeight;
48141
final double bottomViewInset;
49142
final double endPadding;
50143
final TextDirection textDirection;
144+
final _ScaffoldGeometryNotifier geometryNotifier;
51145

52146
@override
53147
void performLayout(Size size) {
@@ -68,10 +162,12 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
68162
positionChild(_ScaffoldSlot.appBar, Offset.zero);
69163
}
70164

165+
double bottomNavigationBarTop;
71166
if (hasChild(_ScaffoldSlot.bottomNavigationBar)) {
72167
final double bottomNavigationBarHeight = layoutChild(_ScaffoldSlot.bottomNavigationBar, fullWidthConstraints).height;
73168
bottomWidgetsHeight += bottomNavigationBarHeight;
74-
positionChild(_ScaffoldSlot.bottomNavigationBar, new Offset(0.0, math.max(0.0, bottom - bottomWidgetsHeight)));
169+
bottomNavigationBarTop = math.max(0.0, bottom - bottomWidgetsHeight);
170+
positionChild(_ScaffoldSlot.bottomNavigationBar, new Offset(0.0, bottomNavigationBarTop));
75171
}
76172

77173
if (hasChild(_ScaffoldSlot.persistentFooter)) {
@@ -127,6 +223,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
127223
positionChild(_ScaffoldSlot.snackBar, new Offset(0.0, contentBottom - snackBarSize.height));
128224
}
129225

226+
Rect floatingActionButtonRect;
130227
if (hasChild(_ScaffoldSlot.floatingActionButton)) {
131228
final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints);
132229
double fabX;
@@ -145,6 +242,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
145242
if (bottomSheetSize.height > 0.0)
146243
fabY = math.min(fabY, contentBottom - bottomSheetSize.height - fabSize.height / 2.0);
147244
positionChild(_ScaffoldSlot.floatingActionButton, new Offset(fabX, fabY));
245+
floatingActionButtonRect = new Offset(fabX, fabY) & fabSize;
148246
}
149247

150248
if (hasChild(_ScaffoldSlot.statusBar)) {
@@ -161,6 +259,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
161259
layoutChild(_ScaffoldSlot.endDrawer, new BoxConstraints.tight(size));
162260
positionChild(_ScaffoldSlot.endDrawer, Offset.zero);
163261
}
262+
263+
geometryNotifier._updateWith(
264+
bottomNavigationBarTop: bottomNavigationBarTop,
265+
floatingActionButtonArea: floatingActionButtonRect,
266+
);
164267
}
165268

166269
@override
@@ -176,9 +279,11 @@ class _FloatingActionButtonTransition extends StatefulWidget {
176279
const _FloatingActionButtonTransition({
177280
Key key,
178281
this.child,
282+
this.geometryNotifier,
179283
}) : super(key: key);
180284

181285
final Widget child;
286+
final _ScaffoldGeometryNotifier geometryNotifier;
182287

183288
@override
184289
_FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState();
@@ -203,6 +308,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
203308
parent: _previousController,
204309
curve: Curves.easeIn
205310
);
311+
_previousAnimation.addListener(_onProgressChanged);
206312

207313
_currentController = new AnimationController(
208314
duration: _kFloatingActionButtonSegue,
@@ -212,11 +318,18 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
212318
parent: _currentController,
213319
curve: Curves.easeIn
214320
);
321+
_currentAnimation.addListener(_onProgressChanged);
215322

216-
// If we start out with a child, have the child appear fully visible instead
217-
// of animating in.
218-
if (widget.child != null)
323+
if (widget.child != null) {
324+
// If we start out with a child, have the child appear fully visible instead
325+
// of animating in.
219326
_currentController.value = 1.0;
327+
}
328+
else {
329+
// If we start without a child we update the geometry object with a
330+
// floating action button scale of 0, as it is not showing on the screen.
331+
_updateGeometryScale(0.0);
332+
}
220333
}
221334

222335
@override
@@ -284,6 +397,23 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
284397
}
285398
return new Stack(children: children);
286399
}
400+
401+
void _onProgressChanged() {
402+
if (_previousAnimation.status != AnimationStatus.dismissed) {
403+
_updateGeometryScale(_previousAnimation.value);
404+
return;
405+
}
406+
if (_currentAnimation.status != AnimationStatus.dismissed) {
407+
_updateGeometryScale(_currentAnimation.value);
408+
return;
409+
}
410+
}
411+
412+
void _updateGeometryScale(double scale) {
413+
widget.geometryNotifier._updateWith(
414+
floatingActionButtonScale: scale,
415+
);
416+
}
287417
}
288418

289419
/// Implements the basic material design visual layout structure.
@@ -514,6 +644,48 @@ class Scaffold extends StatefulWidget {
514644
);
515645
}
516646

647+
/// Returns a [ValueListenable] for the [ScaffoldGeometry] for the closest
648+
/// [Scaffold] ancestor of the given context.
649+
///
650+
/// The [ValueListenable.value] is only available at paint time.
651+
///
652+
/// Notifications are guaranteed to be sent before the first paint pass
653+
/// with the new geometry, but there is no guarantee whether a build or
654+
/// layout passes are going to happen between the notification and the next
655+
/// paint pass.
656+
///
657+
/// The closest [Scaffold] ancestor for the context might change, e.g when
658+
/// an element is moved from one scaffold to another. For [StatefulWidget]s
659+
/// using this listenable, a change of the [Scaffold] ancestor will
660+
/// trigger a [State.didChangeDependencies].
661+
///
662+
/// A typical pattern for listening to the scaffold geometry would be to
663+
/// call [Scaffold.geometryOf] in [State.didChangeDependencies], compare the
664+
/// return value with the previous listenable, if it has changed, unregister
665+
/// the listener, and register a listener to the new [ScaffoldGeometry]
666+
/// listenable.
667+
static ValueListenable<ScaffoldGeometry> geometryOf(BuildContext context) {
668+
final _ScaffoldScope scaffoldScope = context.inheritFromWidgetOfExactType(_ScaffoldScope);
669+
if (scaffoldScope == null)
670+
throw new FlutterError(
671+
'Scaffold.geometryOf() called with a context that does not contain a Scaffold.\n'
672+
'This usually happens when the context provided is from the same StatefulWidget as that '
673+
'whose build function actually creates the Scaffold widget being sought.\n'
674+
'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
675+
'context that is "under" the Scaffold. For an example of this, please see the '
676+
'documentation for Scaffold.of():\n'
677+
' https://docs.flutter.io/flutter/material/Scaffold/of.html\n'
678+
'A more efficient solution is to split your build function into several widgets. This '
679+
'introduces a new context from which you can obtain the Scaffold. In this solution, '
680+
'you would have an outer widget that creates the Scaffold populated by instances of '
681+
'your new inner widgets, and then in these inner widgets you would use Scaffold.geometryOf().\n'
682+
'The context used was:\n'
683+
' $context'
684+
);
685+
686+
return scaffoldScope.geometryNotifier;
687+
}
688+
517689
/// Whether the Scaffold that most tightly encloses the given context has a
518690
/// drawer.
519691
///
@@ -798,12 +970,21 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
798970

799971
// INTERNALS
800972

973+
_ScaffoldGeometryNotifier _geometryNotifier;
974+
975+
@override
976+
void initState() {
977+
super.initState();
978+
_geometryNotifier = new _ScaffoldGeometryNotifier(null, context);
979+
}
980+
801981
@override
802982
void dispose() {
803983
_snackBarController?.dispose();
804984
_snackBarController = null;
805985
_snackBarTimer?.cancel();
806986
_snackBarTimer = null;
987+
_geometryNotifier.dispose();
807988
for (_PersistentBottomSheet bottomSheet in _dismissedBottomSheets)
808989
bottomSheet.animationController.dispose();
809990
if (_currentBottomSheet != null)
@@ -970,6 +1151,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
9701151
children,
9711152
new _FloatingActionButtonTransition(
9721153
child: widget.floatingActionButton,
1154+
geometryNotifier: _geometryNotifier,
9731155
),
9741156
_ScaffoldSlot.floatingActionButton,
9751157
removeLeftPadding: true,
@@ -1044,6 +1226,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
10441226

10451227
return new _ScaffoldScope(
10461228
hasDrawer: hasDrawer,
1229+
geometryNotifier: _geometryNotifier,
10471230
child: new PrimaryScrollController(
10481231
controller: _primaryScrollController,
10491232
child: new Material(
@@ -1055,6 +1238,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
10551238
bottomViewInset: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0,
10561239
endPadding: endPadding,
10571240
textDirection: textDirection,
1241+
geometryNotifier: _geometryNotifier,
10581242
),
10591243
),
10601244
),
@@ -1161,11 +1345,13 @@ class PersistentBottomSheetController<T> extends ScaffoldFeatureController<_Pers
11611345
class _ScaffoldScope extends InheritedWidget {
11621346
const _ScaffoldScope({
11631347
@required this.hasDrawer,
1348+
@required this.geometryNotifier,
11641349
@required Widget child,
11651350
}) : assert(hasDrawer != null),
11661351
super(child: child);
11671352

11681353
final bool hasDrawer;
1354+
final _ScaffoldGeometryNotifier geometryNotifier;
11691355

11701356
@override
11711357
bool updateShouldNotify(_ScaffoldScope oldWidget) {

0 commit comments

Comments
 (0)