| // 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/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); |
| |
| bool _isSameEvent(PointerSignalEvent event1, PointerSignalEvent event2) { |
| return (event1.original ?? event1) == (event2.original ?? event2); |
| } |
| |
| /// Mediates disputes over which listener should handle pointer signal events |
| /// when multiple listeners wish to handle those events. |
| /// |
| /// 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. |
| /// |
| /// 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. |
| /// |
| /// When this example is set to _not_ use the resolver, then triggering the |
| /// mouse wheel over the outer box will cause only the outer box to change |
| /// 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). |
| /// |
| /// When this example is set to _use_ the resolver, then only the box located |
| /// directly under the cursor will change color when the mouse wheel is |
| /// triggered. |
| /// |
| /// ```dart imports |
| /// import 'package:flutter/gestures.dart'; |
| /// ``` |
| /// |
| /// ```dart preamble |
| /// class ColorChanger extends StatefulWidget { |
| /// const ColorChanger({ |
| /// Key? key, |
| /// required this.initialColor, |
| /// required this.useResolver, |
| /// required this.child, |
| /// }) : super(key: key); |
| /// |
| /// final HSVColor initialColor; |
| /// final bool useResolver; |
| /// final Widget child; |
| /// |
| /// @override |
| /// _ColorChangerState createState() => _ColorChangerState(); |
| /// } |
| /// |
| /// class _ColorChangerState extends State<ColorChanger> { |
| /// late HSVColor color; |
| /// |
| /// void rotateColor() { |
| /// setState(() { |
| /// color = color.withHue((color.hue + 6) % 360.0); |
| /// }); |
| /// } |
| /// |
| /// @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(); |
| /// } |
| /// }, |
| /// child: widget.child, |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// ``` |
| /// |
| /// ```dart |
| /// bool useResolver = false; |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return Material( |
| /// child: Stack( |
| /// fit: StackFit.expand, |
| /// children: <Widget>[ |
| /// 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, |
| /// child: const AbsorbPointer(), |
| /// ), |
| /// ), |
| /// ), |
| /// Align( |
| /// alignment: Alignment.topLeft, |
| /// child: Row( |
| /// crossAxisAlignment: CrossAxisAlignment.center, |
| /// children: <Widget>[ |
| /// Switch( |
| /// value: useResolver, |
| /// onChanged: (bool value) { |
| /// setState(() { |
| /// useResolver = value; |
| /// }); |
| /// }, |
| /// ), |
| /// const Text( |
| /// 'Use the PointerSignalResolver?', |
| /// style: TextStyle(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); |
| assert(_currentEvent == null || _isSameEvent(_currentEvent!, event)); |
| if (_firstRegisteredCallback != null) { |
| return; |
| } |
| _currentEvent = event; |
| _firstRegisteredCallback = callback; |
| } |
| |
| /// Resolves the event, calling the first registered callback if there was |
| /// one. |
| /// |
| /// 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); |
| return; |
| } |
| assert(_isSameEvent(_currentEvent!, event)); |
| try { |
| _firstRegisteredCallback!(_currentEvent!); |
| } catch (exception, stack) { |
| InformationCollector? collector; |
| assert(() { |
| collector = () sync* { |
| yield DiagnosticsProperty<PointerSignalEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty); |
| }; |
| return true; |
| }()); |
| FlutterError.reportError(FlutterErrorDetails( |
| exception: exception, |
| stack: stack, |
| library: 'gesture library', |
| context: ErrorDescription('while resolving a PointerSignalEvent'), |
| informationCollector: collector |
| )); |
| } |
| _firstRegisteredCallback = null; |
| _currentEvent = null; |
| } |
| } |