| // 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/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'bottom_app_bar_theme.dart'; |
| import 'elevation_overlay.dart'; |
| import 'material.dart'; |
| import 'scaffold.dart'; |
| import 'theme.dart'; |
| |
| // Examples can assume: |
| // late Widget bottomAppBarContents; |
| |
| /// A container that is typically used with [Scaffold.bottomNavigationBar], and |
| /// can have a notch along the top that makes room for an overlapping |
| /// [FloatingActionButton]. |
| /// |
| /// Typically used with a [Scaffold] and a [FloatingActionButton]. |
| /// |
| /// {@tool snippet} |
| /// ```dart |
| /// Scaffold( |
| /// bottomNavigationBar: BottomAppBar( |
| /// color: Colors.white, |
| /// child: bottomAppBarContents, |
| /// ), |
| /// floatingActionButton: const FloatingActionButton(onPressed: null), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// {@tool dartpad --template=freeform} |
| /// This example shows the [BottomAppBar], which can be configured to have a notch using the |
| /// [BottomAppBar.shape] property. This also includes an optional [FloatingActionButton], which illustrates |
| /// the [FloatingActionButtonLocation]s in relation to the [BottomAppBar]. |
| /// ```dart imports |
| /// import 'package:flutter/material.dart'; |
| /// ``` |
| /// |
| /// ```dart |
| /// void main() { |
| /// runApp(const BottomAppBarDemo()); |
| /// } |
| /// |
| /// class BottomAppBarDemo extends StatefulWidget { |
| /// const BottomAppBarDemo({Key? key}) : super(key: key); |
| /// |
| /// @override |
| /// State createState() => _BottomAppBarDemoState(); |
| /// } |
| /// |
| /// class _BottomAppBarDemoState extends State<BottomAppBarDemo> { |
| /// bool _showFab = true; |
| /// bool _showNotch = true; |
| /// FloatingActionButtonLocation _fabLocation = FloatingActionButtonLocation.endDocked; |
| /// |
| /// void _onShowNotchChanged(bool value) { |
| /// setState(() { |
| /// _showNotch = value; |
| /// }); |
| /// } |
| /// |
| /// void _onShowFabChanged(bool value) { |
| /// setState(() { |
| /// _showFab = value; |
| /// }); |
| /// } |
| /// |
| /// void _onFabLocationChanged(FloatingActionButtonLocation? value) { |
| /// setState(() { |
| /// _fabLocation = value ?? FloatingActionButtonLocation.endDocked; |
| /// }); |
| /// } |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return MaterialApp( |
| /// home: Scaffold( |
| /// appBar: AppBar( |
| /// automaticallyImplyLeading: false, |
| /// title: const Text('Bottom App Bar Demo'), |
| /// ), |
| /// body: ListView( |
| /// padding: const EdgeInsets.only(bottom: 88), |
| /// children: <Widget>[ |
| /// SwitchListTile( |
| /// title: const Text( |
| /// 'Floating Action Button', |
| /// ), |
| /// value: _showFab, |
| /// onChanged: _onShowFabChanged, |
| /// ), |
| /// SwitchListTile( |
| /// title: const Text('Notch'), |
| /// value: _showNotch, |
| /// onChanged: _onShowNotchChanged, |
| /// ), |
| /// const Padding( |
| /// padding: EdgeInsets.all(16), |
| /// child: Text('Floating action button position'), |
| /// ), |
| /// RadioListTile<FloatingActionButtonLocation>( |
| /// title: const Text('Docked - End'), |
| /// value: FloatingActionButtonLocation.endDocked, |
| /// groupValue: _fabLocation, |
| /// onChanged: _onFabLocationChanged, |
| /// ), |
| /// RadioListTile<FloatingActionButtonLocation>( |
| /// title: const Text('Docked - Center'), |
| /// value: FloatingActionButtonLocation.centerDocked, |
| /// groupValue: _fabLocation, |
| /// onChanged: _onFabLocationChanged, |
| /// ), |
| /// RadioListTile<FloatingActionButtonLocation>( |
| /// title: const Text('Floating - End'), |
| /// value: FloatingActionButtonLocation.endFloat, |
| /// groupValue: _fabLocation, |
| /// onChanged: _onFabLocationChanged, |
| /// ), |
| /// RadioListTile<FloatingActionButtonLocation>( |
| /// title: const Text('Floating - Center'), |
| /// value: FloatingActionButtonLocation.centerFloat, |
| /// groupValue: _fabLocation, |
| /// onChanged: _onFabLocationChanged, |
| /// ), |
| /// ], |
| /// ), |
| /// floatingActionButton: _showFab |
| /// ? FloatingActionButton( |
| /// onPressed: () {}, |
| /// child: const Icon(Icons.add), |
| /// tooltip: 'Create', |
| /// ) |
| /// : null, |
| /// floatingActionButtonLocation: _fabLocation, |
| /// bottomNavigationBar: _DemoBottomAppBar( |
| /// fabLocation: _fabLocation, |
| /// shape: _showNotch ? const CircularNotchedRectangle() : null, |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// |
| /// class _DemoBottomAppBar extends StatelessWidget { |
| /// const _DemoBottomAppBar({ |
| /// this.fabLocation = FloatingActionButtonLocation.endDocked, |
| /// this.shape = const CircularNotchedRectangle(), |
| /// }); |
| /// |
| /// final FloatingActionButtonLocation fabLocation; |
| /// final NotchedShape? shape; |
| /// |
| /// static final List<FloatingActionButtonLocation> centerLocations = <FloatingActionButtonLocation>[ |
| /// FloatingActionButtonLocation.centerDocked, |
| /// FloatingActionButtonLocation.centerFloat, |
| /// ]; |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return BottomAppBar( |
| /// shape: shape, |
| /// color: Colors.blue, |
| /// child: IconTheme( |
| /// data: IconThemeData(color: Theme.of(context).colorScheme.onPrimary), |
| /// child: Row( |
| /// children: <Widget>[ |
| /// IconButton( |
| /// tooltip: 'Open navigation menu', |
| /// icon: const Icon(Icons.menu), |
| /// onPressed: () {}, |
| /// ), |
| /// if (centerLocations.contains(fabLocation)) const Spacer(), |
| /// IconButton( |
| /// tooltip: 'Search', |
| /// icon: const Icon(Icons.search), |
| /// onPressed: () {}, |
| /// ), |
| /// IconButton( |
| /// tooltip: 'Favorite', |
| /// icon: const Icon(Icons.favorite), |
| /// onPressed: () {}, |
| /// ), |
| /// ], |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [NotchedShape] which calculates the notch for a notched [BottomAppBar]. |
| /// * [FloatingActionButton] which the [BottomAppBar] makes a notch for. |
| /// * [AppBar] for a toolbar that is shown at the top of the screen. |
| class BottomAppBar extends StatefulWidget { |
| /// Creates a bottom application bar. |
| /// |
| /// The [clipBehavior] argument defaults to [Clip.none] and must not be null. |
| /// Additionally, [elevation] must be non-negative. |
| /// |
| /// If [color], [elevation], or [shape] are null, their [BottomAppBarTheme] values will be used. |
| /// If the corresponding [BottomAppBarTheme] property is null, then the default |
| /// specified in the property's documentation will be used. |
| const BottomAppBar({ |
| Key? key, |
| this.color, |
| this.elevation, |
| this.shape, |
| this.clipBehavior = Clip.none, |
| this.notchMargin = 4.0, |
| this.child, |
| }) : assert(elevation == null || elevation >= 0.0), |
| assert(notchMargin != null), |
| assert(clipBehavior != null), |
| super(key: key); |
| |
| /// The widget below this widget in the tree. |
| /// |
| /// {@macro flutter.widgets.ProxyWidget.child} |
| /// |
| /// Typically this the child will be a [Row], with the first child |
| /// being an [IconButton] with the [Icons.menu] icon. |
| final Widget? child; |
| |
| /// The bottom app bar's background color. |
| /// |
| /// If this property is null then [BottomAppBarTheme.color] of |
| /// [ThemeData.bottomAppBarTheme] is used. If that's null then |
| /// [ThemeData.bottomAppBarColor] is used. |
| final Color? color; |
| |
| /// The z-coordinate at which to place this bottom app bar relative to its |
| /// parent. |
| /// |
| /// This controls the size of the shadow below the bottom app bar. The |
| /// value is non-negative. |
| /// |
| /// If this property is null then [BottomAppBarTheme.elevation] of |
| /// [ThemeData.bottomAppBarTheme] is used. If that's null, the default value |
| /// is 8. |
| final double? elevation; |
| |
| /// The notch that is made for the floating action button. |
| /// |
| /// If this property is null then [BottomAppBarTheme.shape] of |
| /// [ThemeData.bottomAppBarTheme] is used. If that's null then the shape will |
| /// be rectangular with no notch. |
| final NotchedShape? shape; |
| |
| /// {@macro flutter.material.Material.clipBehavior} |
| /// |
| /// Defaults to [Clip.none], and must not be null. |
| final Clip clipBehavior; |
| |
| /// The margin between the [FloatingActionButton] and the [BottomAppBar]'s |
| /// notch. |
| /// |
| /// Not used if [shape] is null. |
| final double notchMargin; |
| |
| @override |
| State createState() => _BottomAppBarState(); |
| } |
| |
| class _BottomAppBarState extends State<BottomAppBar> { |
| late ValueListenable<ScaffoldGeometry> geometryListenable; |
| final GlobalKey materialKey = GlobalKey(); |
| static const double _defaultElevation = 8.0; |
| |
| @override |
| void didChangeDependencies() { |
| super.didChangeDependencies(); |
| geometryListenable = Scaffold.geometryOf(context); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final BottomAppBarTheme babTheme = BottomAppBarTheme.of(context); |
| final NotchedShape? notchedShape = widget.shape ?? babTheme.shape; |
| final CustomClipper<Path> clipper = notchedShape != null |
| ? _BottomAppBarClipper( |
| geometry: geometryListenable, |
| shape: notchedShape, |
| materialKey: materialKey, |
| notchMargin: widget.notchMargin, |
| ) |
| : const ShapeBorderClipper(shape: RoundedRectangleBorder()); |
| final double elevation = widget.elevation ?? babTheme.elevation ?? _defaultElevation; |
| final Color color = widget.color ?? babTheme.color ?? Theme.of(context).bottomAppBarColor; |
| final Color effectiveColor = ElevationOverlay.applyOverlay(context, color, elevation); |
| return PhysicalShape( |
| clipper: clipper, |
| elevation: elevation, |
| color: effectiveColor, |
| clipBehavior: widget.clipBehavior, |
| child: Material( |
| key: materialKey, |
| type: MaterialType.transparency, |
| child: widget.child == null |
| ? null |
| : SafeArea(child: widget.child!), |
| ), |
| ); |
| } |
| } |
| |
| class _BottomAppBarClipper extends CustomClipper<Path> { |
| const _BottomAppBarClipper({ |
| required this.geometry, |
| required this.shape, |
| required this.materialKey, |
| required this.notchMargin, |
| }) : assert(geometry != null), |
| assert(shape != null), |
| assert(notchMargin != null), |
| super(reclip: geometry); |
| |
| final ValueListenable<ScaffoldGeometry> geometry; |
| final NotchedShape shape; |
| final GlobalKey materialKey; |
| final double notchMargin; |
| |
| // Returns the top of the BottomAppBar in global coordinates. |
| double get bottomNavigationBarTop { |
| final RenderBox? box = materialKey.currentContext?.findRenderObject() as RenderBox?; |
| return box?.localToGlobal(Offset.zero).dy ?? 0; |
| } |
| |
| @override |
| Path getClip(Size size) { |
| // button is the floating action button's bounding rectangle in the |
| // coordinate system whose origin is at the appBar's top left corner, |
| // or null if there is no floating action button. |
| final Rect? button = geometry.value.floatingActionButtonArea?.translate(0.0, bottomNavigationBarTop * -1.0); |
| return shape.getOuterPath(Offset.zero & size, button?.inflate(notchMargin)); |
| } |
| |
| @override |
| bool shouldReclip(_BottomAppBarClipper oldClipper) { |
| return oldClipper.geometry != geometry |
| || oldClipper.shape != shape |
| || oldClipper.notchMargin != notchMargin; |
| } |
| } |