floating_action_button.dart 13.1 KB
Newer Older
1 2 3 4
// Copyright 2015 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.

5 6
import 'dart:math' as math;

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/painting.dart';
9
import 'package:flutter/widgets.dart';
10

11
import 'button.dart';
12
import 'scaffold.dart';
13
import 'theme.dart';
Adam Barth's avatar
Adam Barth committed
14
import 'tooltip.dart';
15

16 17 18 19 20 21 22 23 24 25 26 27 28 29
const BoxConstraints _kSizeConstraints = const BoxConstraints.tightFor(
  width: 56.0,
  height: 56.0,
);

const BoxConstraints _kMiniSizeConstraints = const BoxConstraints.tightFor(
  width: 40.0,
  height: 40.0,
);

const BoxConstraints _kExtendedSizeConstraints = const BoxConstraints(
  minHeight: 48.0,
  maxHeight: 48.0,
);
30 31 32 33 34 35

class _DefaultHeroTag {
  const _DefaultHeroTag();
  @override
  String toString() => '<default FloatingActionButton tag>';
}
36

37
// TODO(amirh): update the documentation once the BAB notch can be disabled.
38
/// A material design floating action button.
39 40
///
/// A floating action button is a circular icon button that hovers over content
41 42
/// to promote a primary action in the application. Floating action buttons are
/// most commonly used in the [Scaffold.floatingActionButton] field.
43 44 45 46 47
///
/// Use at most a single floating action button per screen. Floating action
/// buttons should be used for positive actions such as "create", "share", or
/// "navigate".
///
48 49
/// If the [onPressed] callback is null, then the button will be disabled and
/// will not react to touch.
50
///
51 52 53 54 55 56
/// If the floating action button is a descendant of a [Scaffold] that also has a
/// [BottomAppBar], the [BottomAppBar] will show a notch to accomodate the
/// [FloatingActionButton] when it overlaps the [BottomAppBar]. The notch's
/// shape is an arc for a circle whose radius is the floating action button's
/// radius plus [FloatingActionButton.notchMargin].
///
57 58
/// See also:
///
59
///  * [Scaffold]
60 61
///  * [RaisedButton]
///  * [FlatButton]
62
///  * <https://material.google.com/components/buttons-floating-action-button.html>
63
class FloatingActionButton extends StatefulWidget {
64
  /// Creates a circular floating action button.
65
  ///
66 67
  /// The [elevation], [highlightElevation], [mini], [notchMargin], and [shape]
  /// arguments must not be null.
68
  const FloatingActionButton({
69
    Key key,
70
    this.child,
Adam Barth's avatar
Adam Barth committed
71
    this.tooltip,
72
    this.foregroundColor,
73
    this.backgroundColor,
74
    this.heroTag: const _DefaultHeroTag(),
75 76
    this.elevation: 6.0,
    this.highlightElevation: 12.0,
77
    @required this.onPressed,
78 79
    this.mini: false,
    this.notchMargin: 4.0,
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
    this.shape: const CircleBorder(),
    this.isExtended: false,
  }) :  assert(elevation != null),
        assert(highlightElevation != null),
        assert(mini != null),
        assert(notchMargin != null),
        assert(shape != null),
        assert(isExtended != null),
        _sizeConstraints = mini ? _kMiniSizeConstraints : _kSizeConstraints,
        super(key: key);

  /// Creates a wider [StadiumBorder] shaped floating action button with both
  /// an [icon] and a [label].
  ///
  /// The [label], [icon], [elevation], [highlightElevation]
  /// [notchMargin], and [shape] arguments must not be null.
  FloatingActionButton.extended({
    Key key,
    this.tooltip,
    this.foregroundColor,
    this.backgroundColor,
    this.heroTag: const _DefaultHeroTag(),
    this.elevation: 6.0,
    this.highlightElevation: 12.0,
    @required this.onPressed,
    this.notchMargin: 4.0,
    this.shape: const StadiumBorder(),
    this.isExtended: true,
    @required Widget icon,
    @required Widget label,
  }) :  assert(elevation != null),
        assert(highlightElevation != null),
        assert(notchMargin != null),
        assert(shape != null),
        assert(isExtended != null),
        _sizeConstraints = _kExtendedSizeConstraints,
        mini = false,
        child = new Row(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            const SizedBox(width: 16.0),
            icon,
            const SizedBox(width: 8.0),
            label,
            const SizedBox(width: 20.0),
          ],
        ),
        super(key: key);
128

129
  /// The widget below this widget in the tree.
130 131
  ///
  /// Typically an [Icon].
132
  final Widget child;
133

134 135 136 137
  /// Text that describes the action that will occur when the button is pressed.
  ///
  /// This text is displayed when the user long-presses on the button and is
  /// used for accessibility.
