Skip to content

Commit

Permalink
Revert "Add ability for ModalRoutes to ignore pointers during trans…
Browse files Browse the repository at this point in the history
…itions and do so on `Cupertino` routes (#95757)" (#104520)

This reverts commit 4c0b0be.
  • Loading branch information
XilaiZhang authored May 24, 2022
1 parent 7ca4984 commit de230d3
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 508 deletions.
9 changes: 0 additions & 9 deletions packages/flutter/lib/src/cupertino/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,6 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
return result;
}

@override
bool get ignorePointerDuringTransitions => true;

// Called by _CupertinoBackGestureDetector when a pop ("back") drag start
// gesture is detected. The returned controller handles all of the subsequent
// drag events.
Expand Down Expand Up @@ -1052,9 +1049,6 @@ class CupertinoModalPopupRoute<T> extends PopupRoute<T> {
@override
Duration get transitionDuration => _kModalPopupTransitionDuration;

@override
bool get ignorePointerDuringTransitions => true;

Animation<double>? _animation;

late Tween<Offset> _offsetTween;
Expand Down Expand Up @@ -1355,7 +1349,4 @@ class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
);

@override
bool get ignorePointerDuringTransitions => true;
}
207 changes: 75 additions & 132 deletions packages/flutter/lib/src/widgets/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -293,85 +293,73 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
final VoidCallback? previousTrainHoppingListenerRemover = _trainHoppingListenerRemover;
_trainHoppingListenerRemover = null;

