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'; ...@@ -24,16 +24,6 @@ import 'tabs.dart';
import 'theme.dart'; import 'theme.dart';
import 'typography.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 { enum _ToolbarSlot {
leading, leading,
title, title,
...@@ -156,7 +146,7 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate { ...@@ -156,7 +146,7 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
/// * [FlexibleSpaceBar], which is used with [flexibleSpace] when the app bar /// * [FlexibleSpaceBar], which is used with [flexibleSpace] when the app bar
/// can expand and collapse. /// can expand and collapse.
/// * <https://material.google.com/layout/structure.html#structure-toolbars> /// * <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. /// Creates a material design app bar.
/// ///
/// Typically used in the [Scaffold.appBar] property. /// Typically used in the [Scaffold.appBar] property.
...@@ -176,7 +166,7 @@ class AppBar extends StatefulWidget { ...@@ -176,7 +166,7 @@ class AppBar extends StatefulWidget {
this.centerTitle, this.centerTitle,
this.toolbarOpacity: 1.0, this.toolbarOpacity: 1.0,
this.bottomOpacity: 1.0, this.bottomOpacity: 1.0,
}) : _bottomHeight = bottom?.bottomHeight ?? 0.0, }) : preferredSize = new Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
super(key: key) { super(key: key) {
assert(elevation != null); assert(elevation != null);
assert(primary != null); assert(primary != null);
...@@ -229,17 +219,20 @@ class AppBar extends StatefulWidget { ...@@ -229,17 +219,20 @@ class AppBar extends StatefulWidget {
/// ///
/// A flexible space isn't actually flexible unless the [AppBar]'s container /// 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 size. A [SliverAppBar] in a [CustomScrollView]
/// changes the [AppBar]'s height when scrolled. A [Scaffold] always sets the /// changes the [AppBar]'s height when scrolled.
/// [AppBar] to the [minExtent].
/// ///
/// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details. /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details.
final Widget flexibleSpace; final Widget flexibleSpace;
/// This widget appears across the bottom of the app bar. /// 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. /// 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. /// The z-coordinate at which to place this app bar.
/// ///
...@@ -274,8 +267,9 @@ class AppBar extends StatefulWidget { ...@@ -274,8 +267,9 @@ class AppBar extends StatefulWidget {
/// Whether this app bar is being displayed at the top of the screen. /// 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 /// If true, the appbar's toolbar elements and [bottom] widget will be
/// added to the top of the toolbar. See also [minExtent]. /// 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; final bool primary;
/// Whether the title should be centered. /// Whether the title should be centered.
...@@ -301,17 +295,12 @@ class AppBar extends StatefulWidget { ...@@ -301,17 +295,12 @@ class AppBar extends StatefulWidget {
/// bar is scrolled. /// bar is scrolled.
final double bottomOpacity; final double bottomOpacity;
final double _bottomHeight; /// A size whose height is the sum of [kToolbarHeight] and the [bottom] widget's
/// preferred height.
/// 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.
/// ///
/// If [primary] is true, the parent should increase this height by the height /// [Scaffold] uses this this size to set its app bar's height.
/// of the top padding specified by the [MediaQuery] in scope for the @override
/// [AppBar]. final Size preferredSize;
double get minExtent => kToolbarHeight + _bottomHeight;
bool _getEffectiveCenterTitle(ThemeData themeData) { bool _getEffectiveCenterTitle(ThemeData themeData) {
if (centerTitle != null) if (centerTitle != null)
...@@ -566,7 +555,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -566,7 +555,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required this.floating, @required this.floating,
@required this.pinned, @required this.pinned,
@required this.snapConfiguration, @required this.snapConfiguration,
}) : _bottomHeight = bottom?.bottomHeight ?? 0.0 { }) : _bottomHeight = bottom?.preferredSize?.height ?? 0.0 {
assert(primary || topPadding == 0.0); assert(primary || topPadding == 0.0);
} }
...@@ -574,7 +563,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -574,7 +563,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final Widget title; final Widget title;
final List<Widget> actions; final List<Widget> actions;
final Widget flexibleSpace; final Widget flexibleSpace;
final AppBarBottomWidget bottom; final PreferredSizeWidget bottom;
final int elevation; final int elevation;
final Color backgroundColor; final Color backgroundColor;
final Brightness brightness; final Brightness brightness;
...@@ -769,9 +758,13 @@ class SliverAppBar extends StatefulWidget { ...@@ -769,9 +758,13 @@ class SliverAppBar extends StatefulWidget {
/// This widget appears across the bottom of the appbar. /// This widget appears across the bottom of the appbar.
/// ///
/// Typically a [TabBar]. This widget must be a widget that implements the /// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can
/// [AppBarBottomWidget] interface. /// 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. /// The z-coordinate at which to place this app bar.
/// ///
...@@ -901,7 +894,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -901,7 +894,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0; final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null) final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? widget.bottom.bottomHeight + topPadding : null; ? widget.bottom.preferredSize.height + topPadding : null;
return new SliverPersistentHeader( return new SliverPersistentHeader(
floating: widget.floating, floating: widget.floating,
......
...@@ -283,7 +283,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -283,7 +283,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// * <https://material.google.com/layout/structure.html> /// * <https://material.google.com/layout/structure.html>
class Scaffold extends StatefulWidget { class Scaffold extends StatefulWidget {
/// Creates a visual scaffold for material design widgets. /// Creates a visual scaffold for material design widgets.
Scaffold({ const Scaffold({
Key key, Key key,
this.appBar, this.appBar,
this.body, this.body,
...@@ -292,11 +292,12 @@ class Scaffold extends StatefulWidget { ...@@ -292,11 +292,12 @@ class Scaffold extends StatefulWidget {
this.drawer, this.drawer,
this.bottomNavigationBar, this.bottomNavigationBar,
this.backgroundColor, this.backgroundColor,
this.resizeToAvoidBottomPadding: true this.resizeToAvoidBottomPadding: true,
}) : super(key: key); this.primary: true,
}) : assert(primary != null), super(key: key);
/// An app bar to display at the top of the scaffold. /// An app bar to display at the top of the scaffold.
final AppBar appBar; final PreferredSizeWidget appBar;
/// The primary content of the scaffold. /// The primary content of the scaffold.
/// ///
...@@ -363,6 +364,15 @@ class Scaffold extends StatefulWidget { ...@@ -363,6 +364,15 @@ class Scaffold extends StatefulWidget {
/// Defaults to true. /// Defaults to true.
final bool resizeToAvoidBottomPadding; 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. /// The state from the closest instance of this class that encloses the given context.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
...@@ -782,21 +792,17 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -782,21 +792,17 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_addIfNonNull(children, widget.body, _ScaffoldSlot.body); _addIfNonNull(children, widget.body, _ScaffoldSlot.body);
if (widget.appBar != null) { 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.primary ? padding.top : 0.0;
final double topPadding = widget.appBar.primary ? padding.top : 0.0; final double extent = widget.appBar.preferredSize.height + topPadding;
Widget appBar = widget.appBar; assert(extent >= 0.0 && extent.isFinite);
final double extent = widget.appBar.minExtent + topPadding;
if (widget.appBar.flexibleSpace != null) {
appBar = FlexibleSpaceBar.createSettings(
currentExtent: extent,
child: appBar,
);
}
_addIfNonNull( _addIfNonNull(
children, children,
new ConstrainedBox( new ConstrainedBox(
constraints: new BoxConstraints(maxHeight: extent), constraints: new BoxConstraints(maxHeight: extent),
child: appBar, child: FlexibleSpaceBar.createSettings(
currentExtent: extent,
child: widget.appBar,
),
), ),
_ScaffoldSlot.appBar, _ScaffoldSlot.appBar,
); );
......
...@@ -349,7 +349,7 @@ class _DragAnimation extends Animation<double> with AnimationWithParentMixin<dou ...@@ -349,7 +349,7 @@ class _DragAnimation extends Animation<double> with AnimationWithParentMixin<dou
/// ///
/// * [TabBarView], which displays the contents that the tab bar is selecting /// * [TabBarView], which displays the contents that the tab bar is selecting
/// between. /// between.
class TabBar extends StatefulWidget implements AppBarBottomWidget { class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a material design tab bar. /// Creates a material design tab bar.
/// ///
/// The [tabs] argument must not be null and must have more than one widget. /// The [tabs] argument must not be null and must have more than one widget.
...@@ -420,16 +420,19 @@ class TabBar extends StatefulWidget implements AppBarBottomWidget { ...@@ -420,16 +420,19 @@ class TabBar extends StatefulWidget implements AppBarBottomWidget {
/// is null then the text style of the theme's body2 definition is used. /// is null then the text style of the theme's body2 definition is used.
final TextStyle unselectedLabelStyle; 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 @override
double get bottomHeight { Size get preferredSize {
for (Widget item in tabs) { for (Widget item in tabs) {
if (item is Tab) { if (item is Tab) {
final Tab tab = item; final Tab tab = item;
if (tab.text != null && tab.icon != null) 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 @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'; ...@@ -44,6 +44,7 @@ export 'src/widgets/page_view.dart';
export 'src/widgets/pages.dart'; export 'src/widgets/pages.dart';
export 'src/widgets/performance_overlay.dart'; export 'src/widgets/performance_overlay.dart';
export 'src/widgets/placeholder.dart'; export 'src/widgets/placeholder.dart';
export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart'; export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
......
...@@ -637,4 +637,79 @@ void main() { ...@@ -637,4 +637,79 @@ void main() {
expect(appBarTop(tester), lessThanOrEqualTo(0.0)); expect(appBarTop(tester), lessThanOrEqualTo(0.0));
expect(appBarBottom(tester), kTextTabBarHeight); 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() { ...@@ -28,7 +28,7 @@ void main() {
testWidgets('Floating Action Button tooltip', (WidgetTester tester) async { testWidgets('Floating Action Button tooltip', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
home: new Scaffold( home: const Scaffold(
floatingActionButton: const FloatingActionButton( floatingActionButton: const FloatingActionButton(
onPressed: null, onPressed: null,
tooltip: 'Add', tooltip: 'Add',
......
...@@ -191,10 +191,10 @@ void main() { ...@@ -191,10 +191,10 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android), 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>{ routes: <String, WidgetBuilder>{
'/next': (BuildContext context) { '/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() { ...@@ -222,10 +222,10 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS), 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>{ routes: <String, WidgetBuilder>{
'/next': (BuildContext context) { '/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() { ...@@ -264,13 +264,13 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS), 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>( tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<Null>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Scaffold(body: const Text('Page 2')); return const Scaffold(body: const Text('Page 2'));
}, },
fullscreenDialog: true, fullscreenDialog: true,
)); ));
......
...@@ -82,7 +82,7 @@ void main() { ...@@ -82,7 +82,7 @@ void main() {
}); });
testWidgets('Floating action animation', (WidgetTester tester) async { testWidgets('Floating action animation', (WidgetTester tester) async {
await tester.pumpWidget(new Scaffold( await tester.pumpWidget(const Scaffold(
floatingActionButton: const FloatingActionButton( floatingActionButton: const FloatingActionButton(
key: const Key('one'), key: const Key('one'),
onPressed: null, onPressed: null,
...@@ -92,7 +92,7 @@ void main() { ...@@ -92,7 +92,7 @@ void main() {
expect(tester.binding.transientCallbackCount, 0); expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(new Scaffold( await tester.pumpWidget(const Scaffold(
floatingActionButton: const FloatingActionButton( floatingActionButton: const FloatingActionButton(
key: const Key('two'), key: const Key('two'),
onPressed: null, onPressed: null,
...@@ -103,10 +103,10 @@ void main() { ...@@ -103,10 +103,10 @@ void main() {
expect(tester.binding.transientCallbackCount, greaterThan(0)); expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpWidget(new Container()); await tester.pumpWidget(new Container());
expect(tester.binding.transientCallbackCount, 0); expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(new Scaffold()); await tester.pumpWidget(const Scaffold());
expect(tester.binding.transientCallbackCount, 0); expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(new Scaffold( await tester.pumpWidget(const Scaffold(
floatingActionButton: const FloatingActionButton( floatingActionButton: const FloatingActionButton(
key: const Key('one'), key: const Key('one'),
onPressed: null, onPressed: null,
......
...@@ -206,7 +206,7 @@ void main() { ...@@ -206,7 +206,7 @@ void main() {
onTap: () { onTap: () {
showDialog<Null>( showDialog<Null>(
context: context, context: context,
child: new Scaffold( child: const Scaffold(
body: const SizedBox( body: const SizedBox(
width: 200.0, width: 200.0,
height: 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