mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Rewrite CupertinoActionSheet, and fix layout deviation (#149334)
This PR rewrite `CupertinoActionSheet` to be based on layout widgets instead of the custom layout code in `_CupertinoDialogRenderWidget`, making it much easier to understand. It also fixes a layout deviation from SwiftUI, and add a few tests. `_CupertinoDialogRenderWidget` is a custom layout widget that contains too much boilerplate code and is hard to understand where is customized. The plan is to replace it with widgets as much as possible. (Since this widget is used by both `CupertinoActionSheet` and `CupertinoAlertDialog`, this PR only removes the branches related to `isActionSheet`). The need to use custom layout widget probably came from the following difficulties: 1. The parent needs to know whether an action button is being pressed to correctly render or hide each divider, but the action button is provided by the user as a built widget that also needs to respond to the tap gesture. 2. The main sheet's column needs to _prioritize_ allocating the space to the content section before allocating the remaining to the actions section, while also leaving the actions section a minimum height. This is not supported by simply `Column`. 3. The minimum height of the action section is a non-trivial algorithm. Luckily, I've found out that all these problems are solvable with widgets. All existing tests pass.
This commit is contained in:
parent
84cff3166d
commit
2b700dda38
@ -90,6 +90,7 @@ const double _kActionSheetEdgeVerticalPadding = 10.0;
|
||||
const double _kActionSheetContentHorizontalPadding = 16.0;
|
||||
const double _kActionSheetContentVerticalPadding = 12.0;
|
||||
const double _kActionSheetButtonHeight = 56.0;
|
||||
const double _kActionSheetActionsSectionMinHeight = 84.3;
|
||||
|
||||
// A translucent color that is painted on top of the blurred backdrop as the
|
||||
// dialog's background color
|
||||
@ -561,9 +562,11 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get hasContent => widget.title != null || widget.message != null;
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
final List<Widget> content = <Widget>[];
|
||||
if (widget.title != null || widget.message != null) {
|
||||
if (hasContent) {
|
||||
final Widget titleSection = _CupertinoAlertContentSection(
|
||||
title: widget.title,
|
||||
message: widget.message,
|
||||
@ -601,28 +604,16 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActions() {
|
||||
if (widget.actions == null || widget.actions!.isEmpty) {
|
||||
return const LimitedBox(
|
||||
maxWidth: 0,
|
||||
child: SizedBox(width: double.infinity, height: 0),
|
||||
);
|
||||
}
|
||||
return _CupertinoAlertActionSection(
|
||||
scrollController: _effectiveActionScrollController,
|
||||
hasCancelButton: widget.cancelButton != null,
|
||||
isActionSheet: true,
|
||||
children: widget.actions!,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCancelButton() {
|
||||
assert(widget.cancelButton != null);
|
||||
final double cancelPadding = (widget.actions != null || widget.message != null || widget.title != null)
|
||||
? _kActionSheetCancelButtonPadding : 0.0;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: cancelPadding),
|
||||
child: _CupertinoActionSheetCancelButton(
|
||||
child: widget.cancelButton,
|
||||
child: _ActionSheetButtonBackground(
|
||||
isCancel: true,
|
||||
onPressStateChange: (_) {},
|
||||
child: widget.cancelButton!,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -632,15 +623,18 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
|
||||
final List<Widget> children = <Widget>[
|
||||
Flexible(child: ClipRRect(
|
||||
Flexible(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount),
|
||||
child: _CupertinoDialogRenderWidget(
|
||||
child: _ActionSheetMainSheet(
|
||||
scrollController: _effectiveActionScrollController,
|
||||
hasContent: hasContent,
|
||||
hasCancelButton: widget.cancelButton != null,
|
||||
contentSection: Builder(builder: _buildContent),
|
||||
actionsSection: _buildActions(),
|
||||
actions: widget.actions,
|
||||
dividerColor: _kActionSheetButtonDividerColor,
|
||||
isActionSheet: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -684,7 +678,10 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A button typically used in a [CupertinoActionSheet].
|
||||
/// The content of a typical action button in a [CupertinoActionSheet].
|
||||
///
|
||||
/// This widget draws the content of a button, i.e. the text, while the
|
||||
/// background of the button is drawn by [CupertinoActionSheet].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -759,37 +756,63 @@ class CupertinoActionSheetAction extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoActionSheetCancelButton extends StatefulWidget {
|
||||
const _CupertinoActionSheetCancelButton({
|
||||
this.child,
|
||||
// Renders the background of a button (both the pressed background and the idle
|
||||
// background) and reports its state to the parent with `onPressStateChange`.
|
||||
class _ActionSheetButtonBackground extends StatefulWidget {
|
||||
const _ActionSheetButtonBackground({
|
||||
this.isCancel = false,
|
||||
this.onPressStateChange,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
final bool isCancel;
|
||||
|
||||
/// Called when the user taps down or lifts up on the button.
|
||||
///
|
||||
/// The boolean value is true if the user is tapping down on the button.
|
||||
final ValueSetter<bool>? onPressStateChange;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// Typically a [Text] widget.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
_CupertinoActionSheetCancelButtonState createState() => _CupertinoActionSheetCancelButtonState();
|
||||
_ActionSheetButtonBackgroundState createState() => _ActionSheetButtonBackgroundState();
|
||||
}
|
||||
|
||||
class _CupertinoActionSheetCancelButtonState extends State<_CupertinoActionSheetCancelButton> {
|
||||
class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackground> {
|
||||
bool isBeingPressed = false;
|
||||
|
||||
void _onTapDown(TapDownDetails event) {
|
||||
setState(() { isBeingPressed = true; });
|
||||
widget.onPressStateChange?.call(true);
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails event) {
|
||||
setState(() { isBeingPressed = false; });
|
||||
widget.onPressStateChange?.call(false);
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
setState(() { isBeingPressed = false; });
|
||||
widget.onPressStateChange?.call(false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color backgroundColor = isBeingPressed
|
||||
? _kActionSheetCancelPressedColor
|
||||
late final Color backgroundColor;
|
||||
BorderRadius? borderRadius;
|
||||
if (!widget.isCancel) {
|
||||
backgroundColor = isBeingPressed
|
||||
? _kPressedColor
|
||||
: CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context);
|
||||
} else {
|
||||
backgroundColor = isBeingPressed
|
||||
? _kActionSheetCancelPressedColor
|
||||
: CupertinoColors.secondarySystemGroupedBackground;
|
||||
borderRadius = const BorderRadius.all(Radius.circular(_kCornerRadius));
|
||||
}
|
||||
return GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTapDown: _onTapDown,
|
||||
@ -797,15 +820,251 @@ class _CupertinoActionSheetCancelButtonState extends State<_CupertinoActionSheet
|
||||
onTapCancel: _onTapCancel,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoDynamicColor.resolve(backgroundColor, context),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(_kCornerRadius)),
|
||||
color: backgroundColor,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: widget.child,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The divider of an action sheet.
|
||||
//
|
||||
// If the divider is not `hidden`, then it displays the `dividerColor`.
|
||||
// Otherwise it displays the background color. A divider is hidden when either
|
||||
// of its neighbor button is pressed.
|
||||
class _ActionSheetDivider extends StatelessWidget {
|
||||
const _ActionSheetDivider({
|
||||
required this.dividerColor,
|
||||
required this.hidden,
|
||||
});
|
||||
|
||||
final Color dividerColor;
|
||||
final bool hidden;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color backgroundColor = CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context);
|
||||
return Container(
|
||||
height: _kDividerThickness,
|
||||
decoration: BoxDecoration(
|
||||
color: hidden ? backgroundColor : dividerColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef _PressedUpdateHandler = void Function(int actionIndex, bool state);
|
||||
|
||||
// The list of actions in an action sheet.
|
||||
//
|
||||
// This excludes the divider between the action section and the content section.
|
||||
class _ActionSheetActionSection extends StatelessWidget {
|
||||
const _ActionSheetActionSection({
|
||||
required this.actions,
|
||||
required this.pressedIndex,
|
||||
required this.dividerColor,
|
||||
required this.backgroundColor,
|
||||
required this.onPressedUpdate,
|
||||
required this.scrollController,
|
||||
});
|
||||
|
||||
final List<Widget>? actions;
|
||||
final _PressedUpdateHandler onPressedUpdate;
|
||||
final int? pressedIndex;
|
||||
final Color dividerColor;
|
||||
final Color backgroundColor;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (actions == null || actions!.isEmpty) {
|
||||
return const LimitedBox(
|
||||
maxWidth: 0,
|
||||
child: SizedBox(width: double.infinity, height: 0),
|
||||
);
|
||||
}
|
||||
final List<Widget> column = <Widget>[];
|
||||
for (int actionIndex = 0; actionIndex < actions!.length; actionIndex += 1) {
|
||||
if (actionIndex != 0) {
|
||||
column.add(_ActionSheetDivider(
|
||||
dividerColor: dividerColor,
|
||||
hidden: pressedIndex == actionIndex - 1 || pressedIndex == actionIndex,
|
||||
));
|
||||
}
|
||||
column.add(_ActionSheetButtonBackground(
|
||||
onPressStateChange: (bool state) {
|
||||
onPressedUpdate(actionIndex, state);
|
||||
},
|
||||
child: actions![actionIndex],
|
||||
));
|
||||
}
|
||||
return CupertinoScrollbar(
|
||||
controller: scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
children: column,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The part of an action sheet without the cancel button.
|
||||
class _ActionSheetMainSheet extends StatefulWidget {
|
||||
const _ActionSheetMainSheet({
|
||||
required this.scrollController,
|
||||
required this.actions,
|
||||
required this.hasContent,
|
||||
required this.hasCancelButton,
|
||||
required this.contentSection,
|
||||
required this.dividerColor,
|
||||
});
|
||||
|
||||
final ScrollController? scrollController;
|
||||
final List<Widget>? actions;
|
||||
final bool hasContent;
|
||||
final bool hasCancelButton;
|
||||
final Widget contentSection;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
_ActionSheetMainSheetState createState() => _ActionSheetMainSheetState();
|
||||
}
|
||||
|
||||
class _ActionSheetMainSheetState extends State<_ActionSheetMainSheet> {
|
||||
int? _pressedIndex;
|
||||
double _topOverscroll = 0;
|
||||
double _bottomOverscroll = 0;
|
||||
|
||||
// Fills the overscroll area at the top and bottom of the sheet. This is
|
||||
// necessary because the action section's background is rendered by the
|
||||
// buttons, so that a button's background can be _replaced_ by a different
|
||||
// color when the button is pressed.
|
||||
Widget _buildOverscroll() {
|
||||
final Color backgroundColor = CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: backgroundColor,
|
||||
height: _topOverscroll,
|
||||
),
|
||||
Container(
|
||||
color: backgroundColor,
|
||||
height: _bottomOverscroll,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool _onScrollUpdate(ScrollUpdateNotification notification) {
|
||||
final ScrollMetrics metrics = notification.metrics;
|
||||
setState(() {
|
||||
_topOverscroll = math.max(metrics.minScrollExtent - metrics.pixels, 0);
|
||||
_bottomOverscroll = math.max(metrics.pixels - metrics.maxScrollExtent, 0);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _hasActions() => (widget.actions?.length ?? 0) != 0;
|
||||
|
||||
// If `aggressivelyLayout` is true, then the content section takes as much
|
||||
// space as needed up to `maxHeight`.
|
||||
//
|
||||
// If `aggressivelyLayout` is false, then the content section takes whatever
|
||||
// space is left by the other sections.
|
||||
Widget _buildContent({
|
||||
required bool hasActions,
|
||||
required bool aggressivelyLayout,
|
||||
required double maxHeight,
|
||||
}) {
|
||||
if (hasActions && aggressivelyLayout) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
child: widget.contentSection,
|
||||
);
|
||||
}
|
||||
return Flexible(
|
||||
child: widget.contentSection,
|
||||
);
|
||||
}
|
||||
|
||||
void _onPressedUpdate(int actionIndex, bool state) {
|
||||
if (!state) {
|
||||
if (_pressedIndex == actionIndex) {
|
||||
setState(() {
|
||||
_pressedIndex = null;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_pressedIndex = actionIndex;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// The layout rule:
|
||||
//
|
||||
// 1. If there are <= 3 buttons and a cancel button, or 1 button without a
|
||||
// cancel button, then the actions section should never scroll.
|
||||
// 2. Otherwise, then the content section takes priority to take over spaces
|
||||
// but must leave at least `actionsMinHeight` for the actions section.
|
||||
final int numActions = widget.actions?.length ?? 0;
|
||||
final bool actionsMightScroll =
|
||||
(numActions > 3 && widget.hasCancelButton) ||
|
||||
(numActions > 1 && !widget.hasCancelButton) ;
|
||||
final Color backgroundColor = CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context);
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
_buildContent(
|
||||
hasActions: _hasActions(),
|
||||
aggressivelyLayout: actionsMightScroll,
|
||||
maxHeight: constraints.maxHeight - _kActionSheetActionsSectionMinHeight - _kDividerThickness,
|
||||
),
|
||||
if (widget.hasContent && _hasActions())
|
||||
_ActionSheetDivider(
|
||||
dividerColor: widget.dividerColor,
|
||||
hidden: false,
|
||||
),
|
||||
Flexible(
|
||||
flex: actionsMightScroll ? 1 : 0,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(
|
||||
child: _buildOverscroll(),
|
||||
),
|
||||
NotificationListener<ScrollUpdateNotification>(
|
||||
onNotification: _onScrollUpdate,
|
||||
child: _ActionSheetActionSection(
|
||||
actions: widget.actions,
|
||||
scrollController: widget.scrollController,
|
||||
pressedIndex: _pressedIndex,
|
||||
dividerColor: widget.dividerColor,
|
||||
backgroundColor: backgroundColor,
|
||||
onPressedUpdate: _onPressedUpdate,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// iOS style layout policy widget for sizing an alert dialog's content section and
|
||||
// action button section.
|
||||
//
|
||||
@ -815,44 +1074,36 @@ class _CupertinoDialogRenderWidget extends RenderObjectWidget {
|
||||
required this.contentSection,
|
||||
required this.actionsSection,
|
||||
required this.dividerColor,
|
||||
this.isActionSheet = false,
|
||||
});
|
||||
|
||||
final Widget contentSection;
|
||||
final Widget actionsSection;
|
||||
final Color dividerColor;
|
||||
final bool isActionSheet;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderCupertinoDialog(
|
||||
dividerThickness: _kDividerThickness,
|
||||
isInAccessibilityMode: _isInAccessibilityMode(context) && !isActionSheet,
|
||||
isInAccessibilityMode: _isInAccessibilityMode(context),
|
||||
dividerColor: CupertinoDynamicColor.resolve(dividerColor, context),
|
||||
isActionSheet: isActionSheet,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, _RenderCupertinoDialog renderObject) {
|
||||
renderObject
|
||||
..isInAccessibilityMode = _isInAccessibilityMode(context) && !isActionSheet
|
||||
..dividerColor = CupertinoDynamicColor.resolve(dividerColor, context)
|
||||
..isActionSheet = isActionSheet;
|
||||
..isInAccessibilityMode = _isInAccessibilityMode(context)
|
||||
..dividerColor = CupertinoDynamicColor.resolve(dividerColor, context);
|
||||
}
|
||||
|
||||
@override
|
||||
RenderObjectElement createElement() {
|
||||
return _CupertinoDialogRenderElement(this, allowMoveRenderObjectChild: isActionSheet);
|
||||
return _CupertinoDialogRenderElement(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoDialogRenderElement extends RenderObjectElement {
|
||||
_CupertinoDialogRenderElement(_CupertinoDialogRenderWidget super.widget, {this.allowMoveRenderObjectChild = false});
|
||||
|
||||
// Whether to allow overridden method moveRenderObjectChild call or default to super.
|
||||
// CupertinoActionSheet should default to [super] but CupertinoAlertDialog not.
|
||||
final bool allowMoveRenderObjectChild;
|
||||
_CupertinoDialogRenderElement(_CupertinoDialogRenderWidget super.widget);
|
||||
|
||||
Element? _contentElement;
|
||||
Element? _actionsElement;
|
||||
@ -885,12 +1136,8 @@ class _CupertinoDialogRenderElement extends RenderObjectElement {
|
||||
|
||||
@override
|
||||
void moveRenderObjectChild(RenderObject child, _AlertDialogSections oldSlot, _AlertDialogSections newSlot) {
|
||||
if (!allowMoveRenderObjectChild) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_placeChildInSlot(child, newSlot);
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -964,13 +1211,11 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
RenderBox? actionsSection,
|
||||
double dividerThickness = 0.0,
|
||||
bool isInAccessibilityMode = false,
|
||||
bool isActionSheet = false,
|
||||
required Color dividerColor,
|
||||
}) : _contentSection = contentSection,
|
||||
_actionsSection = actionsSection,
|
||||
_dividerThickness = dividerThickness,
|
||||
_isInAccessibilityMode = isInAccessibilityMode,
|
||||
_isActionSheet = isActionSheet,
|
||||
_dividerPaint = Paint()
|
||||
..color = dividerColor
|
||||
..style = PaintingStyle.fill;
|
||||
@ -1012,15 +1257,6 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isActionSheet;
|
||||
bool get isActionSheet => _isActionSheet;
|
||||
set isActionSheet(bool newValue) {
|
||||
if (newValue != _isActionSheet) {
|
||||
_isActionSheet = newValue;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
double get _dialogWidth => isInAccessibilityMode
|
||||
? _kAccessibilityCupertinoDialogWidth
|
||||
: _kCupertinoDialogWidth;
|
||||
@ -1072,7 +1308,7 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (!isActionSheet && child.parentData is! BoxParentData) {
|
||||
if (child.parentData is! BoxParentData) {
|
||||
child.parentData = BoxParentData();
|
||||
} else if (child.parentData is! MultiChildLayoutParentData) {
|
||||
child.parentData = MultiChildLayoutParentData();
|
||||
@ -1097,12 +1333,12 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
return isActionSheet ? constraints.minWidth : _dialogWidth;
|
||||
return _dialogWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
return isActionSheet ? constraints.maxWidth : _dialogWidth;
|
||||
return _dialogWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1110,11 +1346,8 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
final double contentHeight = contentSection!.getMinIntrinsicHeight(width);
|
||||
final double actionsHeight = actionsSection!.getMinIntrinsicHeight(width);
|
||||
final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
|
||||
double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
|
||||
final double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
|
||||
|
||||
if (isActionSheet && (actionsHeight > 0 || contentHeight > 0)) {
|
||||
height -= 2 * _kActionSheetEdgeVerticalPadding;
|
||||
}
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
@ -1126,11 +1359,8 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
final double contentHeight = contentSection!.getMaxIntrinsicHeight(width);
|
||||
final double actionsHeight = actionsSection!.getMaxIntrinsicHeight(width);
|
||||
final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
|
||||
double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
|
||||
final double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
|
||||
|
||||
if (isActionSheet && (actionsHeight > 0 || contentHeight > 0)) {
|
||||
height -= 2 * _kActionSheetEdgeVerticalPadding;
|
||||
}
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
@ -1155,17 +1385,9 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
|
||||
// Set the position of the actions box to sit at the bottom of the dialog.
|
||||
// The content box defaults to the top left, which is where we want it.
|
||||
assert(
|
||||
(!isActionSheet && actionsSection!.parentData is BoxParentData) ||
|
||||
(isActionSheet && actionsSection!.parentData is MultiChildLayoutParentData),
|
||||
);
|
||||
if (isActionSheet) {
|
||||
final MultiChildLayoutParentData actionParentData = actionsSection!.parentData! as MultiChildLayoutParentData;
|
||||
actionParentData.offset = Offset(0.0, dialogSizes.contentHeight + dialogSizes.dividerThickness);
|
||||
} else {
|
||||
final BoxParentData actionParentData = actionsSection!.parentData! as BoxParentData;
|
||||
actionParentData.offset = Offset(0.0, dialogSizes.contentHeight + dialogSizes.dividerThickness);
|
||||
}
|
||||
assert(actionsSection!.parentData is BoxParentData);
|
||||
final BoxParentData actionParentData = actionsSection!.parentData! as BoxParentData;
|
||||
actionParentData.offset = Offset(0.0, dialogSizes.contentHeight + dialogSizes.dividerThickness);
|
||||
}
|
||||
|
||||
_AlertDialogSizes _performLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
|
||||
@ -1202,9 +1424,7 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height;
|
||||
|
||||
return _AlertDialogSizes(
|
||||
size: isActionSheet
|
||||
? Size(constraints.maxWidth, dialogHeight)
|
||||
: constraints.constrain(Size(_dialogWidth, dialogHeight)),
|
||||
size: constraints.constrain(Size(_dialogWidth, dialogHeight)),
|
||||
contentHeight: contentSize.height,
|
||||
dividerThickness: dividerThickness,
|
||||
);
|
||||
@ -1263,26 +1483,16 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (isActionSheet) {
|
||||
final MultiChildLayoutParentData contentParentData = contentSection!.parentData! as MultiChildLayoutParentData;
|
||||
contentSection!.paint(context, offset + contentParentData.offset);
|
||||
} else {
|
||||
final BoxParentData contentParentData = contentSection!.parentData! as BoxParentData;
|
||||
contentSection!.paint(context, offset + contentParentData.offset);
|
||||
}
|
||||
final BoxParentData contentParentData = contentSection!.parentData! as BoxParentData;
|
||||
contentSection!.paint(context, offset + contentParentData.offset);
|
||||
|
||||
final bool hasDivider = contentSection!.size.height > 0.0 && actionsSection!.size.height > 0.0;
|
||||
if (hasDivider) {
|
||||
_paintDividerBetweenContentAndActions(context.canvas, offset);
|
||||
}
|
||||
|
||||
if (isActionSheet) {
|
||||
final MultiChildLayoutParentData actionsParentData = actionsSection!.parentData! as MultiChildLayoutParentData;
|
||||
actionsSection!.paint(context, offset + actionsParentData.offset);
|
||||
} else {
|
||||
final BoxParentData actionsParentData = actionsSection!.parentData! as BoxParentData;
|
||||
actionsSection!.paint(context, offset + actionsParentData.offset);
|
||||
}
|
||||
final BoxParentData actionsParentData = actionsSection!.parentData! as BoxParentData;
|
||||
actionsSection!.paint(context, offset + actionsParentData.offset);
|
||||
}
|
||||
|
||||
void _paintDividerBetweenContentAndActions(Canvas canvas, Offset offset) {
|
||||
@ -1299,27 +1509,6 @@ class _RenderCupertinoDialog extends RenderBox {
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
||||
if (isActionSheet) {
|
||||
final MultiChildLayoutParentData contentSectionParentData = contentSection!.parentData! as MultiChildLayoutParentData;
|
||||
final MultiChildLayoutParentData actionsSectionParentData = actionsSection!.parentData! as MultiChildLayoutParentData;
|
||||
return result.addWithPaintOffset(
|
||||
offset: contentSectionParentData.offset,
|
||||
position: position,
|
||||
hitTest: (BoxHitTestResult result, Offset transformed) {
|
||||
assert(transformed == position - contentSectionParentData.offset);
|
||||
return contentSection!.hitTest(result, position: transformed);
|
||||
},
|
||||
) ||
|
||||
result.addWithPaintOffset(
|
||||
offset: actionsSectionParentData.offset,
|
||||
position: position,
|
||||
hitTest: (BoxHitTestResult result, Offset transformed) {
|
||||
assert(transformed == position - actionsSectionParentData.offset);
|
||||
return actionsSection!.hitTest(result, position: transformed);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final BoxParentData contentSectionParentData = contentSection!.parentData! as BoxParentData;
|
||||
final BoxParentData actionsSectionParentData = actionsSection!.parentData! as BoxParentData;
|
||||
return result.addWithPaintOffset(
|
||||
@ -1467,8 +1656,6 @@ class _CupertinoAlertActionSection extends StatelessWidget {
|
||||
const _CupertinoAlertActionSection({
|
||||
required this.children,
|
||||
this.scrollController,
|
||||
this.hasCancelButton = false,
|
||||
this.isActionSheet = false,
|
||||
});
|
||||
|
||||
final List<Widget> children;
|
||||
@ -1480,14 +1667,6 @@ class _CupertinoAlertActionSection extends StatelessWidget {
|
||||
// don't have many actions.
|
||||
final ScrollController? scrollController;
|
||||
|
||||
// Used in ActionSheet to denote if ActionSheet has a separate so-called
|
||||
// cancel button.
|
||||
//
|
||||
// Defaults to false, and is not needed in dialogs.
|
||||
final bool hasCancelButton;
|
||||
|
||||
final bool isActionSheet;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoScrollbar(
|
||||
@ -1500,8 +1679,6 @@ class _CupertinoAlertActionSection extends StatelessWidget {
|
||||
_PressableActionButton(child: child),
|
||||
],
|
||||
dividerThickness: _kDividerThickness,
|
||||
hasCancelButton: hasCancelButton,
|
||||
isActionSheet: isActionSheet,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -1783,47 +1960,38 @@ class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget {
|
||||
required List<Widget> actionButtons,
|
||||
double dividerThickness = 0.0,
|
||||
bool hasCancelButton = false,
|
||||
bool isActionSheet = false,
|
||||
}) : _dividerThickness = dividerThickness,
|
||||
_hasCancelButton = hasCancelButton,
|
||||
_isActionSheet = isActionSheet,
|
||||
super(children: actionButtons);
|
||||
|
||||
final double _dividerThickness;
|
||||
final bool _hasCancelButton;
|
||||
final bool _isActionSheet;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderCupertinoDialogActions(
|
||||
dialogWidth: _isActionSheet
|
||||
? null
|
||||
: _isInAccessibilityMode(context)
|
||||
dialogWidth: _isInAccessibilityMode(context)
|
||||
? _kAccessibilityCupertinoDialogWidth
|
||||
: _kCupertinoDialogWidth,
|
||||
dividerThickness: _dividerThickness,
|
||||
dialogColor: CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetBackgroundColor : _kDialogColor, context),
|
||||
dialogColor: CupertinoDynamicColor.resolve(_kDialogColor, context),
|
||||
dialogPressedColor: CupertinoDynamicColor.resolve(_kPressedColor, context),
|
||||
dividerColor: CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetButtonDividerColor : CupertinoColors.separator, context),
|
||||
dividerColor: CupertinoDynamicColor.resolve(CupertinoColors.separator, context),
|
||||
hasCancelButton: _hasCancelButton,
|
||||
isActionSheet: _isActionSheet,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, _RenderCupertinoDialogActions renderObject) {
|
||||
renderObject
|
||||
..dialogWidth = _isActionSheet
|
||||
? null
|
||||
: _isInAccessibilityMode(context)
|
||||
..dialogWidth = _isInAccessibilityMode(context)
|
||||
? _kAccessibilityCupertinoDialogWidth
|
||||
: _kCupertinoDialogWidth
|
||||
..dividerThickness = _dividerThickness
|
||||
..dialogColor = CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetBackgroundColor : _kDialogColor, context)
|
||||
..dialogColor = CupertinoDynamicColor.resolve(_kDialogColor, context)
|
||||
..dialogPressedColor = CupertinoDynamicColor.resolve(_kPressedColor, context)
|
||||
..dividerColor = CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetButtonDividerColor : CupertinoColors.separator, context)
|
||||
..hasCancelButton = _hasCancelButton
|
||||
..isActionSheet = _isActionSheet;
|
||||
..dividerColor = CupertinoDynamicColor.resolve(CupertinoColors.separator, context)
|
||||
..hasCancelButton = _hasCancelButton;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1872,8 +2040,7 @@ class _RenderCupertinoDialogActions extends RenderBox
|
||||
required Color dialogPressedColor,
|
||||
required Color dividerColor,
|
||||
bool hasCancelButton = false,
|
||||
bool isActionSheet = false,
|
||||
}) : assert(isActionSheet || dialogWidth != null),
|
||||
}) : assert(dialogWidth != null),
|
||||
_dialogWidth = dialogWidth,
|
||||
_buttonBackgroundPaint = Paint()
|
||||
..color = dialogColor
|
||||
@ -1885,8 +2052,7 @@ class _RenderCupertinoDialogActions extends RenderBox
|
||||
..color = dividerColor
|
||||
..style = PaintingStyle.fill,
|
||||
_dividerThickness = dividerThickness,
|
||||
_hasCancelButton = hasCancelButton,
|
||||
_isActionSheet = isActionSheet {
|
||||
_hasCancelButton = hasCancelButton {
|
||||
addAll(children);
|
||||
}
|
||||
|
||||
@ -1953,17 +2119,6 @@ class _RenderCupertinoDialogActions extends RenderBox
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
bool get isActionSheet => _isActionSheet;
|
||||
bool _isActionSheet;
|
||||
set isActionSheet(bool value) {
|
||||
if (value == _isActionSheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isActionSheet = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Iterable<RenderBox> get _pressedButtons {
|
||||
final List<RenderBox> boxes = <RenderBox>[];
|
||||
RenderBox? currentChild = firstChild;
|
||||
@ -2000,26 +2155,18 @@ class _RenderCupertinoDialogActions extends RenderBox
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
return isActionSheet ? constraints.minWidth : dialogWidth!;
|
||||
return dialogWidth!;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
return isActionSheet ? constraints.maxWidth : dialogWidth!;
|
||||
return dialogWidth!;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
if (childCount == 0) {
|
||||
return 0.0;
|
||||
} else if (isActionSheet) {
|
||||
if (childCount == 1) {
|
||||
return firstChild!.getMaxIntrinsicHeight(width) + dividerThickness;
|
||||
}
|
||||
if (hasCancelButton && childCount < 4) {
|
||||
return _computeMinIntrinsicHeightWithCancel(width);
|
||||
}
|
||||
return _computeMinIntrinsicHeightStacked(width);
|
||||
} else if (childCount == 1) {
|
||||
// If only 1 button, display the button across the entire dialog.
|
||||
return _computeMinIntrinsicHeightSideBySide(width);
|
||||
@ -2032,21 +2179,6 @@ class _RenderCupertinoDialogActions extends RenderBox
|
||||
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'
|
||||
// min intrinsic heights.
|
||||
double _computeMinIntrinsicHeightSideBySide(double width) {
|
||||
@ -2084,11 +2216,6 @@ class _RenderCupertinoDialogActions extends RenderBox
|
||||
if (childCount == 0) {
|
||||
// No buttons. Zero height.
|
||||
return 0.0;
|
||||
} else if (isActionSheet) {
|
||||
if (childCount == 1) {
|
||||
return firstChild!.getMaxIntrinsicHeight(width) + dividerThickness;
|
||||
}
|
||||
return _computeMaxIntrinsicHeightStacked(width);
|
||||
} else if (childCount == 1) {
|
||||
// One button. Our max intrinsic height is equal to the button's.
|
||||
return firstChild!.getMaxIntrinsicHeight(width);
|
||||
@ -2160,7 +2287,7 @@ class _RenderCupertinoDialogActions extends RenderBox
|
||||
? ChildLayoutHelper.dryLayoutChild
|
||||
: ChildLayoutHelper.layoutChild;
|
||||
|
||||
if (!isActionSheet && _isSingleButtonRow(dialogWidth!)) {
|
||||
if (_isSingleButtonRow(dialogWidth!)) {
|
||||
if (childCount == 1) {
|
||||
// We have 1 button. Our size is the width of the dialog and the height
|
||||
// of the single button.
|
||||
@ -2249,7 +2376,7 @@ class _RenderCupertinoDialogActions extends RenderBox
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final Canvas canvas = context.canvas;
|
||||
|
||||
if (!isActionSheet && _isSingleButtonRow(size.width)) {
|
||||
if (_isSingleButtonRow(size.width)) {
|
||||
_drawButtonBackgroundsAndDividersSingleRow(canvas, offset);
|
||||
} else {
|
||||
_drawButtonBackgroundsAndDividersStacked(canvas, offset);
|
||||
|
@ -2,6 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// This file is run as part of a reduced test set in CI on Mac and Windows
|
||||
// machines.
|
||||
@Tags(<String>['reduced-test-set'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -266,7 +271,7 @@ void main() {
|
||||
|
||||
final Finder finder = find.byElementPredicate(
|
||||
(Element element) {
|
||||
return element.widget.runtimeType.toString() == '_CupertinoAlertActionSection';
|
||||
return element.widget.runtimeType.toString() == '_ActionSheetActionSection';
|
||||
},
|
||||
);
|
||||
|
||||
@ -428,6 +433,62 @@ void main() {
|
||||
expect(scrollbars[0].controller != scrollbars[1].controller, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('Actions section correctly renders overscrolls', (WidgetTester tester) async {
|
||||
// Verifies that when the actions section overscrolls, the overscroll part
|
||||
// is correctly covered with background.
|
||||
final ScrollController actionScrollController = ScrollController();
|
||||
addTearDown(actionScrollController.dispose);
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
actions: List<Widget>.generate(12, (int i) =>
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {},
|
||||
child: Text('Button ${'*' * i}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button *')));
|
||||
await tester.pumpAndSettle();
|
||||
// The button should be pressed now, since the scrolling gesture has not
|
||||
// taken over.
|
||||
await expectLater(
|
||||
find.byType(CupertinoActionSheet),
|
||||
matchesGoldenFile('cupertinoActionSheet.overscroll.0.png'),
|
||||
);
|
||||
// The dragging gesture must be dispatched in at least two segments.
|
||||
// After the first movement, the gesture is started, but the delta is still
|
||||
// zero. The second movement gives the delta.
|
||||
await gesture.moveBy(const Offset(0, 40));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.moveBy(const Offset(0, 100));
|
||||
// Test the top overscroll. Use `pump` not `pumpAndSettle` to verify the
|
||||
// rendering result of the immediate next frame.
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byType(CupertinoActionSheet),
|
||||
matchesGoldenFile('cupertinoActionSheet.overscroll.1.png'),
|
||||
);
|
||||
|
||||
await gesture.moveBy(const Offset(0, -300));
|
||||
// Test the bottom overscroll. Use `pump` not `pumpAndSettle` to verify the
|
||||
// rendering result of the immediate next frame.
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byType(CupertinoActionSheet),
|
||||
matchesGoldenFile('cupertinoActionSheet.overscroll.2.png'),
|
||||
);
|
||||
await gesture.up();
|
||||
});
|
||||
|
||||
testWidgets('Tap on button calls onPressed', (WidgetTester tester) async {
|
||||
bool wasPressed = false;
|
||||
await tester.pumpWidget(
|
||||
@ -465,6 +526,47 @@ void main() {
|
||||
expect(find.text('One'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Tap at the padding of buttons calls onPressed', (WidgetTester tester) async {
|
||||
// Ensures that the entire button responds to hit tests, not just the text
|
||||
// part.
|
||||
bool wasPressed = false;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('One'),
|
||||
onPressed: () {
|
||||
wasPressed = true;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(wasPressed, isFalse);
|
||||
|
||||
await tester.tapAt(
|
||||
tester.getTopLeft(find.text('One')) - const Offset(20, 0),
|
||||
);
|
||||
|
||||
expect(wasPressed, isTrue);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('One'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Action sheet width is correct when given infinite horizontal space', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
@ -1112,7 +1214,7 @@ void main() {
|
||||
RenderBox findScrollableActionsSectionRenderBox(WidgetTester tester) {
|
||||
final RenderObject actionsSection = tester.renderObject(
|
||||
find.byElementPredicate((Element element) {
|
||||
return element.widget.runtimeType.toString() == '_CupertinoAlertActionSection';
|
||||
return element.widget.runtimeType.toString() == '_ActionSheetActionSection';
|
||||
}),
|
||||
);
|
||||
assert(actionsSection is RenderBox);
|
||||
|
Loading…
Reference in New Issue
Block a user