// 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'; import '../../gallery/demo.dart'; class BottomAppBarDemo extends StatefulWidget { static const String routeName = '/material/bottom_app_bar'; @override State createState() => _BottomAppBarDemoState(); } // Flutter generally frowns upon abbrevation however this class uses two // abbrevations extensively: "fab" for floating action button, and "bab" // for bottom application bar. class _BottomAppBarDemoState extends State { static final GlobalKey _scaffoldKey = GlobalKey(); // FAB shape static const _ChoiceValue kNoFab = _ChoiceValue( title: 'None', label: 'do not show a floating action button', value: null, ); static const _ChoiceValue kCircularFab = _ChoiceValue( title: 'Circular', label: 'circular floating action button', value: FloatingActionButton( onPressed: _showSnackbar, child: Icon(Icons.add, semanticLabel: 'Action'), backgroundColor: Colors.orange, ), ); static const _ChoiceValue kDiamondFab = _ChoiceValue( title: 'Diamond', label: 'diamond shape floating action button', value: _DiamondFab( onPressed: _showSnackbar, child: Icon(Icons.add, semanticLabel: 'Action'), ), ); // Notch static const _ChoiceValue kShowNotchTrue = _ChoiceValue( title: 'On', label: 'show bottom appbar notch', value: true, ); static const _ChoiceValue kShowNotchFalse = _ChoiceValue( title: 'Off', label: 'do not show bottom appbar notch', value: false, ); // FAB Position static const _ChoiceValue kFabEndDocked = _ChoiceValue( title: 'Attached - End', label: 'floating action button is docked at the end of the bottom app bar', value: FloatingActionButtonLocation.endDocked, ); static const _ChoiceValue kFabCenterDocked = _ChoiceValue( title: 'Attached - Center', label: 'floating action button is docked at the center of the bottom app bar', value: FloatingActionButtonLocation.centerDocked, ); static const _ChoiceValue kFabEndFloat= _ChoiceValue( title: 'Free - End', label: 'floating action button floats above the end of the bottom app bar', value: FloatingActionButtonLocation.endFloat, ); static const _ChoiceValue kFabCenterFloat = _ChoiceValue( 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.'; _scaffoldKey.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 _fabShape = kCircularFab; _ChoiceValue _showNotch = kShowNotchTrue; _ChoiceValue _fabLocation = kFabEndDocked; Color _babColor = kBabColors.first.color; void _onShowNotchChanged(_ChoiceValue value) { setState(() { _showNotch = value; }); } void _onFabShapeChanged(_ChoiceValue value) { setState(() { _fabShape = value; }); } void _onFabLocationChanged(_ChoiceValue value) { setState(() { _fabLocation = value; }); } void _onBabColorChanged(Color value) { setState(() { _babColor = value; }); } @override Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, appBar: AppBar( title: const Text('Bottom app bar'), elevation: 0.0, actions: [ 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( padding: const EdgeInsets.only(bottom: 88.0), children: [ const _Heading('FAB Shape'), _RadioItem(kCircularFab, _fabShape, _onFabShapeChanged), _RadioItem(kDiamondFab, _fabShape, _onFabShapeChanged), _RadioItem(kNoFab, _fabShape, _onFabShapeChanged), const Divider(), const _Heading('Notch'), _RadioItem(kShowNotchTrue, _showNotch, _onShowNotchChanged), _RadioItem(kShowNotchFalse, _showNotch, _onShowNotchChanged), const Divider(), const _Heading('FAB Position'), _RadioItem(kFabEndDocked, _fabLocation, _onFabLocationChanged), _RadioItem(kFabCenterDocked, _fabLocation, _onFabLocationChanged), _RadioItem(kFabEndFloat, _fabLocation, _onFabLocationChanged), _RadioItem(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 { 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 extends StatelessWidget { const _RadioItem(this.value, this.groupValue, this.onChanged); final _ChoiceValue value; final _ChoiceValue groupValue; final ValueChanged<_ChoiceValue> 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: [ Radio<_ChoiceValue>( 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.subhead, ), ), ), ), ], ), ), ); } } 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 onChanged; @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: colors.map((_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.body1.copyWith( color: theme.primaryColor, ), ), ); } } class _DemoBottomAppBar extends StatelessWidget { const _DemoBottomAppBar({ this.color, this.fabLocation, this.shape, }); final Color color; final FloatingActionButtonLocation fabLocation; final NotchedShape shape; static final List kCenterLocations = [ FloatingActionButtonLocation.centerDocked, FloatingActionButtonLocation.centerFloat, ]; @override Widget build(BuildContext context) { return BottomAppBar( color: color, shape: shape, child: Row(children: [ IconButton( icon: const Icon(Icons.menu, semanticLabel: 'Show bottom sheet'), onPressed: () { showModalBottomSheet( 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: () { Scaffold.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: () { Scaffold.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 [ 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, child: InkWell( onTap: onPressed, child: Container( width: 56.0, height: 56.0, child: IconTheme.merge( data: IconThemeData(color: Theme.of(context).accentIconTheme.color), child: child, ), ), ), elevation: 6.0, ); } } 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 const EdgeInsets.only(); } @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 null; } }