Dismiss modal with any button press (#32770)
This PR makes ModalBarrier dismiss modal with any button press instead of primary button up, by making it use a private recognizer _AnyTapGestureRecognizer that claims victor and calls onAnyTapDown immediately after it receives any PointerDownEvent.
This commit is contained in:
parent
adb2aeebe3
commit
1aa4628fa2
@ -3,6 +3,9 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart' show
|
||||||
|
PrimaryPointerGestureRecognizer,
|
||||||
|
GestureDisposition;
|
||||||
|
|
||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
import 'container.dart';
|
import 'container.dart';
|
||||||
@ -81,12 +84,11 @@ class ModalBarrier extends StatelessWidget {
|
|||||||
// On Android, the back button is used to dismiss a modal. On iOS, some
|
// On Android, the back button is used to dismiss a modal. On iOS, some
|
||||||
// modal barriers are not dismissible in accessibility mode.
|
// modal barriers are not dismissible in accessibility mode.
|
||||||
excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible,
|
excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible,
|
||||||
child: GestureDetector(
|
child: _ModalBarrierGestureDetector(
|
||||||
onTapDown: (TapDownDetails details) {
|
onAnyTapDown: () {
|
||||||
if (dismissible)
|
if (dismissible)
|
||||||
Navigator.maybePop(context);
|
Navigator.maybePop(context);
|
||||||
},
|
},
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
child: Semantics(
|
child: Semantics(
|
||||||
label: semanticsDismissible ? semanticsLabel : null,
|
label: semanticsDismissible ? semanticsLabel : null,
|
||||||
textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
|
textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
|
||||||
@ -175,3 +177,102 @@ class AnimatedModalBarrier extends AnimatedWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recognizes tap down by any pointer button unconditionally. When it receives a
|
||||||
|
// PointerDownEvent, it immediately claims victor of arena and calls
|
||||||
|
// [onAnyTapDown] without any checks.
|
||||||
|
//
|
||||||
|
// It is used by ModalBarrier to detect any taps on the overlay.
|
||||||
|
class _AnyTapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||||
|
_AnyTapGestureRecognizer({
|
||||||
|
Object debugOwner,
|
||||||
|
this.onAnyTapDown,
|
||||||
|
}) : super(debugOwner: debugOwner);
|
||||||
|
|
||||||
|
VoidCallback onAnyTapDown;
|
||||||
|
|
||||||
|
bool _sentTapDown = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addAllowedPointer(PointerDownEvent event) {
|
||||||
|
super.addAllowedPointer(event);
|
||||||
|
resolve(GestureDisposition.accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handlePrimaryPointer(PointerEvent event) {
|
||||||
|
if (!_sentTapDown) {
|
||||||
|
if (onAnyTapDown != null)
|
||||||
|
onAnyTapDown();
|
||||||
|
_sentTapDown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didStopTrackingLastPointer(int pointer) {
|
||||||
|
super.didStopTrackingLastPointer(pointer);
|
||||||
|
_sentTapDown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get debugDescription => 'any tap';
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
|
||||||
|
const _ModalBarrierSemanticsDelegate({this.onAnyTapDown});
|
||||||
|
|
||||||
|
final VoidCallback onAnyTapDown;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void assignSemantics(RenderSemanticsGestureHandler renderObject) {
|
||||||
|
renderObject.onTap = onAnyTapDown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGestureRecognizer> {
|
||||||
|
const _AnyTapGestureRecognizerFactory({this.onAnyTapDown});
|
||||||
|
|
||||||
|
final VoidCallback onAnyTapDown;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AnyTapGestureRecognizer constructor() => _AnyTapGestureRecognizer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initializer(_AnyTapGestureRecognizer instance) {
|
||||||
|
instance.onAnyTapDown = onAnyTapDown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A GestureDetector used by ModalBarrier. It only has one callback,
|
||||||
|
// [onAnyTapDown], which recognizes tap down unconditionally.
|
||||||
|
class _ModalBarrierGestureDetector extends StatelessWidget {
|
||||||
|
const _ModalBarrierGestureDetector({
|
||||||
|
Key key,
|
||||||
|
@required this.child,
|
||||||
|
@required this.onAnyTapDown,
|
||||||
|
}) : assert(child != null),
|
||||||
|
assert(onAnyTapDown != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// The widget below this widget in the tree.
|
||||||
|
/// See [RawGestureDetector.child].
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Immediately called when a pointer causes a tap down.
|
||||||
|
/// See [_AnyTapGestureRecognizer.onAnyTapDown].
|
||||||
|
final VoidCallback onAnyTapDown;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{
|
||||||
|
_AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapDown: onAnyTapDown),
|
||||||
|
};
|
||||||
|
|
||||||
|
return RawGestureDetector(
|
||||||
|
gestures: gestures,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
semantics: _ModalBarrierSemanticsDelegate(onAnyTapDown: onAnyTapDown),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter/gestures.dart' show kSecondaryButton;
|
||||||
|
|
||||||
import 'semantics_tester.dart';
|
import 'semantics_tester.dart';
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ void main() {
|
|||||||
reason: 'because the tap is not prevented by ModalBarrier');
|
reason: 'because the tap is not prevented by ModalBarrier');
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('ModalBarrier pops the Navigator when dismissed', (WidgetTester tester) async {
|
testWidgets('ModalBarrier pops the Navigator when dismissed by primay tap', (WidgetTester tester) async {
|
||||||
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||||
'/': (BuildContext context) => FirstWidget(),
|
'/': (BuildContext context) => FirstWidget(),
|
||||||
'/modal': (BuildContext context) => SecondWidget(),
|
'/modal': (BuildContext context) => SecondWidget(),
|
||||||
@ -85,6 +86,56 @@ void main() {
|
|||||||
reason: 'The route should have been dismissed by tapping the barrier.');
|
reason: 'The route should have been dismissed by tapping the barrier.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('ModalBarrier pops the Navigator when dismissed by primary tap down', (WidgetTester tester) async {
|
||||||
|
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => FirstWidget(),
|
||||||
|
'/modal': (BuildContext context) => SecondWidget(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(routes: routes));
|
||||||
|
|
||||||
|
// Initially the barrier is not visible
|
||||||
|
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
|
||||||
|
|
||||||
|
// Tapping on X routes to the barrier
|
||||||
|
await tester.tap(find.text('X'));
|
||||||
|
await tester.pump(); // begin transition
|
||||||
|
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||||
|
|
||||||
|
// Tap on the barrier to dismiss it
|
||||||
|
await tester.press(find.byKey(const ValueKey<String>('barrier')));
|
||||||
|
await tester.pump(); // begin transition
|
||||||
|
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||||
|
|
||||||
|
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing,
|
||||||
|
reason: 'The route should have been dismissed by tapping the barrier.');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ModalBarrier pops the Navigator when dismissed by non-primary tap down', (WidgetTester tester) async {
|
||||||
|
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => FirstWidget(),
|
||||||
|
'/modal': (BuildContext context) => SecondWidget(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(routes: routes));
|
||||||
|
|
||||||
|
// Initially the barrier is not visible
|
||||||
|
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
|
||||||
|
|
||||||
|
// Tapping on X routes to the barrier
|
||||||
|
await tester.tap(find.text('X'));
|
||||||
|
await tester.pump(); // begin transition
|
||||||
|
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||||
|
|
||||||
|
// Tap on the barrier to dismiss it
|
||||||
|
await tester.press(find.byKey(const ValueKey<String>('barrier')), buttons: kSecondaryButton);
|
||||||
|
await tester.pump(); // begin transition
|
||||||
|
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||||
|
|
||||||
|
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing,
|
||||||
|
reason: 'The route should have been dismissed by tapping the barrier.');
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('ModalBarrier does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async {
|
testWidgets('ModalBarrier does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async {
|
||||||
bool willPopCalled = false;
|
bool willPopCalled = false;
|
||||||
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user