Unverified Commit 19f79ac8 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Add extendBody parameter to Scaffold, body MediaQuery reflects BAB height (#27973)

parent c8c67b79
...@@ -37,3 +37,4 @@ Sander Dalby Larsen <srdlarsen@gmail.com> ...@@ -37,3 +37,4 @@ Sander Dalby Larsen <srdlarsen@gmail.com>
Marco Scannadinari <m@scannadinari.co.uk> Marco Scannadinari <m@scannadinari.co.uk>
Frederik Schweiger <mail@flschweiger.net> Frederik Schweiger <mail@flschweiger.net>
Martin Staadecker <machstg@gmail.com> Martin Staadecker <machstg@gmail.com>
Igor Katsuba <katsuba.igor@gmail.com>
...@@ -275,6 +275,76 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl ...@@ -275,6 +275,76 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl
} }
} }
// Used to communicate the height of the Scaffold's bottomNavigationBar and
// persistentFooterButtons to the LayoutBuilder which builds the Scaffold's body.
//
// Scaffold expects a _BodyBoxConstraints to be passed to the _BodyBuilder
// widget's LayoutBuilder, see _ScaffoldLayout.performLayout(). The BoxConstraints
// methods that construct new BoxConstraints objects, like copyWith() have not
// been overridden here because we expect the _BodyBoxConstraintsObject to be
// passed along unmodified to the LayoutBuilder. If that changes in the future
// then _BodyBuilder will assert.
class _BodyBoxConstraints extends BoxConstraints {
const _BodyBoxConstraints({
double minWidth = 0.0,
double maxWidth = double.infinity,
double minHeight = 0.0,
double maxHeight = double.infinity,
@required this.bottomWidgetsHeight,
}) : assert(bottomWidgetsHeight != null),
assert(bottomWidgetsHeight >= 0),
super(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight);
final double bottomWidgetsHeight;
// RenderObject.layout() will only short-circuit its call to its performLayout
// method if the new layout constraints are not == to the current constraints.
// If the height of the bottom widgets has changed, even though the constraints'
// min and max values have not, we still want performLayout to happen.
@override
bool operator ==(dynamic other) {
if (super != other)
return false;
final _BodyBoxConstraints typedOther = other;
return bottomWidgetsHeight == typedOther.bottomWidgetsHeight;
}
@override
int get hashCode {
return hashValues(super.hashCode, bottomWidgetsHeight);
}
}
// Used when Scaffold.extendBody is true to wrap the scaffold's body in a MediaQuery
// whose padding accounts for the height of the bottomNavigationBar and/or the
// persistentFooterButtons.
//
// The bottom widgets' height is passed along via the _BodyBoxConstraints parameter.
// The constraints parameter is constructed in_ScaffoldLayout.performLayout().
class _BodyBuilder extends StatelessWidget {
const _BodyBuilder({ Key key, this.body }) : super(key: key);
final Widget body;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final _BodyBoxConstraints bodyConstraints = constraints;
final MediaQueryData metrics = MediaQuery.of(context);
return MediaQuery(
data: metrics.copyWith(
padding: metrics.padding.copyWith(
bottom: math.max(metrics.padding.bottom, bodyConstraints.bottomWidgetsHeight),
),
),
child: body,
);
},
);
}
}
class _ScaffoldLayout extends MultiChildLayoutDelegate { class _ScaffoldLayout extends MultiChildLayoutDelegate {
_ScaffoldLayout({ _ScaffoldLayout({
@required this.minInsets, @required this.minInsets,
...@@ -285,12 +355,15 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -285,12 +355,15 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
@required this.currentFloatingActionButtonLocation, @required this.currentFloatingActionButtonLocation,
@required this.floatingActionButtonMoveAnimationProgress, @required this.floatingActionButtonMoveAnimationProgress,
@required this.floatingActionButtonMotionAnimator, @required this.floatingActionButtonMotionAnimator,
@required this.extendBody,
}) : assert(minInsets != null), }) : assert(minInsets != null),
assert(textDirection != null), assert(textDirection != null),
assert(geometryNotifier != null), assert(geometryNotifier != null),
assert(previousFloatingActionButtonLocation != null), assert(previousFloatingActionButtonLocation != null),
assert(currentFloatingActionButtonLocation != null); assert(currentFloatingActionButtonLocation != null),
assert(extendBody != null);
final bool extendBody;
final EdgeInsets minInsets; final EdgeInsets minInsets;
final TextDirection textDirection; final TextDirection textDirection;
final _ScaffoldGeometryNotifier geometryNotifier; final _ScaffoldGeometryNotifier geometryNotifier;
...@@ -343,9 +416,17 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -343,9 +416,17 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
final double contentBottom = math.max(0.0, bottom - math.max(minInsets.bottom, bottomWidgetsHeight)); final double contentBottom = math.max(0.0, bottom - math.max(minInsets.bottom, bottomWidgetsHeight));
if (hasChild(_ScaffoldSlot.body)) { if (hasChild(_ScaffoldSlot.body)) {
final BoxConstraints bodyConstraints = BoxConstraints( double bodyMaxHeight = math.max(0.0, contentBottom - contentTop);
if (extendBody) {
bodyMaxHeight += bottomWidgetsHeight;
assert(bodyMaxHeight <= math.max(0.0, looseConstraints.maxHeight - contentTop));
}
final BoxConstraints bodyConstraints = _BodyBoxConstraints(
maxWidth: fullWidthConstraints.maxWidth, maxWidth: fullWidthConstraints.maxWidth,
maxHeight: math.max(0.0, contentBottom - contentTop), maxHeight: bodyMaxHeight,
bottomWidgetsHeight: extendBody ? bottomWidgetsHeight : 0.0,
); );
layoutChild(_ScaffoldSlot.body, bodyConstraints); layoutChild(_ScaffoldSlot.body, bodyConstraints);
positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop)); positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop));
...@@ -795,11 +876,29 @@ class Scaffold extends StatefulWidget { ...@@ -795,11 +876,29 @@ class Scaffold extends StatefulWidget {
this.resizeToAvoidBottomPadding, this.resizeToAvoidBottomPadding,
this.resizeToAvoidBottomInset, this.resizeToAvoidBottomInset,
this.primary = true, this.primary = true,
this.extendBody = false,
this.drawerDragStartBehavior = DragStartBehavior.down, this.drawerDragStartBehavior = DragStartBehavior.down,
}) : assert(primary != null), }) : assert(primary != null),
assert(extendBody != null),
assert(drawerDragStartBehavior != null), assert(drawerDragStartBehavior != null),
super(key: key); super(key: key);
/// If true, and [bottomNavigationBar] or [persistentFooterButtons]
/// is specified, then the [body] extends to the bottom of the Scaffold,
/// instead of only extending to the top of the [bottomNavigationBar]
/// or the [persistentFooterButtons].
///
/// If true, a [MediaQuery] widget whose bottom padding matches the
/// the height of the [bottomNavigationBar] will be added above the
/// scaffold's [body].
///
/// This property is often useful when the [bottomNavigationBar] has
/// a non-rectangular shape, like [CircularNotchedRectangle], which
/// adds a [FloatingActionButton] sized notch to the top edge of the bar.
/// In this case specifying `extendBody: true` ensures that that scaffold's
/// body will be visible through the bottom navigation bar's notch.
final bool extendBody;
/// An app bar to display at the top of the scaffold. /// An app bar to display at the top of the scaffold.
final PreferredSizeWidget appBar; final PreferredSizeWidget appBar;
...@@ -1697,7 +1796,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1697,7 +1796,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_addIfNonNull( _addIfNonNull(
children, children,
widget.body, widget.body != null && widget.extendBody ? _BodyBuilder(body: widget.body) : widget.body,
_ScaffoldSlot.body, _ScaffoldSlot.body,
removeLeftPadding: false, removeLeftPadding: false,
removeTopPadding: widget.appBar != null, removeTopPadding: widget.appBar != null,
...@@ -1850,6 +1949,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1850,6 +1949,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0, bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0,
); );
// extendBody locked when keyboard is open
final bool _extendBody = minInsets.bottom > 0 ? false : widget.extendBody;
return _ScaffoldScope( return _ScaffoldScope(
hasDrawer: hasDrawer, hasDrawer: hasDrawer,
geometryNotifier: _geometryNotifier, geometryNotifier: _geometryNotifier,
...@@ -1861,6 +1963,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1861,6 +1963,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
return CustomMultiChildLayout( return CustomMultiChildLayout(
children: children, children: children,
delegate: _ScaffoldLayout( delegate: _ScaffoldLayout(
extendBody: _extendBody,
minInsets: minInsets, minInsets: minInsets,
currentFloatingActionButtonLocation: _floatingActionButtonLocation, currentFloatingActionButtonLocation: _floatingActionButtonLocation,
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value, floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
......
...@@ -574,7 +574,7 @@ class BoxConstraints extends Constraints { ...@@ -574,7 +574,7 @@ class BoxConstraints extends Constraints {
assert(debugAssertIsValid()); assert(debugAssertIsValid());
if (identical(this, other)) if (identical(this, other))
return true; return true;
if (other is! BoxConstraints) if (runtimeType != other.runtimeType)
return false; return false;
final BoxConstraints typedOther = other; final BoxConstraints typedOther = other;
assert(typedOther.debugAssertIsValid()); assert(typedOther.debugAssertIsValid());
......
...@@ -574,6 +574,63 @@ void main() { ...@@ -574,6 +574,63 @@ void main() {
expect(tester.element(find.byKey(testKey)).size, const Size(88.0, 48.0)); expect(tester.element(find.byKey(testKey)).size, const Size(88.0, 48.0));
expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
}); });
testWidgets('body size with extendBody', (WidgetTester tester) async {
final Key bodyKey = UniqueKey();
double mediaQueryBottom;
Widget buildFrame({ bool extendBody, bool resizeToAvoidBottomInset, double viewInsetBottom = 0.0 }) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData(
viewInsets: EdgeInsets.only(bottom: viewInsetBottom),
),
child: Scaffold(
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
extendBody: extendBody,
body: Builder(
builder: (BuildContext context) {
mediaQueryBottom = MediaQuery.of(context).padding.bottom;
return Container(key: bodyKey);
},
),
bottomNavigationBar: const BottomAppBar(
child: SizedBox(height: 48.0,),
),
),
),
);
}
await tester.pumpWidget(buildFrame(extendBody: true));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(mediaQueryBottom, 48.0);
await tester.pumpWidget(buildFrame(extendBody: false));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 552.0)); // 552 = 600 - 48 (BAB height)
expect(mediaQueryBottom, 0.0);
// If resizeToAvoidBottomInsets is false, same results as if it was unspecified (null).
await tester.pumpWidget(buildFrame(extendBody: true, resizeToAvoidBottomInset: false, viewInsetBottom: 100.0));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(mediaQueryBottom, 48.0);
await tester.pumpWidget(buildFrame(extendBody: false, resizeToAvoidBottomInset: false, viewInsetBottom: 100.0));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 552.0));
expect(mediaQueryBottom, 0.0);
// If resizeToAvoidBottomInsets is true and viewInsets.bottom is > the bottom
// navigation bar's height then the body always resizes and the MediaQuery
// isn't adjusted. This case corresponds to the keyboard appearing.
await tester.pumpWidget(buildFrame(extendBody: true, resizeToAvoidBottomInset: true, viewInsetBottom: 100.0));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
expect(mediaQueryBottom, 0.0);
await tester.pumpWidget(buildFrame(extendBody: false, resizeToAvoidBottomInset: true, viewInsetBottom: 100.0));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
expect(mediaQueryBottom, 0.0);
});
}); });
testWidgets('Open drawer hides underlying semantics tree', (WidgetTester tester) async { testWidgets('Open drawer hides underlying semantics tree', (WidgetTester tester) async {
......
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