Skip to content

Commit 1e0a1a2

Browse files
authored
Add an example and update GestureDetector documentation (#102360)
1 parent 336aa26 commit 1e0a1a2

File tree

3 files changed

+271
-0
lines changed

3 files changed

+271
-0
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/gestures.dart';
6+
import 'package:flutter/material.dart';
7+
8+
void main() {
9+
debugPrintGestureArenaDiagnostics = true;
10+
runApp(const NestedGestureDetectorsApp());
11+
}
12+
13+
enum _OnTapWinner { none, yellow, green }
14+
15+
class NestedGestureDetectorsApp extends StatelessWidget {
16+
const NestedGestureDetectorsApp({super.key});
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return MaterialApp(
21+
home: Scaffold(
22+
appBar: AppBar(title: const Text('Nested GestureDetectors')),
23+
body: const NestedGestureDetectorsExample(),
24+
),
25+
);
26+
}
27+
}
28+
29+
class NestedGestureDetectorsExample extends StatefulWidget {
30+
const NestedGestureDetectorsExample({super.key});
31+
32+
@override
33+
State<NestedGestureDetectorsExample> createState() => _NestedGestureDetectorsExampleState();
34+
}
35+
36+
class _NestedGestureDetectorsExampleState
37+
extends State<NestedGestureDetectorsExample> {
38+
bool _isYellowTranslucent = false;
39+
_OnTapWinner _winner = _OnTapWinner.none;
40+
final Border highlightBorder = Border.all(color: Colors.red, width: 5);
41+
42+
@override
43+
Widget build(BuildContext context) {
44+
return Column(
45+
children: <Widget>[
46+
Expanded(
47+
child: GestureDetector(
48+
onTap: () {
49+
debugPrint('Green onTap');
50+
setState(() {
51+
_winner = _OnTapWinner.green;
52+
});
53+
},
54+
onTapDown: (_) => debugPrint('Green onTapDown'),
55+
onTapCancel: () => debugPrint('Green onTapCancel'),
56+
child: Container(
57+
alignment: Alignment.center,
58+
decoration: BoxDecoration(
59+
border: _winner == _OnTapWinner.green ? highlightBorder : null,
60+
color: Colors.green,
61+
),
62+
child: GestureDetector(
63+
// Setting behavior to transparent or opaque as no impact on
64+
// parent-child hit testing. A tap on 'Yellow' is also in
65+
// 'Green' bounds. Both enter the gesture arena, 'Yellow' wins
66+
// because it is in front.
67+
behavior: _isYellowTranslucent
68+
? HitTestBehavior.translucent
69+
: HitTestBehavior.opaque,
70+
onTap: () {
71+
debugPrint('Yellow onTap');
72+
setState(() {
73+
_winner = _OnTapWinner.yellow;
74+
});
75+
},
76+
child: Container(
77+
alignment: Alignment.center,
78+
decoration: BoxDecoration(
79+
border: _winner == _OnTapWinner.yellow ? highlightBorder : null,
80+
color: Colors.amber,
81+
),
82+
width: 200,
83+
height: 200,
84+
child: Text(
85+
'HitTextBehavior.${_isYellowTranslucent ? 'translucent' : 'opaque'}',
86+
textAlign: TextAlign.center,
87+
),
88+
),
89+
),
90+
),
91+
),
92+
),
93+
Padding(
94+
padding: const EdgeInsets.all(8.0),
95+
child: Row(
96+
children: <Widget>[
97+
ElevatedButton(
98+
child: const Text('Reset'),
99+
onPressed: () {
100+
setState(() {
101+
_isYellowTranslucent = false;
102+
_winner = _OnTapWinner.none;
103+
});
104+
},
105+
),
106+
const SizedBox(width: 8),
107+
ElevatedButton(
108+
child: Text(
109+
'Set Yellow behavior to ${_isYellowTranslucent ? 'opaque' : 'translucent'}',
110+
),
111+
onPressed: () {
112+
setState(() => _isYellowTranslucent = !_isYellowTranslucent);
113+
},
114+
),
115+
],
116+
),
117+
),
118+
],
119+
);
120+
}
121+
122+
@override
123+
void dispose() {
124+
debugPrintGestureArenaDiagnostics = false;
125+
super.dispose();
126+
}
127+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_api_samples/widgets/gesture_detector/gesture_detector.2.dart'
7+
as example;
8+
import 'package:flutter_test/flutter_test.dart';
9+
10+
void main() {
11+
12+
void expectBorders(
13+
WidgetTester tester, {
14+
required bool expectGreenHasBorder,
15+
required bool expectYellowHasBorder,
16+
}) {
17+
final Finder containerFinder = find.byType(Container);
18+
final Finder greenFinder = containerFinder.first;
19+
final Finder yellowFinder = containerFinder.last;
20+
21+
final Container greenContainer = tester.firstWidget<Container>(greenFinder);
22+
final BoxDecoration? greenDecoration = greenContainer.decoration as BoxDecoration?;
23+
expect(greenDecoration?.border, expectGreenHasBorder ? isNot(null) : null);
24+
25+
final Container yellowContainer = tester.firstWidget<Container>(yellowFinder);
26+
final BoxDecoration? yellowDecoration = yellowContainer.decoration as BoxDecoration?;
27+
expect(yellowDecoration?.border, expectYellowHasBorder ? isNot(null) : null);
28+
}
29+
30+
void expectInnerGestureDetectorBehavior(WidgetTester tester, HitTestBehavior behavior) {
31+
// Note that there is a third GestureDetector added by Scaffold
32+
final Finder innerGestureDetectorFinder = find.byType(GestureDetector).at(1);
33+
final GestureDetector innerGestureDetector = tester.firstWidget<GestureDetector>(innerGestureDetectorFinder);
34+
expect(innerGestureDetector.behavior, behavior);
35+
}
36+
37+
testWidgets('Only the green Container shows a red border when tapped', (WidgetTester tester) async {
38+
await tester.pumpWidget(
39+
const example.NestedGestureDetectorsApp(),
40+
);
41+
42+
final Finder greenFinder = find.byType(Container).first;
43+
final Offset greenTopLeftCorner = tester.getTopLeft(greenFinder);
44+
await tester.tapAt(greenTopLeftCorner);
45+
await tester.pumpAndSettle();
46+
expectBorders(tester, expectGreenHasBorder: true, expectYellowHasBorder: false);
47+
48+
// Tap on the button to toggle inner GestureDetector.behavior
49+
final Finder toggleBehaviorFinder = find.byType(ElevatedButton).last;
50+
await tester.tap(toggleBehaviorFinder);
51+
await tester.pump();
52+
expectInnerGestureDetectorBehavior(tester, HitTestBehavior.translucent);
53+
54+
// Tap again on the green container, expect nothing changed
55+
await tester.tapAt(greenTopLeftCorner);
56+
await tester.pump();
57+
expectBorders(tester, expectGreenHasBorder: true, expectYellowHasBorder: false);
58+
59+
// Tap on the reset button
60+
final Finder resetFinder = find.byType(ElevatedButton).first;
61+
await tester.tap(resetFinder);
62+
await tester.pump();
63+
expectInnerGestureDetectorBehavior(tester, HitTestBehavior.opaque);
64+
});
65+
66+
testWidgets('Only the yellow Container shows a red border when tapped', (WidgetTester tester) async {
67+
await tester.pumpWidget(
68+
const example.NestedGestureDetectorsApp(),
69+
);
70+
71+
final Finder yellowFinder = find.byType(Container).last;
72+
final Offset yellowTopLeftCorner = tester.getTopLeft(yellowFinder);
73+
await tester.tapAt(yellowTopLeftCorner);
74+
await tester.pump();
75+
expectBorders(tester, expectGreenHasBorder: false, expectYellowHasBorder: true);
76+
77+
// Tap on the button to toggle inner GestureDetector.behavior
78+
final Finder toggleBehaviorFinder = find.byType(ElevatedButton).last;
79+
await tester.tap(toggleBehaviorFinder);
80+
await tester.pump();
81+
expectInnerGestureDetectorBehavior(tester, HitTestBehavior.translucent);
82+
83+
// Tap again on the yellow container, expect nothing changed
84+
await tester.tapAt(yellowTopLeftCorner);
85+
await tester.pump();
86+
expectBorders(tester, expectGreenHasBorder: false, expectYellowHasBorder: true);
87+
88+
// Tap on the reset button
89+
final Finder resetFinder = find.byType(ElevatedButton).first;
90+
await tester.tap(resetFinder);
91+
await tester.pump();
92+
expectInnerGestureDetectorBehavior(tester, HitTestBehavior.opaque);
93+
});
94+
}

packages/flutter/lib/src/widgets/gesture_detector.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,56 @@ class GestureRecognizerFactoryWithHandlers<T extends GestureRecognizer> extends
150150
/// ** See code in examples/api/lib/widgets/gesture_detector/gesture_detector.1.dart **
151151
/// {@end-tool}
152152
///
153+
/// ### Troubleshooting
154+
///
155+
/// Why isn't my parent [GestureDetector.onTap] method called?
156+
///
157+
/// Given a parent [GestureDetector] with an onTap callback, and a child
158+
/// GestureDetector that also defines an onTap callback, when the inner
159+
/// GestureDetector is tapped, both GestureDetectors send a [GestureRecognizer]
160+
/// into the gesture arena. This is because the pointer coordinates are within the
161+
/// bounds of both GestureDetectors. The child GestureDetector wins in this
162+
/// scenario because it was the first to enter the arena, resolving as first come,
163+
/// first served. The child onTap is called, and the parent's is not as the gesture has
164+
/// been consumed.
165+
/// For more information on gesture disambiguation see:
166+
/// [Gesture disambiguation](https://docs.flutter.dev/development/ui/advanced/gestures#gesture-disambiguation).
167+
///
168+
/// Setting [GestureDetector.behavior] to [HitTestBehavior.opaque]
169+
/// or [HitTestBehavior.translucent] has no impact on parent-child relationships:
170+
/// both GestureDetectors send a GestureRecognizer into the gesture arena, only one wins.
171+
///
172+
/// Some callbacks (e.g. onTapDown) can fire before a recognizer wins the arena,
173+
/// and others (e.g. onTapCancel) fire even when it loses the arena. Therefore,
174+
/// the parent detector in the example above may call some of its callbacks even
175+
/// though it loses in the arena.
176+
///
177+
/// {@tool dartpad}
178+
/// This example uses a [GestureDetector] that wraps a green [Container] and a second
179+
/// GestureDetector that wraps a yellow Container. The second GestureDetector is
180+
/// a child of the green Container.
181+
/// Both GestureDetectors define an onTap callback. When the callback is called it
182+
/// adds a red border to the corresponding Container.
183+
///
184+
/// When the green Container is tapped, it's parent GestureDetector enters
185+
/// the gesture arena. It wins because there is no competing GestureDetector and
186+
/// the green Container shows a red border.
187+
/// When the yellow Container is tapped, it's parent GestureDetector enters
188+
/// the gesture arena. The GestureDetector that wraps the green Container also
189+
/// enters the gesture arena (the pointer events coordinates are inside both
190+
/// GestureDetectors bounds). The GestureDetector that wraps the yellow Container
191+
/// wins because it was the first detector to enter the arena.
192+
///
193+
/// This example sets [debugPrintGestureArenaDiagnostics] to true.
194+
/// This flag prints useful information about gesture arenas.
195+
///
196+
/// Changing the [GestureDetector.behavior] property to [HitTestBehavior.translucent]
197+
/// or [HitTestBehavior.opaque] has no impact: both GestureDetectors send a [GestureRecognizer]
198+
/// into the gesture arena, only one wins.
199+
///
200+
/// ** See code in examples/api/lib/widgets/gesture_detector/gesture_detector.2.dart **
201+
/// {@end-tool}
202+
///
153203
/// ## Debugging
154204
///
155205
/// To see how large the hit test box of a [GestureDetector] is for debugging

0 commit comments

Comments
 (0)