Added Scaffold.extendBodyBehindAppBar (#39156)

* Co-authored-by: Brett Morgan <brettmorgan@google.com>
This commit is contained in:
Hans Muller 2019-08-26 11:31:19 -07:00 committed by GitHub
parent 62463a22ff
commit 35c916d733
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 167 additions and 8 deletions

View file

@ -299,11 +299,15 @@ class _BodyBoxConstraints extends BoxConstraints {
double minHeight = 0.0,
double maxHeight = double.infinity,
@required this.bottomWidgetsHeight,
@required this.appBarHeight,
}) : assert(bottomWidgetsHeight != null),
assert(bottomWidgetsHeight >= 0),
assert(appBarHeight != null),
assert(appBarHeight >= 0),
super(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight);
final double bottomWidgetsHeight;
final double appBarHeight;
// RenderObject.layout() will only short-circuit its call to its performLayout
// method if the new layout constraints are not == to the current constraints.
@ -314,12 +318,13 @@ class _BodyBoxConstraints extends BoxConstraints {
if (super != other)
return false;
final _BodyBoxConstraints typedOther = other;
return bottomWidgetsHeight == typedOther.bottomWidgetsHeight;
return bottomWidgetsHeight == typedOther.bottomWidgetsHeight
&& appBarHeight == typedOther.appBarHeight;
}
@override
int get hashCode {
return hashValues(super.hashCode, bottomWidgetsHeight);
return hashValues(super.hashCode, bottomWidgetsHeight, appBarHeight);
}
}
@ -330,20 +335,43 @@ class _BodyBoxConstraints extends BoxConstraints {
// 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);
const _BodyBuilder({
Key key,
@required this.extendBody,
@required this.extendBodyBehindAppBar,
@required this.body
}) : assert(extendBody != null),
assert(extendBodyBehindAppBar != null),
assert(body != null),
super(key: key);
final Widget body;
final bool extendBody;
final bool extendBodyBehindAppBar;
@override
Widget build(BuildContext context) {
if (!extendBody && !extendBodyBehindAppBar)
return body;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final _BodyBoxConstraints bodyConstraints = constraints;
final MediaQueryData metrics = MediaQuery.of(context);
final double bottom = extendBody
? math.max(metrics.padding.bottom, bodyConstraints.bottomWidgetsHeight)
: metrics.padding.bottom;
final double top = extendBodyBehindAppBar
? math.max(metrics.padding.top, bodyConstraints.appBarHeight)
: metrics.padding.top;
return MediaQuery(
data: metrics.copyWith(
padding: metrics.padding.copyWith(
bottom: math.max(metrics.padding.bottom, bodyConstraints.bottomWidgetsHeight),
top: top,
bottom: bottom,
),
),
child: body,
@ -365,14 +393,17 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
@required this.floatingActionButtonMotionAnimator,
@required this.isSnackBarFloating,
@required this.extendBody,
@required this.extendBodyBehindAppBar,
}) : assert(minInsets != null),
assert(textDirection != null),
assert(geometryNotifier != null),
assert(previousFloatingActionButtonLocation != null),
assert(currentFloatingActionButtonLocation != null),
assert(extendBody != null);
assert(extendBody != null),
assert(extendBodyBehindAppBar != null);
final bool extendBody;
final bool extendBodyBehindAppBar;
final EdgeInsets minInsets;
final TextDirection textDirection;
final _ScaffoldGeometryNotifier geometryNotifier;
@ -397,9 +428,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
final double bottom = size.height;
double contentTop = 0.0;
double bottomWidgetsHeight = 0.0;
double appBarHeight = 0.0;
if (hasChild(_ScaffoldSlot.appBar)) {
contentTop = layoutChild(_ScaffoldSlot.appBar, fullWidthConstraints).height;
appBarHeight = layoutChild(_ScaffoldSlot.appBar, fullWidthConstraints).height;
contentTop = extendBodyBehindAppBar ? 0.0 : appBarHeight;
positionChild(_ScaffoldSlot.appBar, Offset.zero);
}
@ -439,6 +472,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
maxWidth: fullWidthConstraints.maxWidth,
maxHeight: bodyMaxHeight,
bottomWidgetsHeight: extendBody ? bottomWidgetsHeight : 0.0,
appBarHeight: appBarHeight,
);
layoutChild(_ScaffoldSlot.body, bodyConstraints);
positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop));
@ -546,7 +580,9 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|| oldDelegate.textDirection != textDirection
|| oldDelegate.floatingActionButtonMoveAnimationProgress != floatingActionButtonMoveAnimationProgress
|| oldDelegate.previousFloatingActionButtonLocation != previousFloatingActionButtonLocation
|| oldDelegate.currentFloatingActionButtonLocation != currentFloatingActionButtonLocation;
|| oldDelegate.currentFloatingActionButtonLocation != currentFloatingActionButtonLocation
|| oldDelegate.extendBody != extendBody
|| oldDelegate.extendBodyBehindAppBar != extendBodyBehindAppBar;
}
}
@ -966,10 +1002,12 @@ class Scaffold extends StatefulWidget {
this.primary = true,
this.drawerDragStartBehavior = DragStartBehavior.start,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.drawerScrimColor,
this.drawerEdgeDragWidth,
}) : assert(primary != null),
assert(extendBody != null),
assert(extendBodyBehindAppBar != null),
assert(drawerDragStartBehavior != null),
super(key: key);
@ -987,8 +1025,28 @@ class Scaffold extends StatefulWidget {
/// 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.
///
/// See also:
///
/// * [extendBodyBehindAppBar], which extends the height of the body
/// to the top of the scaffold.
final bool extendBody;
/// If true, and an [appBar] is specified, then the height of the [body] is
/// extended to include the height of the app bar and the top of the body
/// is aligned with the top of the app bar.
///
/// This is useful if the app bar's [AppBar.backgroundColor] is not
/// completely opaque.
///
/// This property is false by default. It must not be null.
///
/// See also:
///
/// * [extendBody], which extends the height of the body to the bottom
/// of the scaffold.
final bool extendBodyBehindAppBar;
/// An app bar to display at the top of the scaffold.
final PreferredSizeWidget appBar;
@ -2084,7 +2142,11 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
final List<LayoutId> children = <LayoutId>[];
_addIfNonNull(
children,
widget.body != null && widget.extendBody ? _BodyBuilder(body: widget.body) : widget.body,
widget.body == null ? null : _BodyBuilder(
extendBody: widget.extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
body: widget.body
),
_ScaffoldSlot.body,
removeLeftPadding: false,
removeTopPadding: widget.appBar != null,
@ -2270,6 +2332,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
children: children,
delegate: _ScaffoldLayout(
extendBody: _extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
minInsets: minInsets,
currentFloatingActionButtonLocation: _floatingActionButtonLocation,
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,

View file

@ -732,6 +732,102 @@ void main() {
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
expect(mediaQueryBottom, 0.0);
});
testWidgets('body size with extendBodyBehindAppBar', (WidgetTester tester) async {
final Key appBarKey = UniqueKey();
final Key bodyKey = UniqueKey();
const double appBarHeight = 100;
const double windowPaddingTop = 24;
bool fixedHeightAppBar;
double mediaQueryTop;
Widget buildFrame({ bool extendBodyBehindAppBar, bool hasAppBar }) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(top: windowPaddingTop),
),
child: Builder(
builder: (BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: extendBodyBehindAppBar,
appBar: !hasAppBar ? null : PreferredSize(
key: appBarKey,
preferredSize: const Size.fromHeight(appBarHeight),
child: Container(
constraints: BoxConstraints(
minHeight: appBarHeight,
maxHeight: fixedHeightAppBar ? appBarHeight : double.infinity,
),
),
),
body: Builder(
builder: (BuildContext context) {
mediaQueryTop = MediaQuery.of(context).padding.top;
return Container(key: bodyKey);
}
),
);
},
),
),
);
}
fixedHeightAppBar = false;
// When an appbar is provided, the Scaffold's body is built within a
// MediaQuery with padding.top = 0, and the appBar's maxHeight is
// constrained to its preferredSize.height + the original MediaQuery
// padding.top. When extendBodyBehindAppBar is true, an additional
// inner MediaQuery is added around the Scaffold's body with padding.top
// equal to the overall height of the appBar. See _BodyBuilder in
// material/scaffold.dart.
await tester.pumpWidget(buildFrame(extendBodyBehindAppBar: true, hasAppBar: true));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(tester.getSize(find.byKey(appBarKey)), const Size(800.0, appBarHeight + windowPaddingTop));
expect(mediaQueryTop, appBarHeight + windowPaddingTop);
await tester.pumpWidget(buildFrame(extendBodyBehindAppBar: true, hasAppBar: false));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(find.byKey(appBarKey), findsNothing);
expect(mediaQueryTop, windowPaddingTop);
await tester.pumpWidget(buildFrame(extendBodyBehindAppBar: false, hasAppBar: true));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0 - appBarHeight - windowPaddingTop));
expect(tester.getSize(find.byKey(appBarKey)), const Size(800.0, appBarHeight + windowPaddingTop));
expect(mediaQueryTop, 0.0);
await tester.pumpWidget(buildFrame(extendBodyBehindAppBar: false, hasAppBar: false));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(find.byKey(appBarKey), findsNothing);
expect(mediaQueryTop, windowPaddingTop);
fixedHeightAppBar = true;
await tester.pumpWidget(buildFrame(extendBodyBehindAppBar: true, hasAppBar: true));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(tester.getSize(find.byKey(appBarKey)), const Size(800.0, appBarHeight));
expect(mediaQueryTop, appBarHeight);
await tester.pumpWidget(buildFrame(extendBodyBehindAppBar: true, hasAppBar: false));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(find.byKey(appBarKey), findsNothing);
expect(mediaQueryTop, windowPaddingTop);
await tester.pumpWidget(buildFrame(extendBodyBehindAppBar: false, hasAppBar: true));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0 - appBarHeight));
expect(tester.getSize(find.byKey(appBarKey)), const Size(800.0, appBarHeight));
expect(mediaQueryTop, 0.0);
await tester.pumpWidget(buildFrame(extendBodyBehindAppBar: false, hasAppBar: false));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(find.byKey(appBarKey), findsNothing);
expect(mediaQueryTop, windowPaddingTop);
});
});
testWidgets('Open drawer hides underlying semantics tree', (WidgetTester tester) async {