ScaffoldGeometry plumbing. (#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.
This commit is contained in:
parent
2aa9bb2be7
commit
f802cf6d16
@ -7,6 +7,7 @@ import 'dart:collection';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'app_bar.dart';
|
import 'app_bar.dart';
|
||||||
@ -36,18 +37,111 @@ enum _ScaffoldSlot {
|
|||||||
statusBar,
|
statusBar,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Examples can assume:
|
||||||
|
// ScaffoldGeometry scaffoldGeometry;
|
||||||
|
|
||||||
|
/// Geometry information for scaffold components.
|
||||||
|
///
|
||||||
|
/// To get a [ValueNotifier] for the scaffold geometry call
|
||||||
|
/// [Scaffold.geometryOf].
|
||||||
|
@immutable
|
||||||
|
class ScaffoldGeometry {
|
||||||
|
const ScaffoldGeometry({
|
||||||
|
this.bottomNavigationBarTop,
|
||||||
|
this.floatingActionButtonArea,
|
||||||
|
this.floatingActionButtonScale: 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The distance from the scaffold's top edge to the top edge of the
|
||||||
|
/// rectangle in which the [Scaffold.bottomNavigationBar] bar is being laid
|
||||||
|
/// out.
|
||||||
|
///
|
||||||
|
/// When there is no [Scaffold.bottomNavigationBar] set, this will be null.
|
||||||
|
final double bottomNavigationBarTop;
|
||||||
|
|
||||||
|
/// The rectangle in which the scaffold is laying out
|
||||||
|
/// [Scaffold.floatingActionButton].
|
||||||
|
///
|
||||||
|
/// The floating action button might be scaled inside this rectangle, to get
|
||||||
|
/// the bounding rectangle in which the floating action is painted scale this
|
||||||
|
/// value by [floatingActionButtonScale].
|
||||||
|
///
|
||||||
|
/// ## Sample code
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// final Rect scaledFab = Rect.lerp(
|
||||||
|
/// scaffoldGeometry.floatingActionButtonArea.center & Size.zero,
|
||||||
|
/// scaffoldGeometry.floatingActionButtonArea,
|
||||||
|
/// scaffoldGeometry.floatingActionButtonScale
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This is null when there is no floating action button showing.
|
||||||
|
final Rect floatingActionButtonArea;
|
||||||
|
|
||||||
|
/// The amount by which the [Scaffold.floatingActionButton] is scaled.
|
||||||
|
///
|
||||||
|
/// To get the bounding rectangle in which the floating action button is
|
||||||
|
/// painted scaled [floatingActionPosition] by this proportion.
|
||||||
|
///
|
||||||
|
/// This will be 0 when there is no [Scaffold.floatingActionButton] set.
|
||||||
|
final double floatingActionButtonScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScaffoldGeometryNotifier extends ValueNotifier<ScaffoldGeometry> {
|
||||||
|
_ScaffoldGeometryNotifier(ScaffoldGeometry geometry, this.context)
|
||||||
|
: assert (context != null),
|
||||||
|
super(geometry);
|
||||||
|
|
||||||
|
final BuildContext context;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ScaffoldGeometry get value {
|
||||||
|
assert(() {
|
||||||
|
final RenderObject renderObject = context.findRenderObject();
|
||||||
|
if (renderObject == null || !renderObject.owner.debugDoingPaint)
|
||||||
|
throw new FlutterError(
|
||||||
|
'Scaffold.geometryOf() must only be accessed during the paint phase.\n'
|
||||||
|
'The ScaffoldGeometry is only available during the paint phase, because\n'
|
||||||
|
'its value is computed during the animation and layout phases prior to painting.'
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
return super.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateWith({
|
||||||
|
double bottomNavigationBarTop,
|
||||||
|
Rect floatingActionButtonArea,
|
||||||
|
double floatingActionButtonScale,
|
||||||
|
}) {
|
||||||
|
final double newFloatingActionButtonScale = floatingActionButtonScale ?? super.value?.floatingActionButtonScale;
|
||||||
|
Rect newFloatingActionButtonArea;
|
||||||
|
if (newFloatingActionButtonScale != 0.0)
|
||||||
|
newFloatingActionButtonArea = floatingActionButtonArea ?? super.value?.floatingActionButtonArea;
|
||||||
|
|
||||||
|
value = new ScaffoldGeometry(
|
||||||
|
bottomNavigationBarTop: bottomNavigationBarTop ?? super.value?.bottomNavigationBarTop,
|
||||||
|
floatingActionButtonArea: newFloatingActionButtonArea,
|
||||||
|
floatingActionButtonScale: newFloatingActionButtonScale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||||
_ScaffoldLayout({
|
_ScaffoldLayout({
|
||||||
@required this.statusBarHeight,
|
@required this.statusBarHeight,
|
||||||
@required this.bottomViewInset,
|
@required this.bottomViewInset,
|
||||||
@required this.endPadding, // for floating action button
|
@required this.endPadding, // for floating action button
|
||||||
@required this.textDirection,
|
@required this.textDirection,
|
||||||
|
@required this.geometryNotifier,
|
||||||
});
|
});
|
||||||
|
|
||||||
final double statusBarHeight;
|
final double statusBarHeight;
|
||||||
final double bottomViewInset;
|
final double bottomViewInset;
|
||||||
final double endPadding;
|
final double endPadding;
|
||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
|
final _ScaffoldGeometryNotifier geometryNotifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void performLayout(Size size) {
|
void performLayout(Size size) {
|
||||||
@ -68,10 +162,12 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||||||
positionChild(_ScaffoldSlot.appBar, Offset.zero);
|
positionChild(_ScaffoldSlot.appBar, Offset.zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double bottomNavigationBarTop;
|
||||||
if (hasChild(_ScaffoldSlot.bottomNavigationBar)) {
|
if (hasChild(_ScaffoldSlot.bottomNavigationBar)) {
|
||||||
final double bottomNavigationBarHeight = layoutChild(_ScaffoldSlot.bottomNavigationBar, fullWidthConstraints).height;
|
final double bottomNavigationBarHeight = layoutChild(_ScaffoldSlot.bottomNavigationBar, fullWidthConstraints).height;
|
||||||
bottomWidgetsHeight += bottomNavigationBarHeight;
|
bottomWidgetsHeight += bottomNavigationBarHeight;
|
||||||
positionChild(_ScaffoldSlot.bottomNavigationBar, new Offset(0.0, math.max(0.0, bottom - bottomWidgetsHeight)));
|
bottomNavigationBarTop = math.max(0.0, bottom - bottomWidgetsHeight);
|
||||||
|
positionChild(_ScaffoldSlot.bottomNavigationBar, new Offset(0.0, bottomNavigationBarTop));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasChild(_ScaffoldSlot.persistentFooter)) {
|
if (hasChild(_ScaffoldSlot.persistentFooter)) {
|
||||||
@ -127,6 +223,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||||||
positionChild(_ScaffoldSlot.snackBar, new Offset(0.0, contentBottom - snackBarSize.height));
|
positionChild(_ScaffoldSlot.snackBar, new Offset(0.0, contentBottom - snackBarSize.height));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rect floatingActionButtonRect;
|
||||||
if (hasChild(_ScaffoldSlot.floatingActionButton)) {
|
if (hasChild(_ScaffoldSlot.floatingActionButton)) {
|
||||||
final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints);
|
final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints);
|
||||||
double fabX;
|
double fabX;
|
||||||
@ -145,6 +242,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||||||
if (bottomSheetSize.height > 0.0)
|
if (bottomSheetSize.height > 0.0)
|
||||||
fabY = math.min(fabY, contentBottom - bottomSheetSize.height - fabSize.height / 2.0);
|
fabY = math.min(fabY, contentBottom - bottomSheetSize.height - fabSize.height / 2.0);
|
||||||
positionChild(_ScaffoldSlot.floatingActionButton, new Offset(fabX, fabY));
|
positionChild(_ScaffoldSlot.floatingActionButton, new Offset(fabX, fabY));
|
||||||
|
floatingActionButtonRect = new Offset(fabX, fabY) & fabSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasChild(_ScaffoldSlot.statusBar)) {
|
if (hasChild(_ScaffoldSlot.statusBar)) {
|
||||||
@ -161,6 +259,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||||||
layoutChild(_ScaffoldSlot.endDrawer, new BoxConstraints.tight(size));
|
layoutChild(_ScaffoldSlot.endDrawer, new BoxConstraints.tight(size));
|
||||||
positionChild(_ScaffoldSlot.endDrawer, Offset.zero);
|
positionChild(_ScaffoldSlot.endDrawer, Offset.zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
geometryNotifier._updateWith(
|
||||||
|
bottomNavigationBarTop: bottomNavigationBarTop,
|
||||||
|
floatingActionButtonArea: floatingActionButtonRect,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -176,9 +279,11 @@ class _FloatingActionButtonTransition extends StatefulWidget {
|
|||||||
const _FloatingActionButtonTransition({
|
const _FloatingActionButtonTransition({
|
||||||
Key key,
|
Key key,
|
||||||
this.child,
|
this.child,
|
||||||
|
this.geometryNotifier,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final _ScaffoldGeometryNotifier geometryNotifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState();
|
_FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState();
|
||||||
@ -203,6 +308,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
|
|||||||
parent: _previousController,
|
parent: _previousController,
|
||||||
curve: Curves.easeIn
|
curve: Curves.easeIn
|
||||||
);
|
);
|
||||||
|
_previousAnimation.addListener(_onProgressChanged);
|
||||||
|
|
||||||
_currentController = new AnimationController(
|
_currentController = new AnimationController(
|
||||||
duration: _kFloatingActionButtonSegue,
|
duration: _kFloatingActionButtonSegue,
|
||||||
@ -212,11 +318,18 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
|
|||||||
parent: _currentController,
|
parent: _currentController,
|
||||||
curve: Curves.easeIn
|
curve: Curves.easeIn
|
||||||
);
|
);
|
||||||
|
_currentAnimation.addListener(_onProgressChanged);
|
||||||
|
|
||||||
// If we start out with a child, have the child appear fully visible instead
|
if (widget.child != null) {
|
||||||
// of animating in.
|
// If we start out with a child, have the child appear fully visible instead
|
||||||
if (widget.child != null)
|
// of animating in.
|
||||||
_currentController.value = 1.0;
|
_currentController.value = 1.0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we start without a child we update the geometry object with a
|
||||||
|
// floating action button scale of 0, as it is not showing on the screen.
|
||||||
|
_updateGeometryScale(0.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -284,6 +397,23 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
|
|||||||
}
|
}
|
||||||
return new Stack(children: children);
|
return new Stack(children: children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onProgressChanged() {
|
||||||
|
if (_previousAnimation.status != AnimationStatus.dismissed) {
|
||||||
|
_updateGeometryScale(_previousAnimation.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_currentAnimation.status != AnimationStatus.dismissed) {
|
||||||
|
_updateGeometryScale(_currentAnimation.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateGeometryScale(double scale) {
|
||||||
|
widget.geometryNotifier._updateWith(
|
||||||
|
floatingActionButtonScale: scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implements the basic material design visual layout structure.
|
/// Implements the basic material design visual layout structure.
|
||||||
@ -514,6 +644,48 @@ class Scaffold extends StatefulWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a [ValueListenable] for the [ScaffoldGeometry] for the closest
|
||||||
|
/// [Scaffold] ancestor of the given context.
|
||||||
|
///
|
||||||
|
/// The [ValueListenable.value] is only available at paint time.
|
||||||
|
///
|
||||||
|
/// Notifications are guaranteed to be sent before the first paint pass
|
||||||
|
/// with the new geometry, but there is no guarantee whether a build or
|
||||||
|
/// layout passes are going to happen between the notification and the next
|
||||||
|
/// paint pass.
|
||||||
|
///
|
||||||
|
/// The closest [Scaffold] ancestor for the context might change, e.g when
|
||||||
|
/// an element is moved from one scaffold to another. For [StatefulWidget]s
|
||||||
|
/// using this listenable, a change of the [Scaffold] ancestor will
|
||||||
|
/// trigger a [State.didChangeDependencies].
|
||||||
|
///
|
||||||
|
/// A typical pattern for listening to the scaffold geometry would be to
|
||||||
|
/// call [Scaffold.geometryOf] in [State.didChangeDependencies], compare the
|
||||||
|
/// return value with the previous listenable, if it has changed, unregister
|
||||||
|
/// the listener, and register a listener to the new [ScaffoldGeometry]
|
||||||
|
/// listenable.
|
||||||
|
static ValueListenable<ScaffoldGeometry> geometryOf(BuildContext context) {
|
||||||
|
final _ScaffoldScope scaffoldScope = context.inheritFromWidgetOfExactType(_ScaffoldScope);
|
||||||
|
if (scaffoldScope == null)
|
||||||
|
throw new FlutterError(
|
||||||
|
'Scaffold.geometryOf() called with a context that does not contain a Scaffold.\n'
|
||||||
|
'This usually happens when the context provided is from the same StatefulWidget as that '
|
||||||
|
'whose build function actually creates the Scaffold widget being sought.\n'
|
||||||
|
'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
|
||||||
|
'context that is "under" the Scaffold. For an example of this, please see the '
|
||||||
|
'documentation for Scaffold.of():\n'
|
||||||
|
' https://docs.flutter.io/flutter/material/Scaffold/of.html\n'
|
||||||
|
'A more efficient solution is to split your build function into several widgets. This '
|
||||||
|
'introduces a new context from which you can obtain the Scaffold. In this solution, '
|
||||||
|
'you would have an outer widget that creates the Scaffold populated by instances of '
|
||||||
|
'your new inner widgets, and then in these inner widgets you would use Scaffold.geometryOf().\n'
|
||||||
|
'The context used was:\n'
|
||||||
|
' $context'
|
||||||
|
);
|
||||||
|
|
||||||
|
return scaffoldScope.geometryNotifier;
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the Scaffold that most tightly encloses the given context has a
|
/// Whether the Scaffold that most tightly encloses the given context has a
|
||||||
/// drawer.
|
/// drawer.
|
||||||
///
|
///
|
||||||
@ -798,12 +970,21 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||||||
|
|
||||||
// INTERNALS
|
// INTERNALS
|
||||||
|
|
||||||
|
_ScaffoldGeometryNotifier _geometryNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_geometryNotifier = new _ScaffoldGeometryNotifier(null, context);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_snackBarController?.dispose();
|
_snackBarController?.dispose();
|
||||||
_snackBarController = null;
|
_snackBarController = null;
|
||||||
_snackBarTimer?.cancel();
|
_snackBarTimer?.cancel();
|
||||||
_snackBarTimer = null;
|
_snackBarTimer = null;
|
||||||
|
_geometryNotifier.dispose();
|
||||||
for (_PersistentBottomSheet bottomSheet in _dismissedBottomSheets)
|
for (_PersistentBottomSheet bottomSheet in _dismissedBottomSheets)
|
||||||
bottomSheet.animationController.dispose();
|
bottomSheet.animationController.dispose();
|
||||||
if (_currentBottomSheet != null)
|
if (_currentBottomSheet != null)
|
||||||
@ -970,6 +1151,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||||||
children,
|
children,
|
||||||
new _FloatingActionButtonTransition(
|
new _FloatingActionButtonTransition(
|
||||||
child: widget.floatingActionButton,
|
child: widget.floatingActionButton,
|
||||||
|
geometryNotifier: _geometryNotifier,
|
||||||
),
|
),
|
||||||
_ScaffoldSlot.floatingActionButton,
|
_ScaffoldSlot.floatingActionButton,
|
||||||
removeLeftPadding: true,
|
removeLeftPadding: true,
|
||||||
@ -1044,6 +1226,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||||||
|
|
||||||
return new _ScaffoldScope(
|
return new _ScaffoldScope(
|
||||||
hasDrawer: hasDrawer,
|
hasDrawer: hasDrawer,
|
||||||
|
geometryNotifier: _geometryNotifier,
|
||||||
child: new PrimaryScrollController(
|
child: new PrimaryScrollController(
|
||||||
controller: _primaryScrollController,
|
controller: _primaryScrollController,
|
||||||
child: new Material(
|
child: new Material(
|
||||||
@ -1055,6 +1238,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||||||
bottomViewInset: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0,
|
bottomViewInset: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0,
|
||||||
endPadding: endPadding,
|
endPadding: endPadding,
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
|
geometryNotifier: _geometryNotifier,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1161,11 +1345,13 @@ class PersistentBottomSheetController<T> extends ScaffoldFeatureController<_Pers
|
|||||||
class _ScaffoldScope extends InheritedWidget {
|
class _ScaffoldScope extends InheritedWidget {
|
||||||
const _ScaffoldScope({
|
const _ScaffoldScope({
|
||||||
@required this.hasDrawer,
|
@required this.hasDrawer,
|
||||||
|
@required this.geometryNotifier,
|
||||||
@required Widget child,
|
@required Widget child,
|
||||||
}) : assert(hasDrawer != null),
|
}) : assert(hasDrawer != null),
|
||||||
super(child: child);
|
super(child: child);
|
||||||
|
|
||||||
final bool hasDrawer;
|
final bool hasDrawer;
|
||||||
|
final _ScaffoldGeometryNotifier geometryNotifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(_ScaffoldScope oldWidget) {
|
bool updateShouldNotify(_ScaffoldScope oldWidget) {
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../widgets/semantics_tester.dart';
|
import '../widgets/semantics_tester.dart';
|
||||||
|
|
||||||
@ -770,4 +771,180 @@ void main() {
|
|||||||
semantics.dispose();
|
semantics.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('ScaffoldGeometry', () {
|
||||||
|
testWidgets('bottomNavigationBar', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = new GlobalKey();
|
||||||
|
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
|
||||||
|
body: new Container(),
|
||||||
|
bottomNavigationBar: new ConstrainedBox(
|
||||||
|
key: key,
|
||||||
|
constraints: const BoxConstraints.expand(height: 80.0),
|
||||||
|
child: new GeometryListener(),
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
|
||||||
|
final RenderBox navigationBox = tester.renderObject(find.byKey(key));
|
||||||
|
final RenderBox appBox = tester.renderObject(find.byType(MaterialApp));
|
||||||
|
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
|
||||||
|
final ScaffoldGeometry geometry = listenerState.cache.value;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
geometry.bottomNavigationBarTop,
|
||||||
|
appBox.size.height - navigationBox.size.height
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('no bottomNavigationBar', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
|
||||||
|
body: new ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints.expand(height: 80.0),
|
||||||
|
child: new GeometryListener(),
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
|
||||||
|
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
|
||||||
|
final ScaffoldGeometry geometry = listenerState.cache.value;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
geometry.bottomNavigationBarTop,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('floatingActionButton', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = new GlobalKey();
|
||||||
|
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
|
||||||
|
body: new Container(),
|
||||||
|
floatingActionButton: new FloatingActionButton(
|
||||||
|
key: key,
|
||||||
|
child: new GeometryListener(),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
|
||||||
|
final RenderBox floatingActionButtonBox = tester.renderObject(find.byKey(key));
|
||||||
|
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
|
||||||
|
final ScaffoldGeometry geometry = listenerState.cache.value;
|
||||||
|
|
||||||
|
final Rect fabRect = floatingActionButtonBox.localToGlobal(Offset.zero) & floatingActionButtonBox.size;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
geometry.floatingActionButtonArea,
|
||||||
|
fabRect
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
geometry.floatingActionButtonScale,
|
||||||
|
1.0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('no floatingActionButton', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
|
||||||
|
body: new ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints.expand(height: 80.0),
|
||||||
|
child: new GeometryListener(),
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
|
||||||
|
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
|
||||||
|
final ScaffoldGeometry geometry = listenerState.cache.value;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
geometry.floatingActionButtonScale,
|
||||||
|
0.0
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
geometry.floatingActionButtonArea,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('floatingActionButton animation', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = new GlobalKey();
|
||||||
|
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
|
||||||
|
body: new ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints.expand(height: 80.0),
|
||||||
|
child: new GeometryListener(),
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
|
||||||
|
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
|
||||||
|
body: new Container(),
|
||||||
|
floatingActionButton: new FloatingActionButton(
|
||||||
|
key: key,
|
||||||
|
child: new GeometryListener(),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
|
||||||
|
final ScaffoldGeometry geometry = listenerState.cache.value;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
geometry.floatingActionButtonScale,
|
||||||
|
inExclusiveRange(0.0, 1.0),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeometryListener extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State createState() => new GeometryListenerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeometryListenerState extends State<GeometryListener> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new CustomPaint(
|
||||||
|
painter: cache
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int numNotifications = 0;
|
||||||
|
ValueListenable<ScaffoldGeometry> geometryListenable;
|
||||||
|
GeometryCachePainter cache;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
|
||||||
|
if (geometryListenable == newListenable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (geometryListenable != null)
|
||||||
|
geometryListenable.removeListener(onGeometryChanged);
|
||||||
|
|
||||||
|
geometryListenable = newListenable;
|
||||||
|
geometryListenable.addListener(onGeometryChanged);
|
||||||
|
cache = new GeometryCachePainter(geometryListenable);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onGeometryChanged() {
|
||||||
|
numNotifications += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Scaffold.geometryOf() value is only available at paint time.
|
||||||
|
// To fetch it for the tests we implement this CustomPainter that just
|
||||||
|
// caches the ScaffoldGeometry value in its paint method.
|
||||||
|
class GeometryCachePainter extends CustomPainter {
|
||||||
|
GeometryCachePainter(this.geometryListenable);
|
||||||
|
|
||||||
|
final ValueListenable<ScaffoldGeometry> geometryListenable;
|
||||||
|
|
||||||
|
ScaffoldGeometry value;
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
value = geometryListenable.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(GeometryCachePainter oldDelegate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user