Fixes an issue where onTapOutside was incorrectly triggered across routes in TapRegion (#155297)
Fixes https://github.com/flutter/flutter/issues/153093 This PR fixes the issue of onTapOutside being triggered on other routes incorrectly.
This commit is contained in:
parent
da71e6e93d
commit
56a33efd73
@ -15,6 +15,7 @@ import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'editable_text.dart';
|
||||
import 'framework.dart';
|
||||
import 'routes.dart';
|
||||
|
||||
// Enable if you want verbose logging about tap region changes.
|
||||
const bool _kDebugTapRegion = false;
|
||||
@ -329,6 +330,9 @@ class _DummyTapRecognizer extends GestureArenaMember {
|
||||
/// regions in the group will act as one.
|
||||
///
|
||||
/// If there is no [TapRegionSurface] ancestor, [TapRegion] will do nothing.
|
||||
///
|
||||
/// [TapRegion] is aware of the [Route]s in the [Navigator], so that [onTapOutside]
|
||||
/// isn't called after the user navigates to a different page.
|
||||
class TapRegion extends SingleChildRenderObjectWidget {
|
||||
/// Creates a const [TapRegion].
|
||||
///
|
||||
@ -403,12 +407,14 @@ class TapRegion extends SingleChildRenderObjectWidget {
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
final bool isCurrent = ModalRoute.isCurrentOf(context) ?? true;
|
||||
|
||||
return RenderTapRegion(
|
||||
registry: TapRegionRegistry.maybeOf(context),
|
||||
enabled: enabled,
|
||||
consumeOutsideTaps: consumeOutsideTaps,
|
||||
behavior: behavior,
|
||||
onTapOutside: onTapOutside,
|
||||
onTapOutside: isCurrent ? onTapOutside : null,
|
||||
onTapInside: onTapInside,
|
||||
groupId: groupId,
|
||||
debugLabel: debugLabel,
|
||||
@ -417,12 +423,14 @@ class TapRegion extends SingleChildRenderObjectWidget {
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, covariant RenderTapRegion renderObject) {
|
||||
final bool isCurrent = ModalRoute.isCurrentOf(context) ?? true;
|
||||
|
||||
renderObject
|
||||
..registry = TapRegionRegistry.maybeOf(context)
|
||||
..enabled = enabled
|
||||
..behavior = behavior
|
||||
..groupId = groupId
|
||||
..onTapOutside = onTapOutside
|
||||
..onTapOutside = isCurrent ? onTapOutside : null
|
||||
..onTapInside = onTapInside;
|
||||
if (!kReleaseMode) {
|
||||
renderObject.debugLabel = debugLabel;
|
||||
|
@ -1053,4 +1053,166 @@ void main() {
|
||||
await click(find.text('Outside Surface'));
|
||||
expect(tappedInside, isEmpty);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/153093.
|
||||
testWidgets('TapRegion onTapOutside should only trigger on the current route during navigation', (WidgetTester tester) async {
|
||||
const ValueKey<String> tapRegion1Key = ValueKey<String>('TapRegion');
|
||||
const ValueKey<String> tapRegion2Key = ValueKey<String>('TapRegion2');
|
||||
|
||||
int count1 = 0;
|
||||
int count2 = 0;
|
||||
|
||||
final TapRegion tapRegion1 = TapRegion(
|
||||
key: tapRegion1Key,
|
||||
onTapOutside: (PointerEvent event) {
|
||||
count1 += 1;
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const SizedBox.square(dimension: 100),
|
||||
);
|
||||
|
||||
final TapRegion tapRegion2 = TapRegion(
|
||||
key: tapRegion2Key,
|
||||
onTapOutside: (PointerEvent event) {
|
||||
count2 += 1;
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const SizedBox.square(dimension: 100),
|
||||
);
|
||||
|
||||
Future<void> tapOutside(WidgetTester tester, Finder regionFinder) async {
|
||||
// Find the RenderBox of the region.
|
||||
final RenderBox renderBox = tester.firstRenderObject(find.byType(Scaffold).last);
|
||||
final Offset outsidePoint = renderBox.localToGlobal(Offset.zero) + const Offset(200, 200);
|
||||
|
||||
await tester.tapAt(outsidePoint);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(child: tapRegion1),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
tester.element(find.byType(FloatingActionButton)),
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) => Scaffold(
|
||||
body: Center(child: tapRegion2),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap outside the first TapRegion to trigger onTapOutside.
|
||||
await tapOutside(tester, find.byKey(tapRegion1Key));
|
||||
expect(count1, 1);
|
||||
expect(count2, 0);
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap outside the second TapRegion to trigger onTapOutside
|
||||
await tapOutside(tester, find.byKey(tapRegion2Key));
|
||||
expect(count1, 2); // When the Fab is pressed, the first TapRegion is still active.
|
||||
expect(count2, 1);
|
||||
|
||||
// Back to the first page.
|
||||
Navigator.pop(tester.element(find.byType(Scaffold).last));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap outside the first TapRegion to trigger onTapOutside
|
||||
await tapOutside(tester, find.byKey(tapRegion1Key));
|
||||
expect(count1, 3);
|
||||
expect(count2, 1);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/153093.
|
||||
testWidgets('TapRegion on non-current routes should not respond to onTapOutside events', (WidgetTester tester) async {
|
||||
const ValueKey<String> tapRegion1Key = ValueKey<String>('TapRegion1');
|
||||
const ValueKey<String> tapRegion2Key = ValueKey<String>('TapRegion2');
|
||||
|
||||
int count1 = 0;
|
||||
int count2 = 0;
|
||||
|
||||
final TapRegion tapRegion1 = TapRegion(
|
||||
key: tapRegion1Key,
|
||||
onTapOutside: (PointerEvent event) {
|
||||
count1 += 1;
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const SizedBox.square(dimension: 100),
|
||||
);
|
||||
|
||||
final TapRegion tapRegion2 = TapRegion(
|
||||
key: tapRegion2Key,
|
||||
onTapOutside: (PointerEvent event) {
|
||||
count2 += 1;
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const SizedBox.square(dimension: 100),
|
||||
);
|
||||
|
||||
Future<void> tapOutside(WidgetTester tester, Finder regionFinder) async {
|
||||
// Find the RenderBox of the region.
|
||||
final RenderBox renderBox = tester.firstRenderObject(find.byType(Scaffold).last);
|
||||
final Offset outsidePoint = renderBox.localToGlobal(Offset.zero) + const Offset(200, 200);
|
||||
|
||||
await tester.tapAt(outsidePoint);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
initialRoute: '/',
|
||||
routes: <String, WidgetBuilder>{
|
||||
'/': (BuildContext context) => Scaffold(
|
||||
body: Center(child: tapRegion1),
|
||||
),
|
||||
'/second': (BuildContext context) => Scaffold(
|
||||
body: Center(child: tapRegion2),
|
||||
),
|
||||
},
|
||||
onGenerateInitialRoutes: (String initialRouteName) {
|
||||
return <Route<void>>[
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) => Scaffold(
|
||||
body: Center(child: tapRegion1),
|
||||
),
|
||||
),
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) => Scaffold(
|
||||
body: Center(child: tapRegion2),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// At this point, tapRegion2 is on top of tapRegion1.
|
||||
// Tap outside tapRegion2.
|
||||
await tapOutside(tester, find.byKey(tapRegion2Key));
|
||||
expect(count1, 0); // tapRegion1 should not respond.
|
||||
expect(count2, 1); // tapRegion2 should respond.
|
||||
|
||||
// Now pop the top route to reveal tapRegion1.
|
||||
Navigator.pop(tester.element(find.byType(Scaffold).last));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap outside tapRegion1.
|
||||
await tapOutside(tester, find.byKey(tapRegion1Key));
|
||||
expect(count1, 1); // tapRegion1 should respond.
|
||||
expect(count2, 1); // tapRegion2 should not respond anymore.
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user