@@ -7,6 +7,7 @@ import 'dart:collection';
7
7
import 'dart:math' as math;
8
8
9
9
import 'package:flutter/foundation.dart' ;
10
+ import 'package:flutter/rendering.dart' ;
10
11
import 'package:flutter/widgets.dart' ;
11
12
12
13
import 'app_bar.dart' ;
@@ -36,18 +37,111 @@ enum _ScaffoldSlot {
36
37
statusBar,
37
38
}
38
39
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
+
39
131
class _ScaffoldLayout extends MultiChildLayoutDelegate {
40
132
_ScaffoldLayout ({
41
133
@required this .statusBarHeight,
42
134
@required this .bottomViewInset,
43
135
@required this .endPadding, // for floating action button
44
136
@required this .textDirection,
137
+ @required this .geometryNotifier,
45
138
});
46
139
47
140
final double statusBarHeight;
48
141
final double bottomViewInset;
49
142
final double endPadding;
50
143
final TextDirection textDirection;
144
+ final _ScaffoldGeometryNotifier geometryNotifier;
51
145
52
146
@override
53
147
void performLayout (Size size) {
@@ -68,10 +162,12 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
68
162
positionChild (_ScaffoldSlot .appBar, Offset .zero);
69
163
}
70
164
165
+ double bottomNavigationBarTop;
71
166
if (hasChild (_ScaffoldSlot .bottomNavigationBar)) {
72
167
final double bottomNavigationBarHeight = layoutChild (_ScaffoldSlot .bottomNavigationBar, fullWidthConstraints).height;
73
168
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));
75
171
}
76
172
77
173
if (hasChild (_ScaffoldSlot .persistentFooter)) {
@@ -127,6 +223,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
127
223
positionChild (_ScaffoldSlot .snackBar, new Offset (0.0 , contentBottom - snackBarSize.height));
128
224
}
129
225
226
+ Rect floatingActionButtonRect;
130
227
if (hasChild (_ScaffoldSlot .floatingActionButton)) {
131
228
final Size fabSize = layoutChild (_ScaffoldSlot .floatingActionButton, looseConstraints);
132
229
double fabX;
@@ -145,6 +242,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
145
242
if (bottomSheetSize.height > 0.0 )
146
243
fabY = math.min (fabY, contentBottom - bottomSheetSize.height - fabSize.height / 2.0 );
147
244
positionChild (_ScaffoldSlot .floatingActionButton, new Offset (fabX, fabY));
245
+ floatingActionButtonRect = new Offset (fabX, fabY) & fabSize;
148
246
}
149
247
150
248
if (hasChild (_ScaffoldSlot .statusBar)) {
@@ -161,6 +259,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
161
259
layoutChild (_ScaffoldSlot .endDrawer, new BoxConstraints .tight (size));
162
260
positionChild (_ScaffoldSlot .endDrawer, Offset .zero);
163
261
}
262
+
263
+ geometryNotifier._updateWith (
264
+ bottomNavigationBarTop: bottomNavigationBarTop,
265
+ floatingActionButtonArea: floatingActionButtonRect,
266
+ );
164
267
}
165
268
166
269
@override
@@ -176,9 +279,11 @@ class _FloatingActionButtonTransition extends StatefulWidget {
176
279
const _FloatingActionButtonTransition ({
177
280
Key key,
178
281
this .child,
282
+ this .geometryNotifier,
179
283
}) : super (key: key);
180
284
181
285
final Widget child;
286
+ final _ScaffoldGeometryNotifier geometryNotifier;
182
287
183
288
@override
184
289
_FloatingActionButtonTransitionState createState () => new _FloatingActionButtonTransitionState ();
@@ -203,6 +308,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
203
308
parent: _previousController,
204
309
curve: Curves .easeIn
205
310
);
311
+ _previousAnimation.addListener (_onProgressChanged);
206
312
207
313
_currentController = new AnimationController (
208
314
duration: _kFloatingActionButtonSegue,
@@ -212,11 +318,18 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
212
318
parent: _currentController,
213
319
curve: Curves .easeIn
214
320
);
321
+ _currentAnimation.addListener (_onProgressChanged);
215
322
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.
219
326
_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
+ }
220
333
}
221
334
222
335
@override
@@ -284,6 +397,23 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
284
397
}
285
398
return new Stack (children: children);
286
399
}
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
+ }
287
417
}
288
418
289
419
/// Implements the basic material design visual layout structure.
@@ -514,6 +644,48 @@ class Scaffold extends StatefulWidget {
514
644
);
515
645
}
516
646
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
+
517
689
/// Whether the Scaffold that most tightly encloses the given context has a
518
690
/// drawer.
519
691
///
@@ -798,12 +970,21 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
798
970
799
971
// INTERNALS
800
972
973
+ _ScaffoldGeometryNotifier _geometryNotifier;
974
+
975
+ @override
976
+ void initState () {
977
+ super .initState ();
978
+ _geometryNotifier = new _ScaffoldGeometryNotifier (null , context);
979
+ }
980
+
801
981
@override
802
982
void dispose () {
803
983
_snackBarController? .dispose ();
804
984
_snackBarController = null ;
805
985
_snackBarTimer? .cancel ();
806
986
_snackBarTimer = null ;
987
+ _geometryNotifier.dispose ();
807
988
for (_PersistentBottomSheet bottomSheet in _dismissedBottomSheets)
808
989
bottomSheet.animationController.dispose ();
809
990
if (_currentBottomSheet != null )
@@ -970,6 +1151,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
970
1151
children,
971
1152
new _FloatingActionButtonTransition (
972
1153
child: widget.floatingActionButton,
1154
+ geometryNotifier: _geometryNotifier,
973
1155
),
974
1156
_ScaffoldSlot .floatingActionButton,
975
1157
removeLeftPadding: true ,
@@ -1044,6 +1226,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
1044
1226
1045
1227
return new _ScaffoldScope (
1046
1228
hasDrawer: hasDrawer,
1229
+ geometryNotifier: _geometryNotifier,
1047
1230
child: new PrimaryScrollController (
1048
1231
controller: _primaryScrollController,
1049
1232
child: new Material (
@@ -1055,6 +1238,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
1055
1238
bottomViewInset: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0 ,
1056
1239
endPadding: endPadding,
1057
1240
textDirection: textDirection,
1241
+ geometryNotifier: _geometryNotifier,
1058
1242
),
1059
1243
),
1060
1244
),
@@ -1161,11 +1345,13 @@ class PersistentBottomSheetController<T> extends ScaffoldFeatureController<_Pers
1161
1345
class _ScaffoldScope extends InheritedWidget {
1162
1346
const _ScaffoldScope ({
1163
1347
@required this .hasDrawer,
1348
+ @required this .geometryNotifier,
1164
1349
@required Widget child,
1165
1350
}) : assert (hasDrawer != null ),
1166
1351
super (child: child);
1167
1352
1168
1353
final bool hasDrawer;
1354
+ final _ScaffoldGeometryNotifier geometryNotifier;
1169
1355
1170
1356
@override
1171
1357
bool updateShouldNotify (_ScaffoldScope oldWidget) {
0 commit comments