diff --git a/examples/flutter_gallery/lib/demo/material/bottom_app_bar_demo.dart b/examples/flutter_gallery/lib/demo/material/bottom_app_bar_demo.dart new file mode 100644 index 00000000000..49eb43bdc02 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/material/bottom_app_bar_demo.dart @@ -0,0 +1,500 @@ +// 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 { + // The key given to the Scaffold so that _showSnackbar can find it. + static final GlobalKey _scaffoldKey = new GlobalKey(); + + // 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; + + static const List babColors = const [ + 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: [ + new Text( + 'Floating action button', + style: Theme.of(context).textTheme.title, + ), + new Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(width: 96.0, + child: const Text('Shape: '), + ), + new Expanded(child: buildFabShapePicker()), + ], + ), + new Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox( + width: 96.0, + child: const Text('Location: '), + ), + new Expanded(child: 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 Padding( + padding: const EdgeInsets.all(8.0), + child: new RaisedButton( + child: const Text('Change'), + onPressed: () { + setState(() { + fabShapeIndex = (fabShapeIndex + 1) % _fabShapeConfigurations.length; + }); + }, + ), + ); + } + + Widget buildFabLocationPicker() { + return 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 colors = [ + const Text('Color:'), + ]; + for (Color color in babColors) { + colors.add( + new Radio( + value: color, + groupValue: babColor, + onChanged: (Color color) { + setState(() { + babColor = color; + }); + }, + ), + ); + colors.add( + 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), + ), + ); + colors.add(const Padding(padding: const EdgeInsets.only(left: 12.0))); + } + return 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) { + final bool showsFirst = babMode == _BabMode.END_FAB; + return new BottomAppBar( + color: color, + hasNotch: enableNotch, + child: new Row( + children: [ + new IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + showModalBottomSheet(context: context, builder: (BuildContext context) => const _DemoDrawer()); }, + ), + new Expanded( + child: new AnimatedCrossFade( + duration: const Duration(milliseconds: 225), + firstChild: buildBabContents(context, _BabMode.END_FAB), + firstCurve: showsFirst ? fadeOutCurve : fadeInCurve, + secondChild: buildBabContents(context, _BabMode.CENTER_FAB), + secondCurve: showsFirst ? fadeInCurve : fadeOutCurve, + crossFadeState: showsFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond, + ), + ), + ], + ), + ); + } + + Widget buildBabContents(BuildContext context, _BabMode babMode) { + final List rowContents = []; + if (babMode == _BabMode.CENTER_FAB) { + rowContents.add( + new Expanded( + child: new ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 0.0), + ), + ), + ); + } + rowContents.addAll( [ + new IconButton( + icon: const Icon(Icons.search), + onPressed: () {}, + ), + new IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () {}, + ) + ]); + 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 [ + 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); + } +} diff --git a/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart b/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart deleted file mode 100644 index 1f265aae374..00000000000 --- a/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart +++ /dev/null @@ -1,147 +0,0 @@ -// 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/material.dart'; - -const String _explanatoryText = - "When the Scaffold's floating action button location changes, " - 'the floating action button animates to its new position'; - -class FabMotionDemo extends StatefulWidget { - static const String routeName = '/material/fab-motion'; - - @override - _FabMotionDemoState createState() { - return new _FabMotionDemoState(); - } -} - -class _FabMotionDemoState extends State { - static const List _floatingActionButtonLocations = const [ - FloatingActionButtonLocation.endFloat, - FloatingActionButtonLocation.centerFloat, - const _StartTopFloatingActionButtonLocation(), - ]; - - bool _showFab = true; - FloatingActionButtonLocation _floatingActionButtonLocation = FloatingActionButtonLocation.endFloat; - - @override - Widget build(BuildContext context) { - final Widget floatingActionButton = _showFab - ? new Builder(builder: (BuildContext context) { - // We use a widget builder here so that this inner context can find the Scaffold. - // This makes it possible to show the snackbar. - return new FloatingActionButton( - backgroundColor: Colors.yellow.shade900, - onPressed: () => _showSnackbar(context), - child: const Icon(Icons.add), - ); - }) - : null; - return new Scaffold( - appBar: new AppBar( - title: const Text('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(), - ), - ), - floatingActionButtonLocation: _floatingActionButtonLocation, - floatingActionButton: floatingActionButton, - body: new Center( - child: new Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - new RaisedButton( - onPressed: _moveFab, - child: const Text('MOVE FAB'), - ), - new Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Toggle FAB'), - new Switch(value: _showFab, onChanged: _toggleFab), - ], - ), - ], - ), - ), - ); - } - - void _moveFab() { - setState(() { - _floatingActionButtonLocation = _floatingActionButtonLocations[(_floatingActionButtonLocations.indexOf(_floatingActionButtonLocation) + 1) % _floatingActionButtonLocations.length]; - }); - } - - void _toggleFab(bool showFab) { - setState(() { - _showFab = showFab; - }); - } - - void _showSnackbar(BuildContext context) { - Scaffold.of(context).showSnackBar(const SnackBar(content: const Text(_explanatoryText))); - } -} - -// 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); - } -} diff --git a/examples/flutter_gallery/lib/demo/material/material.dart b/examples/flutter_gallery/lib/demo/material/material.dart index 1aa45d900b5..723fbfe91ee 100644 --- a/examples/flutter_gallery/lib/demo/material/material.dart +++ b/examples/flutter_gallery/lib/demo/material/material.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. export 'backdrop_demo.dart'; +export 'bottom_app_bar_demo.dart'; export 'bottom_navigation_demo.dart'; export 'buttons_demo.dart'; export 'cards_demo.dart'; @@ -12,7 +13,6 @@ export 'date_and_time_picker_demo.dart'; export 'dialog_demo.dart'; export 'drawer_demo.dart'; export 'expansion_panels_demo.dart'; -export 'fab_motion_demo.dart'; export 'grid_list_demo.dart'; export 'icons_demo.dart'; export 'leave_behind_demo.dart'; diff --git a/examples/flutter_gallery/lib/gallery/item.dart b/examples/flutter_gallery/lib/gallery/item.dart index a9707da6133..ddb8e82a786 100644 --- a/examples/flutter_gallery/lib/gallery/item.dart +++ b/examples/flutter_gallery/lib/gallery/item.dart @@ -88,6 +88,13 @@ List _buildGalleryItems() { routeName: BackdropDemo.routeName, buildRoute: (BuildContext context) => new BackdropDemo(), ), + new GalleryItem( + title: 'Bottom App Bar', + subtitle: 'With repositionable floating action button', + category: 'Material Components', + routeName: BottomAppBarDemo.routeName, + buildRoute: (BuildContext context) => new BottomAppBarDemo(), + ), new GalleryItem( title: 'Bottom navigation', subtitle: 'Bottom navigation with cross-fading views', @@ -165,13 +172,6 @@ List _buildGalleryItems() { routeName: TabsFabDemo.routeName, buildRoute: (BuildContext context) => new TabsFabDemo(), ), - new GalleryItem( - title: 'Floating action button motion', - subtitle: 'Action buttons with customized positions', - category: 'Material Components', - routeName: FabMotionDemo.routeName, - buildRoute: (BuildContext context) => new FabMotionDemo(), - ), new GalleryItem( title: 'Grid', subtitle: 'Row and column layout', diff --git a/packages/flutter/lib/src/material/floating_action_button_location.dart b/packages/flutter/lib/src/material/floating_action_button_location.dart index 8e139cb0aef..f18d3bed479 100644 --- a/packages/flutter/lib/src/material/floating_action_button_location.dart +++ b/packages/flutter/lib/src/material/floating_action_button_location.dart @@ -67,7 +67,7 @@ abstract class FloatingActionButtonLocation { /// /// This is unlikely to be a useful location for apps that lack a bottom /// navigation bar. - static FloatingActionButtonLocation endDocked = const _EndDockedFloatingActionButtonLocation(); + static const FloatingActionButtonLocation endDocked = const _EndDockedFloatingActionButtonLocation(); /// Center-aligned [FloatingActionButton], floating over the /// [Scaffold.bottomNavigationBar] so that the center of the floating @@ -79,7 +79,7 @@ abstract class FloatingActionButtonLocation { /// /// This is unlikely to be a useful location for apps that lack a bottom /// navigation bar. - static FloatingActionButtonLocation centerDocked = const _CenterDockedFloatingActionButtonLocation(); + static const FloatingActionButtonLocation centerDocked = const _CenterDockedFloatingActionButtonLocation(); /// Places the [FloatingActionButton] based on the [Scaffold]'s layout. ///