| // Copyright 2014 The Flutter 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:math' as math; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'button_theme.dart'; |
| import 'constants.dart'; |
| import 'ink_well.dart'; |
| import 'material.dart'; |
| import 'material_state.dart'; |
| import 'theme.dart'; |
| import 'theme_data.dart'; |
| |
| /// Creates a button based on [Semantics], [Material], and [InkWell] |
| /// widgets. |
| /// |
| /// This class does not use the current [Theme] or [ButtonTheme] to |
| /// compute default values for unspecified parameters. It's intended to |
| /// be used for custom Material buttons that optionally incorporate defaults |
| /// from the themes or from app-specific sources. |
| /// |
| /// [RaisedButton] and [FlatButton] configure a [RawMaterialButton] based |
| /// on the current [Theme] and [ButtonTheme]. |
| class RawMaterialButton extends StatefulWidget { |
| /// Create a button based on [Semantics], [Material], and [InkWell] widgets. |
| /// |
| /// The [shape], [elevation], [focusElevation], [hoverElevation], |
| /// [highlightElevation], [disabledElevation], [padding], [constraints], |
| /// [autofocus], and [clipBehavior] arguments must not be null. Additionally, |
| /// [elevation], [focusElevation], [hoverElevation], [highlightElevation], and |
| /// [disabledElevation] must be non-negative. |
| const RawMaterialButton({ |
| Key key, |
| @required this.onPressed, |
| this.onLongPress, |
| this.onHighlightChanged, |
| this.textStyle, |
| this.fillColor, |
| this.focusColor, |
| this.hoverColor, |
| this.highlightColor, |
| this.splashColor, |
| this.elevation = 2.0, |
| this.focusElevation = 4.0, |
| this.hoverElevation = 4.0, |
| this.highlightElevation = 8.0, |
| this.disabledElevation = 0.0, |
| this.padding = EdgeInsets.zero, |
| this.visualDensity = const VisualDensity(), |
| this.constraints = const BoxConstraints(minWidth: 88.0, minHeight: 36.0), |
| this.shape = const RoundedRectangleBorder(), |
| this.animationDuration = kThemeChangeDuration, |
| this.clipBehavior = Clip.none, |
| this.focusNode, |
| this.autofocus = false, |
| MaterialTapTargetSize materialTapTargetSize, |
| this.child, |
| this.enableFeedback = true, |
| }) : materialTapTargetSize = materialTapTargetSize ?? MaterialTapTargetSize.padded, |
| assert(shape != null), |
| assert(elevation != null && elevation >= 0.0), |
| assert(focusElevation != null && focusElevation >= 0.0), |
| assert(hoverElevation != null && hoverElevation >= 0.0), |
| assert(highlightElevation != null && highlightElevation >= 0.0), |
| assert(disabledElevation != null && disabledElevation >= 0.0), |
| assert(padding != null), |
| assert(constraints != null), |
| assert(animationDuration != null), |
| assert(clipBehavior != null), |
| assert(autofocus != null), |
| super(key: key); |
| |
| /// Called when the button is tapped or otherwise activated. |
| /// |
| /// If this callback and [onLongPress] are null, then the button will be disabled. |
| /// |
| /// See also: |
| /// |
| /// * [enabled], which is true if the button is enabled. |
| final VoidCallback onPressed; |
| |
| /// Called when the button is long-pressed. |
| /// |
| /// If this callback and [onPressed] are null, then the button will be disabled. |
| /// |
| /// See also: |
| /// |
| /// * [enabled], which is true if the button is enabled. |
| final VoidCallback onLongPress; |
| |
| /// Called by the underlying [InkWell] widget's [InkWell.onHighlightChanged] |
| /// callback. |
| /// |
| /// If [onPressed] changes from null to non-null while a gesture is ongoing, |
| /// this can fire during the build phase (in which case calling |
| /// [State.setState] is not allowed). |
| final ValueChanged<bool> onHighlightChanged; |
| |
| /// Defines the default text style, with [Material.textStyle], for the |
| /// button's [child]. |
| /// |
| /// If [textStyle.color] is a [MaterialStateProperty<Color>], [MaterialStateProperty.resolve] |
| /// is used for the following [MaterialState]s: |
| /// |
| /// * [MaterialState.pressed]. |
| /// * [MaterialState.hovered]. |
| /// * [MaterialState.focused]. |
| /// * [MaterialState.disabled]. |
| final TextStyle textStyle; |
| |
| /// The color of the button's [Material]. |
| final Color fillColor; |
| |
| /// The color for the button's [Material] when it has the input focus. |
| final Color focusColor; |
| |
| /// The color for the button's [Material] when a pointer is hovering over it. |
| final Color hoverColor; |
| |
| /// The highlight color for the button's [InkWell]. |
| final Color highlightColor; |
| |
| /// The splash color for the button's [InkWell]. |
| final Color splashColor; |
| |
| /// The elevation for the button's [Material] when the button |
| /// is [enabled] but not pressed. |
| /// |
| /// Defaults to 2.0. The value is always non-negative. |
| /// |
| /// See also: |
| /// |
| /// * [highlightElevation], the default elevation. |
| /// * [hoverElevation], the elevation when a pointer is hovering over the |
| /// button. |
| /// * [focusElevation], the elevation when the button is focused. |
| /// * [disabledElevation], the elevation when the button is disabled. |
| final double elevation; |
| |
| /// The elevation for the button's [Material] when the button |
| /// is [enabled] and a pointer is hovering over it. |
| /// |
| /// Defaults to 4.0. The value is always non-negative. |
| /// |
| /// If the button is [enabled], and being pressed (in the highlighted state), |
| /// then the [highlightElevation] take precedence over the [hoverElevation]. |
| /// |
| /// See also: |
| /// |
| /// * [elevation], the default elevation. |
| /// * [focusElevation], the elevation when the button is focused. |
| /// * [disabledElevation], the elevation when the button is disabled. |
| /// * [highlightElevation], the elevation when the button is pressed. |
| final double hoverElevation; |
| |
| /// The elevation for the button's [Material] when the button |
| /// is [enabled] and has the input focus. |
| /// |
| /// Defaults to 4.0. The value is always non-negative. |
| /// |
| /// If the button is [enabled], and being pressed (in the highlighted state), |
| /// or a mouse cursor is hovering over the button, then the [hoverElevation] |
| /// and [highlightElevation] take precedence over the [focusElevation]. |
| /// |
| /// See also: |
| /// |
| /// * [elevation], the default elevation. |
| /// * [hoverElevation], the elevation when a pointer is hovering over the |
| /// button. |
| /// * [disabledElevation], the elevation when the button is disabled. |
| /// * [highlightElevation], the elevation when the button is pressed. |
| final double focusElevation; |
| |
| /// The elevation for the button's [Material] when the button |
| /// is [enabled] and pressed. |
| /// |
| /// Defaults to 8.0. The value is always non-negative. |
| /// |
| /// See also: |
| /// |
| /// * [elevation], the default elevation. |
| /// * [hoverElevation], the elevation when a pointer is hovering over the |
| /// button. |
| /// * [focusElevation], the elevation when the button is focused. |
| /// * [disabledElevation], the elevation when the button is disabled. |
| final double highlightElevation; |
| |
| /// The elevation for the button's [Material] when the button |
| /// is not [enabled]. |
| /// |
| /// Defaults to 0.0. The value is always non-negative. |
| /// |
| /// See also: |
| /// |
| /// * [elevation], the default elevation. |
| /// * [hoverElevation], the elevation when a pointer is hovering over the |
| /// button. |
| /// * [focusElevation], the elevation when the button is focused. |
| /// * [highlightElevation], the elevation when the button is pressed. |
| final double disabledElevation; |
| |
| /// The internal padding for the button's [child]. |
| final EdgeInsetsGeometry padding; |
| |
| /// Defines how compact the button's layout will be. |
| /// |
| /// {@macro flutter.material.themedata.visualDensity} |
| /// |
| /// See also: |
| /// |
| /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all widgets |
| /// within a [Theme]. |
| final VisualDensity visualDensity; |
| |
| /// Defines the button's size. |
| /// |
| /// Typically used to constrain the button's minimum size. |
| final BoxConstraints constraints; |
| |
| /// 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. |
| /// |
| /// If [shape] is a [MaterialStateProperty<ShapeBorder>], [MaterialStateProperty.resolve] |
| /// is used for the following [MaterialState]s: |
| /// |
| /// * [MaterialState.pressed]. |
| /// * [MaterialState.hovered]. |
| /// * [MaterialState.focused]. |
| /// * [MaterialState.disabled]. |
| 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; |
| |
| /// Whether the button is enabled or disabled. |
| /// |
| /// Buttons are disabled by default. To enable a button, set its [onPressed] |
| /// or [onLongPress] properties to a non-null value. |
| bool get enabled => onPressed != null || onLongPress != null; |
| |
| /// Configures the minimum size of the tap target. |
| /// |
| /// Defaults to [MaterialTapTargetSize.padded]. |
| /// |
| /// See also: |
| /// |
| /// * [MaterialTapTargetSize], for a description of how this affects tap targets. |
| final MaterialTapTargetSize materialTapTargetSize; |
| |
| /// {@macro flutter.widgets.Focus.focusNode} |
| final FocusNode focusNode; |
| |
| /// {@macro flutter.widgets.Focus.autofocus} |
| final bool autofocus; |
| |
| /// {@macro flutter.widgets.Clip} |
| /// |
| /// Defaults to [Clip.none], and must not be null. |
| final Clip clipBehavior; |
| |
| /// 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; |
| |
| @override |
| _RawMaterialButtonState createState() => _RawMaterialButtonState(); |
| } |
| |
| class _RawMaterialButtonState extends State<RawMaterialButton> { |
| final Set<MaterialState> _states = <MaterialState>{}; |
| |
| bool get _hovered => _states.contains(MaterialState.hovered); |
| bool get _focused => _states.contains(MaterialState.focused); |
| bool get _pressed => _states.contains(MaterialState.pressed); |
| bool get _disabled => _states.contains(MaterialState.disabled); |
| |
| void _updateState(MaterialState state, bool value) { |
| value ? _states.add(state) : _states.remove(state); |
| } |
| |
| void _handleHighlightChanged(bool value) { |
| if (_pressed != value) { |
| setState(() { |
| _updateState(MaterialState.pressed, value); |
| if (widget.onHighlightChanged != null) { |
| widget.onHighlightChanged(value); |
| } |
| }); |
| } |
| } |
| |
| void _handleHoveredChanged(bool value) { |
| if (_hovered != value) { |
| setState(() { |
| _updateState(MaterialState.hovered, value); |
| }); |
| } |
| } |
| |
| void _handleFocusedChanged(bool value) { |
| if (_focused != value) { |
| setState(() { |
| _updateState(MaterialState.focused, value); |
| }); |
| } |
| } |
| |
| @override |
| void initState() { |
| super.initState(); |
| _updateState(MaterialState.disabled, !widget.enabled); |
| } |
| |
| @override |
| void didUpdateWidget(RawMaterialButton oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| _updateState(MaterialState.disabled, !widget.enabled); |
| // If the button is disabled while a press gesture is currently ongoing, |
| // InkWell makes a call to handleHighlightChanged. This causes an exception |
| // because it calls setState in the middle of a build. To preempt this, we |
| // manually update pressed to false when this situation occurs. |
| if (_disabled && _pressed) { |
| _handleHighlightChanged(false); |
| } |
| } |
| |
| double get _effectiveElevation { |
| // These conditionals are in order of precedence, so be careful about |
| // reorganizing them. |
| if (_disabled) { |
| return widget.disabledElevation; |
| } |
| if (_pressed) { |
| return widget.highlightElevation; |
| } |
| if (_hovered) { |
| return widget.hoverElevation; |
| } |
| if (_focused) { |
| return widget.focusElevation; |
| } |
| return widget.elevation; |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(widget.textStyle?.color, _states); |
| final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states); |
| final Offset densityAdjustment = widget.visualDensity.baseSizeAdjustment; |
| final BoxConstraints effectiveConstraints = widget.visualDensity.effectiveConstraints(widget.constraints); |
| final EdgeInsetsGeometry padding = widget.padding.add( |
| EdgeInsets.only( |
| left: densityAdjustment.dx, |
| top: densityAdjustment.dy, |
| right: densityAdjustment.dx, |
| bottom: densityAdjustment.dy, |
| ), |
| ).clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity); |
| |
| final Widget result = ConstrainedBox( |
| constraints: effectiveConstraints, |
| child: Material( |
| elevation: _effectiveElevation, |
| textStyle: widget.textStyle?.copyWith(color: effectiveTextColor), |
| shape: effectiveShape, |
| color: widget.fillColor, |
| type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button, |
| animationDuration: widget.animationDuration, |
| clipBehavior: widget.clipBehavior, |
| child: InkWell( |
| focusNode: widget.focusNode, |
| canRequestFocus: widget.enabled, |
| onFocusChange: _handleFocusedChanged, |
| autofocus: widget.autofocus, |
| onHighlightChanged: _handleHighlightChanged, |
| splashColor: widget.splashColor, |
| highlightColor: widget.highlightColor, |
| focusColor: widget.focusColor, |
| hoverColor: widget.hoverColor, |
| onHover: _handleHoveredChanged, |
| onTap: widget.onPressed, |
| onLongPress: widget.onLongPress, |
| enableFeedback: widget.enableFeedback, |
| customBorder: effectiveShape, |
| child: IconTheme.merge( |
| data: IconThemeData(color: effectiveTextColor), |
| child: Container( |
| padding: padding, |
| child: Center( |
| widthFactor: 1.0, |
| heightFactor: 1.0, |
| child: widget.child, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| Size minSize; |
| switch (widget.materialTapTargetSize) { |
| case MaterialTapTargetSize.padded: |
| minSize = Size( |
| kMinInteractiveDimension + densityAdjustment.dx, |
| kMinInteractiveDimension + densityAdjustment.dy, |
| ); |
| assert(minSize.width >= 0.0); |
| assert(minSize.height >= 0.0); |
| break; |
| case MaterialTapTargetSize.shrinkWrap: |
| minSize = Size.zero; |
| break; |
| } |
| |
| return Semantics( |
| container: true, |
| button: true, |
| enabled: widget.enabled, |
| child: _InputPadding( |
| minSize: minSize, |
| child: result, |
| ), |
| ); |
| } |
| } |
| |
| /// A widget to pad the area around a [MaterialButton]'s inner [Material]. |
| /// |
| /// Redirect taps that occur in the padded area around the child to the center |
| /// of the child. This increases the size of the button and the button's |
| /// "tap target", but not its material or its ink splashes. |
| class _InputPadding extends SingleChildRenderObjectWidget { |
| const _InputPadding({ |
| Key key, |
| Widget child, |
| this.minSize, |
| }) : super(key: key, child: child); |
| |
| final Size minSize; |
| |
| @override |
| RenderObject createRenderObject(BuildContext context) { |
| return _RenderInputPadding(minSize); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, covariant _RenderInputPadding renderObject) { |
| renderObject.minSize = minSize; |
| } |
| } |
| |
| class _RenderInputPadding extends RenderShiftedBox { |
| _RenderInputPadding(this._minSize, [RenderBox child]) : super(child); |
| |
| Size get minSize => _minSize; |
| Size _minSize; |
| set minSize(Size value) { |
| if (_minSize == value) |
| return; |
| _minSize = value; |
| markNeedsLayout(); |
| } |
| |
| @override |
| double computeMinIntrinsicWidth(double height) { |
| if (child != null) |
| return math.max(child.getMinIntrinsicWidth(height), minSize.width); |
| return 0.0; |
| } |
| |
| @override |
| double computeMinIntrinsicHeight(double width) { |
| if (child != null) |
| return math.max(child.getMinIntrinsicHeight(width), minSize.height); |
| return 0.0; |
| } |
| |
| @override |
| double computeMaxIntrinsicWidth(double height) { |
| if (child != null) |
| return math.max(child.getMaxIntrinsicWidth(height), minSize.width); |
| return 0.0; |
| } |
| |
| @override |
| double computeMaxIntrinsicHeight(double width) { |
| if (child != null) |
| return math.max(child.getMaxIntrinsicHeight(width), minSize.height); |
| return 0.0; |
| } |
| |
| @override |
| void performLayout() { |
| final BoxConstraints constraints = this.constraints; |
| if (child != null) { |
| child.layout(constraints, parentUsesSize: true); |
| final double height = math.max(child.size.width, minSize.width); |
| final double width = math.max(child.size.height, minSize.height); |
| size = constraints.constrain(Size(height, width)); |
| final BoxParentData childParentData = child.parentData as BoxParentData; |
| childParentData.offset = Alignment.center.alongOffset(size - child.size as Offset); |
| } else { |
| size = Size.zero; |
| } |
| } |
| |
| @override |
| bool hitTest(BoxHitTestResult result, { Offset position }) { |
| if (super.hitTest(result, position: position)) { |
| return true; |
| } |
| final Offset center = child.size.center(Offset.zero); |
| return result.addWithRawTransform( |
| transform: MatrixUtils.forceToPoint(center), |
| position: center, |
| hitTest: (BoxHitTestResult result, Offset position) { |
| assert(position == center); |
| return child.hitTest(result, position: center); |
| }, |
| ); |
| } |
| } |