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 'theme.dart';
|
||||||
import 'typography.dart';
|
import 'typography.dart';
|
||||||
|
|
||||||
enum _ToolbarSlot {
|
const double _kLeadingWidth = kToolbarHeight; // So the leading button is square.
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bottom justify the kToolbarHeight child which may overflow the top.
|
// Bottom justify the kToolbarHeight child which may overflow the top.
|
||||||
class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
|
class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
|
||||||
@ -390,7 +327,6 @@ class _AppBarState extends State<AppBar> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Widget> toolbarChildren = <Widget>[];
|
|
||||||
Widget leading = widget.leading;
|
Widget leading = widget.leading;
|
||||||
if (leading == null) {
|
if (leading == null) {
|
||||||
if (hasDrawer) {
|
if (hasDrawer) {
|
||||||
@ -405,47 +341,38 @@ class _AppBarState extends State<AppBar> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (leading != null) {
|
if (leading != null) {
|
||||||
toolbarChildren.add(
|
leading = new ConstrainedBox(
|
||||||
new LayoutId(
|
constraints: const BoxConstraints.tightFor(width: _kLeadingWidth),
|
||||||
id: _ToolbarSlot.leading,
|
child: leading,
|
||||||
child: leading
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.title != null) {
|
Widget title = widget.title;
|
||||||
toolbarChildren.add(
|
if (title != null) {
|
||||||
new LayoutId(
|
title = new DefaultTextStyle(
|
||||||
id: _ToolbarSlot.title,
|
|
||||||
child: new DefaultTextStyle(
|
|
||||||
style: centerStyle,
|
style: centerStyle,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
child: widget.title,
|
child: title,
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget actions;
|
||||||
if (widget.actions != null && widget.actions.isNotEmpty) {
|
if (widget.actions != null && widget.actions.isNotEmpty) {
|
||||||
toolbarChildren.add(
|
actions = new Row(
|
||||||
new LayoutId(
|
|
||||||
id: _ToolbarSlot.actions,
|
|
||||||
child: new Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: widget.actions,
|
children: widget.actions,
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Widget toolbar = new Padding(
|
final Widget toolbar = new Padding(
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
child: new CustomMultiChildLayout(
|
child: new NavigationToolbar(
|
||||||
delegate: new _ToolbarLayout(
|
leading: leading,
|
||||||
centerTitle: widget._getEffectiveCenterTitle(themeData),
|
middle: title,
|
||||||
),
|
trailing: actions,
|
||||||
children: toolbarChildren,
|
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/locale_query.dart';
|
||||||
export 'src/widgets/media_query.dart';
|
export 'src/widgets/media_query.dart';
|
||||||
export 'src/widgets/modal_barrier.dart';
|
export 'src/widgets/modal_barrier.dart';
|
||||||
|
export 'src/widgets/navigation_toolbar.dart';
|
||||||
export 'src/widgets/navigator.dart';
|
export 'src/widgets/navigator.dart';
|
||||||
export 'src/widgets/nested_scroll_view.dart';
|
export 'src/widgets/nested_scroll_view.dart';
|
||||||
export 'src/widgets/notification_listener.dart';
|
export 'src/widgets/notification_listener.dart';
|
||||||
|
@ -216,8 +216,12 @@ void main() {
|
|||||||
|
|
||||||
final Finder title = find.byKey(titleKey);
|
final Finder title = find.byKey(titleKey);
|
||||||
expect(tester.getTopLeft(title).dx, 72.0);
|
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(
|
||||||
expect(tester.getSize(title).width, equals(800.0 - 72.0 - 4.0));
|
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>[
|
actions = <Widget>[
|
||||||
const SizedBox(width: 100.0),
|
const SizedBox(width: 100.0),
|
||||||
@ -227,13 +231,19 @@ void main() {
|
|||||||
|
|
||||||
expect(tester.getTopLeft(title).dx, 72.0);
|
expect(tester.getTopLeft(title).dx, 72.0);
|
||||||
// The title shrinks by 200.0 to allow for the actions widgets.
|
// 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
|
leading = new Container(); // AppBar will constrain the width to 24.0
|
||||||
await tester.pumpWidget(buildApp());
|
await tester.pumpWidget(buildApp());
|
||||||
expect(tester.getTopLeft(title).dx, 72.0);
|
expect(tester.getTopLeft(title).dx, 72.0);
|
||||||
// Adding a leading widget shouldn't effect the title's size
|
// 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 {
|
testWidgets('AppBar centerTitle:true title overflow OK ', (WidgetTester tester) async {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user