Commit a8777ce6 authored by xster's avatar xster Committed by GitHub

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
parent 457554be
......@@ -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),
),
);
......
// 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 {
......
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