if (nextRoute is TransitionRoute<dynamic>) {
if (canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
final Animation<double>? current = _secondaryAnimation.parent;
if (current != null) {
final Animation<double> currentTrain = (current is TrainHoppingAnimation ? current.currentTrain : current)!;
final Animation<double> nextTrain = nextRoute._animation!;
if (
currentTrain.value == nextTrain.value ||
nextTrain.status == AnimationStatus.completed ||
nextTrain.status == AnimationStatus.dismissed
) {
_setSecondaryAnimation(nextTrain, nextRoute.completed);
} else {
// Two trains animate at different values. We have to do train hopping.
// There are three possibilities of train hopping:
// 1. We hop on the nextTrain when two trains meet in the middle using
// TrainHoppingAnimation.
// 2. There is no chance to hop on nextTrain because two trains never
// cross each other. We have to directly set the animation to
// nextTrain once the nextTrain stops animating.
// 3. A new _updateSecondaryAnimation is called before train hopping
// finishes. We leave a listener remover for the next call to
// properly clean up the existing train hopping.
TrainHoppingAnimation? newAnimation;
void jumpOnAnimationEnd(AnimationStatus status) {
switch (status) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
// The nextTrain has stopped animating without train hopping.
// Directly sets the secondary animation and disposes the
// TrainHoppingAnimation.
_setSecondaryAnimation(nextTrain, nextRoute.completed);
if (_trainHoppingListenerRemover != null) {
_trainHoppingListenerRemover!();
_trainHoppingListenerRemover = null;
}
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
}
}
_trainHoppingListenerRemover = () {
nextTrain.removeStatusListener(jumpOnAnimationEnd);
newAnimation?.dispose();
};
nextTrain.addStatusListener(jumpOnAnimationEnd);
newAnimation = TrainHoppingAnimation(
currentTrain,
nextTrain,
onSwitchedTrain: () {
assert(_secondaryAnimation.parent == newAnimation);
assert(newAnimation!.currentTrain == nextRoute._animation);
// We can hop on the nextTrain, so we don't need to listen to
// whether the nextTrain has stopped.
_setSecondaryAnimation(newAnimation!.currentTrain, nextRoute.completed);
if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
final Animation<double>? current = _secondaryAnimation.parent;
if (current != null) {
final Animation<double> currentTrain = (current is TrainHoppingAnimation ? current.currentTrain : current)!;
final Animation<double> nextTrain = nextRoute._animation!;
if (
currentTrain.value == nextTrain.value ||
nextTrain.status == AnimationStatus.completed ||
nextTrain.status == AnimationStatus.dismissed
) {
_setSecondaryAnimation(nextTrain, nextRoute.completed);
} else {
// Two trains animate at different values. We have to do train hopping.
// There are three possibilities of train hopping:
// 1. We hop on the nextTrain when two trains meet in the middle using
// TrainHoppingAnimation.
// 2. There is no chance to hop on nextTrain because two trains never
// cross each other. We have to directly set the animation to
// nextTrain once the nextTrain stops animating.
// 3. A new _updateSecondaryAnimation is called before train hopping
// finishes. We leave a listener remover for the next call to
// properly clean up the existing train hopping.
TrainHoppingAnimation? newAnimation;
void jumpOnAnimationEnd(AnimationStatus status) {
switch (status) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
// The nextTrain has stopped animating without train hopping.
// Directly sets the secondary animation and disposes the
// TrainHoppingAnimation.
_setSecondaryAnimation(nextTrain, nextRoute.completed);
if (_trainHoppingListenerRemover != null) {
_trainHoppingListenerRemover!();
_trainHoppingListenerRemover = null;
}
},
);
_setSecondaryAnimation(newAnimation, nextRoute.completed);
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
}
}
} else { // This route has no secondary animation.
_setSecondaryAnimation(nextRoute._animation, nextRoute.completed);
_trainHoppingListenerRemover = () {
nextTrain.removeStatusListener(jumpOnAnimationEnd);
newAnimation?.dispose();
};
nextTrain.addStatusListener(jumpOnAnimationEnd);
newAnimation = TrainHoppingAnimation(
currentTrain,
nextTrain,
onSwitchedTrain: () {
assert(_secondaryAnimation.parent == newAnimation);
assert(newAnimation!.currentTrain == nextRoute._animation);
// We can hop on the nextTrain, so we don't need to listen to
// whether the nextTrain has stopped.
_setSecondaryAnimation(newAnimation!.currentTrain, nextRoute.completed);
if (_trainHoppingListenerRemover != null) {
_trainHoppingListenerRemover!();
_trainHoppingListenerRemover = null;
}
},
);
_setSecondaryAnimation(newAnimation, nextRoute.completed);
}
} else {
// This route cannot coordinate transitions with nextRoute, so it should
// have no visible secondary animation. By using an AnimationMin, the
// animation's value will always be zero, but it will have nextRoute.animation's
// status until it finishes, allowing this route to wait until all visible
// transitions are complete to stop ignoring pointers.
_setSecondaryAnimation(
AnimationMin<double>(kAlwaysDismissedAnimation, nextRoute._animation!),
nextRoute.completed,
);
_setSecondaryAnimation(nextRoute._animation, nextRoute.completed);
}
} else { // The next route is not a TransitionRoute.
} else {
_setSecondaryAnimation(kAlwaysDismissedAnimation);
}
// Finally, we dispose any previous train hopping animation because it
Expand Down Expand Up @@ -408,9 +396,9 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// the [nextRoute] is popped off of this route, the
/// `secondaryAnimation` will run from 1.0 - 0.0.
///
/// If false, this route's [ModalRoute.buildTransitions] `secondaryAnimation`
/// will proxy an animation with a constant value of 0. In other words, this
/// route will not animate when [nextRoute] is pushed on top of it or when
/// If false, this route's [ModalRoute.buildTransitions] `secondaryAnimation` parameter
/// value will be [kAlwaysDismissedAnimation]. In other words, this route
/// will not animate when [nextRoute] is pushed on top of it or when
/// [nextRoute] is popped off of it.
///
/// Returns true by default.
Expand Down Expand Up @@ -858,19 +846,17 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
context,
widget.route.animation!,
widget.route.secondaryAnimation!,
// _listenable updates when this route's animations change
// values, but the _ignorePointerNotifier can also update
// when the status of animations on popping routes change,
// even when this route's animations' values don't. Also,
// when the value of the _ignorePointerNotifier changes,
// it's only necessary to rebuild the IgnorePointer
// widget and set the focus node's ability to focus.
// This additional AnimatedBuilder is include because if the
// value of the userGestureInProgressNotifier changes, it's
// only necessary to rebuild the IgnorePointer widget and set
// the focus node's ability to focus.
AnimatedBuilder(
animation: widget.route._ignorePointerNotifier,
animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier<bool>(false),
builder: (BuildContext context, Widget? child) {
focusScopeNode.canRequestFocus = !_shouldIgnoreFocusRequest;
final bool ignoreEvents = _shouldIgnoreFocusRequest;
focusScopeNode.canRequestFocus = !ignoreEvents;
return IgnorePointer(
ignoring: widget.route._ignorePointer,
ignoring: ignoreEvents,
child: child,
);
},
Expand Down Expand Up @@ -1154,36 +1140,11 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return child;
}

/// Whether this route should ignore pointers when transitions are in progress.
///
/// Pointers always are ignored when [isCurrent] is false (e.g., when a route
/// has a new route pushed on top of it, or during a route's exit transition
/// after popping). Override this value to also ignore pointers on pages during
/// transitions where this route is the current route (e.g., after the route
/// above this route pops, or during this route's entrance transition).
///
/// Returns false by default.
///
/// See also:
///
/// * [CupertinoRouteTransitionMixin], [CupertinoModalPopupRoute], and
/// [CupertinoDialogRoute], which use this property to specify that
/// Cupertino routes ignore pointers during transitions.
@protected
bool get ignorePointerDuringTransitions => false;

@override
void install() {
super.install();
_animationProxy = ProxyAnimation(super.animation)
..addStatusListener(_handleAnimationStatusChanged);
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation)
..addStatusListener(_handleAnimationStatusChanged);
navigator!.userGestureInProgressNotifier.addListener(_maybeUpdateIgnorePointer);
}

