mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add extendBody parameter to Scaffold, body MediaQuery reflects BAB height (#27973)
This commit is contained in:
parent
c8c67b79c3
commit
19f79ac8da
1
AUTHORS
1
AUTHORS
@ -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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
_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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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() {
|
|||||||
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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user