bottom_app_bar.dart 11.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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/widgets.dart';

8
import 'bottom_app_bar_theme.dart';
9
import 'elevation_overlay.dart';
10 11
import 'material.dart';
import 'scaffold.dart';
12
import 'theme.dart';
13 14

// Examples can assume:
15
// late Widget bottomAppBarContents;
16

17
/// A container that is typically used with [Scaffold.bottomNavigationBar], and
18 19 20 21 22
/// can have a notch along the top that makes room for an overlapping
/// [FloatingActionButton].
///
/// Typically used with a [Scaffold] and a [FloatingActionButton].
///
23
/// {@tool snippet}
24
/// ```dart
25 26
/// Scaffold(
///   bottomNavigationBar: BottomAppBar(
27 28 29
///     color: Colors.white,
///     child: bottomAppBarContents,
///   ),
30
///   floatingActionButton: const FloatingActionButton(onPressed: null),
31 32
/// )
/// ```
33
/// {@end-tool}
34
///
35 36 37 38 39 40 41 42 43 44
/// {@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() {
45
///   runApp(const BottomAppBarDemo());
46 47 48
/// }
///
/// class BottomAppBarDemo extends StatefulWidget {
49
///   const BottomAppBarDemo({Key? key}) : super(key: key);
50 51 52 53 54 55
///
///   @override
///   State createState() => _BottomAppBarDemoState();
/// }
///
/// class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
56 57 58
///   bool _showFab = true;
///   bool _showNotch = true;
///   FloatingActionButtonLocation _fabLocation = FloatingActionButtonLocation.endDocked;
59 60 61 62 63 64 65 66 67 68 69 70 71
///
///   void _onShowNotchChanged(bool value) {
///     setState(() {
///       _showNotch = value;
///     });
///   }
///
///   void _onShowFabChanged(bool value) {
///     setState(() {
///       _showFab = value;
///     });
///   }
///
72
///   void _onFabLocationChanged(FloatingActionButtonLocation? value) {
73
///     setState(() {
74
///       _fabLocation = value ?? FloatingActionButtonLocation.endDocked;
75 76 77 78 79 80 81 82 83
///     });
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return MaterialApp(
///       home: Scaffold(
///         appBar: AppBar(
///           automaticallyImplyLeading: false,
84
///           title: const Text('Bottom App Bar Demo'),
85 86 87
///         ),
///         body: ListView(
///           padding: const EdgeInsets.only(bottom: 88),
88
///           children: <Widget>[
89
///             SwitchListTile(
90 91
///               title: const Text(
///                 'Floating Action Button',
92 93 94 95 96
///               ),
///               value: _showFab,
///               onChanged: _onShowFabChanged,
///             ),
///             SwitchListTile(
97
///               title: const Text('Notch'),
98 99 100
///               value: _showNotch,
///               onChanged: _onShowNotchChanged,
///             ),
101 102 103
///             const Padding(
///               padding: EdgeInsets.all(16),
///               child: Text('Floating action button position'),
104 105
///             ),
///             RadioListTile<FloatingActionButtonLocation>(
106
///               title: const Text('Docked - End'),
107 108 109 110 111
///               value: FloatingActionButtonLocation.endDocked,
///               groupValue: _fabLocation,
///               onChanged: _onFabLocationChanged,
///             ),
///             RadioListTile<FloatingActionButtonLocation>(
112
///               title: const Text('Docked - Center'),
113 114 115 116 117
///               value: FloatingActionButtonLocation.centerDocked,
///               groupValue: _fabLocation,
///               onChanged: _onFabLocationChanged,
///             ),
///             RadioListTile<FloatingActionButtonLocation>(
118
///               title: const Text('Floating - End'),
119 120 121 122 123
///               value: FloatingActionButtonLocation.endFloat,
///               groupValue: _fabLocation,
///               onChanged: _onFabLocationChanged,
///             ),
///             RadioListTile<FloatingActionButtonLocation>(
124
///               title: const Text('Floating - Center'),
125 126 127 128 129 130 131 132 133 134
///               value: FloatingActionButtonLocation.centerFloat,
///               groupValue: _fabLocation,
///               onChanged: _onFabLocationChanged,
///             ),
///           ],
///         ),
///         floatingActionButton: _showFab
///             ? FloatingActionButton(
///                 onPressed: () {},
///                 child: const Icon(Icons.add),
135
///                 tooltip: 'Create',
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
///               )
///             : 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;
155
///   final NotchedShape? shape;
156
///
157
///   static final List<FloatingActionButtonLocation> centerLocations = <FloatingActionButtonLocation>[
158 159 160 161 162 163 164 165 166 167 168 169
///     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(
170
///           children: <Widget>[
171 172 173 174 175 176 177
///             IconButton(
///               tooltip: 'Open navigation menu',
///               icon: const Icon(Icons.menu),
///               onPressed: () {},
///             ),
///             if (centerLocations.contains(fabLocation)) const Spacer(),
///             IconButton(
178
///               tooltip: 'Search',
179 180 181 182
///               icon: const Icon(Icons.search),
///               onPressed: () {},
///             ),
///             IconButton(
183
///               tooltip: 'Favorite',
184 185 186 187 188 189 190 191 192 193 194 195 196
///               icon: const Icon(Icons.favorite),
///               onPressed: () {},
///             ),
///           ],
///         ),
///       ),
///     );
///   }
/// }
///
/// ```
/// {@end-tool}
///
197 198
/// See also:
///
Dan Field's avatar
Dan Field committed
199
///  * [NotchedShape] which calculates the notch for a notched [BottomAppBar].
200 201 202 203 204
///  * [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.
  ///
205
  /// The [clipBehavior] argument defaults to [Clip.none] and must not be null.
206
  /// Additionally, [elevation] must be non-negative.
207 208 209 210
  ///
  /// 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.
211
  const BottomAppBar({
212
    Key? key,
213
    this.color,
214
    this.elevation,
215
    this.shape,
216
    this.clipBehavior = Clip.none,
217
    this.notchMargin = 4.0,
218
    this.child,
219
  }) : assert(elevation == null || elevation >= 0.0),
220
       assert(notchMargin != null),
221
       assert(clipBehavior != null),
222 223 224 225
       super(key: key);

  /// The widget below this widget in the tree.
  ///
226
  /// {@macro flutter.widgets.ProxyWidget.child}
227 228 229
  ///
  /// Typically this the child will be a [Row], with the first child
  /// being an [IconButton] with the [Icons.menu] icon.
230
  final Widget? child;
231 232

  /// The bottom app bar's background color.
233
  ///
234 235 236
  /// If this property is null then [BottomAppBarTheme.color] of
  /// [ThemeData.bottomAppBarTheme] is used. If that's null then
  /// [ThemeData.bottomAppBarColor] is used.
237
  final Color? color;
238

239 240 241 242 243
  /// 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.
244
  ///
245 246 247
  /// If this property is null then [BottomAppBarTheme.elevation] of
  /// [ThemeData.bottomAppBarTheme] is used. If that's null, the default value
  /// is 8.
248
  final double? elevation;
249

250
  /// The notch that is made for the floating action button.
251
  ///
252 253 254
  /// 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.
255
  final NotchedShape? shape;
256

257
  /// {@macro flutter.material.Material.clipBehavior}
258 259
  ///
  /// Defaults to [Clip.none], and must not be null.
260 261
  final Clip clipBehavior;

262 263
  /// The margin between the [FloatingActionButton] and the [BottomAppBar]'s
  /// notch.
264
  ///
265 266
  /// Not used if [shape] is null.
  final double notchMargin;
267

268
  @override
269
  State createState() => _BottomAppBarState();
270 271 272
}

