Skip to content

Instantly share code, notes, and snippets.

@callmephil
Last active November 16, 2025 18:38
Show Gist options
  • Select an option

  • Save callmephil/d5d2fb08998eb0012b737fdc6fadfa17 to your computer and use it in GitHub Desktop.

Select an option

Save callmephil/d5d2fb08998eb0012b737fdc6fadfa17 to your computer and use it in GitHub Desktop.
TwoFingerScrollDetector
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
enum ScrollDirection { up, down, left, right, none }
typedef TwoFingerScrollCallback =
void Function(ScrollDirection direction, double speed, Offset delta);
class TwoFingerScrollDetector extends StatefulWidget {
final Widget child;
final TwoFingerScrollCallback onScroll;
const TwoFingerScrollDetector({
super.key,
required this.child,
required this.onScroll,
});
@override
State<TwoFingerScrollDetector> createState() => _TwoFingerScrollDetectorState();
}
class _TwoFingerScrollDetectorState extends State<TwoFingerScrollDetector> {
int _touchPointers = 0;
Offset? _lastPoint;
late DateTime _lastTime;
ScrollDirection _dirFromDelta(Offset delta) {
if (delta.dx == 0 && delta.dy == 0) return ScrollDirection.none;
if (delta.dy.abs() > delta.dx.abs()) {
return delta.dy < 0 ? ScrollDirection.up : ScrollDirection.down;
} else {
return delta.dx < 0 ? ScrollDirection.left : ScrollDirection.right;
}
}
double _speed(Offset delta, Duration dt) {
if (dt.inMicroseconds == 0) return 0;
return delta.distance / (dt.inMicroseconds / 1e6);
}
@override
Widget build(BuildContext context) {
return Listener(
// mouse/trackpad scroll
onPointerSignal: (signal) {
if (signal is PointerScrollEvent) {
final delta = signal.scrollDelta;
final direction = _dirFromDelta(delta);
final speed = delta.distance * 60; // approx px/s on desktop
widget.onScroll(direction, speed, delta);
}
},
// touch tracking (for physical two-finger gestures)
onPointerDown: (_) => _touchPointers++,
onPointerUp: (_) {
_touchPointers = max(0, _touchPointers - 1);
_lastPoint = null;
},
onPointerCancel: (_) {
_touchPointers = max(0, _touchPointers - 1);
_lastPoint = null;
},
behavior: HitTestBehavior.opaque,
child: GestureDetector(
onScaleStart: (details) {
if (_touchPointers == 2) {
_lastPoint = details.focalPoint;
_lastTime = DateTime.now();
}
},
onScaleUpdate: (details) {
if (_touchPointers != 2) return;
final now = DateTime.now();
final dt = now.difference(_lastTime);
final last = _lastPoint ?? details.focalPoint;
final delta = details.focalPoint - last;
final direction = _dirFromDelta(delta);
final speed = _speed(delta, dt);
widget.onScroll(direction, speed, delta);
_lastPoint = details.focalPoint;
_lastTime = now;
},
onScaleEnd: (_) {
widget.onScroll(ScrollDirection.none, 0, Offset.zero);
},
child: widget.child,
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment