From 351e319a47e27c64e9184285dfcd58e80bc961c8 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Mon, 22 Mar 2021 12:15:03 -0700 Subject: [PATCH] Improve documentation for PointerSignalResolver class (#78738) --- dev/bots/test/test_test.dart | 15 +- packages/flutter/lib/src/gestures/events.dart | 4 + .../src/gestures/pointer_signal_resolver.dart | 143 ++++++++++++++++-- 3 files changed, 149 insertions(+), 13 deletions(-) diff --git a/dev/bots/test/test_test.dart b/dev/bots/test/test_test.dart index 2151f4890a..83565ca9c5 100644 --- a/dev/bots/test/test_test.dart +++ b/dev/bots/test/test_test.dart @@ -12,6 +12,15 @@ import 'package:process/process.dart'; import '../test.dart'; import 'common.dart'; +/// Fails a test if the exit code of `result` is not the expected value. This +/// is favored over `expect(result.exitCode, expectedExitCode)` because this +/// will include the process result's stdio in the failure message. +void expectExitCode(ProcessResult result, int expectedExitCode) { + if (result.exitCode != expectedExitCode) { + fail('Failure due to exit code ${result.exitCode}\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}'); + } +} + void main() { group('verifyVersion()', () { MemoryFileSystem fileSystem; @@ -103,14 +112,14 @@ void main() { ProcessResult result = await runScript( {'SHARD': 'smoke_tests', 'SUBSHARD': '1_3'}, ); - expect(result.exitCode, 0); + expectExitCode(result, 0); // There are currently 6 smoke tests. This shard should contain test 1 and 2. expect(result.stdout, contains('Selecting subshard 1 of 3 (range 1-2 of 6)')); result = await runScript( {'SHARD': 'smoke_tests', 'SUBSHARD': '5_6'}, ); - expect(result.exitCode, 0); + expectExitCode(result, 0); // This shard should contain only test 5. expect(result.stdout, contains('Selecting subshard 5 of 6 (range 5-5 of 6)')); }); @@ -119,7 +128,7 @@ void main() { final ProcessResult result = await runScript( {'SHARD': 'smoke_tests', 'SUBSHARD': '100_99'}, ); - expect(result.exitCode, 1); + expectExitCode(result, 1); expect(result.stdout, contains('Invalid subshard name')); }); }); diff --git a/packages/flutter/lib/src/gestures/events.dart b/packages/flutter/lib/src/gestures/events.dart index 404ddae911..b4d6e4e397 100644 --- a/packages/flutter/lib/src/gestures/events.dart +++ b/packages/flutter/lib/src/gestures/events.dart @@ -1847,6 +1847,8 @@ class _TransformedPointerUpEvent extends _TransformedPointerEvent with _CopyPoin /// /// * [Listener.onPointerSignal], which allows callers to be notified of these /// events in a widget tree. +/// * [PointerSignalResolver], which provides an opt-in mechanism whereby +/// participating agents may disambiguate an event's target. abstract class PointerSignalEvent extends PointerEvent { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. @@ -1916,6 +1918,8 @@ mixin _CopyPointerScrollEvent on PointerEvent { /// /// * [Listener.onPointerSignal], which allows callers to be notified of these /// events in a widget tree. +/// * [PointerSignalResolver], which provides an opt-in mechanism whereby +/// participating agents may disambiguate an event's target. class PointerScrollEvent extends PointerSignalEvent with _PointerEventDescription, _CopyPointerScrollEvent { /// Creates a pointer scroll event. /// diff --git a/packages/flutter/lib/src/gestures/pointer_signal_resolver.dart b/packages/flutter/lib/src/gestures/pointer_signal_resolver.dart index be7b348144..e0fc6c24f2 100644 --- a/packages/flutter/lib/src/gestures/pointer_signal_resolver.dart +++ b/packages/flutter/lib/src/gestures/pointer_signal_resolver.dart @@ -15,22 +15,145 @@ bool _isSameEvent(PointerSignalEvent event1, PointerSignalEvent event2) { return (event1.original ?? event1) == (event2.original ?? event2); } -/// An resolver for pointer signal events. +/// Mediates disputes over which listener should handle pointer signal events +/// when multiple listeners wish to handle those events. /// -/// Objects interested in a [PointerSignalEvent] should register a callback to -/// be called if they should handle the event. The resolver's purpose is to -/// ensure that the same pointer signal is not handled by multiple objects in -/// a hierarchy. +/// Pointer signals (such as [PointerScrollEvent]) are immediate, so unlike +/// events that participate in the gesture arena, pointer signals always +/// resolve at the end of event dispatch. Yet if objects interested in handling +/// these signal events were to handle them directly, it would cause issues +/// such as multiple [Scrollable] widgets in the widget hierarchy responding +/// to the same mouse wheel event. Using this class, these events will only +/// be dispatched to the the first registered handler, which will in turn +/// correspond to the widget that's deepest in the widget hierarchy. /// -/// Pointer signals are immediate, so unlike a gesture arena it always resolves -/// at the end of event dispatch. The first callback registered will be the one -/// that is called. +/// To use this class, objects should register their event handler like so: +/// +/// {@tool snippet} +/// ```dart +/// void handleSignalEvent(PointerSignalEvent event) { +/// GestureBinding.instance!.pointerSignalResolver.register(event, (PointerSignalEvent event) { +/// // handle the event... +/// }); +/// } +/// ``` +/// {@end-tool} +/// +/// {@tool dartpad --template=stateful_widget_material} +/// Here is an example that demonstrates the effect of not using the resolver +/// versus using it. +/// +/// When this example is set to _not_ use the resolver, then scrolling the +/// mouse wheel over the outer box will cause only the outer box to change +/// color, but scrolling the mouse wheel over inner box will cause _both_ the +/// outer and the inner boxes to change color (because they're both receiving +/// the scroll event). +/// +/// When this excample is set to _use_ the resolver, then only the box located +/// directly under the cursor will change color when the mouse wheel is +/// scrolled. +/// +/// ```dart imports +/// import 'package:flutter/gestures.dart'; +/// ``` +/// +/// ```dart +/// HSVColor outerColor = const HSVColor.fromAHSV(0.2, 120.0, 1, 1); +/// HSVColor innerColor = const HSVColor.fromAHSV(1, 60.0, 1, 1); +/// bool useResolver = false; +/// +/// void rotateOuterColor() { +/// setState(() { +/// outerColor = outerColor.withHue((outerColor.hue + 6) % 360.0); +/// }); +/// } +/// +/// void rotateInnerColor() { +/// setState(() { +/// innerColor = innerColor.withHue((innerColor.hue + 6) % 360.0); +/// }); +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return Material( +/// child: Stack( +/// fit: StackFit.expand, +/// children: [ +/// Listener( +/// onPointerSignal: (PointerSignalEvent event) { +/// if (useResolver) { +/// GestureBinding.instance!.pointerSignalResolver.register(event, (PointerSignalEvent event) { +/// rotateOuterColor(); +/// }); +/// } else { +/// rotateOuterColor(); +/// } +/// }, +/// child: DecoratedBox( +/// decoration: BoxDecoration( +/// border: const Border.fromBorderSide(BorderSide()), +/// color: outerColor.toColor(), +/// ), +/// child: FractionallySizedBox( +/// widthFactor: 0.5, +/// heightFactor: 0.5, +/// child: DecoratedBox( +/// decoration: BoxDecoration( +/// border: const Border.fromBorderSide(BorderSide()), +/// color: innerColor.toColor(), +/// ), +/// child: Listener( +/// onPointerSignal: (PointerSignalEvent event) { +/// if (useResolver) { +/// GestureBinding.instance!.pointerSignalResolver.register(event, (PointerSignalEvent event) { +/// rotateInnerColor(); +/// }); +/// } else { +/// rotateInnerColor(); +/// } +/// }, +/// child: const AbsorbPointer(), +/// ), +/// ), +/// ), +/// ), +/// ), +/// Align( +/// alignment: Alignment.topLeft, +/// child: Row( +/// crossAxisAlignment: CrossAxisAlignment.center, +/// children: [ +/// Switch( +/// value: useResolver, +/// onChanged: (bool value) { +/// setState(() { +/// useResolver = value; +/// }); +/// }, +/// ), +/// Text( +/// 'Use the PointerSignalResolver?', +/// style: DefaultTextStyle.of(context).style.copyWith(fontWeight: FontWeight.bold), +/// ), +/// ], +/// ), +/// ), +/// ], +/// ), +/// ); +/// } +/// ``` +/// {@end-tool} class PointerSignalResolver { PointerSignalResolvedCallback? _firstRegisteredCallback; PointerSignalEvent? _currentEvent; /// Registers interest in handling [event]. + /// + /// See the documentation for the [PointerSignalResolver] class on when and + /// how this method should be used. void register(PointerSignalEvent event, PointerSignalResolvedCallback callback) { assert(event != null); assert(callback != null); @@ -45,8 +168,8 @@ class PointerSignalResolver { /// Resolves the event, calling the first registered callback if there was /// one. /// - /// Called after the framework has finished dispatching the pointer signal - /// event. + /// This is called by the [GestureBinding] after the framework has finished + /// dispatching the pointer signal event. void resolve(PointerSignalEvent event) { if (_firstRegisteredCallback == null) { assert(_currentEvent == null);