class _BottomAppBarState extends State<BottomAppBar> {
273
  late ValueListenable<ScaffoldGeometry> geometryListenable;
274
  final GlobalKey materialKey = GlobalKey();
275
  static const double _defaultElevation = 8.0;
276 277 278 279

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
280
    geometryListenable = Scaffold.geometryOf(context);
281 282 283 284
  }

  @override
  Widget build(BuildContext context) {
285
    final BottomAppBarTheme babTheme = BottomAppBarTheme.of(context);
286
    final NotchedShape? notchedShape = widget.shape ?? babTheme.shape;
287
    final CustomClipper<Path> clipper = notchedShape != null
288
      ? _BottomAppBarClipper(
289 290 291 292 293
          geometry: geometryListenable,
          shape: notchedShape,
          materialKey: materialKey,
          notchMargin: widget.notchMargin,
        )
294
      : const ShapeBorderClipper(shape: RoundedRectangleBorder());
295
    final double elevation = widget.elevation ?? babTheme.elevation ?? _defaultElevation;
296
    final Color color = widget.color ?? babTheme.color ?? Theme.of(context).bottomAppBarColor;
297
    final Color effectiveColor = ElevationOverlay.applyOverlay(context, color, elevation);
298
    return PhysicalShape(
299
      clipper: clipper,
300 301
      elevation: elevation,
      color: effectiveColor,
302
      clipBehavior: widget.clipBehavior,
303
      child: Material(
304
        key: materialKey,
305
        type: MaterialType.transparency,
306 307
        child: widget.child == null
          ? null
308
          : SafeArea(child: widget.child!),
309 310 311 312 313 314 315
      ),
    );
  }
}

class _BottomAppBarClipper extends CustomClipper<Path> {
  const _BottomAppBarClipper({
316 317
    required this.geometry,
    required this.shape,
318
    required this.materialKey,
319
    required this.notchMargin,
320
  }) : assert(geometry != null),
321 322
       assert(shape != null),
       assert(notchMargin != null),
323 324 325
       super(reclip: geometry);

  final ValueListenable<ScaffoldGeometry> geometry;
326
  final NotchedShape shape;
327
  final GlobalKey materialKey;
328
  final double notchMargin;
329

330 331 332 333 334 335
  // 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;
  }

336 337 338
  @override
  Path getClip(Size size) {
    // button is the floating action button's bounding rectangle in the
339 340
    // coordinate system whose origin is at the appBar's top left corner,
    // or null if there is no floating action button.
341
    final Rect? button = geometry.value.floatingActionButtonArea?.translate(0.0, bottomNavigationBarTop * -1.0);
342
    return shape.getOuterPath(Offset.zero & size, button?.inflate(notchMargin));
343 344 345
  }

  @override
346 347 348 349 350
  bool shouldReclip(_BottomAppBarClipper oldClipper) {
    return oldClipper.geometry != geometry
        || oldClipper.shape != shape
        || oldClipper.notchMargin != notchMargin;
  }
351
}