blob: e5f8e3f70a3d5a064e93abbb6b8c53adfc14a282 [file] [log] [blame]
// 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.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'flat_button.dart';
import 'ink_well.dart';
import 'material.dart';
import 'raised_button.dart';
import 'theme.dart';
/// Whether a button should use the accent color for its text.
///
/// See also:
///
/// * [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].
enum ButtonTextTheme {
/// The button should use the normal color (e.g., black or white depending on the [ThemeData.brightness]) for its text.
normal,
/// The button should use the accent color (e.g., [ThemeData.accentColor]) for its text.
accent,
}
/// Defines the button color used by a widget subtree.
///
/// See also:
///
/// * [ButtonTextTheme], which is used by [textTheme].
/// * [RaisedButton], which styles itself based on the ambient [ButtonTheme].
/// * [FlatButton], which styles itself based on the ambient [ButtonTheme].
class ButtonTheme extends InheritedWidget {
/// Creates a button theme.
///
/// The child argument is required.
const ButtonTheme({
Key key,
this.textTheme: ButtonTextTheme.normal,
this.minWidth: 88.0,
this.height: 36.0,
this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
Widget child
}) : super(key: key, child: child);
/// Creates a button theme that is appropriate for button bars, as used in
/// dialog footers and in the headers of data tables.
///
/// This theme is denser, with a smaller [minWidth] and [padding], than the
/// default theme. Also, this theme uses [ButtonTextTheme.accent] rather than
/// [ButtonTextTheme.normal].
///
/// 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.
///
/// For example, buttons at the bottom of [Dialog] or [Card] widgets use this
/// button theme.
const ButtonTheme.bar({
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);
/// The button color that this subtree should use.
final ButtonTextTheme textTheme;
/// The smallest horizontal extent that the button will occupy.
///
/// Defaults to 88.0 logical pixels.
final double minWidth;
/// The vertical extent of the button.
///
/// 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.
final EdgeInsets padding;
/// The closest instance of this class that encloses the given context.
///
/// Typical usage is as follows:
///
/// ```dart
/// ButtonTheme theme = ButtonTheme.of(context);
/// ```
static ButtonTheme of(BuildContext context) {
final ButtonTheme result = context.inheritFromWidgetOfExactType(ButtonTheme);
return result ?? const ButtonTheme();
}
@override
bool updateShouldNotify(ButtonTheme oldTheme) {
return textTheme != oldTheme.textTheme
|| padding != oldTheme.padding
|| minWidth != oldTheme.minWidth
|| height != oldTheme.height;
}
}
/// The framework for building material design buttons.
///
/// Rather than using this class directly, consider using [FlatButton] or
/// [RaisedButton], which configure this class with appropriate defaults that
/// match the material design specification.
///
/// MaterialButtons whose [onPressed] handler is null will be disabled. To have
/// an enabled button, make sure to pass a non-null value for onPressed.
///
/// If you want an ink-splash effect for taps, but don't want to use a button,
/// consider using [InkWell] directly.
///
/// See also:
///
/// * [IconButton], to create buttons that contain icons rather than text.
class MaterialButton extends StatefulWidget {
/// Creates a material button.
///
/// Rather than creating a material button directly, consider using
/// [FlatButton] or [RaisedButton].
const MaterialButton({
Key key,
this.colorBrightness,
this.textTheme,
this.textColor,
this.color,
this.highlightColor,
this.splashColor,
this.elevation,
this.highlightElevation,
this.minWidth,
this.height,
this.padding,
@required this.onPressed,
this.child
}) : super(key: key);
/// The theme brightness to use for this button.
///
/// Defaults to the brightness from [ThemeData.brightness].
final Brightness colorBrightness;
/// The color scheme to use for this button's text.
///
/// Defaults to the button color from [ButtonTheme].
final ButtonTextTheme textTheme;
/// The color to use for this button's text.
final Color textColor;
/// 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'),
/// ),
/// ```
final Color color;
/// 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].
final Color splashColor;
/// The secondary color of the button when the button is in the down (pressed)
/// state. The higlight 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.
///
/// Defaults to the Theme's highlight color, [ThemeData.highlightColor].
final Color highlightColor;
/// The z-coordinate at which to place this button. This controls the size of
/// the shadow below the button.
///
/// Defaults to 0.
///
/// 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.
final double elevation;
/// The z-coordinate at which to place this button when highlighted. This
/// controls the size of the shadow below the button.
///
/// Defaults to 0.
///
/// See also:
///
/// * [elevation], the default elevation.
final double highlightElevation;
/// 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].
final EdgeInsets padding;
/// The callback that is called when the button is tapped or otherwise activated.
///
/// If this is set to null, the button will be disabled.
final VoidCallback onPressed;
/// The widget below this widget in the tree.
final Widget child;
/// 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.
bool get enabled => onPressed != null;
@override
_MaterialButtonState createState() => new _MaterialButtonState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
}
}
class _MaterialButtonState extends State<MaterialButton> {
bool _highlight = false;
Brightness get _colorBrightness {
return widget.colorBrightness ?? Theme.of(context).brightness;
}
Color get _textColor {
if (widget.textColor != null)
return widget.textColor;
if (widget.enabled) {
switch (widget.textTheme ?? ButtonTheme.of(context).textTheme) {
case ButtonTextTheme.accent:
return Theme.of(context).accentColor;
case ButtonTextTheme.normal:
switch (_colorBrightness) {
case Brightness.light:
return Colors.black87;
case Brightness.dark:
return Colors.white;
}
}
} else {
assert(_colorBrightness != null);
switch (_colorBrightness) {
case Brightness.light:
return Colors.black26;
case Brightness.dark:
return Colors.white30;
}
}
return null;
}
void _handleHighlightChanged(bool value) {
setState(() {
_highlight = value;
});
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context);
final Color textColor = _textColor;
final TextStyle style = theme.textTheme.button.copyWith(color: textColor);
final ButtonTheme buttonTheme = ButtonTheme.of(context);
final double height = widget.height ?? buttonTheme.height;
final double elevation = (_highlight ? widget.highlightElevation : widget.elevation) ?? 0.0;
final bool hasColorOrElevation = (widget.color != null || elevation > 0);
Widget contents = IconTheme.merge(
data: new IconThemeData(
color: textColor
),
child: new InkWell(
borderRadius: hasColorOrElevation ? null : kMaterialEdges[MaterialType.button],
highlightColor: widget.highlightColor ?? theme.highlightColor,
splashColor: widget.splashColor ?? theme.splashColor,
onTap: widget.onPressed,
onHighlightChanged: _handleHighlightChanged,
child: new Container(
padding: widget.padding ?? ButtonTheme.of(context).padding,
child: new Center(
widthFactor: 1.0,
child: widget.child
)
)
)
);
if (hasColorOrElevation) {
contents = new Material(
type: MaterialType.button,
color: widget.color,
elevation: elevation,
textStyle: style,
child: contents
);
} else {
contents = new AnimatedDefaultTextStyle(
style: style,
duration: kThemeChangeDuration,
child: contents
);
}
return new ConstrainedBox(
constraints: new BoxConstraints(
minWidth: widget.minWidth ?? buttonTheme.minWidth,
minHeight: height,
maxHeight: height
),
child: contents
);
}
}