Unverified Commit 24258141 authored by Marcel Čampa's avatar Marcel Čampa Committed by GitHub

Dedup CupertinoAlertDialog and CupertinoActionSheet source code (#78202)

parent 0efb28d7
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
library cupertino; library cupertino;
export 'src/cupertino/action_sheet.dart';
export 'src/cupertino/activity_indicator.dart'; export 'src/cupertino/activity_indicator.dart';
export 'src/cupertino/app.dart'; export 'src/cupertino/app.dart';
export 'src/cupertino/bottom_tab_bar.dart'; export 'src/cupertino/bottom_tab_bar.dart';
......
// 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<MyStatefulWidget> {
/// @override
/// Widget build(BuildContext context) {
/// return CupertinoPageScaffold(
/// child: Center(
/// child: CupertinoButton(
/// onPressed: () {
/// showCupertinoModalPopup<void>(
/// context: context,
/// builder: (BuildContext context) => CupertinoActionSheet(
/// title: const Text('Title'),
/// message: const Text('Message'),
/// actions: <CupertinoActionSheetAction>[
/// 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.
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
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<Widget>? 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<Widget> content = <Widget>[];
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<Widget> children = <Widget>[
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<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
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<Widget> titleContentGroup = <Widget>[];
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<Widget> 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<Widget> interactiveButtons = <Widget>[];
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<Widget> 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<RenderBox, MultiChildLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
_RenderCupertinoAlertActions({
List<RenderBox>? 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);
}
}
...@@ -13,6 +13,7 @@ import 'colors.dart'; ...@@ -13,6 +13,7 @@ import 'colors.dart';
import 'interface_level.dart'; import 'interface_level.dart';
import 'localizations.dart'; import 'localizations.dart';
import 'scrollbar.dart'; import 'scrollbar.dart';
import 'theme.dart';
// TODO(abarth): These constants probably belong somewhere more general. // TODO(abarth): These constants probably belong somewhere more general.
...@@ -49,18 +50,46 @@ const TextStyle _kCupertinoDialogActionStyle = TextStyle( ...@@ -49,18 +50,46 @@ const TextStyle _kCupertinoDialogActionStyle = TextStyle(
textBaseline: TextBaseline.alphabetic, 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 // 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 // used when the device is in accessibility mode. Each of these widths are
// listed below. // listed below.
const double _kCupertinoDialogWidth = 270.0; const double _kCupertinoDialogWidth = 270.0;
const double _kAccessibilityCupertinoDialogWidth = 310.0; const double _kAccessibilityCupertinoDialogWidth = 310.0;
const double _kDialogEdgePadding = 20.0;
const double _kBlurAmount = 20.0; const double _kDialogMinButtonHeight = 45.0;
const double _kEdgePadding = 20.0; const double _kDialogMinButtonFontSize = 10.0;
const double _kMinButtonHeight = 45.0;
const double _kMinButtonFontSize = 10.0; // ActionSheet specific constants.
const double _kDialogCornerRadius = 14.0; const double _kActionSheetEdgeHorizontalPadding = 8.0;
const double _kDividerThickness = 1.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 // A translucent color that is painted on top of the blurred backdrop as the
// dialog's background color // dialog's background color
...@@ -73,11 +102,36 @@ const Color _kDialogColor = CupertinoDynamicColor.withBrightness( ...@@ -73,11 +102,36 @@ const Color _kDialogColor = CupertinoDynamicColor.withBrightness(
// Translucent light gray that is painted on top of the blurred backdrop as the // Translucent light gray that is painted on top of the blurred backdrop as the
// background color of a pressed button. // background color of a pressed button.
// Eyeballed from iOS 13 beta simulator. // Eyeballed from iOS 13 beta simulator.
const Color _kDialogPressedColor = CupertinoDynamicColor.withBrightness( const Color _kPressedColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFFE1E1E1), color: Color(0xFFE1E1E1),
darkColor: Color(0xFF2E2E2E), 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 // 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 // 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 // many "regular" font sizes and many "large" font sizes. But depending on which
...@@ -101,6 +155,8 @@ bool _isInAccessibilityMode(BuildContext context) { ...@@ -101,6 +155,8 @@ bool _isInAccessibilityMode(BuildContext context) {
/// An iOS-style alert dialog. /// 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 /// An alert dialog informs the user about situations that require
/// acknowledgement. An alert dialog has an optional title, optional content, /// acknowledgement. An alert dialog has an optional title, optional content,
/// and an optional list of actions. The title is displayed above the content /// and an optional list of actions. The title is displayed above the content
...@@ -117,6 +173,58 @@ bool _isInAccessibilityMode(BuildContext context) { ...@@ -117,6 +173,58 @@ bool _isInAccessibilityMode(BuildContext context) {
/// Typically passed as the child widget to [showDialog], which displays the /// Typically passed as the child widget to [showDialog], which displays the
/// dialog. /// 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<MyStatefulWidget> {
/// @override
/// Widget build(BuildContext context) {
/// return CupertinoPageScaffold(
/// child: Center(
/// child: CupertinoButton(
/// onPressed: () {
/// showCupertinoDialog<void>(
/// context: context,
/// builder: (BuildContext context) => CupertinoAlertDialog(
/// title: const Text('Alert'),
/// content: const Text('Proceed with destructive action?'),
/// actions: <CupertinoDialogAction>[
/// 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: /// See also:
/// ///
/// * [CupertinoPopupSurface], which is a generic iOS-style popup surface that /// * [CupertinoPopupSurface], which is a generic iOS-style popup surface that
...@@ -188,14 +296,34 @@ class CupertinoAlertDialog extends StatelessWidget { ...@@ -188,14 +296,34 @@ class CupertinoAlertDialog extends StatelessWidget {
final Curve insetAnimationCurve; final Curve insetAnimationCurve;
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
final List<Widget> children = <Widget>[ final List<Widget> children = <Widget>[
if (title != null || content != null) if (title != null || content != null)
Flexible( Flexible(
flex: 3, flex: 3,
child: _CupertinoAlertContentSection( child: _CupertinoAlertContentSection(
title: title, title: title,
content: content, message: content,
scrollController: scrollController, 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 { ...@@ -218,6 +346,7 @@ class CupertinoAlertDialog extends StatelessWidget {
actionSection = _CupertinoAlertActionSection( actionSection = _CupertinoAlertActionSection(
children: actions, children: actions,
scrollController: actionScrollController, scrollController: actionScrollController,
isActionSheet: false,
); );
} }
...@@ -251,10 +380,10 @@ class CupertinoAlertDialog extends StatelessWidget { ...@@ -251,10 +380,10 @@ class CupertinoAlertDialog extends StatelessWidget {
context: context, context: context,
child: Center( child: Center(
child: Container( child: Container(
margin: const EdgeInsets.symmetric(vertical: _kEdgePadding), margin: const EdgeInsets.symmetric(vertical: _kDialogEdgePadding),
width: isInAccessibilityMode width: isInAccessibilityMode
? _kAccessibilityCupertinoDialogWidth ? _kAccessibilityCupertinoDialogWidth
: _kCupertinoDialogWidth, : _kCupertinoDialogWidth,
child: CupertinoPopupSurface( child: CupertinoPopupSurface(
isSurfacePainted: false, isSurfacePainted: false,
child: Semantics( child: Semantics(
...@@ -265,6 +394,7 @@ class CupertinoAlertDialog extends StatelessWidget { ...@@ -265,6 +394,7 @@ class CupertinoAlertDialog extends StatelessWidget {
child: _CupertinoDialogRenderWidget( child: _CupertinoDialogRenderWidget(
contentSection: _buildContent(context), contentSection: _buildContent(context),
actionsSection: _buildActions(), actionsSection: _buildActions(),
dividerColor: CupertinoColors.separator,
), ),
), ),
), ),
...@@ -318,7 +448,7 @@ class CupertinoPopupSurface extends StatelessWidget { ...@@ -318,7 +448,7 @@ class CupertinoPopupSurface extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(_kDialogCornerRadius), borderRadius: BorderRadius.circular(_kCornerRadius),
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount), filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount),
child: Container( child: Container(
...@@ -330,6 +460,392 @@ class CupertinoPopupSurface extends StatelessWidget { ...@@ -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<MyStatefulWidget> {
/// @override
/// Widget build(BuildContext context) {
/// return CupertinoPageScaffold(
/// child: Center(
/// child: CupertinoButton(
/// onPressed: () {
/// showCupertinoModalPopup<void>(
/// context: context,
/// builder: (BuildContext context) => CupertinoActionSheet(
/// title: const Text('Title'),
/// message: const Text('Message'),
/// actions: <CupertinoActionSheetAction>[
/// 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.
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
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<Widget>? 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<Widget> content = <Widget>[];
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<Widget> children = <Widget>[
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 // iOS style layout policy widget for sizing an alert dialog's content section and
// action button section. // action button section.
// //
...@@ -339,35 +855,44 @@ class _CupertinoDialogRenderWidget extends RenderObjectWidget { ...@@ -339,35 +855,44 @@ class _CupertinoDialogRenderWidget extends RenderObjectWidget {
Key? key, Key? key,
required this.contentSection, required this.contentSection,
required this.actionsSection, required this.actionsSection,
required this.dividerColor,
this.isActionSheet = false,
}) : super(key: key); }) : super(key: key);
final Widget contentSection; final Widget contentSection;
final Widget actionsSection; final Widget actionsSection;
final Color dividerColor;
final bool isActionSheet;
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
return _RenderCupertinoDialog( return _RenderCupertinoDialog(
dividerThickness: _kDividerThickness / MediaQuery.of(context).devicePixelRatio, dividerThickness: _kDividerThickness / MediaQuery.of(context).devicePixelRatio,
isInAccessibilityMode: _isInAccessibilityMode(context), isInAccessibilityMode: _isInAccessibilityMode(context) && !isActionSheet,
dividerColor: CupertinoDynamicColor.resolve(CupertinoColors.separator, context), dividerColor: CupertinoDynamicColor.resolve(dividerColor, context),
isActionSheet: isActionSheet,
); );
} }
@override @override
void updateRenderObject(BuildContext context, _RenderCupertinoDialog renderObject) { void updateRenderObject(BuildContext context, _RenderCupertinoDialog renderObject) {
renderObject renderObject
..isInAccessibilityMode = _isInAccessibilityMode(context) ..isInAccessibilityMode = _isInAccessibilityMode(context) && !isActionSheet
..dividerColor = CupertinoDynamicColor.resolve(CupertinoColors.separator, context); ..dividerColor = CupertinoDynamicColor.resolve(dividerColor, context);
} }
@override @override
RenderObjectElement createElement() { RenderObjectElement createElement() {
return _CupertinoDialogRenderElement(this); return _CupertinoDialogRenderElement(this, allowMoveRenderObjectChild: isActionSheet);
} }
} }
class _CupertinoDialogRenderElement extends RenderObjectElement { 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? _contentElement;
Element? _actionsElement; Element? _actionsElement;
...@@ -397,20 +922,17 @@ class _CupertinoDialogRenderElement extends RenderObjectElement { ...@@ -397,20 +922,17 @@ class _CupertinoDialogRenderElement extends RenderObjectElement {
@override @override
void insertRenderObjectChild(RenderObject child, _AlertDialogSections slot) { void insertRenderObjectChild(RenderObject child, _AlertDialogSections slot) {
assert(slot != null); _placeChildInSlot(child, slot);
switch (slot) {
case _AlertDialogSections.contentSection:
renderObject.contentSection = child as RenderBox;
break;
case _AlertDialogSections.actionsSection:
renderObject.actionsSection = child as RenderBox;
break;
}
} }
@override @override
void moveRenderObjectChild(RenderObject child, _AlertDialogSections oldSlot, _AlertDialogSections newSlot) { void moveRenderObjectChild(RenderObject child, _AlertDialogSections oldSlot, _AlertDialogSections newSlot) {
assert(false); if (!allowMoveRenderObjectChild) {
super.moveRenderObjectChild(child, oldSlot, newSlot);
return;
}
_placeChildInSlot(child, newSlot);
} }
@override @override
...@@ -442,6 +964,18 @@ class _CupertinoDialogRenderElement extends RenderObjectElement { ...@@ -442,6 +964,18 @@ class _CupertinoDialogRenderElement extends RenderObjectElement {
renderObject.actionsSection = null; 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 // iOS style layout policy for sizing an alert dialog's content section and action
...@@ -474,15 +1008,16 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -474,15 +1008,16 @@ class _RenderCupertinoDialog extends RenderBox {
RenderBox? actionsSection, RenderBox? actionsSection,
double dividerThickness = 0.0, double dividerThickness = 0.0,
bool isInAccessibilityMode = false, bool isInAccessibilityMode = false,
bool isActionSheet = false,
required Color dividerColor, required Color dividerColor,
}) : _contentSection = contentSection, }) : _contentSection = contentSection,
_actionsSection = actionsSection, _actionsSection = actionsSection,
_dividerThickness = dividerThickness, _dividerThickness = dividerThickness,
_isInAccessibilityMode = isInAccessibilityMode, _isInAccessibilityMode = isInAccessibilityMode,
_isActionSheet = isActionSheet,
_dividerPaint = Paint() _dividerPaint = Paint()
..color = dividerColor ..color = dividerColor
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
RenderBox? get contentSection => _contentSection; RenderBox? get contentSection => _contentSection;
RenderBox? _contentSection; RenderBox? _contentSection;
...@@ -521,6 +1056,15 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -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 double get _dialogWidth => isInAccessibilityMode
? _kAccessibilityCupertinoDialogWidth ? _kAccessibilityCupertinoDialogWidth
: _kCupertinoDialogWidth; : _kCupertinoDialogWidth;
...@@ -572,8 +1116,10 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -572,8 +1116,10 @@ class _RenderCupertinoDialog extends RenderBox {
@override @override
void setupParentData(RenderBox child) { void setupParentData(RenderBox child) {
if (child.parentData is! BoxParentData) { if (!isActionSheet && child.parentData is! BoxParentData) {
child.parentData = BoxParentData(); child.parentData = BoxParentData();
} else if (child.parentData is! MultiChildLayoutParentData) {
child.parentData = MultiChildLayoutParentData();
} }
} }
...@@ -595,12 +1141,12 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -595,12 +1141,12 @@ class _RenderCupertinoDialog extends RenderBox {
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
return _dialogWidth; return isActionSheet ? constraints.minWidth : _dialogWidth;
} }
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
return _dialogWidth; return isActionSheet ? constraints.maxWidth : _dialogWidth;
} }
@override @override
...@@ -608,10 +1154,14 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -608,10 +1154,14 @@ class _RenderCupertinoDialog extends RenderBox {
final double contentHeight = contentSection!.getMinIntrinsicHeight(width); final double contentHeight = contentSection!.getMinIntrinsicHeight(width);
final double actionsHeight = actionsSection!.getMinIntrinsicHeight(width); final double actionsHeight = actionsSection!.getMinIntrinsicHeight(width);
final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0; 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 height;
}
return 0.0; return 0.0;
} }
...@@ -620,10 +1170,14 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -620,10 +1170,14 @@ class _RenderCupertinoDialog extends RenderBox {
final double contentHeight = contentSection!.getMaxIntrinsicHeight(width); final double contentHeight = contentSection!.getMaxIntrinsicHeight(width);
final double actionsHeight = actionsSection!.getMaxIntrinsicHeight(width); final double actionsHeight = actionsSection!.getMaxIntrinsicHeight(width);
final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0; 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 height;
}
return 0.0; return 0.0;
} }
...@@ -637,7 +1191,7 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -637,7 +1191,7 @@ class _RenderCupertinoDialog extends RenderBox {
@override @override
void performLayout() { void performLayout() {
final _DialogSizes dialogSizes = _performLayout( final _AlertDialogSizes dialogSizes = _performLayout(
constraints: constraints, constraints: constraints,
layoutChild: ChildLayoutHelper.layoutChild, layoutChild: ChildLayoutHelper.layoutChild,
); );
...@@ -645,31 +1199,37 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -645,31 +1199,37 @@ class _RenderCupertinoDialog extends RenderBox {
// Set the position of the actions box to sit at the bottom of the dialog. // 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. // The content box defaults to the top left, which is where we want it.
assert(actionsSection!.parentData is BoxParentData); assert((!isActionSheet && actionsSection!.parentData is BoxParentData)
final BoxParentData actionParentData = actionsSection!.parentData! as BoxParentData; || (isActionSheet && actionsSection!.parentData is MultiChildLayoutParentData));
actionParentData.offset = Offset(0.0, dialogSizes.actionSectionYOffset); 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 return isInAccessibilityMode
? performAccessibilityLayout( ? performAccessibilityLayout(
constraints: constraints, constraints: constraints,
layoutChild: layoutChild, layoutChild: layoutChild,
) : performRegularLayout( ) : performRegularLayout(
constraints: constraints, constraints: constraints,
layoutChild: layoutChild, layoutChild: layoutChild,
); );
} }
// When not in accessibility mode, an alert dialog might reduce the space // 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 // for buttons to just over 1 button's height to make room for the content
// section. // section.
_DialogSizes performRegularLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) { _AlertDialogSizes performRegularLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
final bool hasDivider = contentSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0 final bool hasDivider = contentSection!.getMaxIntrinsicHeight(computeMaxIntrinsicWidth(0)) > 0.0
&& actionsSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0; && actionsSection!.getMaxIntrinsicHeight(computeMaxIntrinsicWidth(0)) > 0.0;
final double dividerThickness = hasDivider ? _dividerThickness : 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( final Size contentSize = layoutChild(
contentSection!, contentSection!,
...@@ -683,15 +1243,18 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -683,15 +1243,18 @@ class _RenderCupertinoDialog extends RenderBox {
final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height; final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height;
return _DialogSizes( return _AlertDialogSizes(
size: constraints.constrain(Size(_dialogWidth, dialogHeight)), size: isActionSheet
actionSectionYOffset: contentSize.height + dividerThickness, ? 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 // 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. // 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 final bool hasDivider = contentSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0
&& actionsSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0; && actionsSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0;
final double dividerThickness = hasDivider ? _dividerThickness : 0.0; final double dividerThickness = hasDivider ? _dividerThickness : 0.0;
...@@ -702,10 +1265,10 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -702,10 +1265,10 @@ class _RenderCupertinoDialog extends RenderBox {
final Size contentSize; final Size contentSize;
final Size actionsSize; final Size actionsSize;
if (maxContentHeight + dividerThickness + maxActionsHeight > constraints.maxHeight) { if (maxContentHeight + dividerThickness + maxActionsHeight > constraints.maxHeight) {
// There isn't enough room for everything. Following iOS's accessibility dialog // AlertDialog: There isn't enough room for everything. Following iOS's
// layout policy, first we allow the actions to take up to 50% of the dialog // accessibility dialog layout policy, first we allow the actions to take
// height. Second we fill the rest of the available space with the content // up to 50% of the dialog height. Second we fill the rest of the
// section. // available space with the content section.
actionsSize = layoutChild( actionsSize = layoutChild(
actionsSection!, actionsSection!,
...@@ -733,24 +1296,35 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -733,24 +1296,35 @@ class _RenderCupertinoDialog extends RenderBox {
// Calculate overall dialog height. // Calculate overall dialog height.
final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height; final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height;
return _DialogSizes( return _AlertDialogSizes(
size: constraints.constrain(Size(_dialogWidth, dialogHeight)), size: constraints.constrain(Size(_dialogWidth, dialogHeight)),
actionSectionYOffset: contentSize.height + dividerThickness, contentHeight: contentSize.height,
dividerThickness: dividerThickness,
); );
} }
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final BoxParentData contentParentData = contentSection!.parentData! as BoxParentData; if (isActionSheet) {
contentSection!.paint(context, offset + contentParentData.offset); 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; final bool hasDivider = contentSection!.size.height > 0.0 && actionsSection!.size.height > 0.0;
if (hasDivider) { if (hasDivider) {
_paintDividerBetweenContentAndActions(context.canvas, offset); _paintDividerBetweenContentAndActions(context.canvas, offset);
} }
final BoxParentData actionsParentData = actionsSection!.parentData! as BoxParentData; if (isActionSheet) {
actionsSection!.paint(context, offset + actionsParentData.offset); 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) { void _paintDividerBetweenContentAndActions(Canvas canvas, Offset offset) {
...@@ -767,32 +1341,58 @@ class _RenderCupertinoDialog extends RenderBox { ...@@ -767,32 +1341,58 @@ class _RenderCupertinoDialog extends RenderBox {
@override @override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { 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 contentSectionParentData = contentSection!.parentData! as BoxParentData;
final BoxParentData actionsSectionParentData = actionsSection!.parentData! as BoxParentData; final BoxParentData actionsSectionParentData = actionsSection!.parentData! as BoxParentData;
return result.addWithPaintOffset( return result.addWithPaintOffset(
offset: contentSectionParentData.offset, offset: contentSectionParentData.offset,
position: position, position: position,
hitTest: (BoxHitTestResult result, Offset transformed) { hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - contentSectionParentData.offset); assert(transformed == position - contentSectionParentData.offset);
return contentSection!.hitTest(result, position: transformed); return contentSection!.hitTest(result, position: transformed);
}, },
) ) ||
|| result.addWithPaintOffset( result.addWithPaintOffset(
offset: actionsSectionParentData.offset, offset: actionsSectionParentData.offset,
position: position, position: position,
hitTest: (BoxHitTestResult result, Offset transformed) { hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - actionsSectionParentData.offset); assert(transformed == position - actionsSectionParentData.offset);
return actionsSection!.hitTest(result, position: transformed); return actionsSection!.hitTest(result, position: transformed);
}, },
); );
} }
} }
class _DialogSizes { class _AlertDialogSizes {
const _DialogSizes({required this.size, required this.actionSectionYOffset}); const _AlertDialogSizes({
required this.size,
required this.contentHeight,
required this.dividerThickness,
});
final Size size; 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 // Visual components of an alert dialog that need to be explicitly sized and
...@@ -811,9 +1411,16 @@ class _CupertinoAlertContentSection extends StatelessWidget { ...@@ -811,9 +1411,16 @@ class _CupertinoAlertContentSection extends StatelessWidget {
const _CupertinoAlertContentSection({ const _CupertinoAlertContentSection({
Key? key, Key? key,
this.title, this.title,
this.content, this.message,
this.scrollController, 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 // The (optional) title of the dialog is displayed in a large font at the top
// of the dialog. // of the dialog.
...@@ -821,11 +1428,11 @@ class _CupertinoAlertContentSection extends StatelessWidget { ...@@ -821,11 +1428,11 @@ class _CupertinoAlertContentSection extends StatelessWidget {
// Typically a Text widget. // Typically a Text widget.
final Widget? title; 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. // dialog in a lighter font.
// //
// Typically a Text widget. // Typically a Text widget.
final Widget? content; final Widget? message;
// A scroll controller that can be used to control the scrolling of the // A scroll controller that can be used to control the scrolling of the
// content in the dialog. // content in the dialog.
...@@ -834,51 +1441,55 @@ class _CupertinoAlertContentSection extends StatelessWidget { ...@@ -834,51 +1441,55 @@ class _CupertinoAlertContentSection extends StatelessWidget {
// are short. // are short.
final ScrollController? scrollController; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (title == null && content == null) { if (title == null && message == null) {
return SingleChildScrollView( return SingleChildScrollView(
controller: scrollController, controller: scrollController,
child: const SizedBox(width: 0.0, height: 0.0), child: const SizedBox(width: 0.0, height: 0.0),
); );
} }
final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
final List<Widget> titleContentGroup = <Widget>[ final List<Widget> titleContentGroup = <Widget>[
if (title != null) if (title != null)
Padding( Padding(
padding: EdgeInsets.only( padding: titlePadding!,
left: _kEdgePadding,
right: _kEdgePadding,
bottom: content == null ? _kEdgePadding : 1.0,
top: _kEdgePadding * textScaleFactor,
),
child: DefaultTextStyle( child: DefaultTextStyle(
style: _kCupertinoDialogTitleStyle.copyWith( style: titleTextStyle!,
color: CupertinoDynamicColor.resolve(CupertinoColors.label, context),
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
child: title!, child: title!,
), ),
), ),
if (content != null) if (message != null)
Padding( Padding(
padding: EdgeInsets.only( padding: messagePadding!,
left: _kEdgePadding,
right: _kEdgePadding,
bottom: _kEdgePadding * textScaleFactor,
top: title == null ? _kEdgePadding : 1.0,
),
child: DefaultTextStyle( child: DefaultTextStyle(
style: _kCupertinoDialogContentStyle.copyWith( style: messageTextStyle!,
color: CupertinoDynamicColor.resolve(CupertinoColors.label, context),
),
textAlign: TextAlign.center, 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( return CupertinoScrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(
controller: scrollController, controller: scrollController,
...@@ -901,6 +1512,8 @@ class _CupertinoAlertActionSection extends StatefulWidget { ...@@ -901,6 +1512,8 @@ class _CupertinoAlertActionSection extends StatefulWidget {
Key? key, Key? key,
required this.children, required this.children,
this.scrollController, this.scrollController,
this.hasCancelButton = false,
this.isActionSheet = false,
}) : assert(children != null), }) : assert(children != null),
super(key: key); super(key: key);
...@@ -913,11 +1526,21 @@ class _CupertinoAlertActionSection extends StatefulWidget { ...@@ -913,11 +1526,21 @@ class _CupertinoAlertActionSection extends StatefulWidget {
// don't have many actions. // don't have many actions.
final ScrollController? scrollController; 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 @override
_CupertinoAlertActionSectionState createState() => _CupertinoAlertActionSectionState(); _CupertinoAlertActionSectionState createState() =>
_CupertinoAlertActionSectionState();
} }
class _CupertinoAlertActionSectionState extends State<_CupertinoAlertActionSection> { class _CupertinoAlertActionSectionState
extends State<_CupertinoAlertActionSection> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final double devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
...@@ -937,6 +1560,8 @@ class _CupertinoAlertActionSectionState extends State<_CupertinoAlertActionSecti ...@@ -937,6 +1560,8 @@ class _CupertinoAlertActionSectionState extends State<_CupertinoAlertActionSecti
child: _CupertinoDialogActionsRenderWidget( child: _CupertinoDialogActionsRenderWidget(
actionButtons: interactiveButtons, actionButtons: interactiveButtons,
dividerThickness: _kDividerThickness / devicePixelRatio, dividerThickness: _kDividerThickness / devicePixelRatio,
hasCancelButton: widget.hasCancelButton,
isActionSheet: widget.isActionSheet,
), ),
), ),
); );
...@@ -995,7 +1620,8 @@ class _PressableActionButtonState extends State<_PressableActionButton> { ...@@ -995,7 +1620,8 @@ class _PressableActionButtonState extends State<_PressableActionButton> {
// _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for // _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for
// updating the pressed state of an _ActionButtonParentData based on the // updating the pressed state of an _ActionButtonParentData based on the
// incoming [isPressed] property. // incoming [isPressed] property.
class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParentData> { class _ActionButtonParentDataWidget
extends ParentDataWidget<_ActionButtonParentData> {
const _ActionButtonParentDataWidget({ const _ActionButtonParentDataWidget({
Key? key, Key? key,
required this.isPressed, required this.isPressed,
...@@ -1007,7 +1633,8 @@ class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParent ...@@ -1007,7 +1633,8 @@ class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParent
@override @override
void applyParentData(RenderObject renderObject) { void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is _ActionButtonParentData); assert(renderObject.parentData is _ActionButtonParentData);
final _ActionButtonParentData parentData = renderObject.parentData! as _ActionButtonParentData; final _ActionButtonParentData parentData =
renderObject.parentData! as _ActionButtonParentData;
if (parentData.isPressed != isPressed) { if (parentData.isPressed != isPressed) {
parentData.isPressed = isPressed; parentData.isPressed = isPressed;
...@@ -1019,7 +1646,8 @@ class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParent ...@@ -1019,7 +1646,8 @@ class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParent
} }
@override @override
Type get debugTypicalAncestorWidgetClass => _CupertinoDialogActionsRenderWidget; Type get debugTypicalAncestorWidgetClass =>
_CupertinoDialogActionsRenderWidget;
} }
// ParentData applied to individual action buttons that report whether or not // ParentData applied to individual action buttons that report whether or not
...@@ -1115,7 +1743,7 @@ class CupertinoDialogAction extends StatelessWidget { ...@@ -1115,7 +1743,7 @@ class CupertinoDialogAction extends StatelessWidget {
// iOS scale factor) vs the minimum text size that we allow in action // iOS scale factor) vs the minimum text size that we allow in action
// buttons. This ratio information is used to automatically scale down action // buttons. This ratio information is used to automatically scale down action
// button text to fit the available space. // 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); final double padding = _calculatePadding(context);
return IntrinsicHeight( return IntrinsicHeight(
...@@ -1162,7 +1790,7 @@ class CupertinoDialogAction extends StatelessWidget { ...@@ -1162,7 +1790,7 @@ class CupertinoDialogAction extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
TextStyle style = _kCupertinoDialogActionStyle.copyWith( TextStyle style = _kCupertinoDialogActionStyle.copyWith(
color: CupertinoDynamicColor.resolve( color: CupertinoDynamicColor.resolve(
isDestructiveAction ? CupertinoColors.systemRed : CupertinoColors.systemBlue, isDestructiveAction ? CupertinoColors.systemRed : CupertinoColors.systemBlue,
context, context,
), ),
); );
...@@ -1183,15 +1811,15 @@ class CupertinoDialogAction extends StatelessWidget { ...@@ -1183,15 +1811,15 @@ class CupertinoDialogAction extends StatelessWidget {
// wrap instead of ellipsizing. We are consciously not implementing that // wrap instead of ellipsizing. We are consciously not implementing that
// now due to complexity. // now due to complexity.
final Widget sizedContent = _isInAccessibilityMode(context) final Widget sizedContent = _isInAccessibilityMode(context)
? _buildContentWithAccessibilitySizingPolicy( ? _buildContentWithAccessibilitySizingPolicy(
textStyle: style, textStyle: style,
content: child, content: child,
) )
: _buildContentWithRegularSizingPolicy( : _buildContentWithRegularSizingPolicy(
context: context, context: context,
textStyle: style, textStyle: style,
content: child, content: child,
); );
return GestureDetector( return GestureDetector(
excludeFromSemantics: true, excludeFromSemantics: true,
...@@ -1199,7 +1827,7 @@ class CupertinoDialogAction extends StatelessWidget { ...@@ -1199,7 +1827,7 @@ class CupertinoDialogAction extends StatelessWidget {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
minHeight: _kMinButtonHeight, minHeight: _kDialogMinButtonHeight,
), ),
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
...@@ -1223,34 +1851,49 @@ class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget { ...@@ -1223,34 +1851,49 @@ class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget {
Key? key, Key? key,
required List<Widget> actionButtons, required List<Widget> actionButtons,
double dividerThickness = 0.0, double dividerThickness = 0.0,
bool hasCancelButton = false,
bool isActionSheet = false,
}) : _dividerThickness = dividerThickness, }) : _dividerThickness = dividerThickness,
_hasCancelButton = hasCancelButton,
_isActionSheet = isActionSheet,
super(key: key, children: actionButtons); super(key: key, children: actionButtons);
final double _dividerThickness; final double _dividerThickness;
final bool _hasCancelButton;
final bool _isActionSheet;
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
return _RenderCupertinoDialogActions( return _RenderCupertinoDialogActions(
dialogWidth: _isInAccessibilityMode(context) dialogWidth: _isActionSheet
? _kAccessibilityCupertinoDialogWidth ? null
: _kCupertinoDialogWidth, : _isInAccessibilityMode(context)
? _kAccessibilityCupertinoDialogWidth
: _kCupertinoDialogWidth,
dividerThickness: _dividerThickness, dividerThickness: _dividerThickness,
dialogColor: CupertinoDynamicColor.resolve(_kDialogColor, context), dialogColor: CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetBackgroundColor : _kDialogColor, context),
dialogPressedColor: CupertinoDynamicColor.resolve(_kDialogPressedColor, context), dialogPressedColor: CupertinoDynamicColor.resolve(_kPressedColor, context),
dividerColor: CupertinoDynamicColor.resolve(CupertinoColors.separator, context), dividerColor: CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetButtonDividerColor : CupertinoColors.separator, context),
hasCancelButton: _hasCancelButton,
isActionSheet: _isActionSheet,
); );
} }
@override @override
void updateRenderObject(BuildContext context, _RenderCupertinoDialogActions renderObject) { void updateRenderObject(
BuildContext context, _RenderCupertinoDialogActions renderObject) {
renderObject renderObject
..dialogWidth = _isInAccessibilityMode(context) ..dialogWidth = _isActionSheet
? _kAccessibilityCupertinoDialogWidth ? null
: _kCupertinoDialogWidth : _isInAccessibilityMode(context)
? _kAccessibilityCupertinoDialogWidth
: _kCupertinoDialogWidth
..dividerThickness = _dividerThickness ..dividerThickness = _dividerThickness
..dialogColor = CupertinoDynamicColor.resolve(_kDialogColor, context) ..dialogColor = CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetBackgroundColor : _kDialogColor, context)
..dialogPressedColor = CupertinoDynamicColor.resolve(_kDialogPressedColor, context) ..dialogPressedColor = CupertinoDynamicColor.resolve(_kPressedColor, context)
..dividerColor = CupertinoDynamicColor.resolve(CupertinoColors.separator, context); ..dividerColor = CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetButtonDividerColor : CupertinoColors.separator, context)
..hasCancelButton = _hasCancelButton
..isActionSheet = _isActionSheet;
} }
} }
...@@ -1293,28 +1936,33 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1293,28 +1936,33 @@ class _RenderCupertinoDialogActions extends RenderBox
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> { RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
_RenderCupertinoDialogActions({ _RenderCupertinoDialogActions({
List<RenderBox>? children, List<RenderBox>? children,
required double dialogWidth, double? dialogWidth,
double dividerThickness = 0.0, double dividerThickness = 0.0,
required Color dialogColor, required Color dialogColor,
required Color dialogPressedColor, required Color dialogPressedColor,
required Color dividerColor, required Color dividerColor,
}) : _dialogWidth = dialogWidth, bool hasCancelButton = false,
bool isActionSheet = false,
}) : assert(isActionSheet || dialogWidth != null),
_dialogWidth = dialogWidth,
_buttonBackgroundPaint = Paint() _buttonBackgroundPaint = Paint()
..color = dialogColor ..color = dialogColor
..style = PaintingStyle.fill, ..style = PaintingStyle.fill,
_pressedButtonBackgroundPaint = Paint() _pressedButtonBackgroundPaint = Paint()
..color = dialogPressedColor ..color = dialogPressedColor
..style = PaintingStyle.fill, ..style = PaintingStyle.fill,
_dividerPaint = Paint() _dividerPaint = Paint()
..color = dividerColor ..color = dividerColor
..style = PaintingStyle.fill, ..style = PaintingStyle.fill,
_dividerThickness = dividerThickness { _dividerThickness = dividerThickness,
_hasCancelButton = hasCancelButton,
_isActionSheet = isActionSheet {
addAll(children); addAll(children);
} }
double get dialogWidth => _dialogWidth; double? get dialogWidth => _dialogWidth;
double _dialogWidth; double? _dialogWidth;
set dialogWidth(double newWidth) { set dialogWidth(double? newWidth) {
if (newWidth != _dialogWidth) { if (newWidth != _dialogWidth) {
_dialogWidth = newWidth; _dialogWidth = newWidth;
markNeedsLayout(); markNeedsLayout();
...@@ -1331,6 +1979,16 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -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; final Paint _buttonBackgroundPaint;
set dialogColor(Color value) { set dialogColor(Color value) {
if (value == _buttonBackgroundPaint.color) if (value == _buttonBackgroundPaint.color)
...@@ -1358,6 +2016,16 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1358,6 +2016,16 @@ class _RenderCupertinoDialogActions extends RenderBox
markNeedsPaint(); markNeedsPaint();
} }
bool _isActionSheet;
bool get isActionSheet => _isActionSheet;
set isActionSheet(bool value) {
if (value == _isActionSheet)
return;
_isActionSheet = value;
markNeedsPaint();
}
Iterable<RenderBox> get _pressedButtons sync* { Iterable<RenderBox> get _pressedButtons sync* {
RenderBox? currentChild = firstChild; RenderBox? currentChild = firstChild;
while (currentChild != null) { while (currentChild != null) {
...@@ -1391,33 +2059,49 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1391,33 +2059,49 @@ class _RenderCupertinoDialogActions extends RenderBox
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
return dialogWidth; return isActionSheet ? constraints.minWidth : dialogWidth!;
} }
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
return dialogWidth; return isActionSheet ? constraints.maxWidth : dialogWidth!;
} }
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
final double minHeight;
if (childCount == 0) { 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) { } else if (childCount == 1) {
// If only 1 button, display the button across the entire dialog. // If only 1 button, display the button across the entire dialog.
minHeight = _computeMinIntrinsicHeightSideBySide(width); return _computeMinIntrinsicHeightSideBySide(width);
} else { } else if (childCount == 2 && _isSingleButtonRow(width)) {
if (childCount == 2 && _isSingleButtonRow(width)) { // The first 2 buttons fit side-by-side. Display them horizontally.
// The first 2 buttons fit side-by-side. Display them horizontally. return _computeMinIntrinsicHeightSideBySide(width);
minHeight = _computeMinIntrinsicHeightSideBySide(width);
} else {
// 3+ buttons are always stacked. The minimum height when stacked is
// 1.5 buttons tall.
minHeight = _computeMinIntrinsicHeightStacked(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' // The minimum height for a single row of buttons is the larger of the buttons'
...@@ -1438,46 +2122,51 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1438,46 +2122,51 @@ class _RenderCupertinoDialogActions extends RenderBox
return minHeight; return minHeight;
} }
// The minimum height for 2+ stacked buttons is the height of the 1st button // Dialog: The minimum height for 2+ stacked buttons is the height of the 1st
// + 50% the height of the 2nd button + the divider between the two. // 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) { double _computeMinIntrinsicHeightStacked(double width) {
assert(childCount >= 2); assert(childCount >= 2);
return firstChild!.getMinIntrinsicHeight(width) return firstChild!.getMinIntrinsicHeight(width)
+ dividerThickness + dividerThickness
+ (0.5 * childAfter(firstChild!)!.getMinIntrinsicHeight(width)); + (0.5 * childAfter(firstChild!)!.getMinIntrinsicHeight(width));
} }
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
final double maxHeight;
if (childCount == 0) { if (childCount == 0) {
// No buttons. Zero height. // 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) { } else if (childCount == 1) {
// One button. Our max intrinsic height is equal to the button's. // One button. Our max intrinsic height is equal to the button's.
maxHeight = firstChild!.getMaxIntrinsicHeight(width); return firstChild!.getMaxIntrinsicHeight(width);
} else if (childCount == 2) { } else if (childCount == 2) {
// Two buttons... // Two buttons...
if (_isSingleButtonRow(width)) { if (_isSingleButtonRow(width)) {
// The 2 buttons fit side by side so our max intrinsic height is equal // The 2 buttons fit side by side so our max intrinsic height is equal
// to the taller of the 2 buttons. // to the taller of the 2 buttons.
final double perButtonWidth = (width - dividerThickness) / 2.0; final double perButtonWidth = (width - dividerThickness) / 2.0;
maxHeight = math.max( return math.max(
firstChild!.getMaxIntrinsicHeight(perButtonWidth), firstChild!.getMaxIntrinsicHeight(perButtonWidth),
lastChild!.getMaxIntrinsicHeight(perButtonWidth), lastChild!.getMaxIntrinsicHeight(perButtonWidth),
); );
} else { } else {
// The 2 buttons do not fit side by side. Measure total height as a // The 2 buttons do not fit side by side. Measure total height as a
// vertical stack. // 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 // Max height of a stack of buttons is the sum of all button heights + a
...@@ -1514,20 +2203,20 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1514,20 +2203,20 @@ class _RenderCupertinoDialogActions extends RenderBox
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
return _computeLayout(constraints: constraints, dry: true); return _performLayout(constraints: constraints, dry: true);
} }
@override @override
void performLayout() { 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 final ChildLayouter layoutChild = dry
? ChildLayoutHelper.dryLayoutChild ? ChildLayoutHelper.dryLayoutChild
: ChildLayoutHelper.layoutChild; : ChildLayoutHelper.layoutChild;
if (_isSingleButtonRow(dialogWidth)) { if (!isActionSheet && _isSingleButtonRow(dialogWidth!)) {
if (childCount == 1) { if (childCount == 1) {
// We have 1 button. Our size is the width of the dialog and the height // We have 1 button. Our size is the width of the dialog and the height
// of the single button. // of the single button.
...@@ -1537,7 +2226,7 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1537,7 +2226,7 @@ class _RenderCupertinoDialogActions extends RenderBox
); );
return constraints.constrain( return constraints.constrain(
Size(dialogWidth, childSize.height), Size(dialogWidth!, childSize.height)
); );
} else { } else {
// Each button gets half the available width, minus a single divider. // Each button gets half the available width, minus a single divider.
...@@ -1568,7 +2257,7 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1568,7 +2257,7 @@ class _RenderCupertinoDialogActions extends RenderBox
// Calculate our size based on the button sizes. // Calculate our size based on the button sizes.
return constraints.constrain( return constraints.constrain(
Size( Size(
dialogWidth, dialogWidth!,
math.max( math.max(
firstChildSize.height, firstChildSize.height,
lastChildSize.height, lastChildSize.height,
...@@ -1609,7 +2298,7 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1609,7 +2298,7 @@ class _RenderCupertinoDialogActions extends RenderBox
// Our height is the accumulated height of all buttons and dividers. // Our height is the accumulated height of all buttons and dividers.
return constraints.constrain( return constraints.constrain(
Size(dialogWidth, verticalOffset), Size(computeMaxIntrinsicWidth(0), verticalOffset),
); );
} }
} }
...@@ -1618,7 +2307,7 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1618,7 +2307,7 @@ class _RenderCupertinoDialogActions extends RenderBox
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
if (_isSingleButtonRow(size.width)) { if (!isActionSheet && _isSingleButtonRow(size.width)) {
_drawButtonBackgroundsAndDividersSingleRow(canvas, offset); _drawButtonBackgroundsAndDividersSingleRow(canvas, offset);
} else { } else {
_drawButtonBackgroundsAndDividersStacked(canvas, offset); _drawButtonBackgroundsAndDividersStacked(canvas, offset);
...@@ -1632,16 +2321,16 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1632,16 +2321,16 @@ class _RenderCupertinoDialogActions extends RenderBox
// the dialog has 2 buttons). The vertical divider is hidden if either the // the dialog has 2 buttons). The vertical divider is hidden if either the
// left or right button is pressed. // left or right button is pressed.
final Rect verticalDivider = childCount == 2 && !_isButtonPressed final Rect verticalDivider = childCount == 2 && !_isButtonPressed
? Rect.fromLTWH( ? Rect.fromLTWH(
offset.dx + firstChild!.size.width, offset.dx + firstChild!.size.width,
offset.dy, offset.dy,
dividerThickness, dividerThickness,
math.max( math.max(
firstChild!.size.height, firstChild!.size.height,
lastChild!.size.height, lastChild!.size.height,
), ),
) )
: Rect.zero; : Rect.zero;
final List<Rect> pressedButtonRects = _pressedButtons.map<Rect>((RenderBox pressedButton) { final List<Rect> pressedButtonRects = _pressedButtons.map<Rect>((RenderBox pressedButton) {
final MultiChildLayoutParentData buttonParentData = pressedButton.parentData! as MultiChildLayoutParentData; final MultiChildLayoutParentData buttonParentData = pressedButton.parentData! as MultiChildLayoutParentData;
...@@ -1770,7 +2459,7 @@ class _RenderCupertinoDialogActions extends RenderBox ...@@ -1770,7 +2459,7 @@ class _RenderCupertinoDialogActions extends RenderBox
} }
@override @override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position); return defaultHitTestChildren(result, position: position);
} }
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment