Unverified Commit c4e84380 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added Material OutlineButton (#14939)

parent da24ad0b
......@@ -65,6 +65,7 @@ export 'src/material/list_tile.dart';
export 'src/material/material.dart';
export 'src/material/material_localizations.dart';
export 'src/material/mergeable_material.dart';
export 'src/material/outline_button.dart';
export 'src/material/page.dart';
export 'src/material/paginated_data_table.dart';
export 'src/material/popup_menu.dart';
......
......@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'button_theme.dart';
import 'colors.dart';
import 'constants.dart';
import 'ink_well.dart';
import 'material.dart';
import 'theme.dart';
......@@ -29,6 +30,7 @@ class RawMaterialButton extends StatefulWidget {
const RawMaterialButton({
Key key,
@required this.onPressed,
this.onHighlightChanged,
this.textStyle,
this.fillColor,
this.highlightColor,
......@@ -39,6 +41,7 @@ class RawMaterialButton extends StatefulWidget {
this.padding: EdgeInsets.zero,
this.constraints: const BoxConstraints(minWidth: 88.0, minHeight: 36.0),
this.shape: const RoundedRectangleBorder(),
this.animationDuration: kThemeChangeDuration,
this.child,
}) : assert(shape != null),
assert(elevation != null),
......@@ -46,6 +49,7 @@ class RawMaterialButton extends StatefulWidget {
assert(disabledElevation != null),
assert(padding != null),
assert(constraints != null),
assert(animationDuration != null),
super(key: key);
/// Called when the button is tapped or otherwise activated.
......@@ -53,6 +57,10 @@ class RawMaterialButton extends StatefulWidget {
/// If this is set to null, the button will be disabled, see [enabled].
final VoidCallback onPressed;
/// Called by the underlying [InkWell] widget's [InkWell.onHighlightChanged]
/// callback.
final ValueChanged<bool> onHighlightChanged;
/// Defines the default text style, with [Material.textStyle], for the
/// button's [child].
final TextStyle textStyle;
......@@ -111,6 +119,11 @@ class RawMaterialButton extends StatefulWidget {
/// button has an elevation, then its drop shadow is defined by this shape.
final ShapeBorder shape;
/// Defines the duration of animated changes for [shape] and [elevation].
///
/// The default value is [kThemeChangeDuration].
final Duration animationDuration;
/// Typically the button's label.
final Widget child;
......@@ -129,6 +142,8 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
void _handleHighlightChanged(bool value) {
setState(() {
_highlight = value;
if (widget.onHighlightChanged != null)
widget.onHighlightChanged(value);
});
}
......@@ -150,6 +165,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
shape: widget.shape,
color: widget.fillColor,
type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button,
animationDuration: widget.animationDuration,
child: new InkWell(
onHighlightChanged: _handleHighlightChanged,
splashColor: widget.splashColor,
......
......@@ -34,7 +34,8 @@ import 'theme.dart';
/// trying to change the button's [color] and it is not having any effect, check
/// that you are passing a non-null [onPressed] handler.
///
/// Flat buttons will expand to fit the child widget, if necessary.
/// Flat buttons have a minimum size of 88.0 by 36.0 which can be overidden
/// with [ButtonTheme].
///
/// See also:
///
......@@ -43,13 +44,14 @@ import 'theme.dart';
/// * [SimpleDialogOption], which is used in [SimpleDialog]s.
/// * [IconButton], to create buttons that just contain icons.
/// * [InkWell], which implements the ink splash part of a flat button.
//// * [RawMaterialButton], the widget this widget is based on.
/// * [RawMaterialButton], the widget this widget is based on.
/// * <https://material.google.com/components/buttons.html>
class FlatButton extends StatelessWidget {
/// Create a simple text button.
const FlatButton({
Key key,
@required this.onPressed,
this.onHighlightChanged,
this.textTheme,
this.textColor,
this.disabledTextColor,
......@@ -73,6 +75,7 @@ class FlatButton extends StatelessWidget {
FlatButton.icon({
Key key,
@required this.onPressed,
this.onHighlightChanged,
this.textTheme,
this.textColor,
this.disabledTextColor,
......@@ -102,6 +105,10 @@ class FlatButton extends StatelessWidget {
/// If this is set to null, the button will be disabled, see [enabled].
final VoidCallback onPressed;
/// Called by the underlying [InkWell] widget's [InkWell.onHighlightChanged]
/// callback.
final ValueChanged<bool> onHighlightChanged;
/// Defines the button's base colors, and the defaults for the button's minimum
/// size, internal padding, and shape.
///
......@@ -276,6 +283,7 @@ class FlatButton extends StatelessWidget {
return new RawMaterialButton(
onPressed: onPressed,
onHighlightChanged: onHighlightChanged,
fillColor: fillColor,
textStyle: theme.textTheme.button.copyWith(color: textColor),
highlightColor: _getHighlightColor(theme, buttonTheme),
......
......@@ -88,7 +88,7 @@ abstract class MaterialInkController {
/// The Material widget is responsible for:
///
/// 1. Clipping: Material clips its widget sub-tree to the shape specified by
/// [type] and [borderRadius].
/// [shape], [type], and [borderRadius].
/// 2. Elevation: Material elevates its widget sub-tree on the Z axis by
/// [elevation] pixels, and draws the appropriate shadow.
/// 3. Ink effects: Material shows ink effects implemented by [InkFeature]s
......@@ -108,14 +108,15 @@ abstract class MaterialInkController {
///
/// In general, the features of a [Material] should not change over time (e.g. a
/// [Material] should not change its [color], [shadowColor] or [type]).
/// Changes to [elevation] and [shadowColor] are animated. Changes to [shape] are
/// animated if [type] is not [MaterialType.transparency] and [ShapeBorder.lerp]
/// between the previous and next [shape] values is supported.
/// Changes to [elevation] and [shadowColor] are animated for [animationDuration].
/// Changes to [shape] are animated if [type] is not [MaterialType.transparency]
/// and [ShapeBorder.lerp] between the previous and next [shape] values is
/// supported. Shape changes are also animated for [animationDuration].
///
///
/// ## Shape
///
/// The shape for material is determined by [type] and [borderRadius].
/// The shape for material is determined by [shape], [type], and [borderRadius].
///
/// - If [shape] is non null, it determines the shape.
/// - If [shape] is null and [borderRadius] is non null, the shape is a
......@@ -153,9 +154,10 @@ abstract class MaterialInkController {
class Material extends StatefulWidget {
/// Creates a piece of material.
///
/// The [type], [elevation] and [shadowColor] arguments must not be null.
/// The [type], [elevation], [shadowColor], and [animationDuration] arguments
/// must not be null.
///
/// If a [shape] is specified, then the [borderRadius] property must not be
/// If a [shape] is specified, then the [borderRadius] property must be
/// null and the [type] property must not be [MaterialType.circle]. If the
/// [borderRadius] is specified, then the [type] property must not be
/// [MaterialType.circle]. In both cases, these restrictions are intended to
......@@ -169,11 +171,13 @@ class Material extends StatefulWidget {
this.textStyle,
this.borderRadius,
this.shape,
this.animationDuration: kThemeChangeDuration,
this.child,
}) : assert(type != null),
assert(elevation != null),
assert(shadowColor != null),
assert(!(shape != null && borderRadius != null)),
assert(animationDuration != null),
assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))),
super(key: key);
......@@ -194,7 +198,7 @@ class Material extends StatefulWidget {
/// widget conceptually defines an independent printed piece of material.
///
/// Defaults to 0. Changing this value will cause the shadow to animate over
/// [kThemeChangeDuration].
/// [animationDuration].
final double elevation;
/// The color to paint the material.
......@@ -222,6 +226,12 @@ class Material extends StatefulWidget {
/// zero.
final ShapeBorder shape;
/// Defines the duration of animated changes for [shape], [elevation],
/// and [shadowColor].
///
/// The default value is [kThemeChangeDuration].
final Duration animationDuration;
/// If non-null, the corners of this box are rounded by this [BorderRadius].
/// Otherwise, the corners specified for the current [type] of material are
/// used.
......@@ -287,7 +297,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
if (contents != null) {
contents = new AnimatedDefaultTextStyle(
style: widget.textStyle ?? Theme.of(context).textTheme.body1,
duration: kThemeChangeDuration,
duration: widget.animationDuration,
child: contents
);
}
......@@ -317,7 +327,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
if (widget.type == MaterialType.canvas && widget.shape == null && widget.borderRadius == null) {
return new AnimatedPhysicalModel(
curve: Curves.fastOutSlowIn,
duration: kThemeChangeDuration,
duration: widget.animationDuration,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.zero,
elevation: widget.elevation,
......@@ -335,7 +345,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
return new _MaterialInterior(
curve: Curves.fastOutSlowIn,
duration: kThemeChangeDuration,
duration: widget.animationDuration,
shape: shape,
elevation: widget.elevation,
color: backgroundColor,
......
// Copyright 2018 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 'button_theme.dart';
import 'colors.dart';
import 'raised_button.dart';
import 'theme.dart';
// The total time to make the button's fill color opaque and change
// its elevation.
const Duration _kPressDuration = const Duration(milliseconds: 150);
// Half of _kPressDuration: just the time to change the button's
// elevation.
const Duration _kElevationDuration = const Duration(milliseconds: 75);
/// A cross between [RaisedButton] and [FlatButton]: a bordered button whose
/// elevation increases and whose background becomes opaque when the button
/// is pressed.
///
/// An outline button's elevation is initially 0.0 and its background [color]
/// is transparent. When the button is pressed its background becomes opaque
/// and then its elevation increases to [highlightElevation].
///
/// The outline button has a border whose shape is defined by [shape]
/// and whose appearance is defined by [borderSide], [disabledBorderColor],
/// and [highlightedBorderColor].
///
/// If the [onPressed] callback is null, then the button will be disabled and by
/// default will resemble a flat button in the [disabledColor].
///
/// If you want an ink-splash effect for taps, but don't want to use a button,
/// consider using [InkWell] directly.
///
/// Outline buttons have a minimum size of 88.0 by 36.0 which can be overidden
/// with [ButtonTheme].
///
/// See also:
///
/// * [RaisedButton], a filled material design button with a shadow.
/// * [FlatButton], a material design button without a shadow.
/// * [DropdownButton], a button that shows options to select from.
/// * [FloatingActionButton], the round button in material applications.
/// * [IconButton], to create buttons that just contain icons.
/// * [InkWell], which implements the ink splash part of a flat button.
/// * <https://material.google.com/components/buttons.html>
class OutlineButton extends StatefulWidget {
/// Create a filled button.
///
/// The [highlightElevation], and [borderWidth]
/// arguments must not be null.
const OutlineButton({
Key key,
@required this.onPressed,
this.textTheme,
this.textColor,
this.disabledTextColor,
this.color,
this.highlightColor,
this.splashColor,
this.highlightElevation: 2.0,
this.borderSide,
this.disabledBorderColor,
this.highlightedBorderColor,
this.padding,
this.shape,
this.child,
}) : assert(highlightElevation != null && highlightElevation >= 0.0),
super(key: key);
/// Create an outline button from a pair of widgets that serve as the button's
/// [icon] and [label].
///
/// The icon and label are arranged in a row and padded by 12 logical pixels
/// at the start, and 16 at the end, with an 8 pixel gap in between.
///
/// The [highlightElevation], [icon], and [label] must not be null.
OutlineButton.icon({
Key key,
@required this.onPressed,
this.textTheme,
this.textColor,
this.disabledTextColor,
this.color,
this.highlightColor,
this.splashColor,
this.highlightElevation: 2.0,
this.borderSide,
this.disabledBorderColor,
this.highlightedBorderColor,
this.shape,
@required Widget icon,
@required Widget label,
}) : assert(highlightElevation != null && highlightElevation >= 0.0),
assert(icon != null),
assert(label != null),
padding = const EdgeInsetsDirectional.only(start: 12.0, end: 16.0),
child = new Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
icon,
const SizedBox(width: 8.0),
label,
],
),
super(key: key);
/// Called when the button is tapped or otherwise activated.
///
/// If this is set to null, the button will be disabled, see [enabled].
final VoidCallback onPressed;
/// Defines the button's base colors, and the defaults for the button's minimum
/// size, internal padding, and shape.
///
/// Defaults to `ButtonTheme.of(context).textTheme`.
final ButtonTextTheme textTheme;
/// The color to use for this button's text.
///
/// The button's [Material.textStyle] will be the current theme's button
/// text style, [ThemeData.textTheme.button], configured with this color.
///
/// The default text color depends on the button theme's text theme,
/// [ButtonThemeData.textTheme].
///
/// See also:
/// * [disabledTextColor], the text color to use when the button has been
/// disabled.
final Color textColor;
/// The color to use for this button's text when the button is disabled.
///
/// The button's [Material.textStyle] will be the current theme's button
/// text style, [ThemeData.textTheme.button], configured with this color.
///
/// The default value is the theme's disabled color,
/// [ThemeData.disabledColor].
///
/// See also:
/// * [textColor] - The color to use for this button's text when the button is [enabled].
final Color disabledTextColor;
/// The button's opaque fill color when it's [enabled] and has been pressed.
///
/// If null this value defaults to white for light themes (see
/// [ThemeData.brightness]), and black for dark themes.
final Color color;
/// The splash color of the button's [InkWell].
///
/// The ink splash indicates that the button has been touched. It
/// appears on top of the button's child and spreads in an expanding
/// circle beginning where the touch occurred.
///
/// If [textTheme] is [ButtonTextTheme.primary], the default splash color is
/// is based on the theme's primary color [ThemeData.primaryColor],
/// otherwise it's the current theme's splash color, [ThemeData.splashColor].
///
/// The appearance of the splash can be configured with the theme's splash
/// factory, [ThemeData.splashFactory].
final Color splashColor;
/// The highlight color of the button's [InkWell].
///
/// The highlight indicates that the button is actively being pressed. It
/// appears on top of the button's child and quickly spreads to fill
/// the button, and then fades out.
///
/// If [textTheme] is [ButtonTextTheme.primary], the default highlight color is
/// transparent (in other words the highlight doesn't appear). Otherwise it's
/// the current theme's highlight color, [ThemeData.highlightColor].
final Color highlightColor;
/// The elevation of the button when it's [enabled] and has been pressed.
///
/// If null, this value defaults to 2.0.
///
/// The elevation of an outline button is always 0.0 unless its enabled
/// and has been pressed.
final double highlightElevation;
/// Defines the color of the border when the button is enabled but not
/// pressed, and the border outline's width and style in general.
///
/// If the border side's [BorderSide.style] is [BorderStyle.none], then
/// an outline is not drawn.
///
/// If null the default border's style is [BorderStyle.solid], its
/// [BorderSide.width] is 2.0, and its color is a light shade of grey.
final BorderSide borderSide;
/// The outline border's color when the button is [enabled] and pressed.
///
/// If null this value defaults to the theme's primary color,
/// [ThemeData.primaryColor].
final Color highlightedBorderColor;
/// The outline border's color when the button is not [enabled].
///
/// If null this value defaults to a very light shade of grey for light
/// themes (see [ThemeData.brightness]), and a very dark shade of grey for
/// dark themes.
final Color disabledBorderColor;
/// The internal padding for the button's [child].
///
/// Defaults to the value from the current [ButtonTheme],
/// [ButtonThemeData.padding].
final EdgeInsetsGeometry padding;
/// The shape of the button's [Material] and its outline.
///
/// The button's highlight and splash are clipped to this shape. If the
/// button has a [highlightElevation], then its drop shadow is defined by this
/// shape as well.
final ShapeBorder shape;
/// The button's label.
///
/// Often a [Text] widget in all caps.
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
_OutlineButtonState createState() => new _OutlineButtonState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
description.add(new DiagnosticsProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: null));
description.add(new DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
description.add(new DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
description.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null));
description.add(new DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
description.add(new DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
description.add(new DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: 2.0));
description.add(new DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null));
description.add(new DiagnosticsProperty<Color>('disabledBorderColor', disabledBorderColor, defaultValue: null));
description.add(new DiagnosticsProperty<Color>('highlightedBorderColor', highlightedBorderColor, defaultValue: null));
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
description.add(new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
}
}
class _OutlineButtonState extends State<OutlineButton> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _fillAnimation;
Animation<double> _elevationAnimation;
bool _pressed = false;
@override
void initState() {
super.initState();
// The Material widget animates its shape (which includes the outline
// border) and elevation over _kElevationDuration. When pressed, the
// button makes its fill color opaque white first, and then sets
// its highlightElevation. We can't change the elevation while the
// button's fill is translucent, because the shadow fills the interior
// of the button.
_controller = new AnimationController(
duration: _kPressDuration,
vsync: this
);
_fillAnimation = new CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5,
curve: Curves.fastOutSlowIn,
),
);
_elevationAnimation = new CurvedAnimation(
parent: _controller,
curve: const Interval(0.5, 0.5),
reverseCurve: const Interval(1.0, 1.0),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
ButtonTextTheme _getTextTheme(ButtonThemeData buttonTheme) {
return widget.textTheme ?? buttonTheme.textTheme;
}
// TODO(hmuller): this method is the same as FlatButton
Color _getTextColor(ThemeData theme, ButtonThemeData buttonTheme) {
final Color color = widget.enabled ? widget.textColor : widget.disabledTextColor;
if (color != null)
return color;
final bool themeIsDark = theme.brightness == Brightness.dark;
switch (_getTextTheme(buttonTheme)) {
case ButtonTextTheme.normal:
return widget.enabled
? (themeIsDark ? Colors.white : Colors.black87)
: theme.disabledColor;
case ButtonTextTheme.accent:
return widget.enabled
? theme.accentColor
: theme.disabledColor;
case ButtonTextTheme.primary:
return widget.enabled
? theme.buttonColor
: (themeIsDark ? Colors.white30 : Colors.black38);
}
return null;
}
Color _getFillColor(ThemeData theme) {
final bool themeIsDark = theme.brightness == Brightness.dark;
final Color color = widget.color ?? (themeIsDark
? const Color(0x00000000)
: const Color(0x00FFFFFF));
final Tween<Color> colorTween = new ColorTween(
begin: color.withAlpha(0x00),
end: color.withAlpha(0xFF),
);
return colorTween.evaluate(_fillAnimation);
}
// TODO(hmuller): this method is the same as FlatButton
Color _getSplashColor(ThemeData theme, ButtonThemeData buttonTheme) {
if (widget.splashColor != null)
return widget.splashColor;
switch (_getTextTheme(buttonTheme)) {
case ButtonTextTheme.normal:
case ButtonTextTheme.accent:
return theme.splashColor;
case ButtonTextTheme.primary:
return theme.brightness == Brightness.dark
? Colors.white12
: theme.primaryColor.withOpacity(0.12);
}
return Colors.transparent;
}
BorderSide _getOutline(ThemeData theme, ButtonThemeData buttonTheme) {
final bool themeIsDark = theme.brightness == Brightness.dark;
if (widget.borderSide?.style == BorderStyle.none)
return widget.borderSide;
final Color color = widget.enabled
? (_pressed
? widget.highlightedBorderColor ?? theme.primaryColor
: (widget.borderSide?.color ??
(themeIsDark ? Colors.grey[600] : Colors.grey[200])))
: (widget.disabledBorderColor ??
(themeIsDark ? Colors.grey[800] : Colors.grey[100]));
return new BorderSide(
color: color,
width: widget.borderSide?.width ?? 2.0,
);
}
double _getHighlightElevation() {
return new Tween<double>(
begin: 0.0,
end: widget.highlightElevation ?? 2.0,
).evaluate(_elevationAnimation);
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ButtonThemeData buttonTheme = ButtonTheme.of(context);
final Color textColor = _getTextColor(theme, buttonTheme);
final Color splashColor = _getSplashColor(theme, buttonTheme);
return new AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return new RaisedButton(
textColor: textColor,
disabledTextColor: widget.disabledTextColor,
color: _getFillColor(theme),
splashColor: splashColor,
highlightColor: widget.highlightColor,
disabledColor: Colors.transparent,
onPressed: widget.onPressed,
elevation: 0.0,
disabledElevation: 0.0,
highlightElevation: _getHighlightElevation(),
onHighlightChanged: (bool value) {
setState(() {
_pressed = value;
if (value)
_controller.forward();
else
_controller.reverse();
});
},
padding: widget.padding,
shape: new _OutlineBorder(
shape: widget.shape ?? buttonTheme.shape,
side: _getOutline(theme, buttonTheme),
),
animationDuration: _kElevationDuration,
child: widget.child,
);
},
);
}
}
// Render the button's outline border using using the OutlineButton's
// border parameters and the button or buttonTheme's shape.
class _OutlineBorder extends ShapeBorder {
const _OutlineBorder({
@required this.shape,
@required this.side,
}) : assert(shape != null),
assert(side != null);
final ShapeBorder shape;
final BorderSide side;
@override
EdgeInsetsGeometry get dimensions {
return new EdgeInsets.all(side.width);
}
@override
ShapeBorder scale(double t) {
return new _OutlineBorder(
shape: shape.scale(t),
side: side.scale(t),
);
}
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
assert(t != null);
if (a is _OutlineBorder) {
return new _OutlineBorder(
side: BorderSide.lerp(a.side, side, t),
shape: shape.lerpFrom(a.shape, t),
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
assert(t != null);
if (b is _OutlineBorder) {
return new _OutlineBorder(
side: BorderSide.lerp(side, b.side, t),
shape: shape.lerpTo(b.shape, t),
);
}
return super.lerpTo(b, t);
}
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return shape.getInnerPath(rect.deflate(side.width), textDirection: textDirection);
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return shape.getOuterPath(rect, textDirection: textDirection);
}
@override
void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
switch (side.style) {
case BorderStyle.none:
break;
case BorderStyle.solid:
canvas.drawPath(shape.getOuterPath(rect), side.toPaint());
}
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final _OutlineBorder typedOther = other;
return side == typedOther.side && shape == typedOther.shape;
}
@override
int get hashCode => hashValues(side, shape);
}
......@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
import 'button.dart';
import 'button_theme.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
/// A material design "raised button".
......@@ -27,7 +28,8 @@ import 'theme.dart';
/// If you want an ink-splash effect for taps, but don't want to use a button,
/// consider using [InkWell] directly.
///
/// Raised buttons will expand to fit the child widget, if necessary.
/// Raised buttons have a minimum size of 88.0 by 36.0 which can be overidden
/// with [ButtonTheme].
///
/// See also:
///
......@@ -36,7 +38,7 @@ import 'theme.dart';
/// * [FloatingActionButton], the round button in material applications.
/// * [IconButton], to create buttons that just contain icons.
/// * [InkWell], which implements the ink splash part of a flat button.
//// * [RawMaterialButton], the widget this widget is based on.
/// * [RawMaterialButton], the widget this widget is based on.
/// * <https://material.google.com/components/buttons.html>
class RaisedButton extends StatelessWidget {
/// Create a filled button.
......@@ -46,6 +48,7 @@ class RaisedButton extends StatelessWidget {
const RaisedButton({
Key key,
@required this.onPressed,
this.onHighlightChanged,
this.textTheme,
this.textColor,
this.disabledTextColor,
......@@ -59,10 +62,12 @@ class RaisedButton extends StatelessWidget {
this.disabledElevation: 0.0,
this.padding,
this.shape,
this.animationDuration: kThemeChangeDuration,
this.child,
}) : assert(elevation != null),
assert(highlightElevation != null),
assert(disabledElevation != null),
assert(animationDuration != null),
super(key: key);
/// Create a filled button from a pair of widgets that serve as the button's
......@@ -76,6 +81,7 @@ class RaisedButton extends StatelessWidget {
RaisedButton.icon({
Key key,
@required this.onPressed,
this.onHighlightChanged,
this.textTheme,
this.textColor,
this.disabledTextColor,
......@@ -88,6 +94,7 @@ class RaisedButton extends StatelessWidget {
this.highlightElevation: 8.0,
this.disabledElevation: 0.0,
this.shape,
this.animationDuration: kThemeChangeDuration,
@required Widget icon,
@required Widget label,
}) : assert(elevation != null),
......@@ -95,6 +102,7 @@ class RaisedButton extends StatelessWidget {
assert(disabledElevation != null),
assert(icon != null),
assert(label != null),
assert(animationDuration != null),
padding = const EdgeInsetsDirectional.only(start: 12.0, end: 16.0),
child = new Row(
mainAxisSize: MainAxisSize.min,
......@@ -111,6 +119,10 @@ class RaisedButton extends StatelessWidget {
/// If this is set to null, the button will be disabled, see [enabled].
final VoidCallback onPressed;
/// Called by the underlying [InkWell] widget's [InkWell.onHighlightChanged]
/// callback.
final ValueChanged<bool> onHighlightChanged;
/// Defines the button's base colors, and the defaults for the button's minimum
/// size, internal padding, and shape.
///
......@@ -272,6 +284,11 @@ class RaisedButton extends StatelessWidget {
/// shape as well.
final ShapeBorder shape;
/// Defines the duration of animated changes for [shape] and [elevation].
///
/// The default value is [kThemeChangeDuration].
final Duration animationDuration;
Brightness _getBrightness(ThemeData theme) {
return colorBrightness ?? theme.brightness;
}
......@@ -350,6 +367,7 @@ class RaisedButton extends StatelessWidget {
return new RawMaterialButton(
onPressed: onPressed,
onHighlightChanged: onHighlightChanged,
fillColor: fillColor,
textStyle: theme.textTheme.button.copyWith(color: textColor),
highlightColor: _getHighlightColor(theme, buttonTheme),
......@@ -360,6 +378,7 @@ class RaisedButton extends StatelessWidget {
padding: padding ?? buttonTheme.padding,
constraints: buttonTheme.constraints,
shape: shape ?? buttonTheme.shape,
animationDuration: animationDuration,
child: child,
);
}
......
......@@ -78,7 +78,7 @@ class ScrollController extends ChangeNotifier {
/// See also:
///
/// * [PageStorageKey], which should be used when more than one
//// scrollable appears in the same route, to distinguish the [PageStorage]
/// scrollable appears in the same route, to distinguish the [PageStorage]
/// locations used to save scroll offsets.
final bool keepScrollOffset;
......
// Copyright 2018 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 'dart:ui' show SemanticsFlag;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('Outline button responds to tap when enabled', (WidgetTester tester) async {
int pressedCount = 0;
Widget buildFrame(VoidCallback onPressed) {
return new Directionality(
textDirection: TextDirection.ltr,
child: new Theme(
data: new ThemeData(),
child: new Center(
child: new OutlineButton(onPressed: onPressed),
),
),
);
}
await tester.pumpWidget(
buildFrame(() { pressedCount += 1; }),
);
expect(tester.widget<OutlineButton>(find.byType(OutlineButton)).enabled, true);
await tester.tap(find.byType(OutlineButton));
await tester.pumpAndSettle();
expect(pressedCount, 1);
await tester.pumpWidget(
buildFrame(null),
);
final Finder outlineButton = find.byType(OutlineButton);
expect(tester.widget<OutlineButton>(outlineButton).enabled, false);
await tester.tap(outlineButton);
await tester.pumpAndSettle();
expect(pressedCount, 1);
});
testWidgets('Outline shape and border overrides', (WidgetTester tester) async {
const Color fillColor = const Color(0xFF00FF00);
const Color borderColor = const Color(0xFFFF0000);
const Color highlightedBorderColor = const Color(0xFF0000FF);
const double borderWidth = 4.0;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Theme(
data: new ThemeData(),
child: new Container(
alignment: Alignment.topLeft,
child: new OutlineButton(
shape: const RoundedRectangleBorder(), // default border radius is 0
color: fillColor,
highlightedBorderColor: highlightedBorderColor,
borderSide: const BorderSide(
width: borderWidth,
color: borderColor,
),
onPressed: () { },
child: const Text('button')
),
),
),
),
);
final Finder outlineButton = find.byType(OutlineButton);
expect(tester.widget<OutlineButton>(outlineButton).enabled, true);
final Rect clipRect = new Rect.fromLTRB(0.0, 0.0, 116.0, 36.0);
final Path clipPath = new Path()..addRect(clipRect);
expect(
outlineButton,
paints
// initially the interior of the button is transparent
..path(color: fillColor.withAlpha(0x00))
..clipPath(pathMatcher: coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)))
..path(color: borderColor, strokeWidth: borderWidth)
);
final Offset center = tester.getCenter(outlineButton);
final TestGesture gesture = await tester.startGesture(center);
await tester.pump(); // start gesture
// Wait for the border's color to change to highlightedBorderColor and
// the fillColor to become opaque.
await tester.pump(const Duration(milliseconds: 200));
expect(
outlineButton,
paints
..path(color: fillColor.withAlpha(0xFF))
..clipPath(pathMatcher: coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)))
..path(color: highlightedBorderColor, strokeWidth: borderWidth)
);
// Tap gesture completes, button returns to its initial configuration.
await gesture.up();
await tester.pumpAndSettle();
expect(
outlineButton,
paints
..path(color: fillColor.withAlpha(0x00))
..clipPath(pathMatcher: coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)))
..path(color: borderColor, strokeWidth: borderWidth)
);
});
testWidgets('OutlineButton contributes semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new OutlineButton(
onPressed: () { },
child: const Text('ABC')
),
),
),
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'ABC',
rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0),
transform: new Matrix4.translationValues(356.0, 282.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
)
],
),
ignoreId: true,
));
semantics.dispose();
});
testWidgets('OutlineButton scales textScaleFactor', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new MediaQuery(
data: const MediaQueryData(textScaleFactor: 1.0),
child: new Center(
child: new OutlineButton(
onPressed: () { },
child: const Text('ABC'),
),
),
),
),
),
);
expect(tester.getSize(find.byType(OutlineButton)), equals(const Size(88.0, 36.0)));
expect(tester.getSize(find.byType(Text)), equals(const Size(42.0, 14.0)));
// textScaleFactor expands text, but not button.
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new MediaQuery(
data: const MediaQueryData(textScaleFactor: 1.3),
child: new Center(
child: new FlatButton(
onPressed: () { },
child: const Text('ABC'),
),
),
),
),
),
);
expect(tester.getSize(find.byType(FlatButton)), equals(const Size(88.0, 36.0)));
// Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(#12357): Update this test when text rendering is fixed.
expect(tester.getSize(find.byType(Text)).width, isIn(<double>[54.0, 55.0]));
expect(tester.getSize(find.byType(Text)).height, isIn(<double>[18.0, 19.0]));
// Set text scale large enough to expand text and button.
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new MediaQuery(
data: const MediaQueryData(textScaleFactor: 3.0),
child: new Center(
child: new FlatButton(
onPressed: () { },
child: const Text('ABC'),
),
),
),
),
),
);
// Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(#12357): Update this test when text rendering is fixed.
expect(tester.getSize(find.byType(FlatButton)).width, isIn(<double>[158.0, 159.0]));
expect(tester.getSize(find.byType(FlatButton)).height, equals(42.0));
expect(tester.getSize(find.byType(Text)).width, isIn(<double>[126.0, 127.0]));
expect(tester.getSize(find.byType(Text)).height, equals(42.0));
});
testWidgets('OutlineButton implements debugFillDescription', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = new DiagnosticPropertiesBuilder();
new OutlineButton(
onPressed: () {},
textColor: const Color(0xFF00FF00),
disabledTextColor: const Color(0xFFFF0000),
color: const Color(0xFF000000),
highlightColor: const Color(0xFF1565C0),
splashColor: const Color(0xFF9E9E9E),
child: const Text('Hello'),
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'textColor: Color(0xff00ff00)',
'disabledTextColor: Color(0xffff0000)',
'color: Color(0xff000000)',
'highlightColor: Color(0xff1565c0)',
'splashColor: Color(0xff9e9e9e)',
]);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment