Commit c96201a3 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Scaffold appBar is-a PreferredSizeWidget, etc (#9380)

* Scaffold appBar is-a PreferredSizeWidget, etc

* Updated

* Updated per review feedback
parent 00dfa224
......@@ -24,16 +24,6 @@ import 'tabs.dart';
import 'theme.dart';
import 'typography.dart';
/// An interface for widgets that can appear at the bottom of an [AppBar] or
/// [SliverAppBar].
///
/// This interface exposes the height of the widget, so that the [Scaffold] and
/// [SliverAppBar] widgets can correctly size an [AppBar].
abstract class AppBarBottomWidget extends Widget {
/// Defines the height of the app bar's optional bottom widget.
double get bottomHeight;
}
enum _ToolbarSlot {
leading,
title,
......@@ -156,7 +146,7 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
/// * [FlexibleSpaceBar], which is used with [flexibleSpace] when the app bar
/// can expand and collapse.
/// * <https://material.google.com/layout/structure.html#structure-toolbars>
class AppBar extends StatefulWidget {
class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a material design app bar.
///
/// Typically used in the [Scaffold.appBar] property.
......@@ -176,7 +166,7 @@ class AppBar extends StatefulWidget {
this.centerTitle,
this.toolbarOpacity: 1.0,
this.bottomOpacity: 1.0,
}) : _bottomHeight = bottom?.bottomHeight ?? 0.0,
}) : preferredSize = new Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
super(key: key) {
assert(elevation != null);
assert(primary != null);
......@@ -229,17 +219,20 @@ class AppBar extends StatefulWidget {
///
/// A flexible space isn't actually flexible unless the [AppBar]'s container
/// changes the [AppBar]'s size. A [SliverAppBar] in a [CustomScrollView]
/// changes the [AppBar]'s height when scrolled. A [Scaffold] always sets the
/// [AppBar] to the [minExtent].
/// changes the [AppBar]'s height when scrolled.
///
/// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details.
final Widget flexibleSpace;
/// This widget appears across the bottom of the app bar.
///
/// Typically a [TabBar]. Only widgets that implement [AppBarBottomWidget] can
/// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can
/// be used at the bottom of an app bar.
final AppBarBottomWidget bottom;
///
/// See also:
///
/// * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
final PreferredSizeWidget bottom;
/// The z-coordinate at which to place this app bar.
///
......@@ -274,8 +267,9 @@ class AppBar extends StatefulWidget {
/// Whether this app bar is being displayed at the top of the screen.
///
/// If this is true, the top padding specified by the [MediaQuery] will be
/// added to the top of the toolbar. See also [minExtent].
/// If true, the appbar's toolbar elements and [bottom] widget will be
/// padded on top by the height of the system status bar. The layout
/// of the [flexibleSpace] is not affected by the [primary] property.
final bool primary;
/// Whether the title should be centered.
......@@ -301,17 +295,12 @@ class AppBar extends StatefulWidget {
/// bar is scrolled.
final double bottomOpacity;
final double _bottomHeight;
/// The height of the toolbar and the [bottom] widget.
///
/// The parent widget should constrain the [AppBar] to a height between this
/// and whatever maximum size it wants the [AppBar] to have.
/// A size whose height is the sum of [kToolbarHeight] and the [bottom] widget's
/// preferred height.
///
/// If [primary] is true, the parent should increase this height by the height
/// of the top padding specified by the [MediaQuery] in scope for the
/// [AppBar].
double get minExtent => kToolbarHeight + _bottomHeight;
/// [Scaffold] uses this this size to set its app bar's height.
@override
final Size preferredSize;
bool _getEffectiveCenterTitle(ThemeData themeData) {
if (centerTitle != null)
......@@ -566,7 +555,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required this.floating,
@required this.pinned,
@required this.snapConfiguration,
}) : _bottomHeight = bottom?.bottomHeight ?? 0.0 {
}) : _bottomHeight = bottom?.preferredSize?.height ?? 0.0 {
assert(primary || topPadding == 0.0);
}
......@@ -574,7 +563,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final Widget title;
final List<Widget> actions;
final Widget flexibleSpace;
final AppBarBottomWidget bottom;
final PreferredSizeWidget bottom;
final int elevation;
final Color backgroundColor;
final Brightness brightness;
......@@ -769,9 +758,13 @@ class SliverAppBar extends StatefulWidget {
/// This widget appears across the bottom of the appbar.
///
/// Typically a [TabBar]. This widget must be a widget that implements the
/// [AppBarBottomWidget] interface.
final AppBarBottomWidget bottom;
/// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can
/// be used at the bottom of an app bar.
///
/// See also:
///
/// * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
final PreferredSizeWidget bottom;
/// The z-coordinate at which to place this app bar.
///
......@@ -901,7 +894,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
Widget build(BuildContext context) {
final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? widget.bottom.bottomHeight + topPadding : null;
? widget.bottom.preferredSize.height + topPadding : null;
return new SliverPersistentHeader(
floating: widget.floating,
......
......@@ -283,7 +283,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// * <https://material.google.com/layout/structure.html>
class Scaffold extends StatefulWidget {
/// Creates a visual scaffold for material design widgets.
Scaffold({
const Scaffold({
Key key,
this.appBar,
this.body,
......@@ -292,11 +292,12 @@ class Scaffold extends StatefulWidget {
this.drawer,
this.bottomNavigationBar,
this.backgroundColor,
this.resizeToAvoidBottomPadding: true
}) : super(key: key);
this.resizeToAvoidBottomPadding: true,
this.primary: true,
}) : assert(primary != null), super(key: key);
/// An app bar to display at the top of the scaffold.
final AppBar appBar;
final PreferredSizeWidget appBar;
/// The primary content of the scaffold.
///
......@@ -363,6 +364,15 @@ class Scaffold extends StatefulWidget {
/// Defaults to true.
final bool resizeToAvoidBottomPadding;
/// Whether this scaffold is being displayed at the top of the screen.
///
/// If true then the height of the [appBar] will be extended by the height
/// of the screen's status bar, i.e. the top padding for [MediaQuery].
///
/// The default value of this property, like the default value of
/// [AppBar.primary], is true.
final bool primary;
/// The state from the closest instance of this class that encloses the given context.
///
/// Typical usage is as follows:
......@@ -782,21 +792,17 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_addIfNonNull(children, widget.body, _ScaffoldSlot.body);
if (widget.appBar != null) {
assert(widget.appBar.primary || padding.top == 0.0, 'A non-primary AppBar was passed to a Scaffold but the MediaQuery in scope has top padding.');
final double topPadding = widget.appBar.primary ? padding.top : 0.0;
Widget appBar = widget.appBar;
final double extent = widget.appBar.minExtent + topPadding;
if (widget.appBar.flexibleSpace != null) {
appBar = FlexibleSpaceBar.createSettings(
currentExtent: extent,
child: appBar,
);
}
final double topPadding = widget.primary ? padding.top : 0.0;
final double extent = widget.appBar.preferredSize.height + topPadding;
assert(extent >= 0.0 && extent.isFinite);
_addIfNonNull(
children,
new ConstrainedBox(
constraints: new BoxConstraints(maxHeight: extent),
child: appBar,
child: FlexibleSpaceBar.createSettings(
currentExtent: extent,
child: widget.appBar,
),
),
_ScaffoldSlot.appBar,
);
......
......@@ -349,7 +349,7 @@ class _DragAnimation extends Animation<double> with AnimationWithParentMixin<dou
///
/// * [TabBarView], which displays the contents that the tab bar is selecting
/// between.
class TabBar extends StatefulWidget implements AppBarBottomWidget {
class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a material design tab bar.
///
/// The [tabs] argument must not be null and must have more than one widget.
......@@ -420,16 +420,19 @@ class TabBar extends StatefulWidget implements AppBarBottomWidget {
/// is null then the text style of the theme's body2 definition is used.
final TextStyle unselectedLabelStyle;
/// A size whose height depends on if the tabs have both icons and text.
///
/// [AppBar] uses this this size to compute its own preferred size.
@override
double get bottomHeight {
Size get preferredSize {
for (Widget item in tabs) {
if (item is Tab) {
final Tab tab = item;
if (tab.text != null && tab.icon != null)
return _kTextAndIconTabHeight + _kTabIndicatorHeight;
return const Size.fromHeight(_kTextAndIconTabHeight + _kTabIndicatorHeight);
}
}
return _kTabHeight + _kTabIndicatorHeight;
return const Size.fromHeight(_kTabHeight + _kTabIndicatorHeight);
}
@override
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
/// An interface for widgets that can return the size this widget would prefer
/// if it were otherwise unconstrained.
///
/// There are a few cases, notably [AppBar] and [TabBar], where it would be
/// undesirable for the widget to constrain its own size but where the widget
/// needs to expose a preferred or "default" size. For example a primary
/// [Scaffold] sets its app bar height to the app bar's preferred height
/// plus the height of the system status bar.
///
/// Use [PreferredSize] to give a preferred size to an arbitrary widget.
abstract class PreferredSizeWidget implements Widget {
/// The size this widget would prefer if it were otherwise unconstrained.
///
/// In many cases it's only necessary to define one preferred dimension.
/// For example the [Scaffold] only depends on its app bar's preferred
/// height. In that case implementations of this method can just return
/// `new Size.fromHeight(myAppBarHeight)`;
Size get preferredSize;
}
/// Give an arbitrary widget a preferred size.
///
/// This class does not impose any constraints on its child, it doesn't affect
/// the child's layout in any way. It just advertises a default size which
/// can be used by the parent.
///
/// See also:
///
/// * [AppBar.bottom] and [Scaffold.appBar], which require preferred size widgets.
/// * [PreferredSizeWidget], the interface which this widget implements to expose
/// its preferred size.
/// * [AppBar] and [TabBar], which implement PreferredSizeWidget.
class PreferredSize extends StatelessWidget implements PreferredSizeWidget {
const PreferredSize({
Key key,
@required this.child,
@required this.preferredSize,
}) : super(key: key);
final Widget child;
@override
final Size preferredSize;
@override
Widget build(BuildContext context) => child;
}
......@@ -44,6 +44,7 @@ export 'src/widgets/page_view.dart';
export 'src/widgets/pages.dart';
export 'src/widgets/performance_overlay.dart';
export 'src/widgets/placeholder.dart';
export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart';
......
......@@ -637,4 +637,79 @@ void main() {
expect(appBarTop(tester), lessThanOrEqualTo(0.0));
expect(appBarBottom(tester), kTextTabBarHeight);
});
testWidgets('AppBar dimensions, with and without bottom, primary', (WidgetTester tester) async {
const MediaQueryData topPadding100 = const MediaQueryData(padding: const EdgeInsets.only(top: 100.0));
await tester.pumpWidget(
new MediaQuery(
data: topPadding100,
child: new Scaffold(
primary: false,
appBar: new AppBar(),
),
),
);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), kToolbarHeight);
await tester.pumpWidget(
new MediaQuery(
data: topPadding100,
child: new Scaffold(
primary: true,
appBar: new AppBar(title: const Text('title'))
),
),
);
expect(appBarTop(tester), 0.0);
expect(tester.getTopLeft(find.text('title')).dy, greaterThan(100.0));
expect(appBarHeight(tester), kToolbarHeight + 100.0);
await tester.pumpWidget(
new MediaQuery(
data: topPadding100,
child: new Scaffold(
primary: false,
appBar: new AppBar(
bottom: new PreferredSize(
preferredSize: const Size.fromHeight(200.0),
child: new Container(),
),
),
),
),
);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), kToolbarHeight + 200.0);
await tester.pumpWidget(
new MediaQuery(
data: topPadding100,
child: new Scaffold(
primary: true,
appBar: new AppBar(
bottom: new PreferredSize(
preferredSize: const Size.fromHeight(200.0),
child: new Container(),
),
),
),
),
);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), kToolbarHeight + 100.0 + 200.0);
await tester.pumpWidget(
new MediaQuery(
data: topPadding100,
child: new AppBar(
primary: false,
title: const Text('title'),
),
),
);
expect(appBarTop(tester), 0.0);
expect(tester.getTopLeft(find.text('title')).dy, lessThan(100.0));
});
}
......@@ -28,7 +28,7 @@ void main() {
testWidgets('Floating Action Button tooltip', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
home: new Scaffold(
home: const Scaffold(
floatingActionButton: const FloatingActionButton(
onPressed: null,
tooltip: 'Add',
......
......@@ -191,10 +191,10 @@ void main() {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
home: new Scaffold(body: const Text('Page 1')),
home: const Scaffold(body: const Text('Page 1')),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return new Scaffold(body: const Text('Page 2'));
return const Scaffold(body: const Text('Page 2'));
},
},
)
......@@ -222,10 +222,10 @@ void main() {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS),
home: new Scaffold(body: const Text('Page 1')),
home: const Scaffold(body: const Text('Page 1')),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return new Scaffold(body: const Text('Page 2'));
return const Scaffold(body: const Text('Page 2'));
},
},
)
......@@ -264,13 +264,13 @@ void main() {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS),
home: new Scaffold(body: const Text('Page 1')),
home: const Scaffold(body: const Text('Page 1')),
)
);
tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new Scaffold(body: const Text('Page 2'));
return const Scaffold(body: const Text('Page 2'));
},
fullscreenDialog: true,
));
......
......@@ -82,7 +82,7 @@ void main() {
});
testWidgets('Floating action animation', (WidgetTester tester) async {
await tester.pumpWidget(new Scaffold(
await tester.pumpWidget(const Scaffold(
floatingActionButton: const FloatingActionButton(
key: const Key('one'),
onPressed: null,
......@@ -92,7 +92,7 @@ void main() {
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(new Scaffold(
await tester.pumpWidget(const Scaffold(
floatingActionButton: const FloatingActionButton(
key: const Key('two'),
onPressed: null,
......@@ -103,10 +103,10 @@ void main() {
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpWidget(new Container());
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(new Scaffold());
await tester.pumpWidget(const Scaffold());
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(new Scaffold(
await tester.pumpWidget(const Scaffold(
floatingActionButton: const FloatingActionButton(
key: const Key('one'),
onPressed: null,
......
......@@ -206,7 +206,7 @@ void main() {
onTap: () {
showDialog<Null>(
context: context,
child: new Scaffold(
child: const Scaffold(
body: const SizedBox(
width: 200.0,
height: 200.0,
......
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