mirror of
https://github.com/flutter/flutter
synced 2024-06-30 23:54:38 +00:00
Revert "Revert "[CupertinoActionSheet] Add sliding tap gesture" (#150147)"
This reverts commit 317ad4ab7c
.
This commit is contained in:
parent
39edf2a31f
commit
b29535785d
|
@ -6,6 +6,7 @@ import 'dart:math' as math;
|
|||
import 'dart:ui' show ImageFilter;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
@ -445,6 +446,280 @@ class CupertinoPopupSurface extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
typedef _HitTester = HitTestResult Function(Offset location);
|
||||
|
||||
// Recognizes taps with possible sliding during the tap.
|
||||
//
|
||||
// This recognizer only tracks one pointer at a time (called the primary
|
||||
// pointer), and other pointers added while the primary pointer is alive are
|
||||
// ignored and can not be used by other gestures either. After the primary
|
||||
// pointer ends, the pointer added next becomes the new primary pointer (which
|
||||
// starts a new gesture sequence).
|
||||
//
|
||||
// This recognizer only allows [kPrimaryMouseButton].
|
||||
class _SlidingTapGestureRecognizer extends VerticalDragGestureRecognizer {
|
||||
_SlidingTapGestureRecognizer({
|
||||
super.debugOwner,
|
||||
}) {
|
||||
dragStartBehavior = DragStartBehavior.down;
|
||||
}
|
||||
|
||||
/// Called whenever the primary pointer moves regardless of whether drag has
|
||||
/// started.
|
||||
///
|
||||
/// The parameter is the global position of the primary pointer.
|
||||
///
|
||||
/// This is similar to `onUpdate`, but allows the caller to track the primary
|
||||
/// pointer's location before the drag starts, which is useful to enhance
|
||||
/// responsiveness.
|
||||
ValueSetter<Offset>? onResponsiveUpdate;
|
||||
|
||||
/// Called whenever the primary pointer is lifted regardless of whether drag
|
||||
/// has started.
|
||||
///
|
||||
/// The parameter is the global position of the primary pointer.
|
||||
///
|
||||
/// This is similar to `onEnd`, but allows know the primary pointer's final
|
||||
/// location even if the drag never started, which is useful to enhance
|
||||
/// responsiveness.
|
||||
ValueSetter<Offset>? onResponsiveEnd;
|
||||
|
||||
int? _primaryPointer;
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
_primaryPointer ??= event.pointer;
|
||||
super.addAllowedPointer(event);
|
||||
}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
if (pointer == _primaryPointer) {
|
||||
_primaryPointer = null;
|
||||
}
|
||||
super.rejectGesture(pointer);
|
||||
}
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event) {
|
||||
if (event.pointer == _primaryPointer) {
|
||||
if (event is PointerMoveEvent) {
|
||||
onResponsiveUpdate?.call(event.position);
|
||||
}
|
||||
// If this gesture has a competing gesture (such as scrolling), and the
|
||||
// pointer has not moved far enough to get this panning accepted, a
|
||||
// pointer up event should still be considered as an accepted tap up.
|
||||
// Manually accept this gesture here, which triggers onDragEnd.
|
||||
if (event is PointerUpEvent) {
|
||||
resolve(GestureDisposition.accepted);
|
||||
stopTrackingPointer(_primaryPointer!);
|
||||
onResponsiveEnd?.call(event.position);
|
||||
} else {
|
||||
super.handleEvent(event);
|
||||
}
|
||||
if (event is PointerUpEvent || event is PointerCancelEvent) {
|
||||
_primaryPointer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'tap slide';
|
||||
}
|
||||
|
||||
// A region (typically a button) that can receive entering, exiting, and
|
||||
// updating events of a "sliding tap" gesture.
|
||||
//
|
||||
// Some Cupertino widgets, such as action sheets or dialogs, allow the user to
|
||||
// select buttons using "sliding taps", where the user can drag around after
|
||||
// pressing on the screen, and whichever button the drag ends in is selected.
|
||||
//
|
||||
// This class is used to define the regions that sliding taps recognize. This
|
||||
// class must be provided to a `MetaData` widget as `data`, and is typically
|
||||
// implemented by a widget state class. When an eligible dragging gesture
|
||||
// enters, leaves, or ends this `MetaData` widget, corresponding methods of this
|
||||
// class will be called.
|
||||
//
|
||||
// Multiple `_ActionSheetSlideTarget`s might be nested.
|
||||
// `_TargetSelectionGestureRecognizer` uses a simple algorithm that only
|
||||
// compares if the inner-most slide target has changed (which suffices our use
|
||||
// case). Semantically, this means that all outer targets will be treated as
|
||||
// identical to the inner-most one, i.e. when the pointer enters or leaves a
|
||||
// slide target, the corresponding method will be called on all targets that
|
||||
// nest it.
|
||||
abstract class _ActionSheetSlideTarget {
|
||||
// A pointer has entered this region.
|
||||
//
|
||||
// This includes:
|
||||
//
|
||||
// * The pointer has moved into this region from outside.
|
||||
// * The point has contacted the screen in this region. In this case, this
|
||||
// method is called as soon as the pointer down event occurs regardless of
|
||||
// whether the gesture wins the arena immediately.
|
||||
void didEnter();
|
||||
|
||||
// A pointer has exited this region.
|
||||
//
|
||||
// This includes:
|
||||
// * The pointer has moved out of this region.
|
||||
// * The pointer is no longer in contact with the screen.
|
||||
// * The pointer is canceled.
|
||||
// * The gesture loses the arena.
|
||||
// * The gesture ends. In this case, this method is called immediately
|
||||
// before [didConfirm].
|
||||
void didLeave();
|
||||
|
||||
// The drag gesture is completed in this region.
|
||||
//
|
||||
// This method is called immediately after a [didLeave].
|
||||
void didConfirm();
|
||||
}
|
||||
|
||||
// Recognizes sliding taps and thereupon interacts with
|
||||
// `_ActionSheetSlideTarget`s.
|
||||
class _TargetSelectionGestureRecognizer extends GestureRecognizer {
|
||||
_TargetSelectionGestureRecognizer({super.debugOwner, required this.hitTest})
|
||||
: _slidingTap = _SlidingTapGestureRecognizer(debugOwner: debugOwner) {
|
||||
_slidingTap
|
||||
..onDown = _onDown
|
||||
..onResponsiveUpdate = _onUpdate
|
||||
..onResponsiveEnd = _onEnd
|
||||
..onCancel = _onCancel;
|
||||
}
|
||||
|
||||
final _HitTester hitTest;
|
||||
|
||||
final List<_ActionSheetSlideTarget> _currentTargets = <_ActionSheetSlideTarget>[];
|
||||
final _SlidingTapGestureRecognizer _slidingTap;
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {
|
||||
_slidingTap.acceptGesture(pointer);
|
||||
}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
_slidingTap.rejectGesture(pointer);
|
||||
}
|
||||
|
||||
@override
|
||||
void addPointer(PointerDownEvent event) {
|
||||
_slidingTap.addPointer(event);
|
||||
}
|
||||
|
||||
@override
|
||||
void addPointerPanZoom(PointerPanZoomStartEvent event) {
|
||||
_slidingTap.addPointerPanZoom(event);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_slidingTap.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Collect the `_ActionSheetSlideTarget`s that are currently hit by the
|
||||
// pointer, check whether the current target have changed, and invoke their
|
||||
// methods if necessary.
|
||||
void _updateDrag(Offset pointerPosition) {
|
||||
final HitTestResult result = hitTest(pointerPosition);
|
||||
|
||||
// A slide target might nest other targets, therefore multiple targets might
|
||||
// be found.
|
||||
final List<_ActionSheetSlideTarget> foundTargets = <_ActionSheetSlideTarget>[];
|
||||
for (final HitTestEntry entry in result.path) {
|
||||
if (entry.target case final RenderMetaData target) {
|
||||
if (target.metaData is _ActionSheetSlideTarget) {
|
||||
foundTargets.add(target.metaData as _ActionSheetSlideTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare whether the active target has changed by simply comparing the
|
||||
// first (inner-most) avatar of the nest, ignoring the cases where
|
||||
// _currentTargets intersect with foundTargets (see _ActionSheetSlideTarget's
|
||||
// document for more explanation).
|
||||
if (_currentTargets.firstOrNull != foundTargets.firstOrNull) {
|
||||
for (final _ActionSheetSlideTarget target in _currentTargets) {
|
||||
target.didLeave();
|
||||
}
|
||||
_currentTargets
|
||||
..clear()
|
||||
..addAll(foundTargets);
|
||||
for (final _ActionSheetSlideTarget target in _currentTargets) {
|
||||
target.didEnter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onDown(DragDownDetails details) {
|
||||
_updateDrag(details.globalPosition);
|
||||
}
|
||||
|
||||
void _onUpdate(Offset globalPosition) {
|
||||
_updateDrag(globalPosition);
|
||||
}
|
||||
|
||||
void _onEnd(Offset globalPosition) {
|
||||
_updateDrag(globalPosition);
|
||||
for (final _ActionSheetSlideTarget target in _currentTargets) {
|
||||
target.didConfirm();
|
||||
}
|
||||
_currentTargets.clear();
|
||||
}
|
||||
|
||||
void _onCancel() {
|
||||
for (final _ActionSheetSlideTarget target in _currentTargets) {
|
||||
target.didLeave();
|
||||
}
|
||||
_currentTargets.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'target selection';
|
||||
}
|
||||
|
||||
// The gesture detector used by action sheets.
|
||||
//
|
||||
// This gesture detector only recognizes one gesture,
|
||||
// `_TargetSelectionGestureRecognizer`.
|
||||
//
|
||||
// This widget's child might contain another VerticalDragGestureRecognizer if
|
||||
// the actions section or the content section scrolls. Conveniently, Flutter's
|
||||
// gesture algorithm makes the inner gesture take priority.
|
||||
class _ActionSheetGestureDetector extends StatelessWidget {
|
||||
const _ActionSheetGestureDetector({
|
||||
this.child,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
|
||||
HitTestResult _hitTest(BuildContext context, Offset globalPosition) {
|
||||
final int viewId = View.of(context).viewId;
|
||||
final HitTestResult result = HitTestResult();
|
||||
WidgetsBinding.instance.hitTestInView(result, globalPosition, viewId);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
|
||||
gestures[_TargetSelectionGestureRecognizer] = GestureRecognizerFactoryWithHandlers<_TargetSelectionGestureRecognizer>(
|
||||
() => _TargetSelectionGestureRecognizer(
|
||||
debugOwner: this,
|
||||
hitTest: (Offset globalPosition) => _hitTest(context, globalPosition),
|
||||
),
|
||||
(_TargetSelectionGestureRecognizer instance) {}
|
||||
);
|
||||
|
||||
return RawGestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
gestures: gestures,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An iOS-style action sheet.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=U-ao8p4A82k}
|
||||
|
@ -518,7 +793,7 @@ class CupertinoActionSheet extends StatefulWidget {
|
|||
|
||||
/// The set of actions that are displayed for the user to select.
|
||||
///
|
||||
/// Typically this is a list of [CupertinoActionSheetAction] widgets.
|
||||
/// This must be a list of [CupertinoActionSheetAction] widgets.
|
||||
final List<Widget>? actions;
|
||||
|
||||
/// A scroll controller that can be used to control the scrolling of the
|
||||
|
@ -537,7 +812,7 @@ class CupertinoActionSheet extends StatefulWidget {
|
|||
/// The optional cancel button that is grouped separately from the other
|
||||
/// actions.
|
||||
///
|
||||
/// Typically this is an [CupertinoActionSheetAction] widget.
|
||||
/// This must be a [CupertinoActionSheetAction] widget.
|
||||
final Widget? cancelButton;
|
||||
|
||||
@override
|
||||
|
@ -663,10 +938,16 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
|
|||
),
|
||||
child: SizedBox(
|
||||
width: actionSheetWidth - _kActionSheetEdgeHorizontalPadding * 2,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
child: _ActionSheetGestureDetector(
|
||||
child: Semantics(
|
||||
explicitChildNodes: true,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -686,7 +967,7 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
|
|||
///
|
||||
/// * [CupertinoActionSheet], an alert that presents the user with a set of two or
|
||||
/// more choices related to the current context.
|
||||
class CupertinoActionSheetAction extends StatelessWidget {
|
||||
class CupertinoActionSheetAction extends StatefulWidget {
|
||||
/// Creates an action for an iOS-style action sheet.
|
||||
const CupertinoActionSheetAction({
|
||||
super.key,
|
||||
|
@ -696,7 +977,10 @@ class CupertinoActionSheetAction extends StatelessWidget {
|
|||
required this.child,
|
||||
});
|
||||
|
||||
/// The callback that is called when the button is tapped.
|
||||
/// The callback that is called when the button is selected.
|
||||
///
|
||||
/// The button can be selected by either by tapping on this button or by
|
||||
/// pressing elsewhere and sliding onto this button before releasing.
|
||||
final VoidCallback onPressed;
|
||||
|
||||
/// Whether this action is the default choice in the action sheet.
|
||||
|
@ -714,22 +998,42 @@ class CupertinoActionSheetAction extends StatelessWidget {
|
|||
/// Typically a [Text] widget.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<CupertinoActionSheetAction> createState() => _CupertinoActionSheetActionState();
|
||||
}
|
||||
|
||||
class _CupertinoActionSheetActionState extends State<CupertinoActionSheetAction>
|
||||
implements _ActionSheetSlideTarget {
|
||||
// |_ActionSheetSlideTarget|
|
||||
@override
|
||||
void didEnter() {}
|
||||
|
||||
// |_ActionSheetSlideTarget|
|
||||
@override
|
||||
void didLeave() {}
|
||||
|
||||
// |_ActionSheetSlideTarget|
|
||||
@override
|
||||
void didConfirm() {
|
||||
widget.onPressed();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle style = _kActionSheetActionStyle.copyWith(
|
||||
color: isDestructiveAction
|
||||
color: widget.isDestructiveAction
|
||||
? CupertinoDynamicColor.resolve(CupertinoColors.systemRed, context)
|
||||
: CupertinoTheme.of(context).primaryColor,
|
||||
);
|
||||
|
||||
if (isDefaultAction) {
|
||||
if (widget.isDefaultAction) {
|
||||
style = style.copyWith(fontWeight: FontWeight.w600);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
|
||||
child: GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: MetaData(
|
||||
metaData: this,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
|
@ -737,6 +1041,7 @@ class CupertinoActionSheetAction extends StatelessWidget {
|
|||
),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
onTap: widget.onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0,
|
||||
|
@ -745,7 +1050,7 @@ class CupertinoActionSheetAction extends StatelessWidget {
|
|||
child: DefaultTextStyle(
|
||||
style: style,
|
||||
textAlign: TextAlign.center,
|
||||
child: Center(child: child),
|
||||
child: Center(child: widget.child),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -780,20 +1085,26 @@ class _ActionSheetButtonBackground extends StatefulWidget {
|
|||
_ActionSheetButtonBackgroundState createState() => _ActionSheetButtonBackgroundState();
|
||||
}
|
||||
|
||||
class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackground> {
|
||||
class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackground> implements _ActionSheetSlideTarget {
|
||||
bool isBeingPressed = false;
|
||||
|
||||
void _onTapDown(TapDownDetails event) {
|
||||
// |_ActionSheetSlideTarget|
|
||||
@override
|
||||
void didEnter() {
|
||||
setState(() { isBeingPressed = true; });
|
||||
widget.onPressStateChange?.call(true);
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails event) {
|
||||
// |_ActionSheetSlideTarget|
|
||||
@override
|
||||
void didLeave() {
|
||||
setState(() { isBeingPressed = false; });
|
||||
widget.onPressStateChange?.call(false);
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
// |_ActionSheetSlideTarget|
|
||||
@override
|
||||
void didConfirm() {
|
||||
setState(() { isBeingPressed = false; });
|
||||
widget.onPressStateChange?.call(false);
|
||||
}
|
||||
|
@ -812,11 +1123,8 @@ class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackgrou
|
|||
: CupertinoColors.secondarySystemGroupedBackground;
|
||||
borderRadius = const BorderRadius.all(Radius.circular(_kCornerRadius));
|
||||
}
|
||||
return GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
return MetaData(
|
||||
metaData: this,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
|
|
|
@ -531,7 +531,7 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets('Tap on button calls onPressed', (WidgetTester tester) async {
|
||||
testWidgets('Taps on button calls onPressed', (WidgetTester tester) async {
|
||||
bool wasPressed = false;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
|
@ -541,6 +541,7 @@ void main() {
|
|||
CupertinoActionSheetAction(
|
||||
child: const Text('One'),
|
||||
onPressed: () {
|
||||
expect(wasPressed, false);
|
||||
wasPressed = true;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
|
@ -568,7 +569,48 @@ void main() {
|
|||
expect(find.text('One'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Tap at the padding of buttons calls onPressed', (WidgetTester tester) async {
|
||||
testWidgets('Can tap after scrolling', (WidgetTester tester) async {
|
||||
int? wasPressed;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
actions: List<Widget>.generate(20, (int i) =>
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
expect(wasPressed, null);
|
||||
wasPressed = i;
|
||||
},
|
||||
child: Text('Button $i'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Button 19').hitTestable(), findsNothing);
|
||||
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button 1')));
|
||||
await tester.pumpAndSettle();
|
||||
// The dragging gesture must be dispatched in at least two segments.
|
||||
// The first movement starts the gesture without setting a delta.
|
||||
await gesture.moveBy(const Offset(0, -20));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.moveBy(const Offset(0, -1000));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Button 19').hitTestable(), findsOne);
|
||||
|
||||
await tester.tap(find.text('Button 19'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(wasPressed, 19);
|
||||
});
|
||||
|
||||
testWidgets('Taps 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;
|
||||
|
@ -580,6 +622,7 @@ void main() {
|
|||
CupertinoActionSheetAction(
|
||||
child: const Text('One'),
|
||||
onPressed: () {
|
||||
expect(wasPressed, false);
|
||||
wasPressed = true;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
|
@ -609,6 +652,321 @@ void main() {
|
|||
expect(find.text('One'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Taps on a button can be slided to other buttons', (WidgetTester tester) async {
|
||||
int? pressed;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('One'),
|
||||
onPressed: () {
|
||||
expect(pressed, null);
|
||||
pressed = 1;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('Two'),
|
||||
onPressed: () {
|
||||
expect(pressed, null);
|
||||
pressed = 2;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(pressed, null);
|
||||
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('One')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.text('Two')));
|
||||
await tester.pumpAndSettle();
|
||||
await expectLater(
|
||||
find.byType(CupertinoActionSheet),
|
||||
matchesGoldenFile('cupertinoActionSheet.press-drag.png'),
|
||||
);
|
||||
|
||||
await gesture.up();
|
||||
expect(pressed, 2);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('One'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Taps on the content can be slided to other buttons', (WidgetTester tester) async {
|
||||
bool wasPressed = false;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: const Text('The title'),
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('One'),
|
||||
onPressed: () {
|
||||
},
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
expect(wasPressed, false);
|
||||
wasPressed = true;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(wasPressed, false);
|
||||
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('The title')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.text('Cancel')));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.up();
|
||||
expect(wasPressed, true);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('One'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Taps on the barrier can not be slided to buttons', (WidgetTester tester) async {
|
||||
bool wasPressed = false;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: const Text('The title'),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
expect(wasPressed, false);
|
||||
wasPressed = true;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(wasPressed, false);
|
||||
|
||||
// Press on the barrier.
|
||||
final TestGesture gesture = await tester.startGesture(const Offset(100, 100));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.text('Cancel')));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.up();
|
||||
expect(wasPressed, false);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Cancel'), findsOne);
|
||||
});
|
||||
|
||||
testWidgets('Sliding taps can still yield to scrolling after horizontal movement', (WidgetTester tester) async {
|
||||
int? pressed;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
message: Text('Long message' * 200),
|
||||
actions: List<Widget>.generate(10, (int i) =>
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
expect(pressed, null);
|
||||
pressed = i;
|
||||
},
|
||||
child: Text('Button $i'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Starts on a button.
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button 0')));
|
||||
await tester.pumpAndSettle();
|
||||
// Move horizontally.
|
||||
await gesture.moveBy(const Offset(-10, 2));
|
||||
await gesture.moveBy(const Offset(-100, 2));
|
||||
await tester.pumpAndSettle();
|
||||
// Scroll up.
|
||||
await gesture.moveBy(const Offset(0, -40));
|
||||
await gesture.moveBy(const Offset(0, -1000));
|
||||
await tester.pumpAndSettle();
|
||||
// Stop scrolling.
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
// The actions section should have been scrolled up and Button 9 is visible.
|
||||
await tester.tap(find.text('Button 9'));
|
||||
expect(pressed, 9);
|
||||
});
|
||||
|
||||
testWidgets('Sliding taps is responsive even before the drag starts', (WidgetTester tester) async {
|
||||
int? pressed;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
message: Text('Long message' * 200),
|
||||
actions: List<Widget>.generate(10, (int i) =>
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
expect(pressed, null);
|
||||
pressed = i;
|
||||
},
|
||||
child: Text('Button $i'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Find the location right within the upper edge of button 1.
|
||||
final Offset start = tester.getTopLeft(find.text('Button 1')) + const Offset(30, -15);
|
||||
// Verify that the start location is within button 1.
|
||||
await tester.tapAt(start);
|
||||
expect(pressed, 1);
|
||||
pressed = null;
|
||||
|
||||
final TestGesture gesture = await tester.startGesture(start);
|
||||
await tester.pumpAndSettle();
|
||||
// Move slightly upwards without starting the drag
|
||||
await gesture.moveBy(const Offset(0, -10));
|
||||
await tester.pumpAndSettle();
|
||||
// Stop scrolling.
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(pressed, 0);
|
||||
});
|
||||
|
||||
testWidgets('Sliding taps only recognizes the primary pointer', (WidgetTester tester) async {
|
||||
int? pressed;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: const Text('The title'),
|
||||
actions: List<Widget>.generate(8, (int i) =>
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
expect(pressed, null);
|
||||
pressed = i;
|
||||
},
|
||||
child: Text('Button $i'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Start gesture 1 at button 0
|
||||
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('Button 0')));
|
||||
await gesture1.moveBy(const Offset(0, 20)); // Starts the gesture
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Start gesture 2 at button 1.
|
||||
final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.text('Button 1')));
|
||||
await gesture2.moveBy(const Offset(0, 20)); // Starts the gesture
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Move gesture 1 to button 2 and release.
|
||||
await gesture1.moveTo(tester.getCenter(find.text('Button 2')));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture1.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(pressed, 2);
|
||||
pressed = null;
|
||||
|
||||
// Tap at button 3, which becomes the new primary pointer and is recognized.
|
||||
await tester.tap(find.text('Button 3'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(pressed, 3);
|
||||
pressed = null;
|
||||
|
||||
// Move gesture 2 to button 4 and release.
|
||||
await gesture2.moveTo(tester.getCenter(find.text('Button 4')));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture2.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Non-primary pointers should not be recognized.
|
||||
expect(pressed, null);
|
||||
});
|
||||
|
||||
testWidgets('Non-primary pointers can trigger scroll', (WidgetTester tester) async {
|
||||
int? pressed;
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
actions: List<Widget>.generate(12, (int i) =>
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
expect(pressed, null);
|
||||
pressed = i;
|
||||
},
|
||||
child: Text('Button $i'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Start gesture 1 at button 0
|
||||
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('Button 0')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.getTopLeft(find.text('Button 11')).dy, greaterThan(400));
|
||||
|
||||
// Start gesture 2 at button 1 and scrolls.
|
||||
final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.text('Button 1')));
|
||||
await gesture2.moveBy(const Offset(0, -20));
|
||||
await gesture2.moveBy(const Offset(0, -500));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.getTopLeft(find.text('Button 11')).dy, lessThan(400));
|
||||
|
||||
// Release gesture 1, which should not trigger any buttons.
|
||||
await gesture1.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(pressed, null);
|
||||
});
|
||||
|
||||
testWidgets('Action sheet width is correct when given infinite horizontal space', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
|
@ -871,6 +1229,7 @@ void main() {
|
|||
cancelButton: CupertinoActionSheetAction(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
expect(wasPressed, false);
|
||||
wasPressed = true;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
|
@ -934,6 +1293,39 @@ void main() {
|
|||
expect(tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Two')).dy, 526.0);
|
||||
});
|
||||
|
||||
testWidgets('Action buttons shows pressed color as soon as the pointer is down', (WidgetTester tester) async {
|
||||
// Verifies that the the pressed color is not delayed for some milliseconds,
|
||||
// a symptom if the color relies on a tap gesture timing out.
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
CupertinoActionSheet(
|
||||
title: const Text('The title'),
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('One'),
|
||||
onPressed: () { },
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('Two'),
|
||||
onPressed: () { },
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Go'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('One')));
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byType(CupertinoActionSheet),
|
||||
matchesGoldenFile('cupertinoActionSheet.pressed.png'),
|
||||
);
|
||||
await pointer.up();
|
||||
});
|
||||
|
||||
testWidgets('Enter/exit animation is correct', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
createAppWithButtonThatLaunchesActionSheet(
|
||||
|
|
Loading…
Reference in New Issue
Block a user