Commit 3a6530d5 authored by Dragoș Tiselice's avatar Dragoș Tiselice Committed by GitHub

Added bottom navigation. (#5877)

parent d3f540d4
......@@ -15,6 +15,7 @@ export 'src/material/about.dart';
export 'src/material/app.dart';
export 'src/material/app_bar.dart';
export 'src/material/arc.dart';
export 'src/material/bottom_navigation_bar.dart';
export 'src/material/bottom_sheet.dart';
export 'src/material/button.dart';
export 'src/material/button_bar.dart';
......
This diff is collapsed.
......@@ -5,6 +5,9 @@
/// The height of the toolbar component of the [AppBar].
const double kToolbarHeight = 56.0;
/// The height of the bottom navigation bar.
const double kBottomNavigationBarHeight = 60.0;
/// The height of a tab bar containing text.
const double kTextTabBarHeight = 48.0;
......
......@@ -53,6 +53,7 @@ enum _ScaffoldSlot {
appBar,
bottomSheet,
snackBar,
bottomNavigationBar,
floatingActionButton,
drawer,
statusBar,
......@@ -80,7 +81,8 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
final BoxConstraints fullWidthConstraints = looseConstraints.tighten(width: size.width);
double contentTop = padding.top;
double contentBottom = size.height - padding.bottom;
double bottom = size.height - padding.bottom;
double contentBottom = bottom;
if (hasChild(_ScaffoldSlot.appBar)) {
final double appBarHeight = layoutChild(_ScaffoldSlot.appBar, fullWidthConstraints).height;
......@@ -89,6 +91,12 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
positionChild(_ScaffoldSlot.appBar, Offset.zero);
}
if (hasChild(_ScaffoldSlot.bottomNavigationBar)) {
final double bottomNavigationBarHeight = layoutChild(_ScaffoldSlot.bottomNavigationBar, fullWidthConstraints).height;
contentBottom -= bottomNavigationBarHeight;
positionChild(_ScaffoldSlot.bottomNavigationBar, new Offset(0.0, contentBottom));
}
if (hasChild(_ScaffoldSlot.body)) {
final double bodyHeight = contentBottom - contentTop;
final BoxConstraints bodyConstraints = fullWidthConstraints.tighten(height: bodyHeight);
......@@ -97,7 +105,10 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
}
// The BottomSheet and the SnackBar are anchored to the bottom of the parent,
// they're as wide as the parent and are given their intrinsic height.
// they're as wide as the parent and are given their intrinsic height. The
// only difference is that SnackBar appears on the top side of the
// BottomNavigationBar while the BottomSheet is stacked on top of it.
//
// If all three elements are present then either the center of the FAB straddles
// the top edge of the BottomSheet or the bottom of the FAB is
// _kFloatingActionButtonMargin above the SnackBar, whichever puts the FAB
......@@ -110,7 +121,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
if (hasChild(_ScaffoldSlot.bottomSheet)) {
bottomSheetSize = layoutChild(_ScaffoldSlot.bottomSheet, fullWidthConstraints);
positionChild(_ScaffoldSlot.bottomSheet, new Offset((size.width - bottomSheetSize.width) / 2.0, contentBottom - bottomSheetSize.height));
positionChild(_ScaffoldSlot.bottomSheet, new Offset((size.width - bottomSheetSize.width) / 2.0, bottom - bottomSheetSize.height));
}
if (hasChild(_ScaffoldSlot.snackBar)) {
......@@ -265,6 +276,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// * [AppBar]
/// * [FloatingActionButton]
/// * [Drawer]
/// * [BottomNavigationBar]
/// * [SnackBar]
/// * [BottomSheet]
/// * [ScaffoldState]
......@@ -281,6 +293,7 @@ class Scaffold extends StatefulWidget {
this.body,
this.floatingActionButton,
this.drawer,
this.bottomNavigationBar,
this.scrollableKey,
this.appBarBehavior: AppBarBehavior.anchor,
this.resizeToAvoidBottomPadding: true
......@@ -306,6 +319,12 @@ class Scaffold extends StatefulWidget {
/// Typically a [Drawer].
final Widget drawer;
/// A bottom navigation bar to display at the bottom of the scaffold.
///
/// Snack bars slide from underneath the botton navigation while bottom sheets
/// are stacked on top.
final Widget bottomNavigationBar;
/// The key of the primary [Scrollable] widget in the [body].
///
/// Used to control scroll-linked effects, such as the collapse of the
......@@ -465,7 +484,7 @@ class ScaffoldState extends State<Scaffold> {
/// content of the app. A persistent bottom sheet remains visible even when
/// the user interacts with other parts of the app.
///
/// A closely related widget is a modal bottom sheet, which is an alternative
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
/// [showModalBottomSheet] function.
......@@ -759,7 +778,18 @@ class ScaffoldState extends State<Scaffold> {
} else {
children.add(new LayoutId(child: _buildScrollableAppBar(context, padding), id: _ScaffoldSlot.appBar));
}
// Otherwise the AppBar will be part of a [app bar, body] Stack. See AppBarBehavior.scroll below.
// Otherwise the AppBar will be part of a [app bar, body] Stack. See
// AppBarBehavior.scroll below.
if (_snackBars.isNotEmpty)
_addIfNonNull(children, _snackBars.first._widget, _ScaffoldSlot.snackBar);
if (config.bottomNavigationBar != null) {
children.add(new LayoutId(
id: _ScaffoldSlot.bottomNavigationBar,
child: config.bottomNavigationBar
));
}
if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) {
final List<Widget> bottomSheets = <Widget>[];
......@@ -774,9 +804,6 @@ class ScaffoldState extends State<Scaffold> {
_addIfNonNull(children, stack, _ScaffoldSlot.bottomSheet);
}
if (_snackBars.isNotEmpty)
_addIfNonNull(children, _snackBars.first._widget, _ScaffoldSlot.snackBar);
children.add(new LayoutId(
id: _ScaffoldSlot.floatingActionButton,
child: new _FloatingActionButtonTransition(
......
// Copyright 2016 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('BottomNavigationBar callback test', (WidgetTester tester) async {
int mutatedIndex;
await tester.pumpWidget(
new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
labels: <DestinationLabel>[
new DestinationLabel(
icon: new Icon(Icons.ac_unit),
title: new Text('AC')
),
new DestinationLabel(
icon: new Icon(Icons.access_alarm),
title: new Text('Alarm')
)
],
onTap: (int index) {
mutatedIndex = index;
}
)
)
);
await tester.tap(find.text('Alarm'));
expect(mutatedIndex, 1);
});
testWidgets('BottomNavigationBar content test', (WidgetTester tester) async {
await tester.pumpWidget(
new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
labels: <DestinationLabel>[
new DestinationLabel(
icon: new Icon(Icons.ac_unit),
title: new Text('AC')
),
new DestinationLabel(
icon: new Icon(Icons.access_alarm),
title: new Text('Alarm')
)
]
)
)
);
RenderBox box = tester.renderObject(find.byType(BottomNavigationBar));
expect(box.size.height, 60.0);
expect(find.text('AC'), findsOneWidget);
expect(find.text('Alarm'), findsOneWidget);
});
testWidgets('BottomNavigationBar action size test', (WidgetTester tester) async {
await tester.pumpWidget(
new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
type: BottomNavigationBarType.shifting,
labels: <DestinationLabel>[
new DestinationLabel(
icon: new Icon(Icons.ac_unit),
title: new Text('AC')
),
new DestinationLabel(
icon: new Icon(Icons.access_alarm),
title: new Text('Alarm')
)
]
)
)
);
Iterable<RenderBox> actions = tester.renderObjectList(find.byType(InkResponse));
expect(actions.length, 2);
expect(actions.elementAt(0).size.width, 158.4);
expect(actions.elementAt(1).size.width, 105.6);
await tester.pumpWidget(
new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
currentIndex: 1,
type: BottomNavigationBarType.shifting,
labels: <DestinationLabel>[
new DestinationLabel(
icon: new Icon(Icons.ac_unit),
title: new Text('AC')
),
new DestinationLabel(
icon: new Icon(Icons.access_alarm),
title: new Text('Alarm')
)
]
)
)
);
await tester.pump(const Duration(milliseconds: 200));
actions = tester.renderObjectList(find.byType(InkResponse));
expect(actions.length, 2);
expect(actions.elementAt(0).size.width, 105.6);
expect(actions.elementAt(1).size.width, 158.4);
});
testWidgets('BottomNavigationBar multiple taps test', (WidgetTester tester) async {
await tester.pumpWidget(
new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
type: BottomNavigationBarType.shifting,
labels: <DestinationLabel>[
new DestinationLabel(
icon: new Icon(Icons.ac_unit),
title: new Text('AC')
),
new DestinationLabel(
icon: new Icon(Icons.access_alarm),
title: new Text('Alarm')
),
new DestinationLabel(
icon: new Icon(Icons.access_time),
title: new Text('Time')
),
new DestinationLabel(
icon: new Icon(Icons.add),
title: new Text('Add')
)
]
)
)
);
// We want to make sure that the last label does not get displaced,
// irrespective of how many taps happen on the first N - 1 labels and how
// they grow.
Iterable<RenderBox> actions = tester.renderObjectList(find.byType(InkResponse));
Point originalOrigin = actions.elementAt(3).localToGlobal(Point.origin);
await tester.tap(find.text('AC'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
actions = tester.renderObjectList(find.byType(InkResponse));
expect(actions.elementAt(3).localToGlobal(Point.origin), equals(originalOrigin));
await tester.tap(find.text('Alarm'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
actions = tester.renderObjectList(find.byType(InkResponse));
expect(actions.elementAt(3).localToGlobal(Point.origin), equals(originalOrigin));
await tester.tap(find.text('Time'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
actions = tester.renderObjectList(find.byType(InkResponse));
expect(actions.elementAt(3).localToGlobal(Point.origin), equals(originalOrigin));
});
}
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