pointer_signal_resolver.dart 7.01 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';

import 'events.dart';

/// The callback to register with a [PointerSignalResolver] to express
/// interest in a pointer signal event.
typedef PointerSignalResolvedCallback = void Function(PointerSignalEvent event);

13 14 15 16
bool _isSameEvent(PointerSignalEvent event1, PointerSignalEvent event2) {
  return (event1.original ?? event1) == (event2.original ?? event2);
}

17 18
/// Mediates disputes over which listener should handle pointer signal events
/// when multiple listeners wish to handle those events.
19
///
20 21 22 23 24 25
/// 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
26
/// be dispatched to the first registered handler, which will in turn
27
/// correspond to the widget that's deepest in the widget hierarchy.
28
///
29 30 31 32 33 34 35 36 37 38 39 40 41 42
/// To use this class, objects should register their event handler like so:
///
/// ```dart
/// void handleSignalEvent(PointerSignalEvent event) {
///   GestureBinding.instance!.pointerSignalResolver.register(event, (PointerSignalEvent event) {
///     // handle the event...
///   });
/// }
/// ```
///
/// {@tool dartpad --template=stateful_widget_material}
/// Here is an example that demonstrates the effect of not using the resolver
/// versus using it.
///
43
/// When this example is set to _not_ use the resolver, then triggering the
44
/// mouse wheel over the outer box will cause only the outer box to change
45 46 47
/// color, but triggering the mouse wheel over the inner box will cause _both_
/// the outer and the inner boxes to change color (because they're both
/// receiving the event).
48
///
49
/// When this example is set to _use_ the resolver, then only the box located
50
/// directly under the cursor will change color when the mouse wheel is
51
/// triggered.
52 53 54 55 56
///
/// ```dart imports
/// import 'package:flutter/gestures.dart';
/// ```
///
57 58 59 60 61 62
/// ```dart preamble
/// class ColorChanger extends StatefulWidget {
///   const ColorChanger({
///     Key? key,
///     required this.initialColor,
///     required this.useResolver,
63
///     this.child,
64
///   }) : super(key: key);
65
///
66 67
///   final HSVColor initialColor;
///   final bool useResolver;
68
///   final Widget? child;
69 70
///
///   @override
71
///   State<ColorChanger> createState() => _ColorChangerState();
72 73
/// }
///
74 75 76 77 78
/// class _ColorChangerState extends State<ColorChanger> {
///   late HSVColor color;
///
///   void rotateColor() {
///     setState(() {
79
///       color = color.withHue((color.hue + 3) % 360.0);
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
///     });
///   }
///
///   @override
///   void initState() {
///     super.initState();
///     color = widget.initialColor;
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return DecoratedBox(
///       decoration: BoxDecoration(
///         border: const Border.fromBorderSide(BorderSide()),
///         color: color.toColor(),
///       ),
///       child: Listener(
///         onPointerSignal: (PointerSignalEvent event) {
///           if (widget.useResolver) {
///             GestureBinding.instance!.pointerSignalResolver.register(event, (PointerSignalEvent event) {
///               rotateColor();
///             });
///           } else {
///             rotateColor();
///           }
///         },
106 107 108 109 110 111 112
///         child: Stack(
///           fit: StackFit.expand,
///           children: <Widget>[
///             const AbsorbPointer(),
///             if (widget.child != null) widget.child!,
///           ],
///         ),
113 114 115
///       ),
///     );
///   }
116
/// }
117 118 119 120
/// ```
///
/// ```dart
/// bool useResolver = false;
121 122 123 124 125 126 127
///
/// @override
/// Widget build(BuildContext context) {
///   return Material(
///     child: Stack(
///       fit: StackFit.expand,
///       children: <Widget>[
128 129 130 131 132 133 134 135 136
///         ColorChanger(
///           initialColor: const HSVColor.fromAHSV(0.2, 120.0, 1, 1),
///           useResolver: useResolver,
///           child: FractionallySizedBox(
///             widthFactor: 0.5,
///             heightFactor: 0.5,
///             child: ColorChanger(
///               initialColor: const HSVColor.fromAHSV(1, 60.0, 1, 1),
///               useResolver: useResolver,
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
///             ),
///           ),
///         ),
///         Align(
///           alignment: Alignment.topLeft,
///           child: Row(
///             crossAxisAlignment: CrossAxisAlignment.center,
///             children: <Widget>[
///               Switch(
///                 value: useResolver,
///                 onChanged: (bool value) {
///                   setState(() {
///                     useResolver = value;
///                   });
///                 },
///               ),
153
///               const Text(
154
///                 'Use the PointerSignalResolver?',
155
///                 style: TextStyle(fontWeight: FontWeight.bold),
156 157 158 159 160 161 162 163 164 165
///               ),
///             ],
///           ),
///         ),
///       ],
///     ),
///   );
/// }
/// ```
/// {@end-tool}
166
class PointerSignalResolver {
167
  PointerSignalResolvedCallback? _firstRegisteredCallback;
168

169
  PointerSignalEvent? _currentEvent;
170 171

  /// Registers interest in handling [event].
172 173 174
  ///
  /// See the documentation for the [PointerSignalResolver] class on when and
  /// how this method should be used.
175 176 177
  void register(PointerSignalEvent event, PointerSignalResolvedCallback callback) {
    assert(event != null);
    assert(callback != null);
178
    assert(_currentEvent == null || _isSameEvent(_currentEvent!, event));
179 180 181 182 183 184 185 186 187 188
    if (_firstRegisteredCallback != null) {
      return;
    }
    _currentEvent = event;
    _firstRegisteredCallback = callback;
  }

  /// Resolves the event, calling the first registered callback if there was
  /// one.
  ///
189 190
  /// This is called by the [GestureBinding] after the framework has finished
  /// dispatching the pointer signal event.
191
  @pragma('vm:notify-debugger-on-exception')
192 193 194 195 196
  void resolve(PointerSignalEvent event) {
    if (_firstRegisteredCallback == null) {
      assert(_currentEvent == null);
      return;
    }
197
    assert(_isSameEvent(_currentEvent!, event));
198
    try {
199
      _firstRegisteredCallback!(_currentEvent!);
200
    } catch (exception, stack) {
201
      InformationCollector? collector;
202 203 204 205 206 207
      assert(() {
        collector = () sync* {
          yield DiagnosticsProperty<PointerSignalEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
        };
        return true;
      }());
208 209 210 211
      FlutterError.reportError(FlutterErrorDetails(
        exception: exception,
        stack: stack,
        library: 'gesture library',
212
        context: ErrorDescription('while resolving a PointerSignalEvent'),
213
        informationCollector: collector,
214 215 216 217 218 219
      ));
    }
    _firstRegisteredCallback = null;
    _currentEvent = null;
  }
}