button.dart 11.5 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
import 'package:flutter/foundation.dart';
6
import 'package:flutter/widgets.dart';
7

8
import 'colors.dart';
9
import 'constants.dart';
10
import 'debug.dart';
11
import 'flat_button.dart';
12 13
import 'ink_well.dart';
import 'material.dart';
14
import 'raised_button.dart';
15
import 'theme.dart';
16

17 18 19 20
/// Whether a button should use the accent color for its text.
///
/// See also:
///
21 22 23
///  * [ButtonTheme], which uses this enum to define the [ButtonTheme.textTheme].
///  * [RaisedButton], which styles itself based on the ambient [ButtonTheme].
///  * [FlatButton], which styles itself based on the ambient [ButtonTheme].
24
enum ButtonTextTheme {
25 26
  /// The button should use the normal color (e.g., black or white depending on
  /// the [ThemeData.brightness]) for its text.
27 28
  normal,

29 30
  /// The button should use the accent color (e.g., [ThemeData.accentColor]) for
  /// its text.
31 32
  accent,
}
33

34 35 36 37
/// Defines the button color used by a widget subtree.
///
/// See also:
///
38 39 40
///  * [ButtonTextTheme], which is used by [textTheme].
///  * [RaisedButton], which styles itself based on the ambient [ButtonTheme].
///  * [FlatButton], which styles itself based on the ambient [ButtonTheme].
41
class ButtonTheme extends InheritedWidget {
42 43 44 45
  /// Creates a button theme.
  ///
  /// The child argument is required.
  const ButtonTheme({
46
    Key key,
47 48 49 50
    this.textTheme: ButtonTextTheme.normal,
    this.minWidth: 88.0,
    this.height: 36.0,
    this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
51
    Widget child
52 53
  }) : super(key: key, child: child);

54 55
  /// Creates a button theme that is appropriate for button bars, as used in
  /// dialog footers and in the headers of data tables.
56 57 58 59 60
  ///
  /// This theme is denser, with a smaller [minWidth] and [padding], than the
  /// default theme. Also, this theme uses [ButtonTextTheme.accent] rather than
  /// [ButtonTextTheme.normal].
  ///
61 62 63 64 65
  /// For best effect, the label of the button at the edge of the container
  /// should have text that ends up wider than 64.0 pixels. This ensures that
  /// the alignment of the text matches the alignment of the edge of the
  /// container.
  ///
66 67
  /// For example, buttons at the bottom of [Dialog] or [Card] widgets use this
  /// button theme.
68
  const ButtonTheme.bar({
69 70 71 72 73 74 75
    Key key,
    this.textTheme: ButtonTextTheme.accent,
    this.minWidth: 64.0,
    this.height: 36.0,
    this.padding: const EdgeInsets.symmetric(horizontal: 8.0),
    Widget child
  }) : super(key: key, child: child);
76

77
  /// The button color that this subtree should use.
78
  final ButtonTextTheme textTheme;
79

80 81 82 83 84 85
  /// The smallest horizontal extent that the button will occupy.
  ///
  /// Defaults to 88.0 logical pixels.
  final double minWidth;

  /// The vertical extent of the button.
86
  ///
87 88 89 90 91 92
  /// Defaults to 36.0 logical pixels.
  final double height;

  /// The amount of space to surround the child inside the bounds of the button.
  ///
  /// Defaults to 16.0 pixels of horizontal padding.
93
  final EdgeInsetsGeometry padding;
94

95
  /// The closest instance of this class that encloses the given context.
96 97 98 99 100 101
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// ButtonTheme theme = ButtonTheme.of(context);
  /// ```
102
  static ButtonTheme of(BuildContext context) {
103
    final ButtonTheme result = context.inheritFromWidgetOfExactType(ButtonTheme);
104
    return result ?? const ButtonTheme();
105 106
  }

107
  @override
108 109 110 111 112 113
  bool updateShouldNotify(ButtonTheme oldTheme) {
    return textTheme != oldTheme.textTheme
        || padding != oldTheme.padding
        || minWidth != oldTheme.minWidth
        || height != oldTheme.height;
  }
114 115
}

