-
-
Save tdenniston/9197f880d2ad7acea04e36fb14d91cfc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MIT License | |
// | |
// Copyright (c) 2024 tdenniston | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
import 'package:flutter/widgets.dart'; | |
/// Utility class providing a mechanism to recognize and dispatch single- and | |
/// multi-tap events. This provides a workaround for a deficiency in Flutter's | |
/// gesture recognition logic, namely the mandatory, hard-coded 300ms delay | |
/// added to tap handlers if a double-tap handler is also installed. | |
/// | |
/// The behavior implemented by this class is: | |
/// | |
/// - Always fire the single-tap handler on the first tap; | |
/// - If a second tap arrives within the recognition time window, fire the | |
/// double-tap handler; | |
/// - If a third tap arrives within the window, fire the triple-tap handler; | |
/// - Subsequent fast taps within the window fire the single-tap handler; | |
/// - After the recognition time window passes, the state is reset and the | |
/// algorithm restarts from the top. | |
/// | |
/// Here is an example sequence of incoming tap events and the resulting | |
/// callbacks made, assuming a recognition window of 300ms: | |
/// - [t=0ms] tap: onTap() | |
/// - [t=231ms] tap: onDoubleTap() | |
/// - [t=401ms] tap: onTripleTap() | |
/// - [t=499ms] tap: onSingleTap() | |
/// - [t=564ms] tap: onSingleTap() | |
/// - [t=900ms] tap: reset; onSingleTap() | |
/// | |
/// Additional custom handling can be implemented on top of this flow. For | |
/// example, if your usecase requires dispatching only a double or triple tap, | |
/// but not both, you can implement a time-based gate similar to this class in | |
/// your double and triple tap handlers. | |
/// | |
/// This class can be used via a provided convenience widget, | |
/// [MultiTapListener], or on its own for more advanced applications. | |
/// | |
/// Related Flutter bugs: | |
/// - https://github.com/flutter/flutter/issues/110300 | |
/// - https://github.com/flutter/flutter/issues/106170 | |
class MultiTapRecognizer { | |
static const _kDefaultRecognitionWindow = Duration(milliseconds: 300); | |
final Duration _window; | |
DateTime? _lastTap; | |
int _consecutiveTaps = 1; | |
/// The callback made on single taps. | |
final void Function()? onTap; | |
/// The callback made on double-taps. | |
final void Function()? onDoubleTap; | |
/// The callback made on triple-taps. | |
final void Function()? onTripleTap; | |
/// Optional override of the recognition window. | |
final Duration? recognitionWindow; | |
/// Constructs a multi-tap recognizer with the given event callbacks. | |
MultiTapRecognizer({ | |
this.onTap, | |
this.onDoubleTap, | |
this.onTripleTap, | |
this.recognitionWindow, | |
}) : _window = recognitionWindow ?? _kDefaultRecognitionWindow; | |
/// Accept an incoming tap event and dispatch to the appropriate callback. | |
void dispatchTap() { | |
final time = DateTime.now(); | |
if (_lastTap == null) { | |
_lastTap = time; | |
onTap?.call(); | |
return; | |
} | |
final elapsed = time.difference(_lastTap!); | |
if (elapsed.compareTo(_window) <= 0) { | |
_consecutiveTaps++; | |
} else { | |
_consecutiveTaps = 1; | |
} | |
switch (_consecutiveTaps) { | |
case 2: | |
onDoubleTap?.call(); | |
case 3: | |
onTripleTap?.call(); | |
default: | |
onTap?.call(); | |
} | |
_lastTap = time; | |
} | |
} | |
/// A widget that recognizes and dispatches multi-tap events. This widget does | |
/// not use or participate in the gesture arena, allowing it to be combined with | |
/// existing Flutter widgets that have built in tap responses that you want to | |
/// keep. The following example creates an InkWell that responds to double-taps, | |
/// while keeping the existing tap handling (e.g. the ink splash effect): | |
/// | |
/// Example: | |
/// | |
/// ```dart | |
/// class DoubleTapInkWell extends StatelessWidget { | |
/// const DoubleTapInkWell({super.key}); | |
/// | |
/// @override | |
/// Widget build(BuildContext context) { | |
/// return MultiTapListener( | |
/// onDoubleTap: () { | |
/// // Handle double tap. | |
/// }, | |
/// child: InkWell( | |
/// onTap: () { | |
/// // Handle normal tap. | |
/// }, | |
/// ), | |
/// ); | |
/// } | |
/// } | |
/// ``` | |
/// | |
/// Because this widget does not use the gesture arena, adding an [onTap] | |
/// callback to the [MultiTapListener] in the above example would result in both | |
/// the InkWell and the [MultiTapListener] responding to single taps. | |
class MultiTapListener extends StatefulWidget { | |
/// The callback made on single taps. | |
final void Function()? onTap; | |
/// The callback made on double-taps. | |
final void Function()? onDoubleTap; | |
/// The callback made on triple-taps. | |
final void Function()? onTripleTap; | |
/// Optional override of the recognition window. | |
final Duration? recognitionWindow; | |
/// Optional child widget. | |
final Widget? child; | |
/// Constructs a [MultiTapListener]. | |
const MultiTapListener( | |
{this.onTap, | |
this.onDoubleTap, | |
this.onTripleTap, | |
this.recognitionWindow, | |
this.child, | |
super.key}); | |
@override | |
State<MultiTapListener> createState() => _MultiTapListenerState(); | |
} | |
class _MultiTapListenerState extends State<MultiTapListener> { | |
late MultiTapRecognizer _recognizer; | |
@override | |
void initState() { | |
super.initState(); | |
_setRecognizer(); | |
} | |
@override | |
void didUpdateWidget(covariant MultiTapListener oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
_setRecognizer(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Listener( | |
onPointerUp: (_) => _recognizer.dispatchTap(), | |
child: widget.child, | |
); | |
} | |
void _setRecognizer() { | |
_recognizer = MultiTapRecognizer( | |
onTap: widget.onTap, | |
onDoubleTap: widget.onDoubleTap, | |
onTripleTap: widget.onTripleTap, | |
recognitionWindow: widget.recognitionWindow, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment