icon_button.dart 14.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// 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/rendering.dart';
9
import 'package:flutter/widgets.dart';
10

11
import 'constants.dart';
12
import 'debug.dart';
13
import 'icons.dart';
14
import 'ink_well.dart';
15
import 'material.dart';
16
import 'theme.dart';
17
import 'theme_data.dart';
Hixie's avatar
Hixie committed
18
import 'tooltip.dart';
19

20
// Minimum logical pixel size of the IconButton.
21 22
// See: <https://material.io/design/usability/accessibility.html#layout-typography>.
const double _kMinButtonSize = kMinInteractiveDimension;
23

24
/// A material design icon button.
25 26
///
/// An icon button is a picture printed on a [Material] widget that reacts to
27
/// touches by filling with color (ink).
28
///
29 30
/// Icon buttons are commonly used in the [AppBar.actions] field, but they can
/// be used in many other places as well.
31
///
32 33
/// If the [onPressed] callback is null, then the button will be disabled and
/// will not react to touch.
34
///
35 36
/// Requires one of its ancestors to be a [Material] widget.
///
37 38
/// The hit region of an icon button will, if possible, be at least
/// kMinInteractiveDimension pixels in size, regardless of the actual
39
/// [iconSize], to satisfy the [touch target size](https://material.io/design/layout/spacing-methods.html#touch-targets)
40 41
/// requirements in the Material Design specification. The [alignment] controls
/// how the icon itself is positioned within the hit region.
42
///
43
/// {@tool dartpad}
44 45 46
/// This sample shows an `IconButton` that uses the Material icon "volume_up" to
/// increase the volume.
///
47 48
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/icon_button.png)
///
49
/// ** See code in examples/api/lib/material/icon_button/icon_button.0.dart **
50
/// {@end-tool}
51
///
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
/// ### Icon sizes
///
/// When creating an icon button with an [Icon], do not override the
/// icon's size with its [Icon.size] parameter, use the icon button's
/// [iconSize] parameter instead.  For example do this:
///
/// ```dart
/// IconButton(iconSize: 72, icon: Icon(Icons.favorite), ...)
/// ```
///
/// Avoid doing this:
///
/// ```dart
/// IconButton(icon: Icon(Icons.favorite, size: 72), ...)
/// ```
///
/// If you do, the button's size will be based on the default icon
/// size, not 72, which may produce unexpected layouts and clipping
/// issues.
///
72 73 74 75 76 77 78 79 80 81 82 83
/// ### Adding a filled background
///
/// Icon buttons don't support specifying a background color or other
/// background decoration because typically the icon is just displayed
/// on top of the parent widget's background. Icon buttons that appear
/// in [AppBar.actions] are an example of this.
///
/// It's easy enough to create an icon button with a filled background
/// using the [Ink] widget. The [Ink] widget renders a decoration on
/// the underlying [Material] along with the splash and highlight
/// [InkResponse] contributed by descendant widgets.
///
84
/// {@tool dartpad}
85 86 87 88 89
/// In this sample the icon button's background color is defined with an [Ink]
/// widget whose child is an [IconButton]. The icon button's filled background
/// is a light shade of blue, it's a filled circle, and it's as big as the
/// button is.
///
90 91
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/icon_button_background.png)
///
92
/// ** See code in examples/api/lib/material/icon_button/icon_button.1.dart **
93 94
/// {@end-tool}
///
95 96
/// See also:
///
97 98 99 100 101
///  * [Icons], a library of predefined icons.
///  * [BackButton], an icon button for a "back" affordance which adapts to the
///    current platform's conventions.
///  * [CloseButton], an icon button for closing pages.
///  * [AppBar], to show a toolbar at the top of an application.
102
///  * [TextButton], [ElevatedButton], [OutlinedButton], for buttons with text labels and an optional icon.
103
///  * [InkResponse] and [InkWell], for the ink splash effect itself.
104
class IconButton extends StatelessWidget {
105 106 107 108 109 110
  /// Creates an icon button.
  ///
  /// Icon buttons are commonly used in the [AppBar.actions] field, but they can
  /// be used in many other places as well.
  ///
  /// Requires one of its ancestors to be a [Material] widget.
111
  ///
112 113
  /// The [iconSize], [padding], [autofocus], and [alignment] arguments must not
  /// be null (though they each have default values).
114
  ///
Ian Hickson's avatar
Ian Hickson committed
115 116
  /// The [icon] argument must be specified, and is typically either an [Icon]
  /// or an [ImageIcon].
117
  const IconButton({
118
    super.key,
119
    this.iconSize,
120
    this.visualDensity,
121 122
    this.padding = const EdgeInsets.all(8.0),
    this.alignment = Alignment.center,
123
    this.splashRadius,
124
    this.color,
125 126
    this.focusColor,
    this.hoverColor,
127 128
    this.highlightColor,
    this.splashColor,
129
    this.disabledColor,
130
    required this.onPressed,
131
    this.mouseCursor,
132
    this.focusNode,
133
    this.autofocus = false,
134
    this.tooltip,
135
    this.enableFeedback = true,
136
    this.constraints,
137
    required this.icon,
138
  }) : assert(padding != null),
139
       assert(alignment != null),
140
       assert(splashRadius == null || splashRadius > 0),
141
       assert(autofocus != null),
142
       assert(icon != null);
143

Adam Barth's avatar
Adam Barth committed
144
  /// The size of the icon inside the button.
145
  ///
146 147
  /// If null, uses [IconThemeData.size]. If it is also null, the default size
  /// is 24.0.
148 149 150 151 152 153 154
  ///
  /// The size given here is passed down to the widget in the [icon] property
  /// via an [IconTheme]. Setting the size here instead of in, for example, the
  /// [Icon.size] property allows the [IconButton] to size the splash area to
  /// fit the [Icon]. If you were to set the size of the [Icon] using
  /// [Icon.size] instead, then the [IconButton] would default to 24.0 and then
  /// the [Icon] itself would likely get clipped.
155
  final double? iconSize;
Adam Barth's avatar
Adam Barth committed
156

157 158 159 160 161 162
  /// Defines how compact the icon button's layout will be.
  ///
  /// {@macro flutter.material.themedata.visualDensity}
  ///
  /// See also:
  ///
163 164
  ///  * [ThemeData.visualDensity], which specifies the [visualDensity] for all
  ///    widgets within a [Theme].
165
  final VisualDensity? visualDensity;
166

167 168
  /// The padding around the button's icon. The entire padded icon will react
  /// to input gestures.
169 170
  ///
  /// This property must not be null. It defaults to 8.0 padding on all sides.
171
  final EdgeInsetsGeometry padding;
172 173

