button.dart 9.69 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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';

8
import 'colors.dart';
9
import 'constants.dart';
xster's avatar
xster committed
10
import 'theme.dart';
xster's avatar
xster committed
11

xster's avatar
xster committed
12
// Measured against iOS 12 in Xcode.
13 14
const EdgeInsets _kButtonPadding = EdgeInsets.all(16.0);
const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric(
15
  vertical: 14.0,
16 17
  horizontal: 64.0,
);
18

Adam Barth's avatar
Adam Barth committed
19
/// An iOS-style button.
20 21 22 23
///
/// Takes in a text or an icon that fades out and in on touch. May optionally have a
/// background.
///
24 25 26 27 28
/// The [padding] defaults to 16.0 pixels. When using a [CupertinoButton] within
/// a fixed height parent, like a [CupertinoNavigationBar], a smaller, or even
/// [EdgeInsets.zero], should be used to prevent clipping larger [child]
/// widgets.
///
29 30 31 32 33 34 35
/// {@tool dartpad}
/// This sample shows produces an enabled and disabled [CupertinoButton] and
/// [CupertinoButton.filled].
///
/// ** See code in examples/api/lib/cupertino/button/cupertino_button.0.dart **
/// {@end-tool}
///
36 37
/// See also:
///
38
///  * <https://developer.apple.com/ios/human-interface-guidelines/controls/buttons/>
39
class CupertinoButton extends StatefulWidget {
Adam Barth's avatar
Adam Barth committed
40
  /// Creates an iOS-style button.
41
  const CupertinoButton({
42
    super.key,
43
    required this.child,
44 45
    this.padding,
    this.color,
46
    this.disabledColor = CupertinoColors.quaternarySystemFill,
47
    this.minSize = kMinInteractiveDimensionCupertino,
48
    this.pressedOpacity = 0.4,
49
    this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
50
    this.alignment = Alignment.center,
51
    required this.onPressed,
xster's avatar
xster committed
52
  }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
53
       assert(disabledColor != null),
54
       assert(alignment != null),
55
       _filled = false;
xster's avatar
xster committed
56 57 58 59 60 61 62 63

  /// Creates an iOS-style button with a filled background.
  ///
  /// The background color is derived from the [CupertinoTheme]'s `primaryColor`.
  ///
  /// To specify a custom background color, use the [color] argument of the
  /// default constructor.
  const CupertinoButton.filled({
64
    super.key,
65
    required this.child,
xster's avatar
xster committed
66
    this.padding,
67
    this.disabledColor = CupertinoColors.quaternarySystemFill,
68
    this.minSize = kMinInteractiveDimensionCupertino,
69
    this.pressedOpacity = 0.4,
xster's avatar
xster committed
70
    this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
71
    this.alignment = Alignment.center,
72
    required this.onPressed,
xster's avatar
xster committed
73
  }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
74
       assert(disabledColor != null),
75
       assert(alignment != null),
xster's avatar
xster committed
76
       color = null,
77
       _filled = true;
78 79 80 81 82 83 84 85 86

  /// The widget below this widget in the tree.
  ///
  /// Typically a [Text] widget.
  final Widget child;

  /// The amount of space to surround the child inside the bounds of the button.
  ///
  /// Defaults to 16.0 pixels.
87
  final EdgeInsetsGeometry? padding;
88 89 90 91

  /// The color of the button's background.
  ///
  /// Defaults to null which produces a button with no background or border.
xster's avatar
xster committed
92 93 94
  ///
  /// Defaults to the [CupertinoTheme]'s `primaryColor` when the
  /// [CupertinoButton.filled] constructor is used.
95
  final Color? color;
96

97 98 99 100
  /// The color of the button's background when the button is disabled.
  ///
  /// Ignored if the [CupertinoButton] doesn't also have a [color].
  ///
101 102
  /// Defaults to [CupertinoColors.quaternarySystemFill] when [color] is
  /// specified. Must not be null.
103 104
  final Color disabledColor;

105 106 107
  /// The callback that is called when the button is tapped or otherwise activated.
  ///
  /// If this is set to null, the button will be disabled.
108
  final VoidCallback? onPressed;
109

110 111
  /// Minimum size of the button.
  ///
112 113
  /// Defaults to kMinInteractiveDimensionCupertino which the iOS Human
  /// Interface Guidelines recommends as the minimum tappable area.
114
  final double? minSize;
115

116 117 118
  /// The opacity that the button will fade to when it is pressed.
  /// The button will have an opacity of 1.0 when it is not pressed.
  ///
119
  /// This defaults to 0.4. If null, opacity will not change on pressed if using
xster's avatar
xster committed
120
  /// your own custom effects is desired.
121
  final double? pressedOpacity;
122

xster's avatar
xster committed
123 124 125
  /// The radius of the button's corners when it has a background color.
  ///
  /// Defaults to round corners of 8 logical pixels.
126
  final BorderRadius? borderRadius;
xster's avatar
xster committed
127

128 129 130 131 132 133 134 135 136 137
  /// The alignment of the button's [child].
  ///
  /// Typically buttons are sized to be just big enough to contain the child and its
  /// [padding]. If the button's size is constrained to a fixed size, for example by
  /// enclosing it with a [SizedBox], this property defines how the child is aligned
  /// within the available space.
  ///
  /// Always defaults to [Alignment.center].
  final AlignmentGeometry alignment;

xster's avatar
xster committed
138 139
  final bool _filled;

140 141 142 143 144
  /// 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
145
  State<CupertinoButton> createState() => _CupertinoButtonState();
146 147

  @override
148 149
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
150
    properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
151 152 153 154 155
  }
}

class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProviderStateMixin {
  // Eyeballed values. Feel free to tweak.
156 157
  static const Duration kFadeOutDuration = Duration(milliseconds: 120);
  static const Duration kFadeInDuration = Duration(milliseconds: 180);
158
  final Tween<double> _opacityTween = Tween<double>(begin: 1.0);
159

160 161
  late AnimationController _animationController;
  late Animation<double> _opacityAnimation;
162

163 164 165
  @override
  void initState() {
    super.initState();
166
    _animationController = AnimationController(
167
      duration: const Duration(milliseconds: 200),
168
      value: 0.0,
169 170
      vsync: this,
    );
171 172 173
    _opacityAnimation = _animationController
      .drive(CurveTween(curve: Curves.decelerate))
      .drive(_opacityTween);
174
    _setTween();
175 176
  }

177 178 179 180 181 182 183 184 185 186
  @override
  void didUpdateWidget(CupertinoButton old) {
    super.didUpdateWidget(old);
    _setTween();
  }