Adam Barth's avatar
Adam Barth committed
138
  final String tooltip;
139

140 141 142 143 144
  /// The default icon and text color.
  ///
  /// Defaults to [ThemeData.accentIconTheme.color] for the current theme.
  final Color foregroundColor;

145 146
  /// The color to use when filling the button.
  ///
147
  /// Defaults to [ThemeData.accentColor] for the current theme.
148
  final Color backgroundColor;
149

150 151 152
  /// The tag to apply to the button's [Hero] widget.
  ///
  /// Defaults to a tag that matches other floating action buttons.
153 154 155 156 157 158 159 160 161
  ///
  /// Set this to null explicitly if you don't want the floating action button to
  /// have a hero tag.
  ///
  /// If this is not explicitly set, then there can only be one
  /// [FloatingActionButton] per route (that is, per screen), since otherwise
  /// there would be a tag conflict (multiple heroes on one route can't have the
  /// same tag). The material design specification recommends only using one
  /// floating action button per screen.
162 163
  final Object heroTag;

164
  /// The callback that is called when the button is tapped or otherwise activated.
165 166
  ///
  /// If this is set to null, the button will be disabled.
167
  final VoidCallback onPressed;
168

169 170
  /// The z-coordinate at which to place this button. This controls the size of
  /// the shadow below the floating action button.
171
  ///
172
  /// Defaults to 6, the appropriate elevation for floating action buttons.
173
  final double elevation;
174

175 176 177
  /// The z-coordinate at which to place this button when the user is touching
  /// the button. This controls the size of the shadow below the floating action
  /// button.
178 179 180
  ///
  /// Defaults to 12, the appropriate elevation for floating action buttons
  /// while they are being touched.
181 182 183 184
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
185
  final double highlightElevation;
186

187 188 189 190 191
  /// Controls the size of this button.
  ///
  /// By default, floating action buttons are non-mini and have a height and
  /// width of 56.0 logical pixels. Mini floating action buttons have a height
  /// and width of 40.0 logical pixels.
Devon Carew's avatar
Devon Carew committed
192
  final bool mini;
193

194 195 196 197 198 199 200 201 202 203 204 205 206
  /// The margin to keep around the floating action button when creating a
  /// notch for it.
  ///
  /// The notch is an arc of a circle with radius r+[notchMargin] where r is the
  /// radius of the floating action button. This expanded radius leaves a margin
  /// around the floating action button.
  ///
  /// See also:
  ///
  ///  * [BottomAppBar], a material design elements that shows a notch for the
  ///    floating action button.
  final double notchMargin;

207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
  /// The shape of the button's [Material].
  ///
  /// The button's highlight and splash are clipped to this shape. If the
  /// button has an elevation, then its drop shadow is defined by this
  /// shape as well.
  final ShapeBorder shape;

  /// True if this is an "extended" floating action button.
  ///
  /// Typically [extended] buttons have a [StadiumBorder] [shape]
  /// and have been created with the [FloatingActionButton.extended]
  /// constructor.
  ///
  /// The [Scaffold] animates the appearance of ordinary floating
  /// action buttons with scale and rotation transitions. Extended
  /// floating action buttons are scaled and faded in.
  final bool isExtended;

  final BoxConstraints _sizeConstraints;

227
  @override
228
  _FloatingActionButtonState createState() => new _FloatingActionButtonState();
229
}
230

