button.dart 9.23 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
/// See also:
///
31
///  * <https://developer.apple.com/ios/human-interface-guidelines/controls/buttons/>
32
class CupertinoButton extends StatefulWidget {
Adam Barth's avatar
Adam Barth committed
33
  /// Creates an iOS-style button.
34
  const CupertinoButton({
35 36
    Key? key,
    required this.child,
37 38
    this.padding,
    this.color,
39
    this.disabledColor = CupertinoColors.quaternarySystemFill,
40
    this.minSize = kMinInteractiveDimensionCupertino,
41
    this.pressedOpacity = 0.4,
42
    this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
43
    this.alignment = Alignment.center,
44
    required this.onPressed,
xster's avatar
xster committed
45
  }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
46
       assert(disabledColor != null),
47
       assert(alignment != null),
48 49
       _filled = false,
       super(key: key);
xster's avatar
xster committed
50 51 52 53 54 55 56 57

  /// 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({
58 59
    Key? key,
    required this.child,
xster's avatar
xster committed
60
    this.padding,
61
    this.disabledColor = CupertinoColors.quaternarySystemFill,
62
    this.minSize = kMinInteractiveDimensionCupertino,
63
    this.pressedOpacity = 0.4,
xster's avatar
xster committed
64
    this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
65
    this.alignment = Alignment.center,
66
    required this.onPressed,
xster's avatar
xster committed
67
  }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
68
       assert(disabledColor != null),
69
       assert(alignment != null),
xster's avatar
xster committed
70
       color = null,
71 72
       _filled = true,
       super(key: key);
73 74 75 76 77 78 79 80 81

  /// 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.
82
  final EdgeInsetsGeometry? padding;
83 84 85 86

  /// The color of the button's background.
  ///
  /// Defaults to null which produces a button with no background or border.
xster's avatar
xster committed
87 88 89
  ///
  /// Defaults to the [CupertinoTheme]'s `primaryColor` when the
  /// [CupertinoButton.filled] constructor is used.
90
  final Color? color;
91

92 93 94 95
  /// The color of the button's background when the button is disabled.
  ///
  /// Ignored if the [CupertinoButton] doesn't also have a [color].
  ///
96 97
  /// Defaults to [CupertinoColors.quaternarySystemFill] when [color] is
  /// specified. Must not be null.
98 99
  final Color disabledColor;

100 101 102
  /// The callback that is called when the button is tapped or otherwise activated.
  ///
  /// If this is set to null, the button will be disabled.
103
  final VoidCallback? onPressed;
104

105 106
  /// Minimum size of the button.
  ///
107 108
  /// Defaults to kMinInteractiveDimensionCupertino which the iOS Human
  /// Interface Guidelines recommends as the minimum tappable area.
109
  final double? minSize;
110

111 112 113
  /// 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.
  ///
114
  /// This defaults to 0.4. If null, opacity will not change on pressed if using
xster's avatar
xster committed
115
  /// your own custom effects is desired.
116
  final double? pressedOpacity;
117

xster's avatar
xster committed
118 119 120
  /// The radius of the button's corners when it has a background color.
  ///
  /// Defaults to round corners of 8 logical pixels.
121
  final BorderRadius? borderRadius;
xster's avatar
xster committed
122

123 124 125 126 127 128 129 130 131 132
  /// 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
133 134
  final bool _filled;

135 136 137 138 139
  /// 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
140
  State<CupertinoButton> createState() => _CupertinoButtonState();
141 142

  @override
143 144
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
145
    properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
146 147 148 149 150
  }
}

class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProviderStateMixin {
  // Eyeballed values. Feel free to tweak.
151 152
  static const Duration kFadeOutDuration = Duration(milliseconds: 10);
  static const Duration kFadeInDuration = Duration(milliseconds: 100);
153
  final Tween<double> _opacityTween = Tween<double>(begin: 1.0);
154

155 156
  late AnimationController _animationController;
  late Animation<double> _opacityAnimation;
157

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

172 173 174 175 176 177 178 179 180 181
  @override
  void didUpdateWidget(CupertinoButton old) {
    super.didUpdateWidget(old);
    _setTween();
  }

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

182 183 184 185 186 187
  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

188 189 190 191 192 193 194 195 196 197 198 199 200 201
  bool _buttonHeldDown = false;

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

  void _handleTapUp(TapUpDetails event) {
    if (_buttonHeldDown) {
      _buttonHeldDown = false;
      _animate();
    }
202 203
  }

204 205 206 207 208
  void _handleTapCancel() {
    if (_buttonHeldDown) {
      _buttonHeldDown = false;
      _animate();
    }
209 210
  }

211 212 213 214
  void _animate() {
    if (_animationController.isAnimating)
      return;
    final bool wasHeldDown = _buttonHeldDown;
215
    final TickerFuture ticker = _buttonHeldDown
216 217
        ? _animationController.animateTo(1.0, duration: kFadeOutDuration)
        : _animationController.animateTo(0.0, duration: kFadeInDuration);
218
    ticker.then<void>((void value) {
219 220 221
      if (mounted && wasHeldDown != _buttonHeldDown)
        _animate();
    });
222 223 224 225
  }

  @override
  Widget build(BuildContext context) {
226
    final bool enabled = widget.enabled;
227
    final CupertinoThemeData themeData = CupertinoTheme.of(context);
228
    final Color primaryColor = themeData.primaryColor;
229
    final Color? backgroundColor = widget.color == null
230
      ? (widget._filled ? primaryColor : null)
231
      : CupertinoDynamicColor.maybeResolve(widget.color, context);
232

233
    final Color foregroundColor = backgroundColor != null
234 235 236
      ? themeData.primaryContrastingColor
      : enabled
        ? primaryColor
237
        : CupertinoDynamicColor.resolve(CupertinoColors.placeholderText, context);
238 239

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

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