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 'editable_text.dart';
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
|
import 'routes.dart';
|
||||||
|
|
||||||
// Enable if you want verbose logging about tap region changes.
|
// Enable if you want verbose logging about tap region changes.
|
||||||
const bool _kDebugTapRegion = false;
|
const bool _kDebugTapRegion = false;
|
||||||
@ -329,6 +330,9 @@ class _DummyTapRecognizer extends GestureArenaMember {
|
|||||||
/// regions in the group will act as one.
|
/// regions in the group will act as one.
|
||||||
///
|
///
|
||||||
/// If there is no [TapRegionSurface] ancestor, [TapRegion] will do nothing.
|
/// 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 {
|
class TapRegion extends SingleChildRenderObjectWidget {
|
||||||
/// Creates a const [TapRegion].
|
/// Creates a const [TapRegion].
|
||||||
///
|
///
|
||||||
@ -403,12 +407,14 @@ class TapRegion extends SingleChildRenderObjectWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
final bool isCurrent = ModalRoute.isCurrentOf(context) ?? true;
|
||||||
|
|
||||||
return RenderTapRegion(
|
return RenderTapRegion(
|
||||||
registry: TapRegionRegistry.maybeOf(context),
|
registry: TapRegionRegistry.maybeOf(context),
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
consumeOutsideTaps: consumeOutsideTaps,
|
consumeOutsideTaps: consumeOutsideTaps,
|
||||||
behavior: behavior,
|
behavior: behavior,
|
||||||
onTapOutside: onTapOutside,
|
onTapOutside: isCurrent ? onTapOutside : null,
|
||||||
onTapInside: onTapInside,
|
onTapInside: onTapInside,
|
||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
debugLabel: debugLabel,
|
debugLabel: debugLabel,
|
||||||
@ -417,12 +423,14 @@ class TapRegion extends SingleChildRenderObjectWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void updateRenderObject(BuildContext context, covariant RenderTapRegion renderObject) {
|
void updateRenderObject(BuildContext context, covariant RenderTapRegion renderObject) {
|
||||||
|
final bool isCurrent = ModalRoute.isCurrentOf(context) ?? true;
|
||||||
|
|
||||||
renderObject
|
renderObject
|
||||||
..registry = TapRegionRegistry.maybeOf(context)
|
..registry = TapRegionRegistry.maybeOf(context)
|
||||||
..enabled = enabled
|
..enabled = enabled
|
||||||
..behavior = behavior
|
..behavior = behavior
|
||||||
..groupId = groupId
|
..groupId = groupId
|
||||||
..onTapOutside = onTapOutside
|
..onTapOutside = isCurrent ? onTapOutside : null
|
||||||
..onTapInside = onTapInside;
|
..onTapInside = onTapInside;
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
renderObject.debugLabel = debugLabel;
|
renderObject.debugLabel = debugLabel;
|
||||||
|
@ -1053,4 +1053,166 @@ void main() {
|
|||||||
await click(find.text('Outside Surface'));
|
await click(find.text('Outside Surface'));
|
||||||
expect(tappedInside, isEmpty);
|
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