231 232
class _FloatingActionButtonState extends State<FloatingActionButton> {
  bool _highlight = false;
233 234
  VoidCallback _clearComputeNotch;

235 236 237 238 239 240
  void _handleHighlightChanged(bool value) {
    setState(() {
      _highlight = value;
    });
  }

241
  @override
242
  Widget build(BuildContext context) {
243 244
    final ThemeData theme = Theme.of(context);
    final Color foregroundColor = widget.foregroundColor ?? theme.accentIconTheme.color;
245 246 247
    Widget result;

    if (widget.child != null) {
248 249 250
      result = IconTheme.merge(
        data: new IconThemeData(
          color: foregroundColor,
251
        ),
252
        child: widget.child,
253 254
      );
    }
Adam Barth's avatar
Adam Barth committed
255

256
    if (widget.tooltip != null) {
257 258 259 260
      final Widget tooltip = new Tooltip(
        message: widget.tooltip,
        child: result,
      );
261 262
      // The long-pressable area for the tooltip should always be as big as
      // the tooltip even if there is no child.
263
      result = widget.child != null ? tooltip : new SizedBox.expand(child: tooltip);
264 265
    }

266 267 268 269 270 271 272 273 274 275 276 277 278 279
    result = new RawMaterialButton(
      onPressed: widget.onPressed,
      onHighlightChanged: _handleHighlightChanged,
      elevation: _highlight ? widget.highlightElevation : widget.elevation,
      constraints: widget._sizeConstraints,
      fillColor: widget.backgroundColor ?? theme.accentColor,
      textStyle: theme.accentTextTheme.button.copyWith(
        color: foregroundColor,
        letterSpacing: 1.2,
      ),
      shape: widget.shape,
      child: result,
    );

280 281 282 283 284 285 286 287
    if (widget.heroTag != null) {
      result = new Hero(
        tag: widget.heroTag,
        child: result,
      );
    }

    return result;
288
  }
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _clearComputeNotch = Scaffold.setFloatingActionButtonNotchFor(context, _computeNotch);
  }

  @override
  void deactivate() {
    if (_clearComputeNotch != null)
      _clearComputeNotch();
    super.deactivate();
  }

  Path _computeNotch(Rect host, Rect guest, Offset start, Offset end) {
    // The FAB's shape is a circle bounded by the guest rectangle.
    // So the FAB's radius is half the guest width.
    final double fabRadius = guest.width / 2.0;
    final double notchRadius = fabRadius + widget.notchMargin;
308 309 310 311 312 313 314 315 316 317 318 319

    assert(_notchAssertions(host, guest, start, end, fabRadius, notchRadius));

    // If there's no overlap between the guest's margin boundary and the host,
    // don't make a notch, just return a straight line from start to end.
    if (!host.overlaps(guest.inflate(widget.notchMargin)))
      return new Path()..lineTo(end.dx, end.dy);

    // We build a path for the notch from 3 segments:
    // Segment A - a Bezier curve from the host's top edge to segment B.
    // Segment B - an arc with radius notchRadius.
    // Segment C - a Bezier curver from segment B back to the host's top edge.
320
    //
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
    // A detailed explanation and the derivation of the formulas below is
    // available at: https://goo.gl/Ufzrqn

    const double s1 = 15.0;
    const double s2 = 1.0;

    final double r = notchRadius;
    final double a = -1.0 * r - s2;
    final double b = host.top - guest.center.dy;

    final double n2 = math.sqrt(b * b * r * r * (a * a + b * b - r * r));
    final double p2xA = ((a * r * r) - n2) / (a * a + b * b);
    final double p2xB = ((a * r * r) + n2) / (a * a + b * b);
    final double p2yA = math.sqrt(r * r - p2xA * p2xA);
    final double p2yB = math.sqrt(r * r - p2xB * p2xB);

    final List<Offset> p = new List<Offset>(6);

    // p0, p1, and p2 are the control points for segment A.
    p[0] = new Offset(a - s1, b);
    p[1] = new Offset(a, b);
    final double cmp = b < 0 ? -1.0 : 1.0;
    p[2] = cmp * p2yA > cmp * p2yB ? new Offset(p2xA, p2yA) : new Offset(p2xB, p2yB);

    // p3, p4, and p5 are the control points for segment B, which is a mirror
    // of segment A around the y axis.
    p[3] = new Offset(-1.0 * p[2].dx, p[2].dy);
    p[4] = new Offset(-1.0 * p[1].dx, p[1].dy);
    p[5] = new Offset(-1.0 * p[0].dx, p[0].dy);

    // translate all points back to the absolute coordinate system.
    for (int i = 0; i < p.length; i += 1)
      p[i] += guest.center;
354 355

    return new Path()
356 357
      ..lineTo(p[0].dx, p[0].dy)
      ..quadraticBezierTo(p[1].dx, p[1].dy, p[2].dx, p[2].dy)
358
      ..arcToPoint(
359
        p[3],
360 361 362
        radius: new Radius.circular(notchRadius),
        clockwise: false,
      )
363
      ..quadraticBezierTo(p[4].dx, p[4].dy, p[5].dx, p[5].dy)
364 365
      ..lineTo(end.dx, end.dy);
  }
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394

  bool _notchAssertions(Rect host, Rect guest, Offset start, Offset end,
    double fabRadius, double notchRadius) {
    if (end.dy != host.top)
      throw new FlutterError(
        'The notch of the floating action button must end at the top edge of the host.\n'
        'The notch\'s path end point: $end is not in the top edge of $host'
      );

    if (start.dy != host.top)
      throw new FlutterError(
        'The notch of the floating action button must start at the top edge of the host.\n'
        'The notch\'s path start point: $start is not in the top edge of $host'
      );

    if (guest.center.dx - notchRadius < start.dx)
      throw new FlutterError(
        'The notch\'s path start point must be to the left of the floating action button.\n'
        'Start point was $start, guest was $guest, notchMargin was ${widget.notchMargin}.'
      );

    if (guest.center.dx + notchRadius > end.dx)
      throw new FlutterError(
        'The notch\'s end point must be to the right of the floating action button.\n'
        'End point was $start, notch was $guest, notchMargin was ${widget.notchMargin}.'
      );

    return true;
  }
395
}