  /// Defines how the icon is positioned within the IconButton.
174
  ///
175
  /// This property must not be null. It defaults to [Alignment.center].
176 177 178 179 180 181 182
  ///
  /// See also:
  ///
  ///  * [Alignment], a class with convenient constants typically used to
  ///    specify an [AlignmentGeometry].
  ///  * [AlignmentDirectional], like [Alignment] for specifying alignments
  ///    relative to text direction.
183
  final AlignmentGeometry alignment;
184

185 186 187
  /// The splash radius.
  ///
  /// If null, default splash radius of [Material.defaultSplashRadius] is used.
188
  final double? splashRadius;
189

Ian Hickson's avatar
Ian Hickson committed
190 191
  /// The icon to display inside the button.
  ///
192
  /// The [Icon.size] and [Icon.color] of the icon is configured automatically
193
  /// based on the [iconSize] and [color] properties of _this_ widget using an
194 195
  /// [IconTheme] and therefore should not be explicitly given in the icon
  /// widget.
196 197
  ///
  /// This property must not be null.
Ian Hickson's avatar
Ian Hickson committed
198 199 200
  ///
  /// See [Icon], [ImageIcon].
  final Widget icon;
Adam Barth's avatar
Adam Barth committed
201

202 203 204
  /// The color for the button's icon when it has the input focus.
  ///
  /// Defaults to [ThemeData.focusColor] of the ambient theme.
205
  final Color? focusColor;
206 207 208 209

