blob: bbfeb447ca3a5c4c836e9a36e5115e4623725e7b [file] [log] [blame]
// Copyright 2018 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/foundation.dart';
import 'package:flutter/material.dart';
class BottomAppBarDemo extends StatefulWidget {
static const String routeName = '/material/bottom_app_bar';
@override
State createState() => new _BottomAppBarDemoState();
}
class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
// The key given to the Scaffold so that _showSnackbar can find it.
static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
// The index of the currently-selected _FabLocationConfiguration.
int fabLocationIndex = 1;
static const List<_FabLocationConfiguration> _fabLocationConfigurations = const <_FabLocationConfiguration>[
const _FabLocationConfiguration('End, undocked above the bottom app bar', _BabMode.END_FAB, FloatingActionButtonLocation.endFloat),
const _FabLocationConfiguration('End, docked to the bottom app bar', _BabMode.END_FAB, FloatingActionButtonLocation.endDocked),
const _FabLocationConfiguration('Center, docked to the bottom app bar', _BabMode.CENTER_FAB, FloatingActionButtonLocation.centerDocked),
const _FabLocationConfiguration('Center, undocked above the bottom app bar', _BabMode.CENTER_FAB, FloatingActionButtonLocation.centerFloat),
// This configuration uses a custom FloatingActionButtonLocation.
const _FabLocationConfiguration('Start, docked to the top app bar', _BabMode.CENTER_FAB, const _StartTopFloatingActionButtonLocation()),
];
// The index of the currently-selected _FabShapeConfiguration.
int fabShapeIndex = 1;
static const List<_FabShapeConfiguration> _fabShapeConfigurations = const <_FabShapeConfiguration>[
const _FabShapeConfiguration('None', null),
const _FabShapeConfiguration('Circular',
const FloatingActionButton(
onPressed: _showSnackbar,
child: const Icon(Icons.add),
backgroundColor: Colors.orange,
),
),
const _FabShapeConfiguration('Diamond',
const _DiamondFab(
onPressed: _showSnackbar,
child: const Icon(Icons.add),
),
),
];
// The currently-selected Color for the Bottom App Bar.
Color babColor;
// Accessible names for the colors that a Screen Reader can use to
// identify them.
static final Map<Color, String> colorToName = <Color, String> {
null: 'White',
Colors.orange: 'Orange',
Colors.green: 'Green',
Colors.lightBlue: 'Light blue',
};
static const List<Color> babColors = const <Color> [
null,
Colors.orange,
Colors.green,
Colors.lightBlue,
];
// Whether or not to show a notch in the Bottom App Bar around the
// Floating Action Button when it is docked.
bool notchEnabled = true;
@override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: const Text('Bottom App Bar with FAB location'),
// Add 48dp of space onto the bottom of the appbar.
// This gives space for the top-start location to attach to without
// blocking the 'back' button.
bottom: const PreferredSize(
preferredSize: const Size.fromHeight(48.0),
child: const SizedBox(),
),
),
body: new SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: buildControls(context),
),
bottomNavigationBar: new _DemoBottomAppBar(_fabLocationConfigurations[fabLocationIndex].babMode, babColor, notchEnabled),
floatingActionButton: _fabShapeConfigurations[fabShapeIndex].fab,
floatingActionButtonLocation: _fabLocationConfigurations[fabLocationIndex].fabLocation,
);
}
Widget buildControls(BuildContext context) {
return new Column(
children: <Widget> [
new Text(
'Floating action button',
style: Theme.of(context).textTheme.title,
),
buildFabShapePicker(),
buildFabLocationPicker(),
const Divider(),
new Text(
'Bottom app bar options',
style: Theme.of(context).textTheme.title,
),
buildBabColorPicker(),
new CheckboxListTile(
title: const Text('Enable notch'),
value: notchEnabled,
onChanged: (bool value) {
setState(() {
notchEnabled = value;
});
},
controlAffinity: ListTileControlAffinity.leading,
),
],
);
}
Widget buildFabShapePicker() {
return new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
const SizedBox(width: 96.0,
child: const Text('Shape: '),
),
new Expanded(
child: new Padding(
padding: const EdgeInsets.all(8.0),
child: new RaisedButton(
child: const Text('Change shape'),
onPressed: () {
setState(() {
fabShapeIndex = (fabShapeIndex + 1) % _fabShapeConfigurations.length;
});
},
),
),
),
],
);
}
Widget buildFabLocationPicker() {
return new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
const SizedBox(
width: 96.0,
child: const Text('Location: '),
),
new Expanded(
child: new Padding(
padding: const EdgeInsets.all(8.0),
child: new RaisedButton(
child: const Text('Move'),
onPressed: () {
setState(() {
fabLocationIndex = (fabLocationIndex + 1) % _fabLocationConfigurations.length;
});
},
),
),
),
],
);
}
Widget buildBabColorPicker() {
final List<Widget> colors = <Widget> [
const Text('Color:'),
];
for (Color color in babColors) {
colors.add(
new Semantics(
label: 'Set Bottom App Bar color to ${colorToName[color]}',
container: true,
child: new Row(children: <Widget> [
new Radio<Color>(
value: color,
groupValue: babColor,
onChanged: (Color color) {
setState(() {
babColor = color;
});
},
),
new Container(
decoration: new BoxDecoration(
color: color,
border: new Border.all(width:2.0, color: Colors.black),
),
child: const SizedBox(width: 20.0, height: 20.0),
),
const Padding(padding: const EdgeInsets.only(left: 12.0)),
]),
),
);
}
return new SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: new Row(
children: colors,
mainAxisAlignment: MainAxisAlignment.center,
),
);
}
static void _showSnackbar() {
_scaffoldKey.currentState.showSnackBar(
const SnackBar(content: const Text(_explanatoryText)),
);
}
}
const String _explanatoryText =
"When the Scaffold's floating action button location changes, "
'the floating action button animates to its new position.'
'The BottomAppBar adapts its shape appropriately.';
// Whether the Bottom App Bar's menu should keep icons away from the center or from the end of the screen.
//
// When the Floating Action Button is positioned at the end of the screen,
// it would cover icons at the end of the screen, so the END_FAB mode tells
// the MyBottomAppBar to place icons away from the end.
//
// Similar logic applies to the CENTER_FAB mode.
enum _BabMode {
END_FAB,
CENTER_FAB,
}
// Pairs the Bottom App Bar's menu mode with a Floating Action Button Location.
class _FabLocationConfiguration {
const _FabLocationConfiguration(this.name, this.babMode, this.fabLocation);
// The name of this configuration.
final String name;
// The _BabMode to place the menu in the bab with.
final _BabMode babMode;
// The location for the Floating Action Button.
final FloatingActionButtonLocation fabLocation;
}
// Map of names to the different shapes of Floating Action Button in this demo.
class _FabShapeConfiguration {
const _FabShapeConfiguration(this.name, this.fab);
final String name;
final Widget fab;
}
// A bottom app bar with a menu inside it.
class _DemoBottomAppBar extends StatelessWidget {
const _DemoBottomAppBar(this.babMode, this.color, this.enableNotch);
final _BabMode babMode;
final Color color;
final bool enableNotch;
final Curve fadeOutCurve = const Interval(0.0, 0.3333);
final Curve fadeInCurve = const Interval(0.3333, 1.0);
@override
Widget build(BuildContext context) {
return new BottomAppBar(
color: color,
hasNotch: enableNotch,
// TODO: Use an AnimatedCrossFade to build contents for centered FAB performantly.
// Using AnimatedCrossFade here previously was causing https://github.com/flutter/flutter/issues/16377.
child: buildBabContents(context, _BabMode.END_FAB),
);
}
Widget buildBabContents(BuildContext context, _BabMode babMode) {
final List<Widget> rowContents = <Widget> [
new IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
showModalBottomSheet<Null>(context: context, builder: (BuildContext context) => const _DemoDrawer());
},
),
];
if (babMode == _BabMode.CENTER_FAB) {
rowContents.add(
new Expanded(
child: new ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 0.0),
),
),
);
}
rowContents.addAll(<Widget> [
new IconButton(
icon: const Icon(Icons.search),
onPressed: () {
Scaffold.of(context).showSnackBar(
const SnackBar(content: const Text('This is a dummy search action.')),
);
},
),
new IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
Scaffold.of(context).showSnackBar(
const SnackBar(content: const Text('This is a dummy menu action.')),
);
},
),
]);
return new Row(
children: rowContents,
);
}
}
// A drawer that pops up from the bottom of the screen.
class _DemoDrawer extends StatelessWidget {
const _DemoDrawer();
@override
Widget build(BuildContext context) {
return new Drawer(
child: new Column(
children: const <Widget>[
const ListTile(
leading: const Icon(Icons.search),
title: const Text('Search'),
),
const ListTile(
leading: const Icon(Icons.threed_rotation),
title: const Text('3D'),
),
],
),
);
}
}
// A diamond-shaped floating action button.
class _DiamondFab extends StatefulWidget {
const _DiamondFab({
this.child,
this.notchMargin: 6.0,
this.onPressed,
});
final Widget child;
final double notchMargin;
final VoidCallback onPressed;
@override
State createState() => new _DiamondFabState();
}
class _DiamondFabState extends State<_DiamondFab> {
VoidCallback _clearComputeNotch;
@override
Widget build(BuildContext context) {
return new Material(
shape: const _DiamondBorder(),
color: Colors.orange,
child: new InkWell(
onTap: widget.onPressed,
child: new Container(
width: 56.0,
height: 56.0,
child: IconTheme.merge(
data: new IconThemeData(color: Theme.of(context).accentIconTheme.color),
child: widget.child,
),
),
),
elevation: 6.0,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_clearComputeNotch = Scaffold.setFloatingActionButtonNotchFor(context, _computeNotch);
}
@override
void deactivate() {
if (_clearComputeNotch != null)
_clearComputeNotch();
super.deactivate();
}
Path _computeNotch(Rect host, Rect guest, Offset start, Offset end) {
final Rect marginedGuest = guest.inflate(widget.notchMargin);
if (!host.overlaps(marginedGuest))
return new Path()..lineTo(end.dx, end.dy);
final Rect intersection = marginedGuest.intersect(host);
// We are computing a "V" shaped notch, as in this diagram:
// -----\**** /-----
// \ /
// \ /
// \ /
//
// "-" marks the top edge of the bottom app bar.
// "\" and "/" marks the notch outline
//
// notchToCenter is the horizontal distance between the guest's center and
// the host's top edge where the notch starts (marked with "*").
// We compute notchToCenter by similar triangles:
final double notchToCenter =
intersection.height * (marginedGuest.height / 2.0)
/ (marginedGuest.width / 2.0);
return new Path()
..lineTo(marginedGuest.center.dx - notchToCenter, host.top)
..lineTo(marginedGuest.left + marginedGuest.width / 2.0, marginedGuest.bottom)
..lineTo(marginedGuest.center.dx + notchToCenter, host.top)
..lineTo(end.dx, end.dy);
}
}
class _DiamondBorder extends ShapeBorder {
const _DiamondBorder();
@override
EdgeInsetsGeometry get dimensions {
return const EdgeInsets.only();
}
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return getOuterPath(rect, textDirection: textDirection);
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..moveTo(rect.left + rect.width / 2.0, rect.top)
..lineTo(rect.right, rect.top + rect.height / 2.0)
..lineTo(rect.left + rect.width / 2.0, rect.bottom)
..lineTo(rect.left, rect.top + rect.height / 2.0)
..close();
}
@override
void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {}
// This border doesn't support scaling.
@override
ShapeBorder scale(double t) {
return null;
}
}
// Places the Floating Action Button at the top of the content area of the
// app, on the border between the body and the app bar.
class _StartTopFloatingActionButtonLocation extends FloatingActionButtonLocation {
const _StartTopFloatingActionButtonLocation();
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
// First, we'll place the X coordinate for the Floating Action Button
// at the start of the screen, based on the text direction.
double fabX;
assert(scaffoldGeometry.textDirection != null);
switch (scaffoldGeometry.textDirection) {
case TextDirection.rtl:
// In RTL layouts, the start of the screen is on the right side,
// and the end of the screen is on the left.
//
// We need to align the right edge of the floating action button with
// the right edge of the screen, then move it inwards by the designated padding.
//
// The Scaffold's origin is at its top-left, so we need to offset fabX
// by the Scaffold's width to get the right edge of the screen.
//
// The Floating Action Button's origin is at its top-left, so we also need
// to subtract the Floating Action Button's width to align the right edge
// of the Floating Action Button instead of the left edge.
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.right;
fabX = scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width - startPadding;
break;
case TextDirection.ltr:
// In LTR layouts, the start of the screen is on the left side,
// and the end of the screen is on the right.
//
// Placing the fabX at 0.0 will align the left edge of the
// Floating Action Button with the left edge of the screen, so all
// we need to do is offset fabX by the designated padding.
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.left;
fabX = startPadding;
break;
}
// Finally, we'll place the Y coordinate for the Floating Action Button
// at the top of the content body.
//
// We want to place the middle of the Floating Action Button on the
// border between the Scaffold's app bar and its body. To do this,
// we place fabY at the scaffold geometry's contentTop, then subtract
// half of the Floating Action Button's height to place the center
// over the contentTop.
//
// We don't have to worry about which way is the top like we did
// for left and right, so we place fabY in this one-liner.
final double fabY = scaffoldGeometry.contentTop - (scaffoldGeometry.floatingActionButtonSize.height / 2.0);
return new Offset(fabX, fabY);
}
}