bottom_app_bar.dart 7.04 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
/// {@tool dartpad}
36 37 38 39
/// 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].
///
40
/// ** See code in examples/api/lib/material/bottom_app_bar/bottom_app_bar.1.dart **
41 42
/// {@end-tool}
///
43 44
/// See also:
///
Dan Field's avatar
Dan Field committed
45
///  * [NotchedShape] which calculates the notch for a notched [BottomAppBar].
46 47 48 49 50
///  * [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.
  ///
51
  /// The [clipBehavior] argument defaults to [Clip.none] and must not be null.
52
  /// Additionally, [elevation] must be non-negative.
53 54 55 56
  ///
  /// 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.
57
  const BottomAppBar({
58
    super.key,
59
    this.color,
60
    this.elevation,
61
    this.shape,
62
    this.clipBehavior = Clip.none,
63
    this.notchMargin = 4.0,
64
    this.child,
65
  }) : assert(elevation == null || elevation >= 0.0),
66
       assert(notchMargin != null),
67
       assert(clipBehavior != null);
68 69 70

  /// The widget below this widget in the tree.
  ///
71
  /// {@macro flutter.widgets.ProxyWidget.child}
72 73 74
  ///
  /// Typically this the child will be a [Row], with the first child
  /// being an [IconButton] with the [Icons.menu] icon.
75
  final Widget? child;
76 77

  /// The bottom app bar's background color.
78
  ///
79 80 81
  /// If this property is null then [BottomAppBarTheme.color] of
  /// [ThemeData.bottomAppBarTheme] is used. If that's null then
  /// [ThemeData.bottomAppBarColor] is used.
82
  final Color? color;
83

84 85 86 87 88
  /// 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.
89
  ///
90 91 92
  /// If this property is null then [BottomAppBarTheme.elevation] of
  /// [ThemeData.bottomAppBarTheme] is used. If that's null, the default value
  /// is 8.
93
  final double? elevation;
94

95
  /// The notch that is made for the floating action button.
96
  ///
97 98 99
  /// 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.
100
  final NotchedShape? shape;
101

102
  /// {@macro flutter.material.Material.clipBehavior}
103 104
  ///
  /// Defaults to [Clip.none], and must not be null.
105 106
  final Clip clipBehavior;

107 108
  /// The margin between the [FloatingActionButton] and the [BottomAppBar]'s
  /// notch.
109
  ///
110 111
  /// Not used if [shape] is null.
  final double notchMargin;
112

113
  @override
114
  State createState() => _BottomAppBarState();
115 116 117
}

class _BottomAppBarState extends State<BottomAppBar> {
118
  late ValueListenable<ScaffoldGeometry> geometryListenable;
119
  final GlobalKey materialKey = GlobalKey();
120
  static const double _defaultElevation = 8.0;
121 122 123 124

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
125
    geometryListenable = Scaffold.geometryOf(context);
126 127 128 129
  }

  @override
  Widget build(BuildContext context) {
130
    final BottomAppBarTheme babTheme = BottomAppBarTheme.of(context);
131
    final bool hasFab = Scaffold.of(context).hasFloatingActionButton;
132
    final NotchedShape? notchedShape = widget.shape ?? babTheme.shape;
133
    final CustomClipper<Path> clipper = notchedShape != null && hasFab
134
      ? _BottomAppBarClipper(
135 136 137 138 139
          geometry: geometryListenable,
          shape: notchedShape,
          materialKey: materialKey,
          notchMargin: widget.notchMargin,
        )
140
      : const ShapeBorderClipper(shape: RoundedRectangleBorder());
141
    final double elevation = widget.elevation ?? babTheme.elevation ?? _defaultElevation;
142
    final Color color = widget.color ?? babTheme.color ?? Theme.of(context).bottomAppBarColor;
143
    final Color effectiveColor = ElevationOverlay.applyOverlay(context, color, elevation);
144
    return PhysicalShape(
145
      clipper: clipper,
146 147
      elevation: elevation,
      color: effectiveColor,
148
      clipBehavior: widget.clipBehavior,
149
      child: Material(
150
        key: materialKey,
151
        type: MaterialType.transparency,
152 153
        child: widget.child == null
          ? null
154
          : SafeArea(child: widget.child!),
155 156 157 158 159 160 161
      ),
    );
  }
}

class _BottomAppBarClipper extends CustomClipper<Path> {
  const _BottomAppBarClipper({
162 163
    required this.geometry,
    required this.shape,
164
    required this.materialKey,
165
    required this.notchMargin,
166
  }) : assert(geometry != null),
167 168
       assert(shape != null),
       assert(notchMargin != null),
169 170 171
       super(reclip: geometry);

  final ValueListenable<ScaffoldGeometry> geometry;
172
  final NotchedShape shape;
173
  final GlobalKey materialKey;
174
  final double notchMargin;
175

176
  // Returns the top of the BottomAppBar in global coordinates.
177 178 179 180
  //
  // If the Scaffold's bottomNavigationBar was specified, then we can use its
  // geometry value, otherwise we compute the location based on the AppBar's
  // Material widget.
181
  double get bottomNavigationBarTop {
182
    final double? bottomNavigationBarTop = geometry.value.bottomNavigationBarTop;
183
    if (bottomNavigationBarTop != null) {
184
      return bottomNavigationBarTop;
185
    }
186 187 188 189
    final RenderBox? box = materialKey.currentContext?.findRenderObject() as RenderBox?;
    return box?.localToGlobal(Offset.zero).dy ?? 0;
  }

190 191 192
  @override
  Path getClip(Size size) {
    // button is the floating action button's bounding rectangle in the
193 194
    // coordinate system whose origin is at the appBar's top left corner,
    // or null if there is no floating action button.
195
    final Rect? button = geometry.value.floatingActionButtonArea?.translate(0.0, bottomNavigationBarTop * -1.0);
196
    return shape.getOuterPath(Offset.zero & size, button?.inflate(notchMargin));
197 198 199
  }

  @override
200 201 202 203 204
  bool shouldReclip(_BottomAppBarClipper oldClipper) {
    return oldClipper.geometry != geometry
        || oldClipper.shape != shape
        || oldClipper.notchMargin != notchMargin;
  }
205
}