  /// The color for the button's icon when a pointer is hovering over it.
  ///
  /// Defaults to [ThemeData.hoverColor] of the ambient theme.
210
  final Color? hoverColor;
211

212
  /// The color to use for the icon inside the button, if the icon is enabled.
Ian Hickson's avatar
Ian Hickson committed
213
  /// Defaults to leaving this up to the [icon] widget.
214 215 216
  ///
  /// The icon is enabled if [onPressed] is not null.
  ///
217
  /// ```dart
218 219 220 221
  /// IconButton(
  ///   color: Colors.blue,
  ///   onPressed: _handleTap,
  ///   icon: Icons.widgets,
222
  /// )
223
  /// ```
224
  final Color? color;
225

226 227 228 229 230 231 232 233
  /// The primary color of the button when the button is in the down (pressed) state.
  /// The splash is represented as a circular overlay that appears above the
  /// [highlightColor] overlay. The splash overlay has a center point that matches
  /// the hit point of the user touch event. The splash overlay will expand to
  /// fill the button area if the touch is held for long enough time. If the splash
  /// color has transparency then the highlight and button color will show through.
  ///
  /// Defaults to the Theme's splash color, [ThemeData.splashColor].
234
  final Color? splashColor;
235 236

  /// The secondary color of the button when the button is in the down (pressed)
237
  /// state. The highlight color is represented as a solid color that is overlaid over the
238 239 240 241
  /// button color (if any). If the highlight color has transparency, the button color
  /// will show through. The highlight fades in quickly as the button is held down.
  ///
  /// Defaults to the Theme's highlight color, [ThemeData.highlightColor].
242
  final Color? highlightColor;
243

244 245 246 247
  /// The color to use for the icon inside the button, if the icon is disabled.
  /// Defaults to the [ThemeData.disabledColor] of the current [Theme].
  ///
  /// The icon is disabled if [onPressed] is null.
248
  final Color? disabledColor;
249

250
  /// The callback that is called when the button is tapped or otherwise activated.
251 252
  ///
  /// If this is set to null, the button will be disabled.
253
  final VoidCallback? onPressed;
Adam Barth's avatar
Adam Barth committed
254

255
  /// {@macro flutter.material.RawMaterialButton.mouseCursor}
256
  ///
257
  /// If set to null, will default to
258
  /// - [SystemMouseCursors.basic], if [onPressed] is null
259 260
  /// - [SystemMouseCursors.click], otherwise
  final MouseCursor? mouseCursor;
261

262
  /// {@macro flutter.widgets.Focus.focusNode}
263
  final FocusNode? focusNode;
264

265 266 267
  /// {@macro flutter.widgets.Focus.autofocus}
  final bool autofocus;

Adam Barth's avatar
Adam Barth committed
268 269 270 271
  /// 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.
272
  final String? tooltip;
273

274 275 276 277 278 279 280 281 282 283
  /// Whether detected gestures should provide acoustic and/or haptic feedback.
  ///
  /// For example, on Android a tap will produce a clicking sound and a
  /// long-press will produce a short vibration, when feedback is enabled.
  ///
  /// See also:
  ///
  ///  * [Feedback] for providing platform-specific feedback to certain actions.
  final bool enableFeedback;

284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
  /// Optional size constraints for the button.
  ///
  /// When unspecified, defaults to:
  /// ```dart
  /// const BoxConstraints(
  ///   minWidth: kMinInteractiveDimension,
  ///   minHeight: kMinInteractiveDimension,
  /// )
  /// ```
  /// where [kMinInteractiveDimension] is 48.0, and then with visual density
  /// applied.
  ///
  /// The default constraints ensure that the button is accessible.
  /// Specifying this parameter enables creation of buttons smaller than
  /// the minimum size, but it is not recommended.
  ///
  /// The visual density uses the [visualDensity] parameter if specified,
  /// and `Theme.of(context).visualDensity` otherwise.
302
  final BoxConstraints? constraints;
303

304
  @override
305
  Widget build(BuildContext context) {
306
    assert(debugCheckHasMaterial(context));
307
    final ThemeData theme = Theme.of(context);
308
    Color? currentColor;
309 310 311
    if (onPressed != null)
      currentColor = color;
    else
312
      currentColor = disabledColor ?? theme.disabledColor;
313

314 315 316 317 318 319 320
    final VisualDensity effectiveVisualDensity = visualDensity ?? theme.visualDensity;

    final BoxConstraints unadjustedConstraints = constraints ?? const BoxConstraints(
      minWidth: _kMinButtonSize,
      minHeight: _kMinButtonSize,
    );
    final BoxConstraints adjustedConstraints = effectiveVisualDensity.effectiveConstraints(unadjustedConstraints);
321
    final double effectiveIconSize = iconSize ?? IconTheme.of(context).size ?? 24.0;
322

323
    Widget result = ConstrainedBox(
324
      constraints: adjustedConstraints,
325 326 327
      child: Padding(
        padding: padding,
        child: SizedBox(
328 329
          height: effectiveIconSize,
          width: effectiveIconSize,
330 331 332 333
          child: Align(
            alignment: alignment,
            child: IconTheme.merge(
              data: IconThemeData(
334
                size: effectiveIconSize,
335
                color: currentColor,
336
              ),
337
              child: icon,
338 339 340 341
            ),
          ),
        ),
      ),
342
    );
343

Hixie's avatar
Hixie committed
344
    if (tooltip != null) {
345
      result = Tooltip(
346
        message: tooltip,
347
        child: result,
Hixie's avatar
Hixie committed
348 349
      );
    }
350 351 352 353

    return Semantics(
      button: true,
      enabled: onPressed != null,
354
      child: InkResponse(
355
        focusNode: focusNode,
356
        autofocus: autofocus,
357
        canRequestFocus: onPressed != null,
358
        onTap: onPressed,
359
        mouseCursor: mouseCursor ?? (onPressed == null ? SystemMouseCursors.basic : SystemMouseCursors.click),
360
        enableFeedback: enableFeedback,
361 362 363 364
        focusColor: focusColor ?? theme.focusColor,
        hoverColor: hoverColor ?? theme.hoverColor,
        highlightColor: highlightColor ?? theme.highlightColor,
        splashColor: splashColor ?? theme.splashColor,
365 366 367 368 369
        radius: splashRadius ?? math.max(
          Material.defaultSplashRadius,
          (effectiveIconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
          // x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
        ),
370
        child: result,
371
      ),
Hixie's avatar
Hixie committed
372
    );
373
  }
Hixie's avatar
Hixie committed
374

375
  @override
376 377
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
378 379
    properties.add(DiagnosticsProperty<Widget>('icon', icon, showName: false));
    properties.add(StringProperty('tooltip', tooltip, defaultValue: null, quoted: false));
380
    properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
381 382 383 384 385 386
    properties.add(ColorProperty('color', color, defaultValue: null));
    properties.add(ColorProperty('disabledColor', disabledColor, defaultValue: null));
    properties.add(ColorProperty('focusColor', focusColor, defaultValue: null));
    properties.add(ColorProperty('hoverColor', hoverColor, defaultValue: null));
    properties.add(ColorProperty('highlightColor', highlightColor, defaultValue: null));
    properties.add(ColorProperty('splashColor', splashColor, defaultValue: null));
387 388
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
Hixie's avatar
Hixie committed
389
  }
390
}