floating_action_button.dart 14.6 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/painting.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/widgets.dart';
10

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

18
const BoxConstraints _kSizeConstraints = BoxConstraints.tightFor(
19 20 21 22
  width: 56.0,
  height: 56.0,
);

23
const BoxConstraints _kMiniSizeConstraints = BoxConstraints.tightFor(
24 25 26 27
  width: 40.0,
  height: 40.0,
);

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

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

39
/// A material design floating action button.
40 41
///
/// A floating action button is a circular icon button that hovers over content
42 43
/// to promote a primary action in the application. Floating action buttons are
/// most commonly used in the [Scaffold.floatingActionButton] field.
44 45 46
///
/// Use at most a single floating action button per screen. Floating action
/// buttons should be used for positive actions such as "create", "share", or
47 48 49
/// "navigate". (If more than one floating action button is used within a
/// [Route], then make sure that each button has a unique [heroTag], otherwise
/// an exception will be thrown.)
50
///
51
/// If the [onPressed] callback is null, then the button will be disabled and
52 53 54 55
/// will not react to touch. It is highly discouraged to disable a floating
/// action button as there is no indication to the user that the button is
/// disabled. Consider changing the [backgroundColor] if disabling the floating
/// action button.
56
///
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 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
/// {@tool snippet --template=stateless_widget_material}
/// This example shows how to make a simple [FloatingActionButton] in a
/// [Scaffold], with a pink [backgroundColor] and a thumbs up [Icon].
///
/// ```dart
/// Widget build(BuildContext context) {
///   return Scaffold(
///     appBar: AppBar(
///       title: Text('Floating Action Button Sample'),
///     ),
///     body: Center(
///       child: Text('Press the button below!')
///     ),
///     floatingActionButton: FloatingActionButton(
///       onPressed: () {
///         // Add your onPressed code here!
///       },
///       child: Icon(Icons.thumb_up),
///       backgroundColor: Colors.pink,
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
/// {@tool snippet --template=stateless_widget_material}
/// This example shows how to make an extended [FloatingActionButton] in a
/// [Scaffold], with a  pink [backgroundColor] and a thumbs up [Icon] and a
/// [Text] label.
///
/// ```dart
/// Widget build(BuildContext context) {
///   return Scaffold(
///     appBar: AppBar(
///       title: Text('Floating Action Button Sample'),
///     ),
///     body: Center(
///       child: Text('Press the extended button below!'),
///     ),
///     floatingActionButton: FloatingActionButton.extended(
///       onPressed: () {
///         // Add your onPressed code here!
///       },
///       label: Text('Approve'),
///       icon: Icon(Icons.thumb_up),
///       backgroundColor: Colors.pink,
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
109 110
/// See also:
///
111 112 113 114
///  * [Scaffold], in which floating action buttons typically live.
///  * [RaisedButton], another kind of button that appears to float above the
///    content.
///  * <https://material.io/design/components/buttons-floating-action-button.html>
115
class FloatingActionButton extends StatelessWidget {
116
  /// Creates a circular floating action button.
117
  ///
118 119 120
  /// The [mini] and [clipBehavior] arguments must be non-null. Additionally,
  /// [elevation], [highlightElevation], and [disabledElevation] (if specified)
  /// must be non-negative.
121
  const FloatingActionButton({
122
    Key key,
123
    this.child,
Adam Barth's avatar
Adam Barth committed
124
    this.tooltip,
125
    this.foregroundColor,
126
    this.backgroundColor,
127
    this.heroTag = const _DefaultHeroTag(),
128 129 130
    this.elevation,
    this.highlightElevation,
    this.disabledElevation,
131
    @required this.onPressed,
132
    this.mini = false,
133
    this.shape,
134
    this.clipBehavior = Clip.none,
135
    this.materialTapTargetSize,
136
    this.isExtended = false,
137 138
  }) : assert(elevation == null || elevation >= 0.0),
       assert(highlightElevation == null || highlightElevation >= 0.0),
139 140 141 142 143
       assert(disabledElevation == null || disabledElevation >= 0.0),
       assert(mini != null),
       assert(isExtended != null),
       _sizeConstraints = mini ? _kMiniSizeConstraints : _kSizeConstraints,
       super(key: key);
144

145 146
  /// Creates a wider [StadiumBorder]-shaped floating action button with
  /// an optional [icon] and a [label].
147
  ///
148 149 150
  /// The [label] and [clipBehavior] arguments must non-null. Additionally,
  /// [elevation], [highlightElevation], and [disabledElevation] (if specified)
  /// must be non-negative.
151 152 153 154 155
  FloatingActionButton.extended({
    Key key,
    this.tooltip,
    this.foregroundColor,
    this.backgroundColor,
156
    this.heroTag = const _DefaultHeroTag(),
157 158 159
    this.elevation,
    this.highlightElevation,
    this.disabledElevation,
160
    @required this.onPressed,
161
    this.shape,
162
    this.isExtended = true,
163
    this.materialTapTargetSize,
164
    this.clipBehavior = Clip.none,
165
    Widget icon,
166
    @required Widget label,
167 168
  }) : assert(elevation == null || elevation >= 0.0),
       assert(highlightElevation == null || highlightElevation >= 0.0),
169 170 171 172 173 174 175 176
       assert(disabledElevation == null || disabledElevation >= 0.0),
       assert(isExtended != null),
       assert(clipBehavior != null),
       _sizeConstraints = _kExtendedSizeConstraints,
       mini = false,
       child = _ChildOverflowBox(
         child: Row(
           mainAxisSize: MainAxisSize.min,
177 178 179 180 181 182 183 184 185 186 187 188 189
           children: icon == null
             ? <Widget>[
                 const SizedBox(width: 20.0),
                 label,
                 const SizedBox(width: 20.0),
               ]
             : <Widget>[
                 const SizedBox(width: 16.0),
                 icon,
                 const SizedBox(width: 8.0),
                 label,
                 const SizedBox(width: 20.0),
               ],
190 191 192
         ),
       ),
       super(key: key);
193

194
  /// The widget below this widget in the tree.
195 196
  ///
  /// Typically an [Icon].
197
  final Widget child;
198

199 200 201 202
  /// 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
203
  final String tooltip;
204

205 206 207 208 209
  /// The default icon and text color.
  ///
  /// Defaults to [ThemeData.accentIconTheme.color] for the current theme.
  final Color foregroundColor;

210 211
  /// The color to use when filling the button.
  ///
212
  /// Defaults to [ThemeData.accentColor] for the current theme.
213
  final Color backgroundColor;
214

215 216 217
  /// The tag to apply to the button's [Hero] widget.
  ///
  /// Defaults to a tag that matches other floating action buttons.
218 219 220 221 222 223 224 225 226
  ///
  /// 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.
227 228
  final Object heroTag;

229
  /// The callback that is called when the button is tapped or otherwise activated.
230 231
  ///
  /// If this is set to null, the button will be disabled.
232
  final VoidCallback onPressed;
233

234
  /// The z-coordinate at which to place this button relative to its parent.
235
  ///
236 237 238 239
  /// This controls the size of the shadow below the floating action button.
  ///
  /// Defaults to 6, the appropriate elevation for floating action buttons. The
  /// value is always non-negative.
240 241 242 243 244
  ///
  /// See also:
  ///
  ///  * [highlightElevation], the elevation when the button is pressed.
  ///  * [disabledElevation], the elevation when the button is disabled.
245
  final double elevation;
246

247 248 249 250
  /// The z-coordinate at which to place this button relative to its parent when
  /// the user is touching the button.
  ///
  /// This controls the size of the shadow below the floating action button.
251 252
  ///
  /// Defaults to 12, the appropriate elevation for floating action buttons
253
  /// while they are being touched. The value is always non-negative.
254 255 256 257
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
258
  final double highlightElevation;
259

260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
  /// The z-coordinate at which to place this button when the button is disabled
  /// ([onPressed] is null).
  ///
  /// This controls the size of the shadow below the floating action button.
  ///
  /// Defaults to the same value as [elevation]. Setting this to zero makes the
  /// floating action button work similar to a [RaisedButton] but the titular
  /// "floating" effect is lost. The value is always non-negative.
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
  ///  * [highlightElevation], the elevation when the button is pressed.
  final double disabledElevation;

275 276 277 278
  /// 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
279
  /// and width of 40.0 logical pixels with a layout width and height of 48.0
280 281 282
  /// logical pixels. (The extra 4 pixels of padding on each side are added as a
  /// result of the floating action button having [MaterialTapTargetSize.padded]
  /// set on the underlying [RawMaterialButton.materialTapTargetSize].)
Devon Carew's avatar
Devon Carew committed
283
  final bool mini;
284

285 286 287 288 289 290 291
  /// 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;

292 293 294
  /// {@macro flutter.widgets.Clip}
  final Clip clipBehavior;

295 296 297 298 299 300 301 302 303 304 305
  /// 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;

306 307 308 309 310 311
  /// Configures the minimum size of the tap target.
  ///
  /// Defaults to [ThemeData.materialTapTargetSize].
  ///
  /// See also:
  ///
312
  ///  * [MaterialTapTargetSize], for a description of how this affects tap targets.
313 314
  final MaterialTapTargetSize materialTapTargetSize;

315 316
  final BoxConstraints _sizeConstraints;

317 318 319 320 321
  static const double _defaultElevation = 6;
  static const double _defaultHighlightElevation = 12;
  static const ShapeBorder _defaultShape = CircleBorder();
  static const ShapeBorder _defaultExtendedShape = StadiumBorder();

322
  @override
323
  Widget build(BuildContext context) {
324
    final ThemeData theme = Theme.of(context);
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
    final FloatingActionButtonThemeData floatingActionButtonTheme = theme.floatingActionButtonTheme;

    final Color backgroundColor = this.backgroundColor
      ?? floatingActionButtonTheme.backgroundColor
      ?? theme.colorScheme.secondary;
    final Color foregroundColor = this.foregroundColor
      ?? floatingActionButtonTheme.foregroundColor
      ?? theme.accentIconTheme.color
      ?? theme.colorScheme.onSecondary;
    final double elevation = this.elevation
      ?? floatingActionButtonTheme.elevation
      ?? _defaultElevation;
    final double disabledElevation = this.disabledElevation
      ?? floatingActionButtonTheme.disabledElevation
      ?? elevation;
    final double highlightElevation = this.highlightElevation
      ?? floatingActionButtonTheme.highlightElevation
      ?? _defaultHighlightElevation;
    final MaterialTapTargetSize materialTapTargetSize = this.materialTapTargetSize
      ?? theme.materialTapTargetSize;
    final TextStyle textStyle = theme.accentTextTheme.button.copyWith(
      color: foregroundColor,
      letterSpacing: 1.2,
    );
    final ShapeBorder shape = this.shape
      ?? floatingActionButtonTheme.shape
      ?? (isExtended ? _defaultExtendedShape : _defaultShape);

353 354
    Widget result;

355
    if (child != null) {
356
      result = IconTheme.merge(
357
        data: IconThemeData(
358
          color: foregroundColor,
359
        ),
360
        child: child,
361 362
      );
    }
Adam Barth's avatar
Adam Barth committed
363

364
    result = RawMaterialButton(
365 366 367 368 369
      onPressed: onPressed,
      elevation: elevation,
      highlightElevation: highlightElevation,
      disabledElevation: disabledElevation,
      constraints: _sizeConstraints,
370 371 372
      materialTapTargetSize: materialTapTargetSize,
      fillColor: backgroundColor,
      textStyle: textStyle,
373 374
      shape: shape,
      clipBehavior: clipBehavior,
375 376 377
      child: result,
    );

378
    if (tooltip != null) {
379 380
      result = MergeSemantics(
        child: Tooltip(
381
          message: tooltip,
382 383 384 385 386
          child: result,
        ),
      );
    }

387
    if (heroTag != null) {
388
      result = Hero(
389
        tag: heroTag,
390 391 392 393 394
        child: result,
      );
    }

    return result;
395 396
  }
}
397 398 399 400 401 402 403 404 405 406 407 408 409 410

