blob: 64a5056592d070f549a92b188658cfbff4c2db43 [file] [log] [blame]
// 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 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
// Measured against iOS 12 in Xcode.
const EdgeInsets _kButtonPadding = EdgeInsets.all(16.0);
const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric(
vertical: 14.0,
horizontal: 64.0,
);
/// An iOS-style button.
///
/// Takes in a text or an icon that fades out and in on touch. May optionally have a
/// background.
///
/// 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.
///
/// {@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}
///
/// See also:
///
/// * <https://developer.apple.com/ios/human-interface-guidelines/controls/buttons/>
class CupertinoButton extends StatefulWidget {
/// Creates an iOS-style button.
const CupertinoButton({
super.key,
required this.child,
this.padding,
this.color,
this.disabledColor = CupertinoColors.quaternarySystemFill,
this.minSize = kMinInteractiveDimensionCupertino,
this.pressedOpacity = 0.4,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
this.alignment = Alignment.center,
required this.onPressed,
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
assert(disabledColor != null),
assert(alignment != null),
_filled = false;
/// 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({
super.key,
required this.child,
this.padding,
this.disabledColor = CupertinoColors.quaternarySystemFill,
this.minSize = kMinInteractiveDimensionCupertino,
this.pressedOpacity = 0.4,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
this.alignment = Alignment.center,
required this.onPressed,
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
assert(disabledColor != null),
assert(alignment != null),
color = null,
_filled = true;
/// 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.
final EdgeInsetsGeometry? padding;
/// The color of the button's background.
///
/// Defaults to null which produces a button with no background or border.
///
/// Defaults to the [CupertinoTheme]'s `primaryColor` when the
/// [CupertinoButton.filled] constructor is used.
final Color? color;
/// The color of the button's background when the button is disabled.
///
/// Ignored if the [CupertinoButton] doesn't also have a [color].
///
/// Defaults to [CupertinoColors.quaternarySystemFill] when [color] is
/// specified. Must not be null.
final Color disabledColor;
/// 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;
/// Minimum size of the button.
///
/// Defaults to kMinInteractiveDimensionCupertino which the iOS Human
/// Interface Guidelines recommends as the minimum tappable area.
final double? minSize;
/// 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.
///
/// This defaults to 0.4. If null, opacity will not change on pressed if using
/// your own custom effects is desired.
final double? pressedOpacity;
/// The radius of the button's corners when it has a background color.
///
/// Defaults to round corners of 8 logical pixels.
final BorderRadius? borderRadius;
/// 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;
final bool _filled;
/// 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
State<CupertinoButton> createState() => _CupertinoButtonState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
}
}
class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProviderStateMixin {
// Eyeballed values. Feel free to tweak.
static const Duration kFadeOutDuration = Duration(milliseconds: 120);
static const Duration kFadeInDuration = Duration(milliseconds: 180);
final Tween<double> _opacityTween = Tween<double>(begin: 1.0);
late AnimationController _animationController;
late Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
value: 0.0,
vsync: this,
);
_opacityAnimation = _animationController
.drive(CurveTween(curve: Curves.decelerate))
.drive(_opacityTween);
_setTween();
}
@override
void didUpdateWidget(CupertinoButton old) {
super.didUpdateWidget(old);
_setTween();
}
void _setTween() {
_opacityTween.end = widget.pressedOpacity ?? 1.0;
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
bool _buttonHeldDown = false;
void _handleTapDown(TapDownDetails event) {
if (!_buttonHeldDown) {
_buttonHeldDown = true;
_animate();
}
}
void _handleTapUp(TapUpDetails event) {
if (_buttonHeldDown) {
_buttonHeldDown = false;
_animate();
}
}
void _handleTapCancel() {
if (_buttonHeldDown) {
_buttonHeldDown = false;
_animate();
}
}
void _animate() {
if (_animationController.isAnimating) {
return;
}
final bool wasHeldDown = _buttonHeldDown;
final TickerFuture ticker = _buttonHeldDown
? _animationController.animateTo(1.0, duration: kFadeOutDuration, curve: Curves.easeInOutCubicEmphasized)
: _animationController.animateTo(0.0, duration: kFadeInDuration, curve: Curves.easeOutCubic);
ticker.then<void>((void value) {
if (mounted && wasHeldDown != _buttonHeldDown) {
_animate();
}
});
}
@override
Widget build(BuildContext context) {
final bool enabled = widget.enabled;
final CupertinoThemeData themeData = CupertinoTheme.of(context);
final Color primaryColor = themeData.primaryColor;
final Color? backgroundColor = widget.color == null
? (widget._filled ? primaryColor : null)
: CupertinoDynamicColor.maybeResolve(widget.color, context);
final Color foregroundColor = backgroundColor != null
? themeData.primaryContrastingColor
: enabled
? primaryColor
: CupertinoDynamicColor.resolve(CupertinoColors.placeholderText, context);
final TextStyle textStyle = themeData.textTheme.textStyle.copyWith(color: foregroundColor);
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,
),
),
),
),
),
),
),
),
),
);
}
}