void _handleAnimationStatusChanged(AnimationStatus status) {
_maybeUpdateIgnorePointer();
_animationProxy = ProxyAnimation(super.animation);
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
}

@override
Expand Down Expand Up @@ -1419,19 +1380,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
Animation<double>? get secondaryAnimation => _secondaryAnimationProxy;
ProxyAnimation? _secondaryAnimationProxy;

bool get _ignorePointer => _ignorePointerNotifier.value;
final ValueNotifier<bool> _ignorePointerNotifier = ValueNotifier<bool>(false);

void _maybeUpdateIgnorePointer() {
bool isTransitioning(Animation<double>? animation) {
return animation?.status == AnimationStatus.forward || animation?.status == AnimationStatus.reverse;
}
_ignorePointerNotifier.value = !isCurrent ||
(navigator?.userGestureInProgress ?? false) ||
(ignorePointerDuringTransitions &&
(isTransitioning(animation) || isTransitioning(secondaryAnimation)));
}

final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];

/// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with
Expand Down Expand Up @@ -1650,14 +1598,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
child: barrier,
);
}
barrier = AnimatedBuilder(
animation: _ignorePointerNotifier,
builder: (BuildContext context, Widget? child) {
return IgnorePointer(
ignoring: _ignorePointer,
child: child,
);
},
barrier = IgnorePointer(
ignoring: animation!.status == AnimationStatus.reverse || // changedInternalState is called when animation.status updates
animation!.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture
child: barrier,
);
if (semanticsDismissible && barrierDismissible) {
Expand Down
29 changes: 15 additions & 14 deletions packages/flutter/test/cupertino/action_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ void main() {
);

await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
await tester.pump();

expect(find.byType(CupertinoActionSheet), findsOneWidget);
expect(find.text('Action Sheet'), findsOneWidget);

await tester.tap(find.byType(ModalBarrier).last);
await tester.pumpAndSettle();
expect(find.byType(CupertinoActionSheet), findsNothing);
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump();
expect(find.text('Action Sheet'), findsNothing);
});

testWidgets('Verify that a tap on title section (not buttons) does not dismiss an action sheet', (WidgetTester tester) async {
Expand Down Expand Up @@ -867,7 +867,7 @@ void main() {
expect(find.byType(CupertinoActionSheet), findsNothing);
});

testWidgets('Modal barrier cannot be dismissed during transition', (WidgetTester tester) async {
testWidgets('Modal barrier is pressed during transition', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
Expand Down Expand Up @@ -906,20 +906,21 @@ void main() {
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(337.1, epsilon: 0.1));

// Attempt to dismiss
// Exit animation
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump(const Duration(milliseconds: 60));

// Enter animation is continuing
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(325.4, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(374.3, epsilon: 0.1));

await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(470.0, epsilon: 0.1));

// Attempt to dismiss again
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0);

// Action sheet has disappeared
await tester.pump(const Duration(milliseconds: 60));
expect(find.byType(CupertinoActionSheet), findsNothing);
});

Expand Down Expand Up @@ -951,7 +952,7 @@ void main() {
);

await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
await tester.pump();

expect(
semantics,
Expand Down
2 changes: 0 additions & 2 deletions packages/flutter/test/cupertino/dialog_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1074,8 +1074,6 @@ void main() {
transition = tester.firstWidget(fadeTransitionFinder);
expect(transition.opacity.value, moreOrLessEquals(1.0, epsilon: 0.001));

await tester.pumpAndSettle();

await tester.tap(find.text('Delete'));

// Exit animation, look at reverse FadeTransition.
Expand Down
Loading

0 comments on commit de230d3

Please sign in to comment.