// This widget's size matches its child's size unless its constraints
// force it to be larger or smaller. The child is centered.
//
// Used to encapsulate extended FABs whose size is fixed, using Row
// and MainAxisSize.min, to be as wide as their label and icon.
class _ChildOverflowBox extends SingleChildRenderObjectWidget {
  const _ChildOverflowBox({
    Key key,
    Widget child,
  }) : super(key: key, child: child);

  @override
  _RenderChildOverflowBox createRenderObject(BuildContext context) {
411
    return _RenderChildOverflowBox(
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
      textDirection: Directionality.of(context),
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderChildOverflowBox renderObject) {
    renderObject
      ..textDirection = Directionality.of(context);
  }
}

class _RenderChildOverflowBox extends RenderAligningShiftedBox {
  _RenderChildOverflowBox({
    RenderBox child,
    TextDirection textDirection,
  }) : super(child: child, alignment: Alignment.center, textDirection: textDirection);

  @override
  double computeMinIntrinsicWidth(double height) => 0.0;

  @override
  double computeMinIntrinsicHeight(double width) => 0.0;

  @override
  void performLayout() {
    if (child != null) {
      child.layout(const BoxConstraints(), parentUsesSize: true);
439
      size = Size(
440 441 442 443 444 445 446 447 448
        math.max(constraints.minWidth, math.min(constraints.maxWidth, child.size.width)),
        math.max(constraints.minHeight, math.min(constraints.maxHeight, child.size.height)),
      );
      alignChild();
    } else {
      size = constraints.biggest;
    }
  }
}