diff --git a/packages/flutter/lib/cupertino.dart b/packages/flutter/lib/cupertino.dart index b49043d4de..1938da10ba 100644 --- a/packages/flutter/lib/cupertino.dart +++ b/packages/flutter/lib/cupertino.dart @@ -22,7 +22,6 @@ library cupertino; -export 'src/cupertino/action_sheet.dart'; export 'src/cupertino/activity_indicator.dart'; export 'src/cupertino/app.dart'; export 'src/cupertino/bottom_tab_bar.dart'; diff --git a/packages/flutter/lib/src/cupertino/action_sheet.dart b/packages/flutter/lib/src/cupertino/action_sheet.dart deleted file mode 100644 index 205ff6cc24..0000000000 --- a/packages/flutter/lib/src/cupertino/action_sheet.dart +++ /dev/null @@ -1,1429 +0,0 @@ -// 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:ui' show ImageFilter; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -import 'colors.dart'; -import 'interface_level.dart'; -import 'scrollbar.dart'; -import 'theme.dart'; - -const TextStyle _kActionSheetActionStyle = TextStyle( - fontFamily: '.SF UI Text', - inherit: false, - fontSize: 20.0, - fontWeight: FontWeight.w400, - textBaseline: TextBaseline.alphabetic, -); - -const TextStyle _kActionSheetContentStyle = TextStyle( - fontFamily: '.SF UI Text', - inherit: false, - fontSize: 13.0, - fontWeight: FontWeight.w400, - color: _kContentTextColor, - textBaseline: TextBaseline.alphabetic, -); - -// Translucent, very light gray that is painted on top of the blurred backdrop -// as the action sheet's background color. -// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/39272. Use -// System Materials once we have them. -// Extracted from https://developer.apple.com/design/resources/. -const Color _kBackgroundColor = CupertinoDynamicColor.withBrightness( - color: Color(0xC7F9F9F9), - darkColor: Color(0xC7252525), -); - -// Translucent, light gray that is painted on top of the blurred backdrop as -// the background color of a pressed button. -// Eye-balled from iOS 13 beta simulator. -const Color _kPressedColor = CupertinoDynamicColor.withBrightness( - color: Color(0xFFE1E1E1), - darkColor: Color(0xFF2E2E2E), -); - -const Color _kCancelPressedColor = CupertinoDynamicColor.withBrightness( - color: Color(0xFFECECEC), - darkColor: Color(0xFF49494B), -); - -// The gray color used for text that appears in the title area. -// Extracted from https://developer.apple.com/design/resources/. -const Color _kContentTextColor = Color(0xFF8F8F8F); - -// Translucent gray that is painted on top of the blurred backdrop in the gap -// areas between the content section and actions section, as well as between -// buttons. -// Eye-balled from iOS 13 beta simulator. -const Color _kButtonDividerColor = _kContentTextColor; - -const double _kBlurAmount = 20.0; -const double _kEdgeHorizontalPadding = 8.0; -const double _kCancelButtonPadding = 8.0; -const double _kEdgeVerticalPadding = 10.0; -const double _kContentHorizontalPadding = 40.0; -const double _kContentVerticalPadding = 14.0; -const double _kButtonHeight = 56.0; -const double _kCornerRadius = 14.0; -const double _kDividerThickness = 1.0; - -/// An iOS-style action sheet. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=U-ao8p4A82k} -/// -/// An action sheet is a specific style of alert that presents the user -/// with a set of two or more choices related to the current context. -/// An action sheet can have a title, an additional message, and a list -/// of actions. The title is displayed above the message and the actions -/// are displayed below this content. -/// -/// This action sheet styles its title and message to match standard iOS action -/// sheet title and message text style. -/// -/// To display action buttons that look like standard iOS action sheet buttons, -/// provide [CupertinoActionSheetAction]s for the [actions] given to this action -/// sheet. -/// -/// To include a iOS-style cancel button separate from the other buttons, -/// provide an [CupertinoActionSheetAction] for the [cancelButton] given to this -/// action sheet. -/// -/// An action sheet is typically passed as the child widget to -/// [showCupertinoModalPopup], which displays the action sheet by sliding it up -/// from the bottom of the screen. -/// -/// {@tool snippet} -/// This sample shows how to use a [CupertinoActionSheet]. -/// The [CupertinoActionSheet] shows an alert with a set of two choices -/// when [CupertinoButton] is pressed. -/// -/// ```dart -/// class MyStatefulWidget extends StatefulWidget { -/// const MyStatefulWidget({Key? key}) : super(key: key); -/// -/// @override -/// _MyStatefulWidgetState createState() => _MyStatefulWidgetState(); -/// } -/// -/// class _MyStatefulWidgetState extends State { -/// @override -/// Widget build(BuildContext context) { -/// return CupertinoPageScaffold( -/// child: Center( -/// child: CupertinoButton( -/// onPressed: () { -/// showCupertinoModalPopup( -/// context: context, -/// builder: (BuildContext context) => CupertinoActionSheet( -/// title: const Text('Title'), -/// message: const Text('Message'), -/// actions: [ -/// CupertinoActionSheetAction( -/// child: const Text('Action One'), -/// onPressed: () { -/// Navigator.pop(context); -/// }, -/// ), -/// CupertinoActionSheetAction( -/// child: const Text('Action Two'), -/// onPressed: () { -/// Navigator.pop(context); -/// }, -/// ) -/// ], -/// ), -/// ); -/// }, -/// child: const Text('CupertinoActionSheet'), -/// ), -/// ), -/// ); -/// } -/// } -/// ``` -/// {@end-tool} -/// -/// See also: -/// -/// * [CupertinoActionSheetAction], which is an iOS-style action sheet button. -/// * -class CupertinoActionSheet extends StatelessWidget { - /// Creates an iOS-style action sheet. - /// - /// An action sheet must have a non-null value for at least one of the - /// following arguments: [actions], [title], [message], or [cancelButton]. - /// - /// Generally, action sheets are used to give the user a choice between - /// two or more choices for the current context. - const CupertinoActionSheet({ - Key? key, - this.title, - this.message, - this.actions, - this.messageScrollController, - this.actionScrollController, - this.cancelButton, - }) : assert( - actions != null || title != null || message != null || cancelButton != null, - 'An action sheet must have a non-null value for at least one of the following arguments: ' - 'actions, title, message, or cancelButton', - ), - super(key: key); - - /// An optional title of the action sheet. When the [message] is non-null, - /// the font of the [title] is bold. - /// - /// Typically a [Text] widget. - final Widget? title; - - /// An optional descriptive message that provides more details about the - /// reason for the alert. - /// - /// Typically a [Text] widget. - final Widget? message; - - /// The set of actions that are displayed for the user to select. - /// - /// Typically this is a list of [CupertinoActionSheetAction] widgets. - final List? actions; - - /// A scroll controller that can be used to control the scrolling of the - /// [message] in the action sheet. - /// - /// This attribute is typically not needed, as alert messages should be - /// short. - final ScrollController? messageScrollController; - - /// A scroll controller that can be used to control the scrolling of the - /// [actions] in the action sheet. - /// - /// This attribute is typically not needed. - final ScrollController? actionScrollController; - - /// The optional cancel button that is grouped separately from the other - /// actions. - /// - /// Typically this is an [CupertinoActionSheetAction] widget. - final Widget? cancelButton; - - Widget _buildContent(BuildContext context) { - final List content = []; - if (title != null || message != null) { - final Widget titleSection = _CupertinoAlertContentSection( - title: title, - message: message, - scrollController: messageScrollController, - ); - content.add(Flexible(child: titleSection)); - } - - return Container( - color: CupertinoDynamicColor.resolve(_kBackgroundColor, context), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: content, - ), - ); - } - - Widget _buildActions() { - if (actions == null || actions!.isEmpty) { - return Container( - height: 0.0, - ); - } - return _CupertinoAlertActionSection( - children: actions!, - scrollController: actionScrollController, - hasCancelButton: cancelButton != null, - ); - } - - Widget _buildCancelButton() { - final double cancelPadding = (actions != null || message != null || title != null) - ? _kCancelButtonPadding : 0.0; - return Padding( - padding: EdgeInsets.only(top: cancelPadding), - child: _CupertinoActionSheetCancelButton( - child: cancelButton, - ), - ); - } - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMediaQuery(context)); - - final List children = [ - Flexible(child: ClipRRect( - borderRadius: BorderRadius.circular(12.0), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount), - child: _CupertinoAlertRenderWidget( - contentSection: Builder(builder: _buildContent), - actionsSection: _buildActions(), - ), - ), - ), - ), - if (cancelButton != null) _buildCancelButton(), - ]; - - final Orientation orientation = MediaQuery.of(context).orientation; - final double actionSheetWidth; - if (orientation == Orientation.portrait) { - actionSheetWidth = MediaQuery.of(context).size.width - (_kEdgeHorizontalPadding * 2); - } else { - actionSheetWidth = MediaQuery.of(context).size.height - (_kEdgeHorizontalPadding * 2); - } - - return SafeArea( - child: Semantics( - namesRoute: true, - scopesRoute: true, - explicitChildNodes: true, - label: 'Alert', - child: CupertinoUserInterfaceLevel( - data: CupertinoUserInterfaceLevelData.elevated, - child: Container( - width: actionSheetWidth, - margin: const EdgeInsets.symmetric( - horizontal: _kEdgeHorizontalPadding, - vertical: _kEdgeVerticalPadding, - ), - child: Column( - children: children, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - ), - ), - ), - ), - ); - } -} - -/// A button typically used in a [CupertinoActionSheet]. -/// -/// See also: -/// -/// * [CupertinoActionSheet], an alert that presents the user with a set of two or -/// more choices related to the current context. -class CupertinoActionSheetAction extends StatelessWidget { - /// Creates an action for an iOS-style action sheet. - /// - /// The [child] and [onPressed] arguments must not be null. - const CupertinoActionSheetAction({ - Key? key, - required this.onPressed, - this.isDefaultAction = false, - this.isDestructiveAction = false, - required this.child, - }) : assert(child != null), - assert(onPressed != null), - super(key: key); - - /// The callback that is called when the button is tapped. - /// - /// This attribute must not be null. - final VoidCallback onPressed; - - /// Whether this action is the default choice in the action sheet. - /// - /// Default buttons have bold text. - final bool isDefaultAction; - - /// Whether this action might change or delete data. - /// - /// Destructive buttons have red text. - final bool isDestructiveAction; - - /// The widget below this widget in the tree. - /// - /// Typically a [Text] widget. - final Widget child; - - @override - Widget build(BuildContext context) { - TextStyle style = _kActionSheetActionStyle.copyWith( - color: isDestructiveAction - ? CupertinoDynamicColor.resolve(CupertinoColors.systemRed, context) - : CupertinoTheme.of(context).primaryColor, - ); - - if (isDefaultAction) { - style = style.copyWith(fontWeight: FontWeight.w600); - } - - return GestureDetector( - onTap: onPressed, - behavior: HitTestBehavior.opaque, - child: ConstrainedBox( - constraints: const BoxConstraints( - minHeight: _kButtonHeight, - ), - child: Semantics( - button: true, - child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 10.0, - ), - child: DefaultTextStyle( - style: style, - child: child, - textAlign: TextAlign.center, - ), - ), - ), - ), - ); - } -} - -class _CupertinoActionSheetCancelButton extends StatefulWidget { - const _CupertinoActionSheetCancelButton({ - Key? key, - this.child, - }) : super(key: key); - - final Widget? child; - - @override - _CupertinoActionSheetCancelButtonState createState() => _CupertinoActionSheetCancelButtonState(); -} - -class _CupertinoActionSheetCancelButtonState extends State<_CupertinoActionSheetCancelButton> { - bool isBeingPressed = false; - - void _onTapDown(TapDownDetails event) { - setState(() { isBeingPressed = true; }); - } - - void _onTapUp(TapUpDetails event) { - setState(() { isBeingPressed = false; }); - } - - void _onTapCancel() { - setState(() { isBeingPressed = false; }); - } - - @override - Widget build(BuildContext context) { - final Color backgroundColor = isBeingPressed - ? _kCancelPressedColor - : CupertinoColors.secondarySystemGroupedBackground; - return GestureDetector( - excludeFromSemantics: true, - onTapDown: _onTapDown, - onTapUp: _onTapUp, - onTapCancel: _onTapCancel, - child: Container( - decoration: BoxDecoration( - color: CupertinoDynamicColor.resolve(backgroundColor, context), - borderRadius: BorderRadius.circular(_kCornerRadius), - ), - child: widget.child, - ), - ); - } -} - -class _CupertinoAlertRenderWidget extends RenderObjectWidget { - const _CupertinoAlertRenderWidget({ - Key? key, - required this.contentSection, - required this.actionsSection, - }) : super(key: key); - - final Widget contentSection; - final Widget actionsSection; - - @override - RenderObject createRenderObject(BuildContext context) { - assert(debugCheckHasMediaQuery(context)); - return _RenderCupertinoAlert( - dividerThickness: _kDividerThickness / MediaQuery.of(context).devicePixelRatio, - dividerColor: CupertinoDynamicColor.resolve(_kButtonDividerColor, context), - ); - } - - @override - void updateRenderObject(BuildContext context, _RenderCupertinoAlert renderObject) { - super.updateRenderObject(context, renderObject); - renderObject.dividerColor = CupertinoDynamicColor.resolve(_kButtonDividerColor, context); - } - - @override - RenderObjectElement createElement() { - return _CupertinoAlertRenderElement(this); - } -} - -class _CupertinoAlertRenderElement extends RenderObjectElement { - _CupertinoAlertRenderElement(_CupertinoAlertRenderWidget widget) : super(widget); - - Element? _contentElement; - Element? _actionsElement; - - @override - _CupertinoAlertRenderWidget get widget => super.widget as _CupertinoAlertRenderWidget; - - @override - _RenderCupertinoAlert get renderObject => super.renderObject as _RenderCupertinoAlert; - - @override - void visitChildren(ElementVisitor visitor) { - if (_contentElement != null) { - visitor(_contentElement!); - } - if (_actionsElement != null) { - visitor(_actionsElement!); - } - } - - @override - void mount(Element? parent, Object? newSlot) { - super.mount(parent, newSlot); - _contentElement = updateChild(_contentElement, widget.contentSection, _AlertSections.contentSection); - _actionsElement = updateChild(_actionsElement, widget.actionsSection, _AlertSections.actionsSection); - } - - @override - void insertRenderObjectChild(RenderObject child, _AlertSections slot) { - _placeChildInSlot(child, slot); - } - - @override - void moveRenderObjectChild(RenderObject child, _AlertSections oldSlot, _AlertSections newSlot) { - _placeChildInSlot(child, newSlot); - } - - @override - void update(RenderObjectWidget newWidget) { - super.update(newWidget); - _contentElement = updateChild(_contentElement, widget.contentSection, _AlertSections.contentSection); - _actionsElement = updateChild(_actionsElement, widget.actionsSection, _AlertSections.actionsSection); - } - - @override - void forgetChild(Element child) { - assert(child == _contentElement || child == _actionsElement); - if (_contentElement == child) { - _contentElement = null; - } else if (_actionsElement == child) { - _actionsElement = null; - } - super.forgetChild(child); - } - - @override - void removeRenderObjectChild(RenderObject child, _AlertSections slot) { - assert(child == renderObject.contentSection || child == renderObject.actionsSection); - if (renderObject.contentSection == child) { - renderObject.contentSection = null; - } else if (renderObject.actionsSection == child) { - renderObject.actionsSection = null; - } - } - - void _placeChildInSlot(RenderObject child, _AlertSections slot) { - assert(slot != null); - switch (slot) { - case _AlertSections.contentSection: - renderObject.contentSection = child as RenderBox; - break; - case _AlertSections.actionsSection: - renderObject.actionsSection = child as RenderBox; - break; - } - } -} - -// An iOS-style layout policy for sizing an alert's content section and action -// button section. -// -// The policy is as follows: -// -// If all content and buttons fit on the screen: -// The content section and action button section are sized intrinsically. -// -// If all content and buttons do not fit on the screen: -// A minimum height for the action button section is calculated. The action -// button section will not be rendered shorter than this minimum. See -// _RenderCupertinoAlertActions for the minimum height calculation. -// -// With the minimum action button section calculated, the content section can -// take up as much of the remaining space as it needs. -// -// After the content section is laid out, the action button section is allowed -// to take up any remaining space that was not consumed by the content section. -class _RenderCupertinoAlert extends RenderBox { - _RenderCupertinoAlert({ - RenderBox? contentSection, - RenderBox? actionsSection, - double dividerThickness = 0.0, - required Color dividerColor, - }) : assert(dividerColor != null), - _contentSection = contentSection, - _actionsSection = actionsSection, - _dividerThickness = dividerThickness, - _dividerPaint = Paint() - ..color = dividerColor - ..style = PaintingStyle.fill; - - RenderBox? get contentSection => _contentSection; - RenderBox? _contentSection; - set contentSection(RenderBox? newContentSection) { - if (newContentSection != _contentSection) { - if (null != _contentSection) { - dropChild(_contentSection!); - } - _contentSection = newContentSection; - if (null != _contentSection) { - adoptChild(_contentSection!); - } - } - } - - RenderBox? get actionsSection => _actionsSection; - RenderBox? _actionsSection; - set actionsSection(RenderBox? newActionsSection) { - if (newActionsSection != _actionsSection) { - if (null != _actionsSection) { - dropChild(_actionsSection!); - } - _actionsSection = newActionsSection; - if (null != _actionsSection) { - adoptChild(_actionsSection!); - } - } - } - - Color get dividerColor => _dividerPaint.color; - set dividerColor(Color value) { - if (value == _dividerPaint.color) - return; - _dividerPaint.color = value; - markNeedsPaint(); - } - - final double _dividerThickness; - - final Paint _dividerPaint; - - @override - void attach(PipelineOwner owner) { - super.attach(owner); - if (null != contentSection) { - contentSection!.attach(owner); - } - if (null != actionsSection) { - actionsSection!.attach(owner); - } - } - - @override - void detach() { - super.detach(); - if (null != contentSection) { - contentSection!.detach(); - } - if (null != actionsSection) { - actionsSection!.detach(); - } - } - - @override - void redepthChildren() { - if (null != contentSection) { - redepthChild(contentSection!); - } - if (null != actionsSection) { - redepthChild(actionsSection!); - } - } - - @override - void setupParentData(RenderBox child) { - if (child.parentData is! MultiChildLayoutParentData) { - child.parentData = MultiChildLayoutParentData(); - } - } - - @override - void visitChildren(RenderObjectVisitor visitor) { - if (contentSection != null) { - visitor(contentSection!); - } - if (actionsSection != null) { - visitor(actionsSection!); - } - } - - @override - List debugDescribeChildren() { - final List value = []; - if (contentSection != null) { - value.add(contentSection!.toDiagnosticsNode(name: 'content')); - } - if (actionsSection != null) { - value.add(actionsSection!.toDiagnosticsNode(name: 'actions')); - } - return value; - } - - @override - double computeMinIntrinsicWidth(double height) { - return constraints.minWidth; - } - - @override - double computeMaxIntrinsicWidth(double height) { - return constraints.maxWidth; - } - - @override - double computeMinIntrinsicHeight(double width) { - final double contentHeight = contentSection!.getMinIntrinsicHeight(width); - final double actionsHeight = actionsSection!.getMinIntrinsicHeight(width); - final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0; - double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight; - - if (actionsHeight > 0 || contentHeight > 0) - height -= 2 * _kEdgeVerticalPadding; - if (height.isFinite) - return height; - return 0.0; - } - - @override - double computeMaxIntrinsicHeight(double width) { - final double contentHeight = contentSection!.getMaxIntrinsicHeight(width); - final double actionsHeight = actionsSection!.getMaxIntrinsicHeight(width); - final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0; - double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight; - - if (actionsHeight > 0 || contentHeight > 0) - height -= 2 * _kEdgeVerticalPadding; - if (height.isFinite) - return height; - return 0.0; - } - - double _computeDividerThickness(BoxConstraints constraints) { - final bool hasDivider = contentSection!.getMaxIntrinsicHeight(constraints.maxWidth) > 0.0 - && actionsSection!.getMaxIntrinsicHeight(constraints.maxWidth) > 0.0; - return hasDivider ? _dividerThickness : 0.0; - } - - _AlertSizes _computeSizes({required BoxConstraints constraints, required ChildLayouter layoutChild, required double dividerThickness}) { - final double minActionsHeight = actionsSection!.getMinIntrinsicHeight(constraints.maxWidth); - - final Size contentSize = layoutChild( - contentSection!, - constraints.deflate(EdgeInsets.only(bottom: minActionsHeight + dividerThickness)), - ); - - final Size actionsSize = layoutChild( - actionsSection!, - constraints.deflate(EdgeInsets.only(top: contentSize.height + dividerThickness)), - ); - - final double actionSheetHeight = contentSize.height + dividerThickness + actionsSize.height; - return _AlertSizes( - size: Size(constraints.maxWidth, actionSheetHeight), - contentHeight: contentSize.height, - ); - } - - @override - Size computeDryLayout(BoxConstraints constraints) { - return _computeSizes( - constraints: constraints, - layoutChild: ChildLayoutHelper.dryLayoutChild, - dividerThickness: _computeDividerThickness(constraints), - ).size; - } - - @override - void performLayout() { - final BoxConstraints constraints = this.constraints; - final double dividerThickness = _computeDividerThickness(constraints); - final _AlertSizes alertSizes = _computeSizes( - constraints: constraints, - layoutChild: ChildLayoutHelper.layoutChild, - dividerThickness: dividerThickness, - ); - - size = alertSizes.size; - - // Set the position of the actions box to sit at the bottom of the alert. - // The content box defaults to the top left, which is where we want it. - assert(actionsSection!.parentData is MultiChildLayoutParentData); - final MultiChildLayoutParentData actionParentData = actionsSection!.parentData! as MultiChildLayoutParentData; - actionParentData.offset = Offset(0.0, alertSizes.contentHeight + dividerThickness); - } - - @override - void paint(PaintingContext context, Offset offset) { - final MultiChildLayoutParentData contentParentData = contentSection!.parentData! as MultiChildLayoutParentData; - contentSection!.paint(context, offset + contentParentData.offset); - - final bool hasDivider = contentSection!.size.height > 0.0 && actionsSection!.size.height > 0.0; - if (hasDivider) { - _paintDividerBetweenContentAndActions(context.canvas, offset); - } - - final MultiChildLayoutParentData actionsParentData = actionsSection!.parentData! as MultiChildLayoutParentData; - actionsSection!.paint(context, offset + actionsParentData.offset); - } - - void _paintDividerBetweenContentAndActions(Canvas canvas, Offset offset) { - canvas.drawRect( - Rect.fromLTWH( - offset.dx, - offset.dy + contentSection!.size.height, - size.width, - _dividerThickness, - ), - _dividerPaint, - ); - } - - @override - bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { - final MultiChildLayoutParentData contentSectionParentData = contentSection!.parentData! as MultiChildLayoutParentData; - final MultiChildLayoutParentData actionsSectionParentData = actionsSection!.parentData! as MultiChildLayoutParentData; - return result.addWithPaintOffset( - offset: contentSectionParentData.offset, - position: position, - hitTest: (BoxHitTestResult result, Offset transformed) { - assert(transformed == position - contentSectionParentData.offset); - return contentSection!.hitTest(result, position: transformed); - }, - ) - || result.addWithPaintOffset( - offset: actionsSectionParentData.offset, - position: position, - hitTest: (BoxHitTestResult result, Offset transformed) { - assert(transformed == position - actionsSectionParentData.offset); - return actionsSection!.hitTest(result, position: transformed); - }, - ); - } -} - -class _AlertSizes { - const _AlertSizes({required this.size, required this.contentHeight}); - - final Size size; - final double contentHeight; -} - -// Visual components of an alert that need to be explicitly sized and -// laid out at runtime. -enum _AlertSections { - contentSection, - actionsSection, -} - -// The "content section" of a CupertinoActionSheet. -// -// If title is missing, then only content is added. If content is -// missing, then only a title is added. If both are missing, then it returns -// a SingleChildScrollView with a zero-sized Container. -class _CupertinoAlertContentSection extends StatelessWidget { - const _CupertinoAlertContentSection({ - Key? key, - this.title, - this.message, - this.scrollController, - }) : super(key: key); - - // An optional title of the action sheet. When the message is non-null, - // the font of the title is bold. - // - // Typically a Text widget. - final Widget? title; - - // An optional descriptive message that provides more details about the - // reason for the alert. - // - // Typically a Text widget. - final Widget? message; - - // A scroll controller that can be used to control the scrolling of the - // content in the action sheet. - // - // Defaults to null, and is typically not needed, since most alert contents - // are short. - final ScrollController? scrollController; - - @override - Widget build(BuildContext context) { - final List titleContentGroup = []; - - if (title != null) { - titleContentGroup.add(Padding( - padding: const EdgeInsets.only( - left: _kContentHorizontalPadding, - right: _kContentHorizontalPadding, - bottom: _kContentVerticalPadding, - top: _kContentVerticalPadding, - ), - child: DefaultTextStyle( - style: message == null ? _kActionSheetContentStyle - : _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600), - textAlign: TextAlign.center, - child: title!, - ), - )); - } - - if (message != null) { - titleContentGroup.add( - Padding( - padding: EdgeInsets.only( - left: _kContentHorizontalPadding, - right: _kContentHorizontalPadding, - bottom: title == null ? _kContentVerticalPadding : 22.0, - top: title == null ? _kContentVerticalPadding : 0.0, - ), - child: DefaultTextStyle( - style: title == null ? _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600) - : _kActionSheetContentStyle, - textAlign: TextAlign.center, - child: message!, - ), - ), - ); - } - - if (titleContentGroup.isEmpty) { - return SingleChildScrollView( - controller: scrollController, - child: const SizedBox( - width: 0.0, - height: 0.0, - ), - ); - } - - // Add padding between the widgets if necessary. - if (titleContentGroup.length > 1) { - titleContentGroup.insert(1, const Padding(padding: EdgeInsets.only(top: 8.0))); - } - - return CupertinoScrollbar( - child: SingleChildScrollView( - controller: scrollController, - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: titleContentGroup, - ), - ), - ); - } -} - -// The "actions section" of a CupertinoActionSheet. -// -// See _RenderCupertinoAlertActions for details about action button sizing -// and layout. -class _CupertinoAlertActionSection extends StatefulWidget { - const _CupertinoAlertActionSection({ - Key? key, - required this.children, - this.scrollController, - this.hasCancelButton, - }) : assert(children != null), - super(key: key); - - final List children; - - // A scroll controller that can be used to control the scrolling of the - // actions in the action sheet. - // - // Defaults to null, and is typically not needed, since most alerts - // don't have many actions. - final ScrollController? scrollController; - - final bool? hasCancelButton; - - @override - _CupertinoAlertActionSectionState createState() => _CupertinoAlertActionSectionState(); -} - -class _CupertinoAlertActionSectionState extends State<_CupertinoAlertActionSection> { - @override - Widget build(BuildContext context) { - final double devicePixelRatio = MediaQuery.of(context).devicePixelRatio; - - final List interactiveButtons = []; - for (int i = 0; i < widget.children.length; i += 1) { - interactiveButtons.add( - _PressableActionButton( - child: widget.children[i], - ), - ); - } - - return CupertinoScrollbar( - child: SingleChildScrollView( - controller: widget.scrollController, - child: _CupertinoAlertActionsRenderWidget( - actionButtons: interactiveButtons, - dividerThickness: _kDividerThickness / devicePixelRatio, - hasCancelButton: widget.hasCancelButton ?? false, - ), - ), - ); - } -} - -// A button that updates its render state when pressed. -// -// The pressed state is forwarded to an _ActionButtonParentDataWidget. The -// corresponding _ActionButtonParentData is then interpreted and rendered -// appropriately by _RenderCupertinoAlertActions. -class _PressableActionButton extends StatefulWidget { - const _PressableActionButton({ - required this.child, - }); - - final Widget child; - - @override - _PressableActionButtonState createState() => _PressableActionButtonState(); -} - -class _PressableActionButtonState extends State<_PressableActionButton> { - bool _isPressed = false; - - @override - Widget build(BuildContext context) { - return _ActionButtonParentDataWidget( - isPressed: _isPressed, - // TODO(mattcarroll): Button press dynamics need overhaul for iOS: - // https://github.com/flutter/flutter/issues/19786 - child: GestureDetector( - excludeFromSemantics: true, - behavior: HitTestBehavior.opaque, - onTapDown: (TapDownDetails details) => setState(() => _isPressed = true), - onTapUp: (TapUpDetails details) => setState(() => _isPressed = false), - // TODO(mattcarroll): Cancel is currently triggered when user moves past - // slop instead of off button: https://github.com/flutter/flutter/issues/19783 - onTapCancel: () => setState(() => _isPressed = false), - child: widget.child, - ), - ); - } -} - -// ParentDataWidget that updates _ActionButtonParentData for an action button. -// -// Each action button requires knowledge of whether or not it is pressed so that -// the alert can correctly render the button. The pressed state is held within -// _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for -// updating the pressed state of an _ActionButtonParentData based on the -// incoming isPressed property. -class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParentData> { - const _ActionButtonParentDataWidget({ - Key? key, - required this.isPressed, - required Widget child, - }) : super(key: key, child: child); - - final bool isPressed; - - @override - void applyParentData(RenderObject renderObject) { - assert(renderObject.parentData is _ActionButtonParentData); - final _ActionButtonParentData parentData = renderObject.parentData! as _ActionButtonParentData; - if (parentData.isPressed != isPressed) { - parentData.isPressed = isPressed; - - // Force a repaint. - final AbstractNode? targetParent = renderObject.parent; - if (targetParent is RenderObject) - targetParent.markNeedsPaint(); - } - } - - @override - Type get debugTypicalAncestorWidgetClass => _CupertinoAlertActionsRenderWidget; -} - -// ParentData applied to individual action buttons that report whether or not -// that button is currently pressed by the user. -class _ActionButtonParentData extends MultiChildLayoutParentData { - _ActionButtonParentData({ - this.isPressed = false, - }); - - bool isPressed; -} - -// An iOS-style alert action button layout. -// -// See _RenderCupertinoAlertActions for specific layout policy details. -class _CupertinoAlertActionsRenderWidget extends MultiChildRenderObjectWidget { - _CupertinoAlertActionsRenderWidget({ - Key? key, - required List actionButtons, - double dividerThickness = 0.0, - bool hasCancelButton = false, - }) : _dividerThickness = dividerThickness, - _hasCancelButton = hasCancelButton, - super(key: key, children: actionButtons); - - final double _dividerThickness; - final bool _hasCancelButton; - - @override - RenderObject createRenderObject(BuildContext context) { - return _RenderCupertinoAlertActions( - dividerThickness: _dividerThickness, - dividerColor: CupertinoDynamicColor.resolve(_kButtonDividerColor, context), - hasCancelButton: _hasCancelButton, - backgroundColor: CupertinoDynamicColor.resolve(_kBackgroundColor, context), - pressedColor: CupertinoDynamicColor.resolve(_kPressedColor, context), - ); - } - - @override - void updateRenderObject(BuildContext context, _RenderCupertinoAlertActions renderObject) { - renderObject - ..dividerThickness = _dividerThickness - ..dividerColor = CupertinoDynamicColor.resolve(_kButtonDividerColor, context) - ..hasCancelButton = _hasCancelButton - ..backgroundColor = CupertinoDynamicColor.resolve(_kBackgroundColor, context) - ..pressedColor = CupertinoDynamicColor.resolve(_kPressedColor, context); - } -} - -// An iOS-style layout policy for sizing and positioning an action sheet's -// buttons. -// -// The policy is as follows: -// -// Action sheet buttons are always stacked vertically. In the case where the -// content section and the action section combined can not fit on the screen -// without scrolling, the height of the action section is determined as -// follows. -// -// If the user has included a separate cancel button, the height of the action -// section can be up to the height of 3 action buttons (i.e., the user can -// include 1, 2, or 3 action buttons and they will appear without needing to -// be scrolled). If 4+ action buttons are provided, the height of the action -// section shrinks to 1.5 buttons tall, and is scrollable. -// -// If the user has not included a separate cancel button, the height of the -// action section is at most 1.5 buttons tall. -class _RenderCupertinoAlertActions extends RenderBox - with ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin { - _RenderCupertinoAlertActions({ - List? children, - double dividerThickness = 0.0, - required Color dividerColor, - bool hasCancelButton = false, - required Color backgroundColor, - required Color pressedColor, - }) : _dividerThickness = dividerThickness, - _hasCancelButton = hasCancelButton, - _buttonBackgroundPaint = Paint() - ..style = PaintingStyle.fill - ..color = backgroundColor, - _pressedButtonBackgroundPaint = Paint() - ..style = PaintingStyle.fill - ..color = pressedColor, - _dividerPaint = Paint() - ..color = dividerColor - ..style = PaintingStyle.fill { - addAll(children); - } - - // The thickness of the divider between buttons. - double get dividerThickness => _dividerThickness; - double _dividerThickness; - set dividerThickness(double newValue) { - if (newValue == _dividerThickness) { - return; - } - - _dividerThickness = newValue; - markNeedsLayout(); - } - - Color get backgroundColor => _buttonBackgroundPaint.color; - set backgroundColor(Color newValue) { - if (newValue == _buttonBackgroundPaint.color) { - return; - } - - _buttonBackgroundPaint.color = newValue; - markNeedsPaint(); - } - - Color get pressedColor => _pressedButtonBackgroundPaint.color; - set pressedColor(Color newValue) { - if (newValue == _pressedButtonBackgroundPaint.color) { - return; - } - - _pressedButtonBackgroundPaint.color = newValue; - markNeedsPaint(); - } - - Color get dividerColor => _dividerPaint.color; - set dividerColor(Color value) { - if (value == _dividerPaint.color) { - return; - } - _dividerPaint.color = value; - markNeedsPaint(); - } - - bool _hasCancelButton; - bool get hasCancelButton => _hasCancelButton; - set hasCancelButton(bool newValue) { - if (newValue == _hasCancelButton) { - return; - } - - _hasCancelButton = newValue; - markNeedsLayout(); - } - - final Paint _buttonBackgroundPaint; - final Paint _pressedButtonBackgroundPaint; - - final Paint _dividerPaint; - - @override - void setupParentData(RenderBox child) { - if (child.parentData is! _ActionButtonParentData) - child.parentData = _ActionButtonParentData(); - } - - @override - double computeMinIntrinsicWidth(double height) { - return constraints.minWidth; - } - - @override - double computeMaxIntrinsicWidth(double height) { - return constraints.maxWidth; - } - - @override - double computeMinIntrinsicHeight(double width) { - if (childCount == 0) - return 0.0; - if (childCount == 1) - return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness; - if (hasCancelButton && childCount < 4) - return _computeMinIntrinsicHeightWithCancel(width); - return _computeMinIntrinsicHeightWithoutCancel(width); - } - - // The minimum height for more than 2-3 buttons when a cancel button is - // included is the full height of button stack. - double _computeMinIntrinsicHeightWithCancel(double width) { - assert(childCount == 2 || childCount == 3); - if (childCount == 2) { - return firstChild!.getMinIntrinsicHeight(width) - + childAfter(firstChild!)!.getMinIntrinsicHeight(width) - + dividerThickness; - } - return firstChild!.getMinIntrinsicHeight(width) - + childAfter(firstChild!)!.getMinIntrinsicHeight(width) - + childAfter(childAfter(firstChild!)!)!.getMinIntrinsicHeight(width) - + (dividerThickness * 2); - } - - // The minimum height for more than 2 buttons when no cancel button or 4+ - // buttons when a cancel button is included is the height of the 1st button - // + 50% the height of the 2nd button + 2 dividers. - double _computeMinIntrinsicHeightWithoutCancel(double width) { - assert(childCount >= 2); - return firstChild!.getMinIntrinsicHeight(width) - + dividerThickness - + (0.5 * childAfter(firstChild!)!.getMinIntrinsicHeight(width)); - } - - @override - double computeMaxIntrinsicHeight(double width) { - if (childCount == 0) - return 0.0; - if (childCount == 1) - return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness; - return _computeMaxIntrinsicHeightStacked(width); - } - - // Max height of a stack of buttons is the sum of all button heights + a - // divider for each button. - double _computeMaxIntrinsicHeightStacked(double width) { - assert(childCount >= 2); - - final double allDividersHeight = (childCount - 1) * dividerThickness; - double heightAccumulation = allDividersHeight; - RenderBox? button = firstChild; - while (button != null) { - heightAccumulation += button.getMaxIntrinsicHeight(width); - button = childAfter(button); - } - return heightAccumulation; - } - - @override - Size computeDryLayout(BoxConstraints constraints) { - return _performLayout(constraints, dry: true); - } - - @override - void performLayout() { - size = _performLayout(constraints, dry: false); - } - - Size _performLayout(BoxConstraints constraints, {bool dry = false}) { - final BoxConstraints perButtonConstraints = constraints.copyWith( - minHeight: 0.0, - maxHeight: double.infinity, - ); - - RenderBox? child = firstChild; - int index = 0; - double verticalOffset = 0.0; - while (child != null) { - final Size childSize; - if (!dry) { - child.layout( - perButtonConstraints, - parentUsesSize: true, - ); - childSize = child.size; - assert(child.parentData is MultiChildLayoutParentData); - final MultiChildLayoutParentData parentData = child.parentData! as MultiChildLayoutParentData; - parentData.offset = Offset(0.0, verticalOffset); - } else { - childSize = child.getDryLayout(constraints); - } - - verticalOffset += childSize.height; - if (index < childCount - 1) { - // Add a gap for the next divider. - verticalOffset += dividerThickness; - } - - index += 1; - child = childAfter(child); - } - - return constraints.constrain( - Size(constraints.maxWidth, verticalOffset), - ); - } - - @override - void paint(PaintingContext context, Offset offset) { - final Canvas canvas = context.canvas; - _drawButtonBackgroundsAndDividersStacked(canvas, offset); - _drawButtons(context, offset); - } - - void _drawButtonBackgroundsAndDividersStacked(Canvas canvas, Offset offset) { - final Offset dividerOffset = Offset(0.0, dividerThickness); - - final Path backgroundFillPath = Path() - ..fillType = PathFillType.evenOdd - ..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height)); - - final Path pressedBackgroundFillPath = Path(); - - final Path dividersPath = Path(); - - Offset accumulatingOffset = offset; - - RenderBox? child = firstChild; - RenderBox? prevChild; - while (child != null) { - assert(child.parentData is _ActionButtonParentData); - final _ActionButtonParentData currentButtonParentData = child.parentData! as _ActionButtonParentData; - final bool isButtonPressed = currentButtonParentData.isPressed; - - bool isPrevButtonPressed = false; - if (prevChild != null) { - assert(prevChild.parentData is _ActionButtonParentData); - final _ActionButtonParentData previousButtonParentData = prevChild.parentData! as _ActionButtonParentData; - isPrevButtonPressed = previousButtonParentData.isPressed; - } - - final bool isDividerPresent = child != firstChild; - final bool isDividerPainted = isDividerPresent && !(isButtonPressed || isPrevButtonPressed); - final Rect dividerRect = Rect.fromLTWH( - accumulatingOffset.dx, - accumulatingOffset.dy, - size.width, - _dividerThickness, - ); - - final Rect buttonBackgroundRect = Rect.fromLTWH( - accumulatingOffset.dx, - accumulatingOffset.dy + (isDividerPresent ? dividerThickness : 0.0), - size.width, - child.size.height, - ); - - // If this button is pressed, then we don't want a white background to be - // painted, so we erase this button from the background path. - if (isButtonPressed) { - backgroundFillPath.addRect(buttonBackgroundRect); - pressedBackgroundFillPath.addRect(buttonBackgroundRect); - } - - // If this divider is needed, then we erase the divider area from the - // background path, and on top of that we paint a translucent gray to - // darken the divider area. - if (isDividerPainted) { - backgroundFillPath.addRect(dividerRect); - dividersPath.addRect(dividerRect); - } - - accumulatingOffset += (isDividerPresent ? dividerOffset : Offset.zero) - + Offset(0.0, child.size.height); - - prevChild = child; - child = childAfter(child); - } - - canvas.drawPath(backgroundFillPath, _buttonBackgroundPaint); - canvas.drawPath(pressedBackgroundFillPath, _pressedButtonBackgroundPaint); - canvas.drawPath(dividersPath, _dividerPaint); - } - - void _drawButtons(PaintingContext context, Offset offset) { - RenderBox? child = firstChild; - while (child != null) { - final MultiChildLayoutParentData childParentData = child.parentData! as MultiChildLayoutParentData; - context.paintChild(child, childParentData.offset + offset); - child = childAfter(child); - } - } - - @override - bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { - return defaultHitTestChildren(result, position: position); - } -} diff --git a/packages/flutter/lib/src/cupertino/dialog.dart b/packages/flutter/lib/src/cupertino/dialog.dart index ca028db6c3..e118aad7d4 100644 --- a/packages/flutter/lib/src/cupertino/dialog.dart +++ b/packages/flutter/lib/src/cupertino/dialog.dart @@ -13,6 +13,7 @@ import 'colors.dart'; import 'interface_level.dart'; import 'localizations.dart'; import 'scrollbar.dart'; +import 'theme.dart'; // TODO(abarth): These constants probably belong somewhere more general. @@ -49,18 +50,46 @@ const TextStyle _kCupertinoDialogActionStyle = TextStyle( textBaseline: TextBaseline.alphabetic, ); +// CupertinoActionSheet-specific text styles. +const TextStyle _kActionSheetActionStyle = TextStyle( + fontFamily: '.SF UI Text', + inherit: false, + fontSize: 20.0, + fontWeight: FontWeight.w400, + textBaseline: TextBaseline.alphabetic, +); + +const TextStyle _kActionSheetContentStyle = TextStyle( + fontFamily: '.SF UI Text', + inherit: false, + fontSize: 13.0, + fontWeight: FontWeight.w400, + color: _kActionSheetContentTextColor, + textBaseline: TextBaseline.alphabetic, +); + +// Generic constants shared between Dialog and ActionSheet. +const double _kBlurAmount = 20.0; +const double _kCornerRadius = 14.0; +const double _kDividerThickness = 1.0; + +// Dialog specific constants. // iOS dialogs have a normal display width and another display width that is // used when the device is in accessibility mode. Each of these widths are // listed below. const double _kCupertinoDialogWidth = 270.0; const double _kAccessibilityCupertinoDialogWidth = 310.0; +const double _kDialogEdgePadding = 20.0; +const double _kDialogMinButtonHeight = 45.0; +const double _kDialogMinButtonFontSize = 10.0; -const double _kBlurAmount = 20.0; -const double _kEdgePadding = 20.0; -const double _kMinButtonHeight = 45.0; -const double _kMinButtonFontSize = 10.0; -const double _kDialogCornerRadius = 14.0; -const double _kDividerThickness = 1.0; +// ActionSheet specific constants. +const double _kActionSheetEdgeHorizontalPadding = 8.0; +const double _kActionSheetCancelButtonPadding = 8.0; +const double _kActionSheetEdgeVerticalPadding = 10.0; +const double _kActionSheetContentHorizontalPadding = 40.0; +const double _kActionSheetContentVerticalPadding = 14.0; +const double _kActionSheetButtonHeight = 56.0; // A translucent color that is painted on top of the blurred backdrop as the // dialog's background color @@ -73,11 +102,36 @@ const Color _kDialogColor = CupertinoDynamicColor.withBrightness( // Translucent light gray that is painted on top of the blurred backdrop as the // background color of a pressed button. // Eyeballed from iOS 13 beta simulator. -const Color _kDialogPressedColor = CupertinoDynamicColor.withBrightness( +const Color _kPressedColor = CupertinoDynamicColor.withBrightness( color: Color(0xFFE1E1E1), darkColor: Color(0xFF2E2E2E), ); +const Color _kActionSheetCancelPressedColor = CupertinoDynamicColor.withBrightness( + color: Color(0xFFECECEC), + darkColor: Color(0xFF49494B), +); + +// Translucent, very light gray that is painted on top of the blurred backdrop +// as the action sheet's background color. +// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/39272. Use +// System Materials once we have them. +// Extracted from https://developer.apple.com/design/resources/. +const Color _kActionSheetBackgroundColor = CupertinoDynamicColor.withBrightness( + color: Color(0xC7F9F9F9), + darkColor: Color(0xC7252525), +); + +// The gray color used for text that appears in the title area. +// Extracted from https://developer.apple.com/design/resources/. +const Color _kActionSheetContentTextColor = Color(0xFF8F8F8F); + +// Translucent gray that is painted on top of the blurred backdrop in the gap +// areas between the content section and actions section, as well as between +// buttons. +// Eye-balled from iOS 13 beta simulator. +const Color _kActionSheetButtonDividerColor = _kActionSheetContentTextColor; + // The alert dialog layout policy changes depending on whether the user is using // a "regular" font size vs a "large" font size. This is a spectrum. There are // many "regular" font sizes and many "large" font sizes. But depending on which @@ -101,6 +155,8 @@ bool _isInAccessibilityMode(BuildContext context) { /// An iOS-style alert dialog. /// +/// {@youtube 560 315 https://www.youtube.com/watch?v=75CsnyRXf5I} +/// /// An alert dialog informs the user about situations that require /// acknowledgement. An alert dialog has an optional title, optional content, /// and an optional list of actions. The title is displayed above the content @@ -117,6 +173,58 @@ bool _isInAccessibilityMode(BuildContext context) { /// Typically passed as the child widget to [showDialog], which displays the /// dialog. /// +/// {@tool snippet} +/// This sample shows how to use a [CupertinoAlertDialog]. +/// The [CupertinoAlertDialog] shows an alert with a set of two choices +/// when [CupertinoButton] is pressed. +/// +/// ```dart +/// class MyStatefulWidget extends StatefulWidget { +/// const MyStatefulWidget({Key? key}) : super(key: key); +/// +/// @override +/// _MyStatefulWidgetState createState() => _MyStatefulWidgetState(); +/// } +/// +/// class _MyStatefulWidgetState extends State { +/// @override +/// Widget build(BuildContext context) { +/// return CupertinoPageScaffold( +/// child: Center( +/// child: CupertinoButton( +/// onPressed: () { +/// showCupertinoDialog( +/// context: context, +/// builder: (BuildContext context) => CupertinoAlertDialog( +/// title: const Text('Alert'), +/// content: const Text('Proceed with destructive action?'), +/// actions: [ +/// CupertinoDialogAction( +/// child: const Text('No'), +/// onPressed: () { +/// Navigator.pop(context); +/// }, +/// ), +/// CupertinoDialogAction( +/// child: const Text('Yes'), +/// isDestructiveAction: true, +/// onPressed: () { +/// // Do something destructive. +/// }, +/// ) +/// ], +/// ), +/// ); +/// }, +/// child: const Text('CupertinoAlertDialog'), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// {@end-tool} +/// /// See also: /// /// * [CupertinoPopupSurface], which is a generic iOS-style popup surface that @@ -188,14 +296,34 @@ class CupertinoAlertDialog extends StatelessWidget { final Curve insetAnimationCurve; Widget _buildContent(BuildContext context) { + final double textScaleFactor = MediaQuery.of(context).textScaleFactor; + final List children = [ if (title != null || content != null) Flexible( flex: 3, child: _CupertinoAlertContentSection( title: title, - content: content, + message: content, scrollController: scrollController, + titlePadding: EdgeInsets.only( + left: _kDialogEdgePadding, + right: _kDialogEdgePadding, + bottom: content == null ? _kDialogEdgePadding : 1.0, + top: _kDialogEdgePadding * textScaleFactor, + ), + messagePadding: EdgeInsets.only( + left: _kDialogEdgePadding, + right: _kDialogEdgePadding, + bottom: _kDialogEdgePadding * textScaleFactor, + top: title == null ? _kDialogEdgePadding : 1.0, + ), + titleTextStyle: _kCupertinoDialogTitleStyle.copyWith( + color: CupertinoDynamicColor.resolve(CupertinoColors.label, context), + ), + messageTextStyle: _kCupertinoDialogContentStyle.copyWith( + color: CupertinoDynamicColor.resolve(CupertinoColors.label, context), + ), ), ), ]; @@ -218,6 +346,7 @@ class CupertinoAlertDialog extends StatelessWidget { actionSection = _CupertinoAlertActionSection( children: actions, scrollController: actionScrollController, + isActionSheet: false, ); } @@ -251,10 +380,10 @@ class CupertinoAlertDialog extends StatelessWidget { context: context, child: Center( child: Container( - margin: const EdgeInsets.symmetric(vertical: _kEdgePadding), + margin: const EdgeInsets.symmetric(vertical: _kDialogEdgePadding), width: isInAccessibilityMode - ? _kAccessibilityCupertinoDialogWidth - : _kCupertinoDialogWidth, + ? _kAccessibilityCupertinoDialogWidth + : _kCupertinoDialogWidth, child: CupertinoPopupSurface( isSurfacePainted: false, child: Semantics( @@ -265,6 +394,7 @@ class CupertinoAlertDialog extends StatelessWidget { child: _CupertinoDialogRenderWidget( contentSection: _buildContent(context), actionsSection: _buildActions(), + dividerColor: CupertinoColors.separator, ), ), ), @@ -318,7 +448,7 @@ class CupertinoPopupSurface extends StatelessWidget { @override Widget build(BuildContext context) { return ClipRRect( - borderRadius: BorderRadius.circular(_kDialogCornerRadius), + borderRadius: BorderRadius.circular(_kCornerRadius), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount), child: Container( @@ -330,6 +460,392 @@ class CupertinoPopupSurface extends StatelessWidget { } } +/// An iOS-style action sheet. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=U-ao8p4A82k} +/// +/// An action sheet is a specific style of alert that presents the user +/// with a set of two or more choices related to the current context. +/// An action sheet can have a title, an additional message, and a list +/// of actions. The title is displayed above the message and the actions +/// are displayed below this content. +/// +/// This action sheet styles its title and message to match standard iOS action +/// sheet title and message text style. +/// +/// To display action buttons that look like standard iOS action sheet buttons, +/// provide [CupertinoActionSheetAction]s for the [actions] given to this action +/// sheet. +/// +/// To include a iOS-style cancel button separate from the other buttons, +/// provide an [CupertinoActionSheetAction] for the [cancelButton] given to this +/// action sheet. +/// +/// An action sheet is typically passed as the child widget to +/// [showCupertinoModalPopup], which displays the action sheet by sliding it up +/// from the bottom of the screen. +/// +/// {@tool snippet} +/// This sample shows how to use a [CupertinoActionSheet]. +/// The [CupertinoActionSheet] shows a modal popup that slides in from the +/// bottom when [CupertinoButton] is pressed. +/// +/// ```dart +/// class MyStatefulWidget extends StatefulWidget { +/// const MyStatefulWidget({Key? key}) : super(key: key); +/// +/// @override +/// _MyStatefulWidgetState createState() => _MyStatefulWidgetState(); +/// } +/// +/// class _MyStatefulWidgetState extends State { +/// @override +/// Widget build(BuildContext context) { +/// return CupertinoPageScaffold( +/// child: Center( +/// child: CupertinoButton( +/// onPressed: () { +/// showCupertinoModalPopup( +/// context: context, +/// builder: (BuildContext context) => CupertinoActionSheet( +/// title: const Text('Title'), +/// message: const Text('Message'), +/// actions: [ +/// CupertinoActionSheetAction( +/// child: const Text('Action One'), +/// onPressed: () { +/// Navigator.pop(context); +/// }, +/// ), +/// CupertinoActionSheetAction( +/// child: const Text('Action Two'), +/// onPressed: () { +/// Navigator.pop(context); +/// }, +/// ) +/// ], +/// ), +/// ); +/// }, +/// child: const Text('CupertinoActionSheet'), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [CupertinoActionSheetAction], which is an iOS-style action sheet button. +/// * +class CupertinoActionSheet extends StatelessWidget { + /// Creates an iOS-style action sheet. + /// + /// An action sheet must have a non-null value for at least one of the + /// following arguments: [actions], [title], [message], or [cancelButton]. + /// + /// Generally, action sheets are used to give the user a choice between + /// two or more choices for the current context. + const CupertinoActionSheet({ + Key? key, + this.title, + this.message, + this.actions, + this.messageScrollController, + this.actionScrollController, + this.cancelButton, + }) : assert( + actions != null || title != null || message != null || cancelButton != null, + 'An action sheet must have a non-null value for at least one of the following arguments: ' + 'actions, title, message, or cancelButton', + ), + super(key: key); + + /// An optional title of the action sheet. When the [message] is non-null, + /// the font of the [title] is bold. + /// + /// Typically a [Text] widget. + final Widget? title; + + /// An optional descriptive message that provides more details about the + /// reason for the alert. + /// + /// Typically a [Text] widget. + final Widget? message; + + /// The set of actions that are displayed for the user to select. + /// + /// Typically this is a list of [CupertinoActionSheetAction] widgets. + final List? actions; + + /// A scroll controller that can be used to control the scrolling of the + /// [message] in the action sheet. + /// + /// This attribute is typically not needed, as alert messages should be + /// short. + final ScrollController? messageScrollController; + + /// A scroll controller that can be used to control the scrolling of the + /// [actions] in the action sheet. + /// + /// This attribute is typically not needed. + final ScrollController? actionScrollController; + + /// The optional cancel button that is grouped separately from the other + /// actions. + /// + /// Typically this is an [CupertinoActionSheetAction] widget. + final Widget? cancelButton; + + Widget _buildContent(BuildContext context) { + final List content = []; + if (title != null || message != null) { + final Widget titleSection = _CupertinoAlertContentSection( + title: title, + message: message, + scrollController: messageScrollController, + titlePadding: const EdgeInsets.only( + left: _kActionSheetContentHorizontalPadding, + right: _kActionSheetContentHorizontalPadding, + bottom: _kActionSheetContentVerticalPadding, + top: _kActionSheetContentVerticalPadding, + ), + messagePadding: EdgeInsets.only( + left: _kActionSheetContentHorizontalPadding, + right: _kActionSheetContentHorizontalPadding, + bottom: title == null ? _kActionSheetContentVerticalPadding : 22.0, + top: title == null ? _kActionSheetContentVerticalPadding : 0.0, + ), + titleTextStyle: message == null + ? _kActionSheetContentStyle + : _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600), + messageTextStyle: title == null + ? _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600) + : _kActionSheetContentStyle, + additionalPaddingBetweenTitleAndMessage: const EdgeInsets.only(top: 8.0), + ); + content.add(Flexible(child: titleSection)); + } + + return Container( + color: CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: content, + ), + ); + } + + Widget _buildActions() { + if (actions == null || actions!.isEmpty) { + return Container( + height: 0.0, + ); + } + return _CupertinoAlertActionSection( + children: actions!, + scrollController: actionScrollController, + hasCancelButton: cancelButton != null, + isActionSheet: true, + ); + } + + Widget _buildCancelButton() { + final double cancelPadding = (actions != null || message != null || title != null) + ? _kActionSheetCancelButtonPadding : 0.0; + return Padding( + padding: EdgeInsets.only(top: cancelPadding), + child: _CupertinoActionSheetCancelButton( + child: cancelButton, + ), + ); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + + final List children = [ + Flexible(child: ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount), + child: _CupertinoDialogRenderWidget( + contentSection: Builder(builder: _buildContent), + actionsSection: _buildActions(), + dividerColor: _kActionSheetButtonDividerColor, + isActionSheet: true, + ), + ), + ), + ), + if (cancelButton != null) _buildCancelButton(), + ]; + + final Orientation orientation = MediaQuery.of(context).orientation; + final double actionSheetWidth; + if (orientation == Orientation.portrait) { + actionSheetWidth = MediaQuery.of(context).size.width - (_kActionSheetEdgeHorizontalPadding * 2); + } else { + actionSheetWidth = MediaQuery.of(context).size.height - (_kActionSheetEdgeHorizontalPadding * 2); + } + + return SafeArea( + child: Semantics( + namesRoute: true, + scopesRoute: true, + explicitChildNodes: true, + label: 'Alert', + child: CupertinoUserInterfaceLevel( + data: CupertinoUserInterfaceLevelData.elevated, + child: Container( + width: actionSheetWidth, + margin: const EdgeInsets.symmetric( + horizontal: _kActionSheetEdgeHorizontalPadding, + vertical: _kActionSheetEdgeVerticalPadding, + ), + child: Column( + children: children, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + ), + ), + ), + ), + ); + } +} + +/// A button typically used in a [CupertinoActionSheet]. +/// +/// See also: +/// +/// * [CupertinoActionSheet], an alert that presents the user with a set of two or +/// more choices related to the current context. +class CupertinoActionSheetAction extends StatelessWidget { + /// Creates an action for an iOS-style action sheet. + /// + /// The [child] and [onPressed] arguments must not be null. + const CupertinoActionSheetAction({ + Key? key, + required this.onPressed, + this.isDefaultAction = false, + this.isDestructiveAction = false, + required this.child, + }) : assert(child != null), + assert(onPressed != null), + super(key: key); + + /// The callback that is called when the button is tapped. + /// + /// This attribute must not be null. + final VoidCallback onPressed; + + /// Whether this action is the default choice in the action sheet. + /// + /// Default buttons have bold text. + final bool isDefaultAction; + + /// Whether this action might change or delete data. + /// + /// Destructive buttons have red text. + final bool isDestructiveAction; + + /// The widget below this widget in the tree. + /// + /// Typically a [Text] widget. + final Widget child; + + @override + Widget build(BuildContext context) { + TextStyle style = _kActionSheetActionStyle.copyWith( + color: isDestructiveAction + ? CupertinoDynamicColor.resolve(CupertinoColors.systemRed, context) + : CupertinoTheme.of(context).primaryColor, + ); + + if (isDefaultAction) { + style = style.copyWith(fontWeight: FontWeight.w600); + } + + return GestureDetector( + onTap: onPressed, + behavior: HitTestBehavior.opaque, + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: _kActionSheetButtonHeight, + ), + child: Semantics( + button: true, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 10.0, + ), + child: DefaultTextStyle( + style: style, + child: child, + textAlign: TextAlign.center, + ), + ), + ), + ), + ); + } +} + +class _CupertinoActionSheetCancelButton extends StatefulWidget { + const _CupertinoActionSheetCancelButton({ + Key? key, + this.child, + }) : super(key: key); + + final Widget? child; + + @override + _CupertinoActionSheetCancelButtonState createState() => _CupertinoActionSheetCancelButtonState(); +} + +class _CupertinoActionSheetCancelButtonState extends State<_CupertinoActionSheetCancelButton> { + bool isBeingPressed = false; + + void _onTapDown(TapDownDetails event) { + setState(() { isBeingPressed = true; }); + } + + void _onTapUp(TapUpDetails event) { + setState(() { isBeingPressed = false; }); + } + + void _onTapCancel() { + setState(() { isBeingPressed = false; }); + } + + @override + Widget build(BuildContext context) { + final Color backgroundColor = isBeingPressed + ? _kActionSheetCancelPressedColor + : CupertinoColors.secondarySystemGroupedBackground; + return GestureDetector( + excludeFromSemantics: true, + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + child: Container( + decoration: BoxDecoration( + color: CupertinoDynamicColor.resolve(backgroundColor, context), + borderRadius: BorderRadius.circular(_kCornerRadius), + ), + child: widget.child, + ), + ); + } +} + // iOS style layout policy widget for sizing an alert dialog's content section and // action button section. // @@ -339,35 +855,44 @@ class _CupertinoDialogRenderWidget extends RenderObjectWidget { Key? key, required this.contentSection, required this.actionsSection, + required this.dividerColor, + this.isActionSheet = false, }) : super(key: key); final Widget contentSection; final Widget actionsSection; + final Color dividerColor; + final bool isActionSheet; @override RenderObject createRenderObject(BuildContext context) { return _RenderCupertinoDialog( dividerThickness: _kDividerThickness / MediaQuery.of(context).devicePixelRatio, - isInAccessibilityMode: _isInAccessibilityMode(context), - dividerColor: CupertinoDynamicColor.resolve(CupertinoColors.separator, context), + isInAccessibilityMode: _isInAccessibilityMode(context) && !isActionSheet, + dividerColor: CupertinoDynamicColor.resolve(dividerColor, context), + isActionSheet: isActionSheet, ); } @override void updateRenderObject(BuildContext context, _RenderCupertinoDialog renderObject) { renderObject - ..isInAccessibilityMode = _isInAccessibilityMode(context) - ..dividerColor = CupertinoDynamicColor.resolve(CupertinoColors.separator, context); + ..isInAccessibilityMode = _isInAccessibilityMode(context) && !isActionSheet + ..dividerColor = CupertinoDynamicColor.resolve(dividerColor, context); } @override RenderObjectElement createElement() { - return _CupertinoDialogRenderElement(this); + return _CupertinoDialogRenderElement(this, allowMoveRenderObjectChild: isActionSheet); } } class _CupertinoDialogRenderElement extends RenderObjectElement { - _CupertinoDialogRenderElement(_CupertinoDialogRenderWidget widget) : super(widget); + _CupertinoDialogRenderElement(_CupertinoDialogRenderWidget widget, {this.allowMoveRenderObjectChild = false}) : super(widget); + + // Whether to allow overriden method moveRenderObjectChild call or default to super. + // CupertinoActionSheet should default to [super] but CupertinoAlertDialog not. + final bool allowMoveRenderObjectChild; Element? _contentElement; Element? _actionsElement; @@ -397,20 +922,17 @@ class _CupertinoDialogRenderElement extends RenderObjectElement { @override void insertRenderObjectChild(RenderObject child, _AlertDialogSections slot) { - assert(slot != null); - switch (slot) { - case _AlertDialogSections.contentSection: - renderObject.contentSection = child as RenderBox; - break; - case _AlertDialogSections.actionsSection: - renderObject.actionsSection = child as RenderBox; - break; - } + _placeChildInSlot(child, slot); } @override void moveRenderObjectChild(RenderObject child, _AlertDialogSections oldSlot, _AlertDialogSections newSlot) { - assert(false); + if (!allowMoveRenderObjectChild) { + super.moveRenderObjectChild(child, oldSlot, newSlot); + return; + } + + _placeChildInSlot(child, newSlot); } @override @@ -442,6 +964,18 @@ class _CupertinoDialogRenderElement extends RenderObjectElement { renderObject.actionsSection = null; } } + + void _placeChildInSlot(RenderObject child, _AlertDialogSections slot) { + assert(slot != null); + switch (slot) { + case _AlertDialogSections.contentSection: + renderObject.contentSection = child as RenderBox; + break; + case _AlertDialogSections.actionsSection: + renderObject.actionsSection = child as RenderBox; + break; + } + } } // iOS style layout policy for sizing an alert dialog's content section and action @@ -474,15 +1008,16 @@ class _RenderCupertinoDialog extends RenderBox { RenderBox? actionsSection, double dividerThickness = 0.0, bool isInAccessibilityMode = false, + bool isActionSheet = false, required Color dividerColor, }) : _contentSection = contentSection, _actionsSection = actionsSection, _dividerThickness = dividerThickness, _isInAccessibilityMode = isInAccessibilityMode, + _isActionSheet = isActionSheet, _dividerPaint = Paint() - ..color = dividerColor - ..style = PaintingStyle.fill; - + ..color = dividerColor + ..style = PaintingStyle.fill; RenderBox? get contentSection => _contentSection; RenderBox? _contentSection; @@ -521,6 +1056,15 @@ class _RenderCupertinoDialog extends RenderBox { } } + bool _isActionSheet; + bool get isActionSheet => _isActionSheet; + set isActionSheet(bool newValue) { + if (newValue != _isActionSheet) { + _isActionSheet = newValue; + markNeedsLayout(); + } + } + double get _dialogWidth => isInAccessibilityMode ? _kAccessibilityCupertinoDialogWidth : _kCupertinoDialogWidth; @@ -572,8 +1116,10 @@ class _RenderCupertinoDialog extends RenderBox { @override void setupParentData(RenderBox child) { - if (child.parentData is! BoxParentData) { + if (!isActionSheet && child.parentData is! BoxParentData) { child.parentData = BoxParentData(); + } else if (child.parentData is! MultiChildLayoutParentData) { + child.parentData = MultiChildLayoutParentData(); } } @@ -595,12 +1141,12 @@ class _RenderCupertinoDialog extends RenderBox { @override double computeMinIntrinsicWidth(double height) { - return _dialogWidth; + return isActionSheet ? constraints.minWidth : _dialogWidth; } @override double computeMaxIntrinsicWidth(double height) { - return _dialogWidth; + return isActionSheet ? constraints.maxWidth : _dialogWidth; } @override @@ -608,10 +1154,14 @@ class _RenderCupertinoDialog extends RenderBox { final double contentHeight = contentSection!.getMinIntrinsicHeight(width); final double actionsHeight = actionsSection!.getMinIntrinsicHeight(width); final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0; - final double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight; + double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight; - if (height.isFinite) + if (isActionSheet && (actionsHeight > 0 || contentHeight > 0)) { + height -= 2 * _kActionSheetEdgeVerticalPadding; + } + if (height.isFinite) { return height; + } return 0.0; } @@ -620,10 +1170,14 @@ class _RenderCupertinoDialog extends RenderBox { final double contentHeight = contentSection!.getMaxIntrinsicHeight(width); final double actionsHeight = actionsSection!.getMaxIntrinsicHeight(width); final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0; - final double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight; + double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight; - if (height.isFinite) + if (isActionSheet && (actionsHeight > 0 || contentHeight > 0)) { + height -= 2 * _kActionSheetEdgeVerticalPadding; + } + if (height.isFinite) { return height; + } return 0.0; } @@ -637,7 +1191,7 @@ class _RenderCupertinoDialog extends RenderBox { @override void performLayout() { - final _DialogSizes dialogSizes = _performLayout( + final _AlertDialogSizes dialogSizes = _performLayout( constraints: constraints, layoutChild: ChildLayoutHelper.layoutChild, ); @@ -645,31 +1199,37 @@ class _RenderCupertinoDialog extends RenderBox { // Set the position of the actions box to sit at the bottom of the dialog. // The content box defaults to the top left, which is where we want it. - assert(actionsSection!.parentData is BoxParentData); - final BoxParentData actionParentData = actionsSection!.parentData! as BoxParentData; - actionParentData.offset = Offset(0.0, dialogSizes.actionSectionYOffset); + assert((!isActionSheet && actionsSection!.parentData is BoxParentData) + || (isActionSheet && actionsSection!.parentData is MultiChildLayoutParentData)); + if (isActionSheet) { + final MultiChildLayoutParentData actionParentData = actionsSection!.parentData! as MultiChildLayoutParentData; + actionParentData.offset = Offset(0.0, dialogSizes.contentHeight + dialogSizes.dividerThickness); + } else { + final BoxParentData actionParentData = actionsSection!.parentData! as BoxParentData; + actionParentData.offset = Offset(0.0, dialogSizes.contentHeight + dialogSizes.dividerThickness); + } } - _DialogSizes _performLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) { + _AlertDialogSizes _performLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) { return isInAccessibilityMode ? performAccessibilityLayout( - constraints: constraints, - layoutChild: layoutChild, - ) : performRegularLayout( - constraints: constraints, - layoutChild: layoutChild, - ); + constraints: constraints, + layoutChild: layoutChild, + ) : performRegularLayout( + constraints: constraints, + layoutChild: layoutChild, + ); } // When not in accessibility mode, an alert dialog might reduce the space // for buttons to just over 1 button's height to make room for the content // section. - _DialogSizes performRegularLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) { - final bool hasDivider = contentSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0 - && actionsSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0; + _AlertDialogSizes performRegularLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) { + final bool hasDivider = contentSection!.getMaxIntrinsicHeight(computeMaxIntrinsicWidth(0)) > 0.0 + && actionsSection!.getMaxIntrinsicHeight(computeMaxIntrinsicWidth(0)) > 0.0; final double dividerThickness = hasDivider ? _dividerThickness : 0.0; - final double minActionsHeight = actionsSection!.getMinIntrinsicHeight(_dialogWidth); + final double minActionsHeight = actionsSection!.getMinIntrinsicHeight(computeMaxIntrinsicWidth(0)); final Size contentSize = layoutChild( contentSection!, @@ -683,15 +1243,18 @@ class _RenderCupertinoDialog extends RenderBox { final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height; - return _DialogSizes( - size: constraints.constrain(Size(_dialogWidth, dialogHeight)), - actionSectionYOffset: contentSize.height + dividerThickness, + return _AlertDialogSizes( + size: isActionSheet + ? Size(constraints.maxWidth, dialogHeight) + : constraints.constrain(Size(_dialogWidth, dialogHeight)), + contentHeight: contentSize.height, + dividerThickness: dividerThickness, ); } // When in accessibility mode, an alert dialog will allow buttons to take // up to 50% of the dialog height, even if the content exceeds available space. - _DialogSizes performAccessibilityLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) { + _AlertDialogSizes performAccessibilityLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) { final bool hasDivider = contentSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0 && actionsSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0; final double dividerThickness = hasDivider ? _dividerThickness : 0.0; @@ -702,10 +1265,10 @@ class _RenderCupertinoDialog extends RenderBox { final Size contentSize; final Size actionsSize; if (maxContentHeight + dividerThickness + maxActionsHeight > constraints.maxHeight) { - // There isn't enough room for everything. Following iOS's accessibility dialog - // layout policy, first we allow the actions to take up to 50% of the dialog - // height. Second we fill the rest of the available space with the content - // section. + // AlertDialog: There isn't enough room for everything. Following iOS's + // accessibility dialog layout policy, first we allow the actions to take + // up to 50% of the dialog height. Second we fill the rest of the + // available space with the content section. actionsSize = layoutChild( actionsSection!, @@ -733,24 +1296,35 @@ class _RenderCupertinoDialog extends RenderBox { // Calculate overall dialog height. final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height; - return _DialogSizes( + return _AlertDialogSizes( size: constraints.constrain(Size(_dialogWidth, dialogHeight)), - actionSectionYOffset: contentSize.height + dividerThickness, + contentHeight: contentSize.height, + dividerThickness: dividerThickness, ); } @override void paint(PaintingContext context, Offset offset) { - final BoxParentData contentParentData = contentSection!.parentData! as BoxParentData; - contentSection!.paint(context, offset + contentParentData.offset); + if (isActionSheet) { + final MultiChildLayoutParentData contentParentData = contentSection!.parentData! as MultiChildLayoutParentData; + contentSection!.paint(context, offset + contentParentData.offset); + } else { + final BoxParentData contentParentData = contentSection!.parentData! as BoxParentData; + contentSection!.paint(context, offset + contentParentData.offset); + } final bool hasDivider = contentSection!.size.height > 0.0 && actionsSection!.size.height > 0.0; if (hasDivider) { _paintDividerBetweenContentAndActions(context.canvas, offset); } - final BoxParentData actionsParentData = actionsSection!.parentData! as BoxParentData; - actionsSection!.paint(context, offset + actionsParentData.offset); + if (isActionSheet) { + final MultiChildLayoutParentData actionsParentData = actionsSection!.parentData! as MultiChildLayoutParentData; + actionsSection!.paint(context, offset + actionsParentData.offset); + } else { + final BoxParentData actionsParentData = actionsSection!.parentData! as BoxParentData; + actionsSection!.paint(context, offset + actionsParentData.offset); + } } void _paintDividerBetweenContentAndActions(Canvas canvas, Offset offset) { @@ -767,32 +1341,58 @@ class _RenderCupertinoDialog extends RenderBox { @override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { + if (isActionSheet) { + final MultiChildLayoutParentData contentSectionParentData = contentSection!.parentData! as MultiChildLayoutParentData; + final MultiChildLayoutParentData actionsSectionParentData = actionsSection!.parentData! as MultiChildLayoutParentData; + return result.addWithPaintOffset( + offset: contentSectionParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - contentSectionParentData.offset); + return contentSection!.hitTest(result, position: transformed); + }, + ) || + result.addWithPaintOffset( + offset: actionsSectionParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - actionsSectionParentData.offset); + return actionsSection!.hitTest(result, position: transformed); + }, + ); + } + final BoxParentData contentSectionParentData = contentSection!.parentData! as BoxParentData; final BoxParentData actionsSectionParentData = actionsSection!.parentData! as BoxParentData; return result.addWithPaintOffset( - offset: contentSectionParentData.offset, - position: position, - hitTest: (BoxHitTestResult result, Offset transformed) { - assert(transformed == position - contentSectionParentData.offset); - return contentSection!.hitTest(result, position: transformed); - }, - ) - || result.addWithPaintOffset( - offset: actionsSectionParentData.offset, - position: position, - hitTest: (BoxHitTestResult result, Offset transformed) { - assert(transformed == position - actionsSectionParentData.offset); - return actionsSection!.hitTest(result, position: transformed); - }, - ); + offset: contentSectionParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - contentSectionParentData.offset); + return contentSection!.hitTest(result, position: transformed); + }, + ) || + result.addWithPaintOffset( + offset: actionsSectionParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - actionsSectionParentData.offset); + return actionsSection!.hitTest(result, position: transformed); + }, + ); } } -class _DialogSizes { - const _DialogSizes({required this.size, required this.actionSectionYOffset}); +class _AlertDialogSizes { + const _AlertDialogSizes({ + required this.size, + required this.contentHeight, + required this.dividerThickness, + }); final Size size; - final double actionSectionYOffset; + final double contentHeight; + final double dividerThickness; } // Visual components of an alert dialog that need to be explicitly sized and @@ -811,9 +1411,16 @@ class _CupertinoAlertContentSection extends StatelessWidget { const _CupertinoAlertContentSection({ Key? key, this.title, - this.content, + this.message, this.scrollController, - }) : super(key: key); + this.titlePadding, + this.messagePadding, + this.titleTextStyle, + this.messageTextStyle, + this.additionalPaddingBetweenTitleAndMessage, + }) : assert(title == null || titlePadding != null && titleTextStyle != null), + assert(message == null || messagePadding != null && messageTextStyle != null), + super(key: key); // The (optional) title of the dialog is displayed in a large font at the top // of the dialog. @@ -821,11 +1428,11 @@ class _CupertinoAlertContentSection extends StatelessWidget { // Typically a Text widget. final Widget? title; - // The (optional) content of the dialog is displayed in the center of the + // The (optional) message of the dialog is displayed in the center of the // dialog in a lighter font. // // Typically a Text widget. - final Widget? content; + final Widget? message; // A scroll controller that can be used to control the scrolling of the // content in the dialog. @@ -834,51 +1441,55 @@ class _CupertinoAlertContentSection extends StatelessWidget { // are short. final ScrollController? scrollController; + // Paddings used around title and message. + // CupertinoAlertDialog and CupertinoActionSheet have different paddings. + final EdgeInsets? titlePadding; + final EdgeInsets? messagePadding; + + // Additional padding to be inserted between title and message. + // Only used for CupertinoActionSheet. + final EdgeInsets? additionalPaddingBetweenTitleAndMessage; + + // Text styles used for title and message. + // CupertinoAlertDialog and CupertinoActionSheet have different text styles. + final TextStyle? titleTextStyle; + final TextStyle? messageTextStyle; + @override Widget build(BuildContext context) { - if (title == null && content == null) { + if (title == null && message == null) { return SingleChildScrollView( controller: scrollController, child: const SizedBox(width: 0.0, height: 0.0), ); } - final double textScaleFactor = MediaQuery.of(context).textScaleFactor; final List titleContentGroup = [ if (title != null) Padding( - padding: EdgeInsets.only( - left: _kEdgePadding, - right: _kEdgePadding, - bottom: content == null ? _kEdgePadding : 1.0, - top: _kEdgePadding * textScaleFactor, - ), + padding: titlePadding!, child: DefaultTextStyle( - style: _kCupertinoDialogTitleStyle.copyWith( - color: CupertinoDynamicColor.resolve(CupertinoColors.label, context), - ), + style: titleTextStyle!, textAlign: TextAlign.center, child: title!, ), ), - if (content != null) + if (message != null) Padding( - padding: EdgeInsets.only( - left: _kEdgePadding, - right: _kEdgePadding, - bottom: _kEdgePadding * textScaleFactor, - top: title == null ? _kEdgePadding : 1.0, - ), + padding: messagePadding!, child: DefaultTextStyle( - style: _kCupertinoDialogContentStyle.copyWith( - color: CupertinoDynamicColor.resolve(CupertinoColors.label, context), - ), + style: messageTextStyle!, textAlign: TextAlign.center, - child: content!, + child: message!, ), ), ]; + // Add padding between the widgets if necessary. + if (additionalPaddingBetweenTitleAndMessage != null && titleContentGroup.length > 1) { + titleContentGroup.insert(1, Padding(padding: additionalPaddingBetweenTitleAndMessage!)); + } + return CupertinoScrollbar( child: SingleChildScrollView( controller: scrollController, @@ -901,6 +1512,8 @@ class _CupertinoAlertActionSection extends StatefulWidget { Key? key, required this.children, this.scrollController, + this.hasCancelButton = false, + this.isActionSheet = false, }) : assert(children != null), super(key: key); @@ -913,11 +1526,21 @@ class _CupertinoAlertActionSection extends StatefulWidget { // don't have many actions. final ScrollController? scrollController; + // Used in ActionSheet to denote if ActionSheet has a separate so-called + // cancel button. + // + // Defaults to false, and is not needed in dialogs. + final bool hasCancelButton; + + final bool isActionSheet; + @override - _CupertinoAlertActionSectionState createState() => _CupertinoAlertActionSectionState(); + _CupertinoAlertActionSectionState createState() => + _CupertinoAlertActionSectionState(); } -class _CupertinoAlertActionSectionState extends State<_CupertinoAlertActionSection> { +class _CupertinoAlertActionSectionState + extends State<_CupertinoAlertActionSection> { @override Widget build(BuildContext context) { final double devicePixelRatio = MediaQuery.of(context).devicePixelRatio; @@ -937,6 +1560,8 @@ class _CupertinoAlertActionSectionState extends State<_CupertinoAlertActionSecti child: _CupertinoDialogActionsRenderWidget( actionButtons: interactiveButtons, dividerThickness: _kDividerThickness / devicePixelRatio, + hasCancelButton: widget.hasCancelButton, + isActionSheet: widget.isActionSheet, ), ), ); @@ -995,7 +1620,8 @@ class _PressableActionButtonState extends State<_PressableActionButton> { // _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for // updating the pressed state of an _ActionButtonParentData based on the // incoming [isPressed] property. -class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParentData> { +class _ActionButtonParentDataWidget + extends ParentDataWidget<_ActionButtonParentData> { const _ActionButtonParentDataWidget({ Key? key, required this.isPressed, @@ -1007,7 +1633,8 @@ class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParent @override void applyParentData(RenderObject renderObject) { assert(renderObject.parentData is _ActionButtonParentData); - final _ActionButtonParentData parentData = renderObject.parentData! as _ActionButtonParentData; + final _ActionButtonParentData parentData = + renderObject.parentData! as _ActionButtonParentData; if (parentData.isPressed != isPressed) { parentData.isPressed = isPressed; @@ -1019,7 +1646,8 @@ class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParent } @override - Type get debugTypicalAncestorWidgetClass => _CupertinoDialogActionsRenderWidget; + Type get debugTypicalAncestorWidgetClass => + _CupertinoDialogActionsRenderWidget; } // ParentData applied to individual action buttons that report whether or not @@ -1115,7 +1743,7 @@ class CupertinoDialogAction extends StatelessWidget { // iOS scale factor) vs the minimum text size that we allow in action // buttons. This ratio information is used to automatically scale down action // button text to fit the available space. - final double fontSizeRatio = (textScaleFactor * textStyle.fontSize!) / _kMinButtonFontSize; + final double fontSizeRatio = (textScaleFactor * textStyle.fontSize!) / _kDialogMinButtonFontSize; final double padding = _calculatePadding(context); return IntrinsicHeight( @@ -1162,7 +1790,7 @@ class CupertinoDialogAction extends StatelessWidget { Widget build(BuildContext context) { TextStyle style = _kCupertinoDialogActionStyle.copyWith( color: CupertinoDynamicColor.resolve( - isDestructiveAction ? CupertinoColors.systemRed : CupertinoColors.systemBlue, + isDestructiveAction ? CupertinoColors.systemRed : CupertinoColors.systemBlue, context, ), ); @@ -1183,15 +1811,15 @@ class CupertinoDialogAction extends StatelessWidget { // wrap instead of ellipsizing. We are consciously not implementing that // now due to complexity. final Widget sizedContent = _isInAccessibilityMode(context) - ? _buildContentWithAccessibilitySizingPolicy( - textStyle: style, - content: child, - ) - : _buildContentWithRegularSizingPolicy( - context: context, - textStyle: style, - content: child, - ); + ? _buildContentWithAccessibilitySizingPolicy( + textStyle: style, + content: child, + ) + : _buildContentWithRegularSizingPolicy( + context: context, + textStyle: style, + content: child, + ); return GestureDetector( excludeFromSemantics: true, @@ -1199,7 +1827,7 @@ class CupertinoDialogAction extends StatelessWidget { behavior: HitTestBehavior.opaque, child: ConstrainedBox( constraints: const BoxConstraints( - minHeight: _kMinButtonHeight, + minHeight: _kDialogMinButtonHeight, ), child: Container( alignment: Alignment.center, @@ -1223,34 +1851,49 @@ class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget { Key? key, required List actionButtons, double dividerThickness = 0.0, + bool hasCancelButton = false, + bool isActionSheet = false, }) : _dividerThickness = dividerThickness, + _hasCancelButton = hasCancelButton, + _isActionSheet = isActionSheet, super(key: key, children: actionButtons); final double _dividerThickness; + final bool _hasCancelButton; + final bool _isActionSheet; @override RenderObject createRenderObject(BuildContext context) { return _RenderCupertinoDialogActions( - dialogWidth: _isInAccessibilityMode(context) - ? _kAccessibilityCupertinoDialogWidth - : _kCupertinoDialogWidth, + dialogWidth: _isActionSheet + ? null + : _isInAccessibilityMode(context) + ? _kAccessibilityCupertinoDialogWidth + : _kCupertinoDialogWidth, dividerThickness: _dividerThickness, - dialogColor: CupertinoDynamicColor.resolve(_kDialogColor, context), - dialogPressedColor: CupertinoDynamicColor.resolve(_kDialogPressedColor, context), - dividerColor: CupertinoDynamicColor.resolve(CupertinoColors.separator, context), + dialogColor: CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetBackgroundColor : _kDialogColor, context), + dialogPressedColor: CupertinoDynamicColor.resolve(_kPressedColor, context), + dividerColor: CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetButtonDividerColor : CupertinoColors.separator, context), + hasCancelButton: _hasCancelButton, + isActionSheet: _isActionSheet, ); } @override - void updateRenderObject(BuildContext context, _RenderCupertinoDialogActions renderObject) { + void updateRenderObject( + BuildContext context, _RenderCupertinoDialogActions renderObject) { renderObject - ..dialogWidth = _isInAccessibilityMode(context) - ? _kAccessibilityCupertinoDialogWidth - : _kCupertinoDialogWidth + ..dialogWidth = _isActionSheet + ? null + : _isInAccessibilityMode(context) + ? _kAccessibilityCupertinoDialogWidth + : _kCupertinoDialogWidth ..dividerThickness = _dividerThickness - ..dialogColor = CupertinoDynamicColor.resolve(_kDialogColor, context) - ..dialogPressedColor = CupertinoDynamicColor.resolve(_kDialogPressedColor, context) - ..dividerColor = CupertinoDynamicColor.resolve(CupertinoColors.separator, context); + ..dialogColor = CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetBackgroundColor : _kDialogColor, context) + ..dialogPressedColor = CupertinoDynamicColor.resolve(_kPressedColor, context) + ..dividerColor = CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetButtonDividerColor : CupertinoColors.separator, context) + ..hasCancelButton = _hasCancelButton + ..isActionSheet = _isActionSheet; } } @@ -1293,28 +1936,33 @@ class _RenderCupertinoDialogActions extends RenderBox RenderBoxContainerDefaultsMixin { _RenderCupertinoDialogActions({ List? children, - required double dialogWidth, + double? dialogWidth, double dividerThickness = 0.0, required Color dialogColor, required Color dialogPressedColor, required Color dividerColor, - }) : _dialogWidth = dialogWidth, + bool hasCancelButton = false, + bool isActionSheet = false, + }) : assert(isActionSheet || dialogWidth != null), + _dialogWidth = dialogWidth, _buttonBackgroundPaint = Paint() - ..color = dialogColor - ..style = PaintingStyle.fill, - _pressedButtonBackgroundPaint = Paint() - ..color = dialogPressedColor - ..style = PaintingStyle.fill, - _dividerPaint = Paint() - ..color = dividerColor - ..style = PaintingStyle.fill, - _dividerThickness = dividerThickness { + ..color = dialogColor + ..style = PaintingStyle.fill, + _pressedButtonBackgroundPaint = Paint() + ..color = dialogPressedColor + ..style = PaintingStyle.fill, + _dividerPaint = Paint() + ..color = dividerColor + ..style = PaintingStyle.fill, + _dividerThickness = dividerThickness, + _hasCancelButton = hasCancelButton, + _isActionSheet = isActionSheet { addAll(children); } - double get dialogWidth => _dialogWidth; - double _dialogWidth; - set dialogWidth(double newWidth) { + double? get dialogWidth => _dialogWidth; + double? _dialogWidth; + set dialogWidth(double? newWidth) { if (newWidth != _dialogWidth) { _dialogWidth = newWidth; markNeedsLayout(); @@ -1331,6 +1979,16 @@ class _RenderCupertinoDialogActions extends RenderBox } } + bool _hasCancelButton; + bool get hasCancelButton => _hasCancelButton; + set hasCancelButton(bool newValue) { + if (newValue == _hasCancelButton) + return; + + _hasCancelButton = newValue; + markNeedsLayout(); + } + final Paint _buttonBackgroundPaint; set dialogColor(Color value) { if (value == _buttonBackgroundPaint.color) @@ -1358,6 +2016,16 @@ class _RenderCupertinoDialogActions extends RenderBox markNeedsPaint(); } + bool _isActionSheet; + bool get isActionSheet => _isActionSheet; + set isActionSheet(bool value) { + if (value == _isActionSheet) + return; + + _isActionSheet = value; + markNeedsPaint(); + } + Iterable get _pressedButtons sync* { RenderBox? currentChild = firstChild; while (currentChild != null) { @@ -1391,33 +2059,49 @@ class _RenderCupertinoDialogActions extends RenderBox @override double computeMinIntrinsicWidth(double height) { - return dialogWidth; + return isActionSheet ? constraints.minWidth : dialogWidth!; } @override double computeMaxIntrinsicWidth(double height) { - return dialogWidth; + return isActionSheet ? constraints.maxWidth : dialogWidth!; } @override double computeMinIntrinsicHeight(double width) { - final double minHeight; if (childCount == 0) { - minHeight = 0.0; + return 0.0; + } else if (isActionSheet) { + if (childCount == 1) + return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness; + if (hasCancelButton && childCount < 4) + return _computeMinIntrinsicHeightWithCancel(width); + return _computeMinIntrinsicHeightStacked(width); } else if (childCount == 1) { // If only 1 button, display the button across the entire dialog. - minHeight = _computeMinIntrinsicHeightSideBySide(width); - } else { - if (childCount == 2 && _isSingleButtonRow(width)) { - // The first 2 buttons fit side-by-side. Display them horizontally. - minHeight = _computeMinIntrinsicHeightSideBySide(width); - } else { - // 3+ buttons are always stacked. The minimum height when stacked is - // 1.5 buttons tall. - minHeight = _computeMinIntrinsicHeightStacked(width); - } + return _computeMinIntrinsicHeightSideBySide(width); + } else if (childCount == 2 && _isSingleButtonRow(width)) { + // The first 2 buttons fit side-by-side. Display them horizontally. + return _computeMinIntrinsicHeightSideBySide(width); } - return minHeight; + // 3+ buttons are always stacked. The minimum height when stacked is + // 1.5 buttons tall. + return _computeMinIntrinsicHeightStacked(width); + } + + // The minimum height for more than 2-3 buttons when a cancel button is + // included is the full height of button stack. + double _computeMinIntrinsicHeightWithCancel(double width) { + assert(childCount == 2 || childCount == 3); + if (childCount == 2) { + return firstChild!.getMinIntrinsicHeight(width) + + childAfter(firstChild!)!.getMinIntrinsicHeight(width) + + dividerThickness; + } + return firstChild!.getMinIntrinsicHeight(width) + + childAfter(firstChild!)!.getMinIntrinsicHeight(width) + + childAfter(childAfter(firstChild!)!)!.getMinIntrinsicHeight(width) + + (dividerThickness * 2); } // The minimum height for a single row of buttons is the larger of the buttons' @@ -1438,46 +2122,51 @@ class _RenderCupertinoDialogActions extends RenderBox return minHeight; } - // The minimum height for 2+ stacked buttons is the height of the 1st button - // + 50% the height of the 2nd button + the divider between the two. + // Dialog: The minimum height for 2+ stacked buttons is the height of the 1st + // button + 50% the height of the 2nd button + the divider between the two. + // + // ActionSheet: The minimum height for more than 2 buttons when no cancel + // button or 4+ buttons when a cancel button is included is the height of the + // 1st button + 50% the height of the 2nd button + 2 dividers. double _computeMinIntrinsicHeightStacked(double width) { assert(childCount >= 2); return firstChild!.getMinIntrinsicHeight(width) - + dividerThickness - + (0.5 * childAfter(firstChild!)!.getMinIntrinsicHeight(width)); + + dividerThickness + + (0.5 * childAfter(firstChild!)!.getMinIntrinsicHeight(width)); } @override double computeMaxIntrinsicHeight(double width) { - final double maxHeight; if (childCount == 0) { // No buttons. Zero height. - maxHeight = 0.0; + return 0.0; + } else if (isActionSheet) { + if (childCount == 1) + return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness; + return _computeMaxIntrinsicHeightStacked(width); } else if (childCount == 1) { // One button. Our max intrinsic height is equal to the button's. - maxHeight = firstChild!.getMaxIntrinsicHeight(width); + return firstChild!.getMaxIntrinsicHeight(width); } else if (childCount == 2) { // Two buttons... if (_isSingleButtonRow(width)) { // The 2 buttons fit side by side so our max intrinsic height is equal // to the taller of the 2 buttons. final double perButtonWidth = (width - dividerThickness) / 2.0; - maxHeight = math.max( + return math.max( firstChild!.getMaxIntrinsicHeight(perButtonWidth), lastChild!.getMaxIntrinsicHeight(perButtonWidth), ); } else { // The 2 buttons do not fit side by side. Measure total height as a // vertical stack. - maxHeight = _computeMaxIntrinsicHeightStacked(width); + return _computeMaxIntrinsicHeightStacked(width); } - } else { - // Three+ buttons. Stack the buttons vertically with dividers and measure - // the overall height. - maxHeight = _computeMaxIntrinsicHeightStacked(width); } - return maxHeight; + // Three+ buttons. Stack the buttons vertically with dividers and measure + // the overall height. + return _computeMaxIntrinsicHeightStacked(width); } // Max height of a stack of buttons is the sum of all button heights + a @@ -1514,20 +2203,20 @@ class _RenderCupertinoDialogActions extends RenderBox @override Size computeDryLayout(BoxConstraints constraints) { - return _computeLayout(constraints: constraints, dry: true); + return _performLayout(constraints: constraints, dry: true); } @override void performLayout() { - size = _computeLayout(constraints: constraints, dry: false); + size = _performLayout(constraints: constraints, dry: false); } - Size _computeLayout({required BoxConstraints constraints, bool dry = false}) { + Size _performLayout({required BoxConstraints constraints, bool dry = false}) { final ChildLayouter layoutChild = dry ? ChildLayoutHelper.dryLayoutChild : ChildLayoutHelper.layoutChild; - if (_isSingleButtonRow(dialogWidth)) { + if (!isActionSheet && _isSingleButtonRow(dialogWidth!)) { if (childCount == 1) { // We have 1 button. Our size is the width of the dialog and the height // of the single button. @@ -1537,7 +2226,7 @@ class _RenderCupertinoDialogActions extends RenderBox ); return constraints.constrain( - Size(dialogWidth, childSize.height), + Size(dialogWidth!, childSize.height) ); } else { // Each button gets half the available width, minus a single divider. @@ -1568,7 +2257,7 @@ class _RenderCupertinoDialogActions extends RenderBox // Calculate our size based on the button sizes. return constraints.constrain( Size( - dialogWidth, + dialogWidth!, math.max( firstChildSize.height, lastChildSize.height, @@ -1609,7 +2298,7 @@ class _RenderCupertinoDialogActions extends RenderBox // Our height is the accumulated height of all buttons and dividers. return constraints.constrain( - Size(dialogWidth, verticalOffset), + Size(computeMaxIntrinsicWidth(0), verticalOffset), ); } } @@ -1618,7 +2307,7 @@ class _RenderCupertinoDialogActions extends RenderBox void paint(PaintingContext context, Offset offset) { final Canvas canvas = context.canvas; - if (_isSingleButtonRow(size.width)) { + if (!isActionSheet && _isSingleButtonRow(size.width)) { _drawButtonBackgroundsAndDividersSingleRow(canvas, offset); } else { _drawButtonBackgroundsAndDividersStacked(canvas, offset); @@ -1632,16 +2321,16 @@ class _RenderCupertinoDialogActions extends RenderBox // the dialog has 2 buttons). The vertical divider is hidden if either the // left or right button is pressed. final Rect verticalDivider = childCount == 2 && !_isButtonPressed - ? Rect.fromLTWH( - offset.dx + firstChild!.size.width, - offset.dy, - dividerThickness, - math.max( - firstChild!.size.height, - lastChild!.size.height, - ), - ) - : Rect.zero; + ? Rect.fromLTWH( + offset.dx + firstChild!.size.width, + offset.dy, + dividerThickness, + math.max( + firstChild!.size.height, + lastChild!.size.height, + ), + ) + : Rect.zero; final List pressedButtonRects = _pressedButtons.map((RenderBox pressedButton) { final MultiChildLayoutParentData buttonParentData = pressedButton.parentData! as MultiChildLayoutParentData; @@ -1770,7 +2459,7 @@ class _RenderCupertinoDialogActions extends RenderBox } @override - bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { return defaultHitTestChildren(result, position: position); } -} +} \ No newline at end of file