Skip to content

Commit

Permalink
Add an example and update GestureDetector documentation (#102360)
Browse files Browse the repository at this point in the history
  • Loading branch information
bleroux authored May 24, 2022
1 parent 336aa26 commit 1e0a1a2
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 0 deletions.
127 changes: 127 additions & 0 deletions examples/api/lib/widgets/gesture_detector/gesture_detector.2.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

void main() {
debugPrintGestureArenaDiagnostics = true;
runApp(const NestedGestureDetectorsApp());
}

enum _OnTapWinner { none, yellow, green }

class NestedGestureDetectorsApp extends StatelessWidget {
const NestedGestureDetectorsApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Nested GestureDetectors')),
body: const NestedGestureDetectorsExample(),
),
);
}
}

class NestedGestureDetectorsExample extends StatefulWidget {
const NestedGestureDetectorsExample({super.key});

@override
State<NestedGestureDetectorsExample> createState() => _NestedGestureDetectorsExampleState();
}

class _NestedGestureDetectorsExampleState
extends State<NestedGestureDetectorsExample> {
bool _isYellowTranslucent = false;
_OnTapWinner _winner = _OnTapWinner.none;
final Border highlightBorder = Border.all(color: Colors.red, width: 5);

@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () {
debugPrint('Green onTap');
setState(() {
_winner = _OnTapWinner.green;
});
},
onTapDown: (_) => debugPrint('Green onTapDown'),
onTapCancel: () => debugPrint('Green onTapCancel'),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
border: _winner == _OnTapWinner.green ? highlightBorder : null,
color: Colors.green,
),
child: GestureDetector(
// Setting behavior to transparent or opaque as no impact on
// parent-child hit testing. A tap on 'Yellow' is also in
// 'Green' bounds. Both enter the gesture arena, 'Yellow' wins
// because it is in front.
behavior: _isYellowTranslucent
? HitTestBehavior.translucent
: HitTestBehavior.opaque,
onTap: () {
debugPrint('Yellow onTap');
setState(() {
_winner = _OnTapWinner.yellow;
});
},
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
border: _winner == _OnTapWinner.yellow ? highlightBorder : null,
color: Colors.amber,
),
width: 200,
height: 200,
child: Text(
'HitTextBehavior.${_isYellowTranslucent ? 'translucent' : 'opaque'}',
textAlign: TextAlign.center,
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
ElevatedButton(
child: const Text('Reset'),
onPressed: () {
setState(() {
_isYellowTranslucent = false;
_winner = _OnTapWinner.none;
});
},
),
const SizedBox(width: 8),
ElevatedButton(
child: Text(
'Set Yellow behavior to ${_isYellowTranslucent ? 'opaque' : 'translucent'}',
),
onPressed: () {
setState(() => _isYellowTranslucent = !_isYellowTranslucent);
},
),
],
),
),
],
);
}

