| // 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); |
| } |
| } |