bottom_app_bar.dart 5.9 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 8
// 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';

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

// Examples can assume:
// Widget bottomAppBarContents;

18
/// A container that is typically used with [Scaffold.bottomNavigationBar], and
19 20 21 22 23
/// can have a notch along the top that makes room for an overlapping
/// [FloatingActionButton].
///
/// Typically used with a [Scaffold] and a [FloatingActionButton].
///
24
/// {@tool snippet}
25
/// ```dart
26 27
/// Scaffold(
///   bottomNavigationBar: BottomAppBar(
28 29 30
///     color: Colors.white,
///     child: bottomAppBarContents,
///   ),
31
///   floatingActionButton: FloatingActionButton(onPressed: null),
32 33
/// )
/// ```
34
/// {@end-tool}
35 36 37
///
/// See also:
///
Dan Field's avatar
Dan Field committed
38
///  * [NotchedShape] which calculates the notch for a notched [BottomAppBar].
39 40 41 42 43
///  * [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.
  ///
44
  /// The [clipBehavior] argument defaults to [Clip.none] and must not be null.
45
  /// Additionally, [elevation] must be non-negative.
46 47 48 49
  ///
  /// 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.
50
  const BottomAppBar({
51
    Key? key,
52
    this.color,
53
    this.elevation,
54
    this.shape,
55
    this.clipBehavior = Clip.none,
56
    this.notchMargin = 4.0,
57
    this.child,
58
  }) : assert(elevation == null || elevation >= 0.0),
59
       assert(notchMargin != null),
60
       assert(clipBehavior != null),
61 62 63 64
       super(key: key);

  /// The widget below this widget in the tree.
  ///
65
  /// {@macro flutter.widgets.ProxyWidget.child}
66 67 68
  ///
  /// Typically this the child will be a [Row], with the first child
  /// being an [IconButton] with the [Icons.menu] icon.
69
  final Widget? child;
70 71

  /// The bottom app bar's background color.
72
  ///
73 74 75
  /// If this property is null then [BottomAppBarTheme.color] of
  /// [ThemeData.bottomAppBarTheme] is used. If that's null then
  /// [ThemeData.bottomAppBarColor] is used.
76
  final Color? color;
77

78 79 80 81 82
  /// 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.
83
  ///
84 85 86
  /// If this property is null then [BottomAppBarTheme.elevation] of
  /// [ThemeData.bottomAppBarTheme] is used. If that's null, the default value
  /// is 8.
87
  final double? elevation;
88

89
  /// The notch that is made for the floating action button.
90
  ///
91 92 93
  /// 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.
94
  final NotchedShape? shape;
95

96
  /// {@macro flutter.material.Material.clipBehavior}
97 98
  ///
  /// Defaults to [Clip.none], and must not be null.
99 100
  final Clip clipBehavior;

101 102
  /// The margin between the [FloatingActionButton] and the [BottomAppBar]'s
  /// notch.
103
  ///
104 105
  /// Not used if [shape] is null.
  final double notchMargin;
106

107
  @override
108
  State createState() => _BottomAppBarState();
109 110 111
}

class _BottomAppBarState extends State<BottomAppBar> {
112
  late ValueListenable<ScaffoldGeometry> geometryListenable;
113
  static const double _defaultElevation = 8.0;
114 115 116 117

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
118
    geometryListenable = Scaffold.geometryOf(context);
119 120 121 122
  }

  @override
  Widget build(BuildContext context) {
123
    final BottomAppBarTheme babTheme = BottomAppBarTheme.of(context);
124
    final NotchedShape? notchedShape = widget.shape ?? babTheme.shape;
125
    final CustomClipper<Path> clipper = notchedShape != null
126
      ? _BottomAppBarClipper(
127
        geometry: geometryListenable,
128
        shape: notchedShape,
129 130
        notchMargin: widget.notchMargin,
      )
131
      : const ShapeBorderClipper(shape: RoundedRectangleBorder());
132
    final double elevation = widget.elevation ?? babTheme.elevation ?? _defaultElevation;
133
    final Color color = widget.color ?? babTheme.color ?? Theme.of(context).bottomAppBarColor;
134
    final Color effectiveColor = ElevationOverlay.applyOverlay(context, color, elevation);
135
    return PhysicalShape(
136
      clipper: clipper,
137 138
      elevation: elevation,
      color: effectiveColor,
139
      clipBehavior: widget.clipBehavior,
140
      child: Material(
141
        type: MaterialType.transparency,
142 143
        child: widget.child == null
          ? null
144
          : SafeArea(child: widget.child!),
145 146 147 148 149 150 151
      ),
    );
  }
}

class _BottomAppBarClipper extends CustomClipper<Path> {
  const _BottomAppBarClipper({
152 153 154
    required this.geometry,
    required this.shape,
    required this.notchMargin,
155
  }) : assert(geometry != null),
156 157
       assert(shape != null),
       assert(notchMargin != null),
158 159 160
       super(reclip: geometry);

  final ValueListenable<ScaffoldGeometry> geometry;
161 162
  final NotchedShape shape;
  final double notchMargin;
163 164 165 166

  @override
  Path getClip(Size size) {
    // button is the floating action button's bounding rectangle in the
167 168
    // coordinate system whose origin is at the appBar's top left corner,
    // or null if there is no floating action button.
169
    final Rect? button = geometry.value.floatingActionButtonArea?.translate(
170
      0.0,
171
      geometry.value.bottomNavigationBarTop! * -1.0,
172 173
    );
    return shape.getOuterPath(Offset.zero & size, button?.inflate(notchMargin));
174 175 176
  }

  @override
177 178 179 180 181
  bool shouldReclip(_BottomAppBarClipper oldClipper) {
    return oldClipper.geometry != geometry
        || oldClipper.shape != shape
        || oldClipper.notchMargin != notchMargin;
  }
182
}