116
/// The framework for building material design buttons.
117
///
118 119 120
/// Rather than using this class directly, consider using [FlatButton] or
/// [RaisedButton], which configure this class with appropriate defaults that
/// match the material design specification.
121 122 123
///
/// MaterialButtons whose [onPressed] handler is null will be disabled. To have
/// an enabled button, make sure to pass a non-null value for onPressed.
124 125 126 127
///
/// If you want an ink-splash effect for taps, but don't want to use a button,
/// consider using [InkWell] directly.
///
128 129
/// The button will expand to fit the child widget, if necessary.
///
130 131 132
/// See also:
///
///  * [IconButton], to create buttons that contain icons rather than text.
133 134 135 136 137
class MaterialButton extends StatefulWidget {
  /// Creates a material button.
  ///
  /// Rather than creating a material button directly, consider using
  /// [FlatButton] or [RaisedButton].
138
  const MaterialButton({
139
    Key key,
140
    this.colorBrightness,
141
    this.textTheme,
142
    this.textColor,
143
    this.color,
144 145
    this.highlightColor,
    this.splashColor,
146 147 148 149 150
    this.elevation,
    this.highlightElevation,
    this.minWidth,
    this.height,
    this.padding,
151
    @required this.onPressed,
152
    this.child
153
  }) : super(key: key);
154

155 156 157
  /// The theme brightness to use for this button.
  ///
  /// Defaults to the brightness from [ThemeData.brightness].
158
  final Brightness colorBrightness;
159 160 161 162

  /// The color scheme to use for this button's text.
  ///
  /// Defaults to the button color from [ButtonTheme].
163
  final ButtonTextTheme textTheme;
164

165
  /// The color to use for this button's text.
166
  final Color textColor;
167

168 169 170 171 172 173 174 175 176 177 178 179 180 181
  /// The primary color of the button, as printed on the [Material], while it
  /// is in its default (unpressed, enabled) state.
  ///
  /// Defaults to null, meaning that the color is automatically derived from the [Theme].
  ///
  /// Typically, a material design color will be used, as follows:
  ///
  /// ```dart
  ///  new MaterialButton(
  ///    color: Colors.blue[500],
  ///    onPressed: _handleTap,
  ///    child: new Text('DEMO'),
  ///  ),
  /// ```
182 183
  final Color color;

184 185 186
  /// The primary color of the button when the button is in the down (pressed)
  /// state.
  ///
187
  /// The splash is represented as a circular overlay that appears above the
188 189 190 191 192
  /// [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.
193
  ///
194
  /// Defaults to the Theme's splash color, [ThemeData.splashColor].
195 196 197
  final Color splashColor;

  /// The secondary color of the button when the button is in the down (pressed)
198 199 200 201 202 203
  /// state.
  ///
  /// The highlight color is represented as a solid color that is overlaid over
  /// the 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.
204
  ///
205
  /// Defaults to the Theme's highlight color, [ThemeData.highlightColor].
206 207
  final Color highlightColor;

208 209
  /// The z-coordinate at which to place this button. This controls the size of
  /// the shadow below the button.
210
  ///
211
  /// Defaults to 0.
212 213 214 215 216 217 218
  ///
  /// See also:
  ///
  ///  * [FlatButton], a material button specialized for the case where the
  ///    elevation is zero.
  ///  * [RaisedButton], a material button specialized for the case where the
  ///    elevation is non-zero.
219
  final double elevation;
220

221 222
  /// The z-coordinate at which to place this button when highlighted. This
  /// controls the size of the shadow below the button.
223
  ///
224
  /// Defaults to 0.
225 226 227 228
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
229
  final double highlightElevation;
230 231 232 233 234 235 236 237 238 239 240 241 242 243

  /// The smallest horizontal extent that the button will occupy.
  ///
  /// Defaults to the value from the current [ButtonTheme].
  final double minWidth;

  /// The vertical extent of the button.
  ///
  /// Defaults to the value from the current [ButtonTheme].
  final double height;

