first commit

This commit is contained in:
2026-07-31 17:09:04 +02:00
commit 747ed57141
8 changed files with 606 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
import "package:flutter/foundation.dart";
import "package:flutter/gestures.dart";
import "package:flutter/material.dart";
/// Forwards unclaimed pointer scroll interactions within [child]'s bounds to
/// the position resolved by [positionProvider].
///
/// A scrollable only reacts to mouse wheel input while the pointer hovers
/// directly over it, and only reacts to drags that begin on it. Wrapping a
/// layout with this widget lets surrounding areas (for example the empty
/// margins beside a centered list) also drive the target scrollable, via
/// wheel/trackpad scroll signals or touch drags. A nested scrollable always
/// claims the interaction first — scroll signals through
/// [PointerSignalResolver] and drags through the gesture arena — so the target
/// only scrolls when the pointer is not interacting with another scrollable.
class ScrollForwarder extends StatefulWidget {
const ScrollForwarder({
required this.positionProvider,
required this.child,
super.key,
});
/// Resolves the scroll position to receive unclaimed scroll interactions.
///
/// Invoked for every signal or gesture so the current position is always
/// used. Return `null` to ignore the interaction.
final ScrollPosition? Function() positionProvider;
final Widget child;
@override
State<ScrollForwarder> createState() => _ScrollForwarderState();
}
class _ScrollForwarderState extends State<ScrollForwarder> {
Drag? _drag;
Set<PointerDeviceKind> _dragDevices = const <PointerDeviceKind>{};
Map<Type, GestureRecognizerFactory> _gestureRecognizers =
const <Type, GestureRecognizerFactory>{};
@override
void didChangeDependencies() {
super.didChangeDependencies();
final Set<PointerDeviceKind> devices = ScrollConfiguration.of(
context,
).dragDevices;
if (setEquals(devices, _dragDevices)) return;
_dragDevices = devices;
_gestureRecognizers = <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(supportedDevices: devices),
(VerticalDragGestureRecognizer instance) {
instance
..onStart = _onDragStart
..onUpdate = _onDragUpdate
..onEnd = _onDragEnd
..onCancel = _onDragCancel;
},
),
};
}
@override
void dispose() {
_drag?.cancel();
super.dispose();
}
void _onPointerSignal(PointerSignalEvent event) {
if (event is! PointerScrollEvent || event.scrollDelta.dy == 0.0) return;
final ScrollPosition? position = widget.positionProvider();
if (position == null || !position.hasContentDimensions) return;
GestureBinding.instance.pointerSignalResolver.register(event, (_) {
position.pointerScroll(event.scrollDelta.dy);
event.respond(allowPlatformDefault: false);
});
}
void _onDragStart(DragStartDetails details) {
final ScrollPosition? position = widget.positionProvider();
if (position == null) return;
_drag = position.drag(details, _disposeDrag);
}
void _disposeDrag() {
_drag = null;
}
void _onDragUpdate(DragUpdateDetails details) {
_drag?.update(details);
}
void _onDragEnd(DragEndDetails details) {
_drag?.end(details);
}
void _onDragCancel() {
_drag?.cancel();
}
@override
Widget build(BuildContext context) {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerSignal: _onPointerSignal,
child: RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: _gestureRecognizers,
child: widget.child,
),
);
}
}