  void _setTween() {
    _opacityTween.end = widget.pressedOpacity ?? 1.0;
  }

187 188 189 190 191 192
  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

193 194 195 196 197 198 199 200 201 202 203 204 205 206
  bool _buttonHeldDown = false;

  void _handleTapDown(TapDownDetails event) {
    if (!_buttonHeldDown) {
      _buttonHeldDown = true;
      _animate();
    }
  }

  void _handleTapUp(TapUpDetails event) {
    if (_buttonHeldDown) {
      _buttonHeldDown = false;
      _animate();
    }
207 208
  }

209 210 211 212 213
  void _handleTapCancel() {
    if (_buttonHeldDown) {
      _buttonHeldDown = false;
      _animate();
    }
214 215
  }

216
  void _animate() {
217
    if (_animationController.isAnimating) {
218
      return;
219
    }
220
    final bool wasHeldDown = _buttonHeldDown;
221
    final TickerFuture ticker = _buttonHeldDown
222 223
        ? _animationController.animateTo(1.0, duration: kFadeOutDuration, curve: Curves.easeInOutCubicEmphasized)
        : _animationController.animateTo(0.0, duration: kFadeInDuration, curve: Curves.easeOutCubic);
224
    ticker.then<void>((void value) {
225
      if (mounted && wasHeldDown != _buttonHeldDown) {
226
        _animate();
227
      }
228
    });
229 230 231 232
  }

  @override
  Widget build(BuildContext context) {
233
    final bool enabled = widget.enabled;
234
    final CupertinoThemeData themeData = CupertinoTheme.of(context);
235
    final Color primaryColor = themeData.primaryColor;
236
    final Color? backgroundColor = widget.color == null
237
      ? (widget._filled ? primaryColor : null)
238
      : CupertinoDynamicColor.maybeResolve(widget.color, context);
239

240
    final Color foregroundColor = backgroundColor != null
241 242 243
      ? themeData.primaryContrastingColor
      : enabled
        ? primaryColor
244
        : CupertinoDynamicColor.resolve(CupertinoColors.placeholderText, context);
245 246

    final TextStyle textStyle = themeData.textTheme.textStyle.copyWith(color: foregroundColor);
247

248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
    return MouseRegion(
      cursor: enabled && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
      child: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTapDown: enabled ? _handleTapDown : null,
        onTapUp: enabled ? _handleTapUp : null,
        onTapCancel: enabled ? _handleTapCancel : null,
        onTap: widget.onPressed,
        child: Semantics(
          button: true,
          child: ConstrainedBox(
            constraints: widget.minSize == null
              ? const BoxConstraints()
              : BoxConstraints(
                  minWidth: widget.minSize!,
                  minHeight: widget.minSize!,
                ),
            child: FadeTransition(
              opacity: _opacityAnimation,
              child: DecoratedBox(
                decoration: BoxDecoration(
                  borderRadius: widget.borderRadius,
                  color: backgroundColor != null && !enabled
                    ? CupertinoDynamicColor.resolve(widget.disabledColor, context)
                    : backgroundColor,
                ),
                child: Padding(
                  padding: widget.padding ?? (backgroundColor != null
                    ? _kBackgroundButtonPadding
                    : _kButtonPadding),
                  child: Align(
                    alignment: widget.alignment,
                    widthFactor: 1.0,
                    heightFactor: 1.0,
                    child: DefaultTextStyle(
                      style: textStyle,
                      child: IconTheme(
                        data: IconThemeData(color: foregroundColor),
                        child: widget.child,
                      ),
xster's avatar
xster committed
288
                    ),
289
                  ),
290 291 292 293 294 295 296 297 298
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}