| // Copyright 2014 The Flutter 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 '../../gallery/demo.dart'; |
| |
| class BottomAppBarDemo extends StatefulWidget { |
| const BottomAppBarDemo({super.key}); |
| |
| static const String routeName = '/material/bottom_app_bar'; |
| |
| @override |
| State createState() => _BottomAppBarDemoState(); |
| } |
| |
| // Flutter generally frowns upon abbreviation however this class uses two |
| // abbreviations extensively: "fab" for floating action button, and "bab" |
| // for bottom application bar. |
| |
| class _BottomAppBarDemoState extends State<BottomAppBarDemo> { |
| static final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); |
| |
| // FAB shape |
| |
| static const _ChoiceValue<Widget> kNoFab = _ChoiceValue<Widget>( |
| title: 'None', |
| label: 'do not show a floating action button', |
| ); |
| |
| static const _ChoiceValue<Widget> kCircularFab = _ChoiceValue<Widget>( |
| title: 'Circular', |
| label: 'circular floating action button', |
| value: FloatingActionButton( |
| onPressed: _showSnackbar, |
| backgroundColor: Colors.orange, |
| child: Icon(Icons.add, semanticLabel: 'Action'), |
| ), |
| ); |
| |
| static const _ChoiceValue<Widget> kDiamondFab = _ChoiceValue<Widget>( |
| title: 'Diamond', |
| label: 'diamond shape floating action button', |
| value: _DiamondFab( |
| onPressed: _showSnackbar, |
| child: Icon(Icons.add, semanticLabel: 'Action'), |
| ), |
| ); |
| |
| // Notch |
| |
| static const _ChoiceValue<bool> kShowNotchTrue = _ChoiceValue<bool>( |
| title: 'On', |
| label: 'show bottom appbar notch', |
| value: true, |
| ); |
| |
| static const _ChoiceValue<bool> kShowNotchFalse = _ChoiceValue<bool>( |
| title: 'Off', |
| label: 'do not show bottom appbar notch', |
| value: false, |
| ); |
| |
| // FAB Position |
| |
| static const _ChoiceValue<FloatingActionButtonLocation> kFabEndDocked = _ChoiceValue<FloatingActionButtonLocation>( |
| title: 'Attached - End', |
| label: 'floating action button is docked at the end of the bottom app bar', |
| value: FloatingActionButtonLocation.endDocked, |
| ); |
| |
| static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterDocked = _ChoiceValue<FloatingActionButtonLocation>( |
| title: 'Attached - Center', |
| label: 'floating action button is docked at the center of the bottom app bar', |
| value: FloatingActionButtonLocation.centerDocked, |
| ); |
| |
| static const _ChoiceValue<FloatingActionButtonLocation> kFabEndFloat= _ChoiceValue<FloatingActionButtonLocation>( |
| title: 'Free - End', |
| label: 'floating action button floats above the end of the bottom app bar', |
| value: FloatingActionButtonLocation.endFloat, |
| ); |
| |
| static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterFloat = _ChoiceValue<FloatingActionButtonLocation>( |
| title: 'Free - Center', |
| label: 'floating action button is floats above the center of the bottom app bar', |
| value: FloatingActionButtonLocation.centerFloat, |
| ); |
| |
| static void _showSnackbar() { |
| const String text = |
| "When the Scaffold's floating action button location changes, " |
| 'the floating action button animates to its new position. ' |
| 'The BottomAppBar adapts its shape appropriately.'; |
| _scaffoldMessengerKey.currentState!.showSnackBar( |
| const SnackBar(content: Text(text)), |
| ); |
| } |
| |
| // App bar color |
| |
| static const List<_NamedColor> kBabColors = <_NamedColor>[ |
| _NamedColor(null, 'Clear'), |
| _NamedColor(Color(0xFFFFC100), 'Orange'), |
| _NamedColor(Color(0xFF91FAFF), 'Light Blue'), |
| _NamedColor(Color(0xFF00D1FF), 'Cyan'), |
| _NamedColor(Color(0xFF00BCFF), 'Cerulean'), |
| _NamedColor(Color(0xFF009BEE), 'Blue'), |
| ]; |
| |
| _ChoiceValue<Widget> _fabShape = kCircularFab; |
| _ChoiceValue<bool> _showNotch = kShowNotchTrue; |
| _ChoiceValue<FloatingActionButtonLocation> _fabLocation = kFabEndDocked; |
| Color? _babColor = kBabColors.first.color; |
| |
| void _onShowNotchChanged(_ChoiceValue<bool>? value) { |
| setState(() { |
| _showNotch = value!; |
| }); |
| } |
| |
| void _onFabShapeChanged(_ChoiceValue<Widget>? value) { |
| setState(() { |
| _fabShape = value!; |
| }); |
| } |
| |
| void _onFabLocationChanged(_ChoiceValue<FloatingActionButtonLocation>? value) { |
| setState(() { |
| _fabLocation = value!; |
| }); |
| } |
| |
| void _onBabColorChanged(Color? value) { |
| setState(() { |
| _babColor = value; |
| }); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return ScaffoldMessenger( |
| key: _scaffoldMessengerKey, |
| child: Builder( |
| builder: (BuildContext context) => Scaffold( |
| appBar: AppBar( |
| title: const Text('Bottom app bar'), |
| elevation: 0.0, |
| actions: <Widget>[ |
| MaterialDemoDocumentationButton(BottomAppBarDemo.routeName), |
| IconButton( |
| icon: const Icon(Icons.sentiment_very_satisfied, semanticLabel: 'Update shape'), |
| onPressed: () { |
| setState(() { |
| _fabShape = _fabShape == kCircularFab ? kDiamondFab : kCircularFab; |
| }); |
| }, |
| ), |
| ], |
| ), |
| body: Scrollbar( |
| child: ListView( |
| primary: true, |
| padding: const EdgeInsets.only(bottom: 88.0), |
| children: <Widget>[ |
| const _Heading('FAB Shape'), |
| |
| _RadioItem<Widget>(kCircularFab, _fabShape, _onFabShapeChanged), |
| _RadioItem<Widget>(kDiamondFab, _fabShape, _onFabShapeChanged), |
| _RadioItem<Widget>(kNoFab, _fabShape, _onFabShapeChanged), |
| |
| const Divider(), |
| const _Heading('Notch'), |
| |
| _RadioItem<bool>(kShowNotchTrue, _showNotch, _onShowNotchChanged), |
| _RadioItem<bool>(kShowNotchFalse, _showNotch, _onShowNotchChanged), |
| |
| const Divider(), |
| const _Heading('FAB Position'), |
| |
| _RadioItem<FloatingActionButtonLocation>(kFabEndDocked, _fabLocation, _onFabLocationChanged), |
| _RadioItem<FloatingActionButtonLocation>(kFabCenterDocked, _fabLocation, _onFabLocationChanged), |
| _RadioItem<FloatingActionButtonLocation>(kFabEndFloat, _fabLocation, _onFabLocationChanged), |
| _RadioItem<FloatingActionButtonLocation>(kFabCenterFloat, _fabLocation, _onFabLocationChanged), |
| |
| const Divider(), |
| const _Heading('App bar color'), |
| |
| _ColorsItem(kBabColors, _babColor, _onBabColorChanged), |
| ], |
| ), |
| ), |
| floatingActionButton: _fabShape.value, |
| floatingActionButtonLocation: _fabLocation.value, |
| bottomNavigationBar: _DemoBottomAppBar( |
| color: _babColor, |
| fabLocation: _fabLocation.value, |
| shape: _selectNotch(), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| NotchedShape? _selectNotch() { |
| if (!_showNotch.value!) { |
| return null; |
| } |
| if (_fabShape == kCircularFab) { |
| return const CircularNotchedRectangle(); |
| } |
| if (_fabShape == kDiamondFab) { |
| return const _DiamondNotchedRectangle(); |
| } |
| return null; |
| } |
| } |
| |
| class _ChoiceValue<T> { |
| const _ChoiceValue({ this.value, this.title, this.label }); |
| |
| final T? value; |
| final String? title; |
| final String? label; // For the Semantics widget that contains title |
| |
| @override |
| String toString() => '$runtimeType("$title")'; |
| } |
| |
| class _RadioItem<T> extends StatelessWidget { |
| const _RadioItem(this.value, this.groupValue, this.onChanged); |
| |
| final _ChoiceValue<T> value; |
| final _ChoiceValue<T> groupValue; |
| final ValueChanged<_ChoiceValue<T>?> onChanged; |
| |
| @override |
| Widget build(BuildContext context) { |
| final ThemeData theme = Theme.of(context); |
| return Container( |
| height: 56.0, |
| padding: const EdgeInsetsDirectional.only(start: 16.0), |
| alignment: AlignmentDirectional.centerStart, |
| child: MergeSemantics( |
| child: Row( |
| children: <Widget>[ |
| Radio<_ChoiceValue<T>>( |
| value: value, |
| groupValue: groupValue, |
| onChanged: onChanged, |
| ), |
| Expanded( |
| child: Semantics( |
| container: true, |
| button: true, |
| label: value.label, |
| child: GestureDetector( |
| behavior: HitTestBehavior.opaque, |
| onTap: () { |
| onChanged(value); |
| }, |
| child: Text( |
| value.title!, |
| style: theme.textTheme.titleMedium, |
| ), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _NamedColor { |
| const _NamedColor(this.color, this.name); |
| |
| final Color? color; |
| final String name; |
| } |
| |
| class _ColorsItem extends StatelessWidget { |
| const _ColorsItem(this.colors, this.selectedColor, this.onChanged); |
| |
| final List<_NamedColor> colors; |
| final Color? selectedColor; |
| final ValueChanged<Color?> onChanged; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Row( |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| children: colors.map<Widget>((_NamedColor namedColor) { |
| return RawMaterialButton( |
| onPressed: () { |
| onChanged(namedColor.color); |
| }, |
| constraints: const BoxConstraints.tightFor( |
| width: 32.0, |
| height: 32.0, |
| ), |
| fillColor: namedColor.color, |
| shape: CircleBorder( |
| side: BorderSide( |
| color: namedColor.color == selectedColor ? Colors.black : const Color(0xFFD5D7DA), |
| width: 2.0, |
| ), |
| ), |
| child: Semantics( |
| value: namedColor.name, |
| selected: namedColor.color == selectedColor, |
| ), |
| ); |
| }).toList(), |
| ); |
| } |
| } |
| |
| class _Heading extends StatelessWidget { |
| const _Heading(this.text); |
| |
| final String text; |
| |
| @override |
| Widget build(BuildContext context) { |
| final ThemeData theme = Theme.of(context); |
| return Container( |
| height: 48.0, |
| padding: const EdgeInsetsDirectional.only(start: 56.0), |
| alignment: AlignmentDirectional.centerStart, |
| child: Text( |
| text, |
| style: theme.textTheme.bodyLarge, |
| ), |
| ); |
| } |
| } |
| |
| class _DemoBottomAppBar extends StatelessWidget { |
| const _DemoBottomAppBar({ |
| this.color, |
| this.fabLocation, |
| this.shape, |
| }); |
| |
| final Color? color; |
| final FloatingActionButtonLocation? fabLocation; |
| final NotchedShape? shape; |
| |
| static final List<FloatingActionButtonLocation> kCenterLocations = <FloatingActionButtonLocation>[ |
| FloatingActionButtonLocation.centerDocked, |
| FloatingActionButtonLocation.centerFloat, |
| ]; |
| |
| @override |
| Widget build(BuildContext context) { |
| return BottomAppBar( |
| color: color, |
| shape: shape, |
| child: Row(children: <Widget>[ |
| IconButton( |
| icon: const Icon(Icons.menu, semanticLabel: 'Show bottom sheet'), |
| onPressed: () { |
| showModalBottomSheet<void>( |
| context: context, |
| builder: (BuildContext context) => const _DemoDrawer(), |
| ); |
| }, |
| ), |
| if (kCenterLocations.contains(fabLocation)) const Expanded(child: SizedBox()), |
| IconButton( |
| icon: const Icon(Icons.search, semanticLabel: 'show search action',), |
| onPressed: () { |
| ScaffoldMessenger.of(context).showSnackBar( |
| const SnackBar(content: Text('This is a dummy search action.')), |
| ); |
| }, |
| ), |
| IconButton( |
| icon: Icon( |
| Theme.of(context).platform == TargetPlatform.iOS |
| ? Icons.more_horiz |
| : Icons.more_vert, |
| semanticLabel: 'Show menu actions', |
| ), |
| onPressed: () { |
| ScaffoldMessenger.of(context).showSnackBar( |
| const SnackBar(content: Text('This is a dummy menu action.')), |
| ); |
| }, |
| ), |
| ]), |
| ); |
| } |
| } |
| |
| // A drawer that pops up from the bottom of the screen. |
| class _DemoDrawer extends StatelessWidget { |
| const _DemoDrawer(); |
| |
| @override |
| Widget build(BuildContext context) { |
| return Drawer( |
| child: Column( |
| children: const <Widget>[ |
| ListTile( |
| leading: Icon(Icons.search), |
| title: Text('Search'), |
| ), |
| ListTile( |
| leading: Icon(Icons.threed_rotation), |
| title: Text('3D'), |
| ), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| // A diamond-shaped floating action button. |
| class _DiamondFab extends StatelessWidget { |
| const _DiamondFab({ |
| this.child, |
| this.onPressed, |
| }); |
| |
| final Widget? child; |
| final VoidCallback? onPressed; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Material( |
| shape: const _DiamondBorder(), |
| color: Colors.orange, |
| elevation: 6.0, |
| child: InkWell( |
| onTap: onPressed, |
| child: SizedBox( |
| width: 56.0, |
| height: 56.0, |
| child: IconTheme.merge( |
| data: IconThemeData(color: Theme.of(context).colorScheme.secondary), |
| child: child!, |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _DiamondNotchedRectangle implements NotchedShape { |
| const _DiamondNotchedRectangle(); |
| |
| @override |
| Path getOuterPath(Rect host, Rect? guest) { |
| if (!host.overlaps(guest!)) { |
| return Path()..addRect(host); |
| } |
| assert(guest.width > 0.0); |
| |
| final Rect intersection = guest.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 * (guest.height / 2.0) |
| / (guest.width / 2.0); |
| |
| return Path() |
| ..moveTo(host.left, host.top) |
| ..lineTo(guest.center.dx - notchToCenter, host.top) |
| ..lineTo(guest.left + guest.width / 2.0, guest.bottom) |
| ..lineTo(guest.center.dx + notchToCenter, host.top) |
| ..lineTo(host.right, host.top) |
| ..lineTo(host.right, host.bottom) |
| ..lineTo(host.left, host.bottom) |
| ..close(); |
| } |
| } |
| |
| class _DiamondBorder extends ShapeBorder { |
| const _DiamondBorder(); |
| |
| @override |
| EdgeInsetsGeometry get dimensions { |
| return EdgeInsets.zero; |
| } |
| |
| @override |
| Path getInnerPath(Rect rect, { TextDirection? textDirection }) { |
| return getOuterPath(rect, textDirection: textDirection); |
| } |
| |
| @override |
| Path getOuterPath(Rect rect, { TextDirection? textDirection }) { |
| return 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 this; |
| } |
| } |