diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 7c7ab7b8a5..c11f6e437f 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -21,70 +21,7 @@ import 'tabs.dart'; import 'theme.dart'; import 'typography.dart'; -enum _ToolbarSlot { - leading, - title, - actions, -} - -class _ToolbarLayout extends MultiChildLayoutDelegate { - _ToolbarLayout({ this.centerTitle }); - - // If false the title should be left or right justified within the space bewteen - // the leading and actions widgets, depending on the locale's writing direction. - // If true the title is centered within the toolbar (not within the horizontal - // space bewteen the leading and actions widgets). - final bool centerTitle; - - static const double kLeadingWidth = 56.0; // So it's square with kToolbarHeight. - static const double kTitleLeftWithLeading = 72.0; // As per https://material.io/guidelines/layout/metrics-keylines.html#metrics-keylines-keylines-spacing. - static const double kTitleLeftWithoutLeading = 16.0; - - @override - void performLayout(Size size) { - double actionsWidth = 0.0; - - if (hasChild(_ToolbarSlot.leading)) { - final BoxConstraints constraints = new BoxConstraints.tight(new Size(kLeadingWidth, size.height)); - layoutChild(_ToolbarSlot.leading, constraints); - positionChild(_ToolbarSlot.leading, Offset.zero); - } - - if (hasChild(_ToolbarSlot.actions)) { - final BoxConstraints constraints = new BoxConstraints.loose(size); - final Size actionsSize = layoutChild(_ToolbarSlot.actions, constraints); - final double actionsLeft = size.width - actionsSize.width; - final double actionsTop = (size.height - actionsSize.height) / 2.0; - actionsWidth = actionsSize.width; - positionChild(_ToolbarSlot.actions, new Offset(actionsLeft, actionsTop)); - } - - if (hasChild(_ToolbarSlot.title)) { - final double titleLeftMargin = - hasChild(_ToolbarSlot.leading) ? kTitleLeftWithLeading : kTitleLeftWithoutLeading; - final double maxWidth = math.max(size.width - titleLeftMargin - actionsWidth, 0.0); - final BoxConstraints constraints = new BoxConstraints.loose(size).copyWith(maxWidth: maxWidth); - final Size titleSize = layoutChild(_ToolbarSlot.title, constraints); - final double titleY = (size.height - titleSize.height) / 2.0; - double titleX = titleLeftMargin; - - // If the centered title will not fit between the leading and actions - // widgets, then align its left or right edge with the adjacent boundary. - if (centerTitle) { - titleX = (size.width - titleSize.width) / 2.0; - if (titleX + titleSize.width > size.width - actionsWidth) - titleX = size.width - actionsWidth - titleSize.width; - else if (titleX < titleLeftMargin) - titleX = titleLeftMargin; - } - - positionChild(_ToolbarSlot.title, new Offset(titleX, titleY)); - } - } - - @override - bool shouldRelayout(_ToolbarLayout oldDelegate) => centerTitle != oldDelegate.centerTitle; -} +const double _kLeadingWidth = kToolbarHeight; // So the leading button is square. // Bottom justify the kToolbarHeight child which may overflow the top. class _ToolbarContainerLayout extends SingleChildLayoutDelegate { @@ -390,7 +327,6 @@ class _AppBarState extends State { ); } - final List toolbarChildren = []; Widget leading = widget.leading; if (leading == null) { if (hasDrawer) { @@ -405,47 +341,38 @@ class _AppBarState extends State { } } if (leading != null) { - toolbarChildren.add( - new LayoutId( - id: _ToolbarSlot.leading, - child: leading - ) + leading = new ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: _kLeadingWidth), + child: leading, ); } - if (widget.title != null) { - toolbarChildren.add( - new LayoutId( - id: _ToolbarSlot.title, - child: new DefaultTextStyle( - style: centerStyle, - softWrap: false, - overflow: TextOverflow.ellipsis, - child: widget.title, - ), - ), + Widget title = widget.title; + if (title != null) { + title = new DefaultTextStyle( + style: centerStyle, + softWrap: false, + overflow: TextOverflow.ellipsis, + child: title, ); } + + Widget actions; if (widget.actions != null && widget.actions.isNotEmpty) { - toolbarChildren.add( - new LayoutId( - id: _ToolbarSlot.actions, - child: new Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: widget.actions, - ), - ), + actions = new Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: widget.actions, ); } final Widget toolbar = new Padding( padding: const EdgeInsets.only(right: 4.0), - child: new CustomMultiChildLayout( - delegate: new _ToolbarLayout( - centerTitle: widget._getEffectiveCenterTitle(themeData), - ), - children: toolbarChildren, + child: new NavigationToolbar( + leading: leading, + middle: title, + trailing: actions, + centerMiddle: widget._getEffectiveCenterTitle(themeData), ), ); diff --git a/packages/flutter/lib/src/widgets/navigation_toolbar.dart b/packages/flutter/lib/src/widgets/navigation_toolbar.dart new file mode 100644 index 0000000000..5a4ef6af2a --- /dev/null +++ b/packages/flutter/lib/src/widgets/navigation_toolbar.dart @@ -0,0 +1,137 @@ +// 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 'dart:math' as math; + +import 'package:flutter/rendering.dart'; + +import 'basic.dart'; +import 'framework.dart'; + +/// [NavigationToolbar] is a layout helper to position 3 widgets or groups of +/// widgets along a horizontal axis that's sensible for an application's +/// navigation bar such as in Material Design and in iOS. +/// +/// [leading] and [trailing] widgets occupy the edges of the widget with +/// reasonable size constraints while the [middle] widget occupies the remaining +/// space in either a center aligned or start aligned fashion. +/// +/// Either directly use the themed app bars such as the Material [AppBar] or +/// the iOS [CupertinoNavigationBar] or wrap this widget with more theming +/// specifications for your own custom app bar. +class NavigationToolbar extends StatelessWidget { + const NavigationToolbar({ + Key key, + this.leading, + this.middle, + this.trailing, + this.centerMiddle: true, + }) : assert(centerMiddle != null), + super(key: key); + + /// Widget to place at the start of the horizontal toolbar. + final Widget leading; + + /// Widget to place in the middle of the horizontal toolbar, occupying + /// as much remaining space as possible. + final Widget middle; + + /// Widget to place at the end of the horizontal toolbar. + final Widget trailing; + + /// Whether to align the [middle] widget to the center of this widget or + /// next to the [leading] widget when false. + final bool centerMiddle; + + @override + Widget build(BuildContext context) { + final List children = []; + + if (leading != null) + children.add(new LayoutId(id: _ToolbarSlot.leading, child: leading)); + + if (middle != null) + children.add(new LayoutId(id: _ToolbarSlot.middle, child: middle)); + + if (trailing != null) + children.add(new LayoutId(id: _ToolbarSlot.trailing, child: trailing)); + + return new CustomMultiChildLayout( + delegate: new _ToolbarLayout( + centerMiddle: centerMiddle, + ), + children: children, + ); + } +} + +enum _ToolbarSlot { + leading, + middle, + trailing, +} + +const double _kMiddleMargin = 16.0; + +// TODO(xster): support RTL. +class _ToolbarLayout extends MultiChildLayoutDelegate { + _ToolbarLayout({ this.centerMiddle }); + + // If false the middle widget should be left justified within the space + // between the leading and trailing widgets. + // If true the middle widget is centered within the toolbar (not within the horizontal + // space bewteen the leading and trailing widgets). + // TODO(xster): document RTL once supported. + final bool centerMiddle; + + @override + void performLayout(Size size) { + double leadingWidth = 0.0; + double trailingWidth = 0.0; + + if (hasChild(_ToolbarSlot.leading)) { + final BoxConstraints constraints = new BoxConstraints( + minWidth: 0.0, + maxWidth: size.width / 3.0, // The leading widget shouldn't take up more than 1/3 of the space. + minHeight: size.height, // The height should be exactly the height of the bar. + maxHeight: size.height, + ); + leadingWidth = layoutChild(_ToolbarSlot.leading, constraints).width; + positionChild(_ToolbarSlot.leading, Offset.zero); + } + + if (hasChild(_ToolbarSlot.trailing)) { + final BoxConstraints constraints = new BoxConstraints.loose(size); + final Size trailingSize = layoutChild(_ToolbarSlot.trailing, constraints); + final double trailingLeft = size.width - trailingSize.width; + final double trailingTop = (size.height - trailingSize.height) / 2.0; + trailingWidth = trailingSize.width; + positionChild(_ToolbarSlot.trailing, new Offset(trailingLeft, trailingTop)); + } + + if (hasChild(_ToolbarSlot.middle)) { + final double maxWidth = math.max(size.width - leadingWidth - trailingWidth - _kMiddleMargin * 2.0, 0.0); + final BoxConstraints constraints = new BoxConstraints.loose(size).copyWith(maxWidth: maxWidth); + final Size middleSize = layoutChild(_ToolbarSlot.middle, constraints); + + final double middleLeftMargin = leadingWidth + _kMiddleMargin; + double middleX = middleLeftMargin; + final double middleY = (size.height - middleSize.height) / 2.0; + // If the centered middle will not fit between the leading and trailing + // widgets, then align its left or right edge with the adjacent boundary. + if (centerMiddle) { + middleX = (size.width - middleSize.width) / 2.0; + if (middleX + middleSize.width > size.width - trailingWidth) + middleX = size.width - trailingWidth - middleSize.width; + else if (middleX < middleLeftMargin) + middleX = middleLeftMargin; + } + + positionChild(_ToolbarSlot.middle, new Offset(middleX, middleY)); + } + } + + @override + bool shouldRelayout(_ToolbarLayout oldDelegate) => centerMiddle != oldDelegate.centerMiddle; +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index a411ff948b..6811b8e747 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -46,6 +46,7 @@ export 'src/widgets/layout_builder.dart'; export 'src/widgets/locale_query.dart'; export 'src/widgets/media_query.dart'; export 'src/widgets/modal_barrier.dart'; +export 'src/widgets/navigation_toolbar.dart'; export 'src/widgets/navigator.dart'; export 'src/widgets/nested_scroll_view.dart'; export 'src/widgets/notification_listener.dart'; diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 293ffbfc55..36ac6f744e 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -216,8 +216,12 @@ void main() { final Finder title = find.byKey(titleKey); expect(tester.getTopLeft(title).dx, 72.0); - // The toolbar's contents are padded on the right by 4.0 - expect(tester.getSize(title).width, equals(800.0 - 72.0 - 4.0)); + expect(tester.getSize(title).width, equals( + 800.0 // Screen width. + - 4.0 // Left margin before the leading button. + - 56.0 // Leading button width. + - 16.0 // Leading button to title padding. + - 16.0)); // Title right side padding. actions = [ const SizedBox(width: 100.0), @@ -227,13 +231,19 @@ void main() { expect(tester.getTopLeft(title).dx, 72.0); // The title shrinks by 200.0 to allow for the actions widgets. - expect(tester.getSize(title).width, equals(800.0 - 72.0 - 4.0 - 200.0)); + expect(tester.getSize(title).width, equals( + 800.0 // Screen width. + - 4.0 // Left margin before the leading button. + - 56.0 // Leading button width. + - 16.0 // Leading button to title padding. + - 16.0 // Title to actions padding + - 200.0)); // Actions' width. leading = new Container(); // AppBar will constrain the width to 24.0 await tester.pumpWidget(buildApp()); expect(tester.getTopLeft(title).dx, 72.0); // Adding a leading widget shouldn't effect the title's size - expect(tester.getSize(title).width, equals(800.0 - 72.0 - 4.0 - 200.0)); + expect(tester.getSize(title).width, equals(800.0 - 4.0 - 56.0 - 16.0 - 16.0 - 200.0)); }); testWidgets('AppBar centerTitle:true title overflow OK ', (WidgetTester tester) async {