Skip to content

Commit 7c1a27c

Browse files
dkwingsmtTytaniumDev
authored andcommitted
[CupertinoActionSheet] Make _ActionSheetButtonBackground stateless (flutter#152283)
This PR is a refactor that makes `_ActionSheetButtonBackground` widgets no longer record their own `pressed` state, but instead receive this state from their parent. In this way, `_ActionSheetButtonBackground` becomes a stateless widget. The children states are duplicate because the parent has to keep track of the state for rendering dividers. An obstacle with this change is that `_ActionSheetButtonBackground` needs an object that is persistent across rebuilds to provide to `Metadata.data`. Either it is kept as a stateful widget without any actual states, or it is made stateless and its `Element` as the object. After discussion, the first option is used. `_ActionSheetSlideTarget` is renamed to `_SlideTarget` since the alert dialog will soon use this class as well. This refactor shouldn't need additional tests. Still, one test is added for a behavior that I broke during development and found not covered by the unit tests then.
1 parent a7f0836 commit 7c1a27c

File tree

2 files changed

+98
-83
lines changed

2 files changed

+98
-83
lines changed

packages/flutter/lib/src/cupertino/dialog.dart

Lines changed: 90 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -663,14 +663,14 @@ class _SlidingTapGestureRecognizer extends VerticalDragGestureRecognizer {
663663
// enters, leaves, or ends this `MetaData` widget, corresponding methods of this
664664
// class will be called.
665665
//
666-
// Multiple `_ActionSheetSlideTarget`s might be nested.
666+
// Multiple `_SlideTarget`s might be nested.
667667
// `_TargetSelectionGestureRecognizer` uses a simple algorithm that only
668668
// compares if the inner-most slide target has changed (which suffices our use
669669
// case). Semantically, this means that all outer targets will be treated as
670670
// identical to the inner-most one, i.e. when the pointer enters or leaves a
671671
// slide target, the corresponding method will be called on all targets that
672672
// nest it.
673-
abstract class _ActionSheetSlideTarget {
673+
abstract class _SlideTarget {
674674
// A pointer has entered this region.
675675
//
676676
// This includes:
@@ -702,7 +702,7 @@ abstract class _ActionSheetSlideTarget {
702702
}
703703

704704
// Recognizes sliding taps and thereupon interacts with
705-
// `_ActionSheetSlideTarget`s.
705+
// `_SlideTarget`s.
706706
class _TargetSelectionGestureRecognizer extends GestureRecognizer {
707707
_TargetSelectionGestureRecognizer({super.debugOwner, required this.hitTest})
708708
: _slidingTap = _SlidingTapGestureRecognizer(debugOwner: debugOwner) {
@@ -715,7 +715,7 @@ class _TargetSelectionGestureRecognizer extends GestureRecognizer {
715715

716716
final _HitTester hitTest;
717717

718-
final List<_ActionSheetSlideTarget> _currentTargets = <_ActionSheetSlideTarget>[];
718+
final List<_SlideTarget> _currentTargets = <_SlideTarget>[];
719719
final _SlidingTapGestureRecognizer _slidingTap;
720720

721721
@override
@@ -744,7 +744,7 @@ class _TargetSelectionGestureRecognizer extends GestureRecognizer {
744744
super.dispose();
745745
}
746746

747-
// Collect the `_ActionSheetSlideTarget`s that are currently hit by the
747+
// Collect the `_SlideTarget`s that are currently hit by the
748748
// pointer, check whether the current target have changed, and invoke their
749749
// methods if necessary.
750750
//
@@ -755,27 +755,27 @@ class _TargetSelectionGestureRecognizer extends GestureRecognizer {
755755

756756
// A slide target might nest other targets, therefore multiple targets might
757757
// be found.
758-
final List<_ActionSheetSlideTarget> foundTargets = <_ActionSheetSlideTarget>[];
758+
final List<_SlideTarget> foundTargets = <_SlideTarget>[];
759759
for (final HitTestEntry entry in result.path) {
760760
if (entry.target case final RenderMetaData target) {
761-
if (target.metaData is _ActionSheetSlideTarget) {
762-
foundTargets.add(target.metaData as _ActionSheetSlideTarget);
761+
if (target.metaData is _SlideTarget) {
762+
foundTargets.add(target.metaData as _SlideTarget);
763763
}
764764
}
765765
}
766766

767767
// Compare whether the active target has changed by simply comparing the
768768
// first (inner-most) avatar of the nest, ignoring the cases where
769-
// _currentTargets intersect with foundTargets (see _ActionSheetSlideTarget's
769+
// _currentTargets intersect with foundTargets (see _SlideTarget's
770770
// document for more explanation).
771771
if (_currentTargets.firstOrNull != foundTargets.firstOrNull) {
772-
for (final _ActionSheetSlideTarget target in _currentTargets) {
772+
for (final _SlideTarget target in _currentTargets) {
773773
target.didLeave();
774774
}
775775
_currentTargets
776776
..clear()
777777
..addAll(foundTargets);
778-
for (final _ActionSheetSlideTarget target in _currentTargets) {
778+
for (final _SlideTarget target in _currentTargets) {
779779
target.didEnter(fromPointerDown: fromPointerDown);
780780
}
781781
}
@@ -791,14 +791,14 @@ class _TargetSelectionGestureRecognizer extends GestureRecognizer {
791791

792792
void _onEnd(Offset globalPosition) {
793793
_updateDrag(globalPosition, fromPointerDown: false);
794-
for (final _ActionSheetSlideTarget target in _currentTargets) {
794+
for (final _SlideTarget target in _currentTargets) {
795795
target.didConfirm();
796796
}
797797
_currentTargets.clear();
798798
}
799799

800800
void _onCancel() {
801-
for (final _ActionSheetSlideTarget target in _currentTargets) {
801+
for (final _SlideTarget target in _currentTargets) {
802802
target.didLeave();
803803
}
804804
_currentTargets.clear();
@@ -949,6 +949,9 @@ class CupertinoActionSheet extends StatefulWidget {
949949
}
950950

951951
class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
952+
int? _pressedIndex;
953+
static const int _kCancelButtonIndex = -1;
954+
952955
ScrollController? _backupMessageScrollController;
953956

954957
ScrollController? _backupActionScrollController;
@@ -1004,6 +1007,20 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
10041007
);
10051008
}
10061009

1010+
void _onPressedUpdate(int actionIndex, bool state) {
1011+
if (!state) {
1012+
if (_pressedIndex == actionIndex) {
1013+
setState(() {
1014+
_pressedIndex = null;
1015+
});
1016+
}
1017+
} else {
1018+
setState(() {
1019+
_pressedIndex = actionIndex;
1020+
});
1021+
}
1022+
}
1023+
10071024
Widget _buildCancelButton() {
10081025
assert(widget.cancelButton != null);
10091026
final double cancelPadding = (widget.actions != null || widget.message != null || widget.title != null)
@@ -1012,7 +1029,10 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
10121029
padding: EdgeInsets.only(top: cancelPadding),
10131030
child: _ActionSheetButtonBackground(
10141031
isCancel: true,
1015-
onPressStateChange: (_) {},
1032+
pressed: _pressedIndex == _kCancelButtonIndex,
1033+
onPressStateChange: (bool state) {
1034+
_onPressedUpdate(_kCancelButtonIndex, state);
1035+
},
10161036
child: widget.cancelButton!,
10171037
),
10181038
);
@@ -1106,6 +1126,8 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
11061126
child: BackdropFilter(
11071127
filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount),
11081128
child: _ActionSheetMainSheet(
1129+
pressedIndex: _pressedIndex,
1130+
onPressedUpdate: _onPressedUpdate,
11091131
scrollController: _effectiveActionScrollController,
11101132
contentSection: _buildContent(context),
11111133
actions: widget.actions ?? List<Widget>.empty(),
@@ -1208,16 +1230,16 @@ class CupertinoActionSheetAction extends StatefulWidget {
12081230
}
12091231

12101232
class _CupertinoActionSheetActionState extends State<CupertinoActionSheetAction>
1211-
implements _ActionSheetSlideTarget {
1212-
// |_ActionSheetSlideTarget|
1233+
implements _SlideTarget {
1234+
// |_SlideTarget|
12131235
@override
12141236
void didEnter({required bool fromPointerDown}) {}
12151237

1216-
// |_ActionSheetSlideTarget|
1238+
// |_SlideTarget|
12171239
@override
12181240
void didLeave() {}
12191241

1220-
// |_ActionSheetSlideTarget|
1242+
// |_SlideTarget|
12211243
@override
12221244
void didConfirm() {
12231245
widget.onPressed();
@@ -1307,15 +1329,23 @@ class _CupertinoActionSheetActionState extends State<CupertinoActionSheetAction>
13071329

13081330
// Renders the background of a button (both the pressed background and the idle
13091331
// background) and reports its state to the parent with `onPressStateChange`.
1332+
//
1333+
// Although this class doesn't keep any states, it's still a stateful widget
1334+
// because the state is used as a persistent object across rebuilds to provide
1335+
// to [MetaData.data].
13101336
class _ActionSheetButtonBackground extends StatefulWidget {
13111337
const _ActionSheetButtonBackground({
13121338
this.isCancel = false,
1339+
required this.pressed,
13131340
this.onPressStateChange,
13141341
required this.child,
13151342
});
13161343

13171344
final bool isCancel;
13181345

1346+
/// Whether the user is holding on this button.
1347+
final bool pressed;
1348+
13191349
/// Called when the user taps down or lifts up on the button.
13201350
///
13211351
/// The boolean value is true if the user is tapping down on the button.
@@ -1330,9 +1360,7 @@ class _ActionSheetButtonBackground extends StatefulWidget {
13301360
_ActionSheetButtonBackgroundState createState() => _ActionSheetButtonBackgroundState();
13311361
}
13321362

1333-
class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackground> implements _ActionSheetSlideTarget {
1334-
bool isBeingPressed = false;
1335-
1363+
class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackground> implements _SlideTarget {
13361364
void _emitVibration(){
13371365
switch (defaultTargetPlatform) {
13381366
case TargetPlatform.iOS:
@@ -1346,27 +1374,24 @@ class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackgrou
13461374
}
13471375
}
13481376

1349-
// |_ActionSheetSlideTarget|
1377+
// |_SlideTarget|
13501378
@override
13511379
void didEnter({required bool fromPointerDown}) {
1352-
setState(() { isBeingPressed = true; });
13531380
widget.onPressStateChange?.call(true);
13541381
if (!fromPointerDown) {
13551382
_emitVibration();
13561383
}
13571384
}
13581385

1359-
// |_ActionSheetSlideTarget|
1386+
// |_SlideTarget|
13601387
@override
13611388
void didLeave() {
1362-
setState(() { isBeingPressed = false; });
13631389
widget.onPressStateChange?.call(false);
13641390
}
13651391

1366-
// |_ActionSheetSlideTarget|
1392+
// |_SlideTarget|
13671393
@override
13681394
void didConfirm() {
1369-
setState(() { isBeingPressed = false; });
13701395
widget.onPressStateChange?.call(false);
13711396
}
13721397

@@ -1375,11 +1400,11 @@ class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackgrou
13751400
late final Color backgroundColor;
13761401
BorderRadius? borderRadius;
13771402
if (!widget.isCancel) {
1378-
backgroundColor = isBeingPressed
1403+
backgroundColor = widget.pressed
13791404
? _kActionSheetPressedColor
13801405
: _kActionSheetBackgroundColor;
13811406
} else {
1382-
backgroundColor = isBeingPressed
1407+
backgroundColor = widget.pressed
13831408
? _kActionSheetCancelPressedColor
13841409
: _kActionSheetCancelColor;
13851410
borderRadius = const BorderRadius.all(Radius.circular(_kCornerRadius));
@@ -1566,6 +1591,7 @@ class _ActionSheetActionSection extends StatelessWidget {
15661591
));
15671592
}
15681593
column.add(_ActionSheetButtonBackground(
1594+
pressed: pressedIndex == actionIndex,
15691595
onPressStateChange: (bool state) {
15701596
onPressedUpdate(actionIndex, state);
15711597
},
@@ -1586,86 +1612,69 @@ class _ActionSheetActionSection extends StatelessWidget {
15861612
}
15871613

15881614
// The part of an action sheet without the cancel button.
1589-
class _ActionSheetMainSheet extends StatefulWidget {
1615+
class _ActionSheetMainSheet extends StatelessWidget {
15901616
const _ActionSheetMainSheet({
1617+
required this.pressedIndex,
1618+
required this.onPressedUpdate,
15911619
required this.scrollController,
15921620
required this.actions,
15931621
required this.contentSection,
15941622
required this.dividerColor,
15951623
});
15961624

1625+
final int? pressedIndex;
1626+
final _PressedUpdateHandler onPressedUpdate;
15971627
final ScrollController? scrollController;
15981628
final List<Widget> actions;
15991629
final Widget? contentSection;
16001630
final Color dividerColor;
16011631

1602-
@override
1603-
_ActionSheetMainSheetState createState() => _ActionSheetMainSheetState();
1604-
}
1605-
1606-
class _ActionSheetMainSheetState extends State<_ActionSheetMainSheet> {
1607-
int? _pressedIndex;
1608-
1609-
bool get _hasContent => widget.contentSection != null;
1610-
bool get _hasActions => widget.actions.isNotEmpty;
1611-
1612-
void _onPressedUpdate(int actionIndex, bool state) {
1613-
if (!state) {
1614-
if (_pressedIndex == actionIndex) {
1615-
setState(() {
1616-
_pressedIndex = null;
1617-
});
1618-
}
1619-
} else {
1620-
setState(() {
1621-
_pressedIndex = actionIndex;
1622-
});
1623-
}
1632+
Widget _scrolledActionsSection(BuildContext context) {
1633+
final Color backgroundColor = CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context);
1634+
return _OverscrollBackground(
1635+
scrollController: scrollController,
1636+
color: backgroundColor,
1637+
child: _ActionSheetActionSection(
1638+
actions: actions,
1639+
scrollController: scrollController,
1640+
dividerColor: dividerColor,
1641+
backgroundColor: backgroundColor,
1642+
pressedIndex: pressedIndex,
1643+
onPressedUpdate: onPressedUpdate,
1644+
),
1645+
);
16241646
}
16251647

16261648
Widget _dividerAndActionsSection(BuildContext context) {
1627-
if (!_hasActions) {
1628-
return _empty;
1629-
}
16301649
final Color backgroundColor = CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context);
16311650
return Column(
16321651
mainAxisSize: MainAxisSize.min,
16331652
crossAxisAlignment: CrossAxisAlignment.stretch,
16341653
children: <Widget>[
1635-
if (_hasContent)
1636-
_Divider(
1637-
dividerColor: widget.dividerColor,
1638-
hiddenColor: backgroundColor,
1639-
hidden: false,
1640-
),
1654+
_Divider(
1655+
dividerColor: dividerColor,
1656+
hiddenColor: backgroundColor,
1657+
hidden: false,
1658+
),
16411659
Flexible(
1642-
child: _OverscrollBackground(
1643-
scrollController: widget.scrollController,
1644-
color: backgroundColor,
1645-
child: _ActionSheetActionSection(
1646-
actions: widget.actions,
1647-
scrollController: widget.scrollController,
1648-
pressedIndex: _pressedIndex,
1649-
dividerColor: widget.dividerColor,
1650-
backgroundColor: backgroundColor,
1651-
onPressedUpdate: _onPressedUpdate,
1652-
),
1653-
),
1660+
child: _scrolledActionsSection(context),
16541661
),
16551662
],
16561663
);
16571664
}
16581665

16591666
@override
16601667
Widget build(BuildContext context) {
1661-
return LayoutBuilder(
1662-
builder: (BuildContext context, BoxConstraints constraints) {
1663-
return _PriorityColumn(
1664-
top: widget.contentSection ?? _empty,
1665-
bottom: _dividerAndActionsSection(context),
1666-
bottomMinHeight: _kActionSheetActionsSectionMinHeight + _kDividerThickness,
1667-
);
1668-
},
1668+
if (actions.isEmpty) {
1669+
return contentSection ?? _empty;
1670+
}
1671+
if (contentSection == null) {
1672+
return _scrolledActionsSection(context);
1673+
}
1674+
return _PriorityColumn(
1675+
top: contentSection!,
1676+
bottom: _dividerAndActionsSection(context),
1677+
bottomMinHeight: _kActionSheetActionsSectionMinHeight + _kDividerThickness,
16691678
);
16701679
}
16711680

packages/flutter/test/cupertino/action_sheet_test.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,13 +1484,19 @@ void main() {
14841484

14851485
expect(wasPressed, isFalse);
14861486

1487-
await tester.tap(find.text('Cancel'));
1487+
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Cancel')));
1488+
await tester.pumpAndSettle();
1489+
// Verify that the cancel button shows the pressed color.
1490+
await expectLater(
1491+
find.byType(CupertinoActionSheet),
1492+
matchesGoldenFile('cupertinoActionSheet.pressedCancel.png'),
1493+
);
14881494

1495+
await gesture.up();
14891496
expect(wasPressed, isTrue);
14901497

14911498
await tester.pump();
14921499
await tester.pump(const Duration(seconds: 1));
1493-
14941500
expect(find.text('Cancel'), findsNothing);
14951501
});
14961502

0 commit comments

Comments
 (0)