@override
void dispose() {
debugPrintGestureArenaDiagnostics = false;
super.dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter_api_samples/widgets/gesture_detector/gesture_detector.2.dart'
as example;
import 'package:flutter_test/flutter_test.dart';

void main() {

void expectBorders(
WidgetTester tester, {
required bool expectGreenHasBorder,
required bool expectYellowHasBorder,
}) {
final Finder containerFinder = find.byType(Container);
final Finder greenFinder = containerFinder.first;
final Finder yellowFinder = containerFinder.last;

final Container greenContainer = tester.firstWidget<Container>(greenFinder);
final BoxDecoration? greenDecoration = greenContainer.decoration as BoxDecoration?;
expect(greenDecoration?.border, expectGreenHasBorder ? isNot(null) : null);

final Container yellowContainer = tester.firstWidget<Container>(yellowFinder);
final BoxDecoration? yellowDecoration = yellowContainer.decoration as BoxDecoration?;
expect(yellowDecoration?.border, expectYellowHasBorder ? isNot(null) : null);
}

void expectInnerGestureDetectorBehavior(WidgetTester tester, HitTestBehavior behavior) {
// Note that there is a third GestureDetector added by Scaffold
final Finder innerGestureDetectorFinder = find.byType(GestureDetector).at(1);
final GestureDetector innerGestureDetector = tester.firstWidget<GestureDetector>(innerGestureDetectorFinder);
expect(innerGestureDetector.behavior, behavior);
}

testWidgets('Only the green Container shows a red border when tapped', (WidgetTester tester) async {
await tester.pumpWidget(
const example.NestedGestureDetectorsApp(),
);

final Finder greenFinder = find.byType(Container).first;
final Offset greenTopLeftCorner = tester.getTopLeft(greenFinder);
await tester.tapAt(greenTopLeftCorner);
await tester.pumpAndSettle();
expectBorders(tester, expectGreenHasBorder: true, expectYellowHasBorder: false);

// Tap on the button to toggle inner GestureDetector.behavior
final Finder toggleBehaviorFinder = find.byType(ElevatedButton).last;
await tester.tap(toggleBehaviorFinder);
await tester.pump();
expectInnerGestureDetectorBehavior(tester, HitTestBehavior.translucent);

// Tap again on the green container, expect nothing changed
await tester.tapAt(greenTopLeftCorner);
await tester.pump();
expectBorders(tester, expectGreenHasBorder: true, expectYellowHasBorder: false);

// Tap on the reset button
final Finder resetFinder = find.byType(ElevatedButton).first;
await tester.tap(resetFinder);
await tester.pump();
expectInnerGestureDetectorBehavior(tester, HitTestBehavior.opaque);
});

testWidgets('Only the yellow Container shows a red border when tapped', (WidgetTester tester) async {
await tester.pumpWidget(
const example.NestedGestureDetectorsApp(),
);

final Finder yellowFinder = find.byType(Container).last;
final Offset yellowTopLeftCorner = tester.getTopLeft(yellowFinder);
await tester.tapAt(yellowTopLeftCorner);
await tester.pump();
expectBorders(tester, expectGreenHasBorder: false, expectYellowHasBorder: true);

// Tap on the button to toggle inner GestureDetector.behavior
final Finder toggleBehaviorFinder = find.byType(ElevatedButton).last;
await tester.tap(toggleBehaviorFinder);
await tester.pump();
expectInnerGestureDetectorBehavior(tester, HitTestBehavior.translucent);

// Tap again on the yellow container, expect nothing changed
await tester.tapAt(yellowTopLeftCorner);
await tester.pump();
expectBorders(tester, expectGreenHasBorder: false, expectYellowHasBorder: true);

// Tap on the reset button
final Finder resetFinder = find.byType(ElevatedButton).first;
await tester.tap(resetFinder);
await tester.pump();
expectInnerGestureDetectorBehavior(tester, HitTestBehavior.opaque);
});
}
50 changes: 50 additions & 0 deletions packages/flutter/lib/src/widgets/gesture_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,56 @@ class GestureRecognizerFactoryWithHandlers<T extends GestureRecognizer> extends
/// ** See code in examples/api/lib/widgets/gesture_detector/gesture_detector.1.dart **
/// {@end-tool}
///
/// ### Troubleshooting
///
/// Why isn't my parent [GestureDetector.onTap] method called?
///
/// Given a parent [GestureDetector] with an onTap callback, and a child
/// GestureDetector that also defines an onTap callback, when the inner
/// GestureDetector is tapped, both GestureDetectors send a [GestureRecognizer]
/// into the gesture arena. This is because the pointer coordinates are within the
/// bounds of both GestureDetectors. The child GestureDetector wins in this
/// scenario because it was the first to enter the arena, resolving as first come,
/// first served. The child onTap is called, and the parent's is not as the gesture has
/// been consumed.
/// For more information on gesture disambiguation see:
/// [Gesture disambiguation](https://docs.flutter.dev/development/ui/advanced/gestures#gesture-disambiguation).
///
/// Setting [GestureDetector.behavior] to [HitTestBehavior.opaque]
/// or [HitTestBehavior.translucent] has no impact on parent-child relationships:
/// both GestureDetectors send a GestureRecognizer into the gesture arena, only one wins.
///
/// Some callbacks (e.g. onTapDown) can fire before a recognizer wins the arena,
/// and others (e.g. onTapCancel) fire even when it loses the arena. Therefore,
/// the parent detector in the example above may call some of its callbacks even
/// though it loses in the arena.
///
/// {@tool dartpad}
/// This example uses a [GestureDetector] that wraps a green [Container] and a second
/// GestureDetector that wraps a yellow Container. The second GestureDetector is
/// a child of the green Container.
/// Both GestureDetectors define an onTap callback. When the callback is called it
/// adds a red border to the corresponding Container.
///
/// When the green Container is tapped, it's parent GestureDetector enters
/// the gesture arena. It wins because there is no competing GestureDetector and
/// the green Container shows a red border.
/// When the yellow Container is tapped, it's parent GestureDetector enters
/// the gesture arena. The GestureDetector that wraps the green Container also
/// enters the gesture arena (the pointer events coordinates are inside both
/// GestureDetectors bounds). The GestureDetector that wraps the yellow Container
/// wins because it was the first detector to enter the arena.
///
/// This example sets [debugPrintGestureArenaDiagnostics] to true.
/// This flag prints useful information about gesture arenas.
///
/// Changing the [GestureDetector.behavior] property to [HitTestBehavior.translucent]
/// or [HitTestBehavior.opaque] has no impact: both GestureDetectors send a [GestureRecognizer]
/// into the gesture arena, only one wins.
///
/// ** See code in examples/api/lib/widgets/gesture_detector/gesture_detector.2.dart **
/// {@end-tool}
///
/// ## Debugging
///
/// To see how large the hit test box of a [GestureDetector] is for debugging
Expand Down

0 comments on commit 1e0a1a2

Please sign in to comment.