CupertinoNavigationBar part 1 - extract common layout logic (#10337)
Extract layout logic in material app bar to a common file that can be reused for cupertino
This commit is contained in:
parent
457554beaf
commit
a8777ce6b0
@ -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<AppBar> {
|
||||
);
|
||||
}
|
||||
|
||||
final List<Widget> toolbarChildren = <Widget>[];
|
||||
Widget leading = widget.leading;
|
||||
if (leading == null) {
|
||||
if (hasDrawer) {
|
||||
@ -405,47 +341,38 @@ class _AppBarState extends State<AppBar> {
|
||||
}
|
||||
}
|
||||
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),
|
||||
),
|
||||
);
|
||||
|
||||
|
137
packages/flutter/lib/src/widgets/navigation_toolbar.dart
Normal file
137
packages/flutter/lib/src/widgets/navigation_toolbar.dart
Normal file
@ -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<Widget> children = <Widget>[];
|
||||
|
||||
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;
|
||||
}
|
@ -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';
|
||||
|
@ -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 = <Widget>[
|
||||
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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user