floating_action_button.dart 10.3 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 'scaffold.dart';
13
import 'theme.dart';
14
import 'theme_data.dart';
Adam Barth's avatar
Adam Barth committed
15
import 'tooltip.dart';
16

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

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

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

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

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
/// See also:
///
53
///  * [Scaffold]
54 55
///  * [RaisedButton]
///  * [FlatButton]
56
///  * <https://material.google.com/components/buttons-floating-action-button.html>
57
class FloatingActionButton extends StatefulWidget {
58
  /// Creates a circular floating action button.
59
  ///
60
  /// The [elevation], [highlightElevation], [mini], [shape], and [clipBehavior]
61
  /// arguments must not be null.
62
  const FloatingActionButton({
63
    Key key,
64
    this.child,
Adam Barth's avatar
Adam Barth committed
65
    this.tooltip,
66
    this.foregroundColor,
67
    this.backgroundColor,
68 69 70
    this.heroTag = const _DefaultHeroTag(),
    this.elevation = 6.0,
    this.highlightElevation = 12.0,
71
    @required this.onPressed,
72 73
    this.mini = false,
    this.shape = const CircleBorder(),
74
    this.clipBehavior = Clip.none,
75
    this.materialTapTargetSize,
76
    this.isExtended = false,
77 78 79 80 81 82 83 84 85 86 87
  }) :  assert(elevation != null),
        assert(highlightElevation != null),
        assert(mini != 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].
  ///
88
  /// The [label], [icon], [elevation], [highlightElevation], [clipBehavior]
89
  /// and [shape] arguments must not be null.
90 91 92 93 94
  FloatingActionButton.extended({
    Key key,
    this.tooltip,
    this.foregroundColor,
    this.backgroundColor,
95 96 97
    this.heroTag = const _DefaultHeroTag(),
    this.elevation = 6.0,
    this.highlightElevation = 12.0,
98
    @required this.onPressed,
99 100
    this.shape = const StadiumBorder(),
    this.isExtended = true,
101
    this.materialTapTargetSize,
102
    this.clipBehavior = Clip.none,
103 104 105 106 107 108
    @required Widget icon,
    @required Widget label,
  }) :  assert(elevation != null),
        assert(highlightElevation != null),
        assert(shape != null),
        assert(isExtended != null),
109
        assert(clipBehavior != null),
110 111
        _sizeConstraints = _kExtendedSizeConstraints,
        mini = false,
112 113 114 115 116 117 118 119 120 121 122
        child = new _ChildOverflowBox(
          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),
            ],
          ),
123 124
        ),
        super(key: key);
125

126
  /// The widget below this widget in the tree.
127 128
  ///
  /// Typically an [Icon].
129
  final Widget child;
130

131 132 133 134
  /// 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
135
  final String tooltip;
136

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

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

147 148 149
  /// The tag to apply to the button's [Hero] widget.
  ///
  /// Defaults to a tag that matches other floating action buttons.
150 151 152 153 154 155 156 157 158
  ///
  /// 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.
159 160
  final Object heroTag;

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

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

172 173 174
  /// 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.
175 176 177
  ///
  /// Defaults to 12, the appropriate elevation for floating action buttons
  /// while they are being touched.
178 179 180 181
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
182
  final double highlightElevation;
183

184 185 186 187
  /// 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
188 189
  /// and width of 40.0 logical pixels with a layout width and height of 48.0
  /// logical pixels.
Devon Carew's avatar
Devon Carew committed
190
  final bool mini;
191

192 193 194 195 196 197 198
  /// 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;

199 200 201
  /// {@macro flutter.widgets.Clip}
  final Clip clipBehavior;

202 203 204 205 206 207 208 209 210 211 212
  /// 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;

213 214 215 216 217 218 219 220 221
  /// Configures the minimum size of the tap target.
  ///
  /// Defaults to [ThemeData.materialTapTargetSize].
  ///
  /// See also:
  ///
  ///   * [MaterialTapTargetSize], for a description of how this affects tap targets.
  final MaterialTapTargetSize materialTapTargetSize;

222 223
  final BoxConstraints _sizeConstraints;

224
  @override
225
  _FloatingActionButtonState createState() => new _FloatingActionButtonState();
226
}
227

228 229
class _FloatingActionButtonState extends State<FloatingActionButton> {
  bool _highlight = false;
230

231 232 233 234 235 236
  void _handleHighlightChanged(bool value) {
    setState(() {
      _highlight = value;
    });
  }

237
  @override
238
  Widget build(BuildContext context) {
239 240
    final ThemeData theme = Theme.of(context);
    final Color foregroundColor = widget.foregroundColor ?? theme.accentIconTheme.color;
241 242 243
    Widget result;

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

252 253 254 255 256
    result = new RawMaterialButton(
      onPressed: widget.onPressed,
      onHighlightChanged: _handleHighlightChanged,
      elevation: _highlight ? widget.highlightElevation : widget.elevation,
      constraints: widget._sizeConstraints,
257
      materialTapTargetSize: widget.materialTapTargetSize ?? theme.materialTapTargetSize,
258 259 260 261 262 263
      fillColor: widget.backgroundColor ?? theme.accentColor,
      textStyle: theme.accentTextTheme.button.copyWith(
        color: foregroundColor,
        letterSpacing: 1.2,
      ),
      shape: widget.shape,
264
      clipBehavior: widget.clipBehavior,
265 266 267
      child: result,
    );

268 269 270 271 272 273 274 275 276
    if (widget.tooltip != null) {
      result = new MergeSemantics(
        child: new Tooltip(
          message: widget.tooltip,
          child: result,
        ),
      );
    }

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

    return result;
285 286
  }
}
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338

// 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) {
    return new _RenderChildOverflowBox(
      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);
      size = new Size(
        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;
    }
  }
}