  /// The amount of space to surround the child inside the bounds of the button.
  ///
  /// Defaults to the value from the current [ButtonTheme].
244
  final EdgeInsetsGeometry padding;
245

246
  /// The callback that is called when the button is tapped or otherwise activated.
247 248
  ///
  /// If this is set to null, the button will be disabled.
249
  final VoidCallback onPressed;
Hixie's avatar
Hixie committed
250

251 252 253
  /// The widget below this widget in the tree.
  final Widget child;

254 255
  /// Whether the button is enabled or disabled. Buttons are disabled by default. To
  /// enable a button, set its [onPressed] property to a non-null value.
256 257
  bool get enabled => onPressed != null;

258 259 260
  @override
  _MaterialButtonState createState() => new _MaterialButtonState();

261
  @override
262
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
263 264
    super.debugFillProperties(description);
    description.add(new FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
Hixie's avatar
Hixie committed
265
  }
266 267
}

268 269
class _MaterialButtonState extends State<MaterialButton> {
  bool _highlight = false;
270

271
  Brightness get _colorBrightness {
272
    return widget.colorBrightness ?? Theme.of(context).brightness;
273 274
  }

275
  Color get _textColor {
276 277 278 279
    if (widget.textColor != null)
      return widget.textColor;
    if (widget.enabled) {
      switch (widget.textTheme ?? ButtonTheme.of(context).textTheme) {
280
        case ButtonTextTheme.accent:
281
          return Theme.of(context).accentColor;
282 283
        case ButtonTextTheme.normal:
          switch (_colorBrightness) {
284
            case Brightness.light:
285
              return Colors.black87;
286
            case Brightness.dark:
287 288 289
              return Colors.white;
          }
      }
290
    } else {
291
      assert(_colorBrightness != null);
292
      switch (_colorBrightness) {
293
        case Brightness.light:
294
          return Colors.black26;
295
        case Brightness.dark:
296 297
          return Colors.white30;
      }
298
    }
pq's avatar
pq committed
299
    return null;
300 301
  }

302 303
  void _handleHighlightChanged(bool value) {
    setState(() {
304
      _highlight = value;
305 306 307
    });
  }

308
  @override
309
  Widget build(BuildContext context) {
310
    assert(debugCheckHasMaterial(context));
311
    final ThemeData theme = Theme.of(context);
312
    final Color textColor = _textColor;
313
    final TextStyle style = theme.textTheme.button.copyWith(color: textColor);
314
    final ButtonTheme buttonTheme = ButtonTheme.of(context);
315
    final double height = widget.height ?? buttonTheme.height;
316
    final double elevation = (_highlight ? widget.highlightElevation : widget.elevation) ?? 0.0;
317
    final bool hasColorOrElevation = (widget.color != null || elevation > 0);
318
    Widget contents = IconTheme.merge(
319 320 321 322
      data: new IconThemeData(
        color: textColor
      ),
      child: new InkWell(
323
        borderRadius: hasColorOrElevation ? null : kMaterialEdges[MaterialType.button],
324 325 326
        highlightColor: widget.highlightColor ?? theme.highlightColor,
        splashColor: widget.splashColor ?? theme.splashColor,
        onTap: widget.onPressed,
327 328
        onHighlightChanged: _handleHighlightChanged,
        child: new Container(
329
          padding: widget.padding ?? ButtonTheme.of(context).padding,
330 331
          child: new Center(
            widthFactor: 1.0,
332
            heightFactor: 1.0,
333
            child: new Semantics(button: true, child: widget.child),
334
          )
335
        )
336
      )
337
    );
338
    if (hasColorOrElevation) {
339 340
      contents = new Material(
        type: MaterialType.button,
341
        color: widget.color,
342 343 344 345 346
        elevation: elevation,
        textStyle: style,
        child: contents
      );
    } else {
347
      contents = new AnimatedDefaultTextStyle(
348
        style: style,
349
        duration: kThemeChangeDuration,
350 351 352
        child: contents
      );
    }
353 354
    return new ConstrainedBox(
      constraints: new BoxConstraints(
355
        minWidth: widget.minWidth ?? buttonTheme.minWidth,
356 357
        minHeight: height,
      ),
358
      child: contents
359 360 361
    );
  }
}