Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 67 additions & 20 deletions lib/src/scroll_state.dart
Original file line number Diff line number Diff line change
@@ -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<void>? _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<void> animationEnd = _animationEnd = controller.animateTo(
futurePosition,
duration: Duration(milliseconds: durationMS),
curve: Curves.linear,
curve: animationCurve,
);
animationEnd.whenComplete(() {if (animationEnd == _animationEnd && physics == kDesktopPhysics) {
physics = mobilePhysics;
notifyListeners();
}});
}
}

Expand Down
11 changes: 9 additions & 2 deletions lib/src/source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand All @@ -23,8 +27,11 @@ class DynMouseScroll extends StatelessWidget {
final scrollState = context.read<ScrollState>();
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),
);
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down