diff --git a/lib/src/scroll_state.dart b/lib/src/scroll_state.dart index 4da8130..60f4b28 100644 --- a/lib/src/scroll_state.dart +++ b/lib/src/scroll_state.dart @@ -1,44 +1,91 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'dart:math' as math; const kMobilePhysics = BouncingScrollPhysics(); const kDesktopPhysics = NeverScrollableScrollPhysics(); class ScrollState with ChangeNotifier { final ScrollController controller = ScrollController(); - ScrollPhysics physics = kDesktopPhysics; + ScrollPhysics physics = kMobilePhysics; double futurePosition = 0; + bool updateState = false; final ScrollPhysics mobilePhysics; final int durationMS; + bool prevDeltaPositive = false; + double? lastLock = null; + + Future? _animationEnd; + + Function()? handlePipelinedScroll; + ScrollState(this.mobilePhysics, this.durationMS); - void handleDesktopScroll(PointerSignalEvent event) { + static double calcMaxDelta(ScrollController controller, double delta) { + return delta > 0 ? + math.min(controller.position.pixels + delta, controller.position.maxScrollExtent) - controller.position.pixels : + math.max(controller.position.pixels + delta, controller.position.minScrollExtent) - controller.position.pixels; + } + + void handleDesktopScroll( + PointerSignalEvent event, double scrollSpeed, Curve animationCurve, [bool readLastDirection = true]) { // Ensure desktop physics is being used. - if (physics == kMobilePhysics) { - physics = kDesktopPhysics; - notifyListeners(); + if (physics == kMobilePhysics || lastLock != null) { + if (lastLock != null) updateState = !updateState; + if (event is PointerScrollEvent) { + double posPixels = controller.position.pixels; + if ((posPixels == controller.position.minScrollExtent && event.scrollDelta.dy < 0) + || (posPixels == controller.position.maxScrollExtent && event.scrollDelta.dy > 0)) return; + else physics = kDesktopPhysics; + bool outOfBounds = posPixels < controller.position.minScrollExtent || posPixels > controller.position.maxScrollExtent; + double calcDelta = calcMaxDelta(controller, event.scrollDelta.dy); + if (!outOfBounds) controller.jumpTo(lastLock ?? (posPixels - calcDelta)); + double deltaDelta = calcDelta - event.scrollDelta.dy; + handlePipelinedScroll = () { + handlePipelinedScroll = null; + double currPos = controller.position.pixels; + double currDelta = event.scrollDelta.dy; + bool shouldLock = lastLock != null ? (lastLock == currPos) : (posPixels != currPos + deltaDelta && + (currPos != controller.position.maxScrollExtent || currDelta < 0) && + (currPos != controller.position.minScrollExtent || currDelta > 0)); + if (!outOfBounds && shouldLock) { + controller.jumpTo(posPixels); + lastLock = posPixels; + controller.position.moveTo(posPixels)..whenComplete(() { + physics = kMobilePhysics; + notifyListeners(); + }); + return; + } + else { + if (lastLock != null || outOfBounds) + controller.jumpTo(lastLock != null ? posPixels : (currPos - calcMaxDelta(controller, currDelta))); + lastLock = null; + handleDesktopScroll(event, scrollSpeed, animationCurve, false); + } + }; + notifyListeners(); + } return; } - if (event is PointerScrollEvent) { - // Return if limit is reached in either direction. - if (controller.position.atEdge) { - final dy = event.scrollDelta.dy; - // Return if bounds exceeded. - if (controller.position.pixels == 0) { - if (dy < 0) return; - } else { - if (dy > 0) return; - } - } - futurePosition += event.scrollDelta.dy; - - controller.animateTo( + else if (event is PointerScrollEvent) { + bool currentDeltaPositive = event.scrollDelta.dy > 0; + if (readLastDirection && currentDeltaPositive == prevDeltaPositive) + futurePosition += event.scrollDelta.dy * scrollSpeed; + else futurePosition = controller.position.pixels + event.scrollDelta.dy * scrollSpeed; + prevDeltaPositive = event.scrollDelta.dy > 0; + + Future animationEnd = _animationEnd = controller.animateTo( futurePosition, duration: Duration(milliseconds: durationMS), - curve: Curves.linear, + curve: animationCurve, ); + animationEnd.whenComplete(() {if (animationEnd == _animationEnd && physics == kDesktopPhysics) { + physics = mobilePhysics; + notifyListeners(); + }}); } } diff --git a/lib/src/source.dart b/lib/src/source.dart index e49b00e..2fc054f 100644 --- a/lib/src/source.dart +++ b/lib/src/source.dart @@ -6,12 +6,16 @@ import 'scroll_state.dart'; class DynMouseScroll extends StatelessWidget { final ScrollPhysics mobilePhysics; final int durationMS; + final double scrollSpeed; + final Curve animationCurve; final Function(BuildContext, ScrollController, ScrollPhysics) builder; const DynMouseScroll({ super.key, this.mobilePhysics = kMobilePhysics, - this.durationMS = 200, + this.durationMS = 380, + this.scrollSpeed = 2, + this.animationCurve = Curves.easeOutQuart, required this.builder, }); @@ -23,8 +27,11 @@ class DynMouseScroll extends StatelessWidget { final scrollState = context.read(); final controller = scrollState.controller; final physics = context.select((ScrollState s) => s.physics); + final updateState = context.select((ScrollState s) => s.updateState); + scrollState.handlePipelinedScroll?.call(); return Listener( - onPointerSignal: scrollState.handleDesktopScroll, + onPointerSignal: (signalEvent) => scrollState.handleDesktopScroll( + signalEvent, scrollSpeed, animationCurve), onPointerDown: scrollState.handleTouchScroll, child: builder(context, controller, physics), ); diff --git a/pubspec.yaml b/pubspec.yaml index 112b6c5..2f255e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dyn_mouse_scroll description: A wrapper for scrollable widgets that enables smooth scrolling with a mouse on all platforms. -version: 1.0.7 +version: 1.0.8 repository: https://github.com/Bluebar1/dyn_mouse_scroll environment: