@@ -1458,13 +1458,17 @@ class RawScrollbar extends StatefulWidget {
14581458/// scrollbar track.
14591459class RawScrollbarState <T extends RawScrollbar > extends State <T > with TickerProviderStateMixin <T > {
14601460 Offset ? _startDragScrollbarAxisOffset;
1461+ Offset ? _lastDragUpdateOffset;
14611462 double ? _startDragThumbOffset;
1462- ScrollController ? _currentController ;
1463+ ScrollController ? _cachedController ;
14631464 Timer ? _fadeoutTimer;
14641465 late AnimationController _fadeoutAnimationController;
14651466 late Animation <double > _fadeoutOpacityAnimation;
14661467 final GlobalKey _scrollbarPainterKey = GlobalKey ();
14671468 bool _hoverIsActive = false ;
1469+ bool _thumbDragging = false ;
1470+
1471+ ScrollController ? get _effectiveScrollController => widget.controller ?? PrimaryScrollController .maybeOf (context);
14681472
14691473 /// Used to paint the scrollbar.
14701474 ///
@@ -1550,12 +1554,11 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
15501554 }
15511555
15521556 void _validateInteractions (AnimationStatus status) {
1553- final ScrollController ? scrollController = widget.controller ?? PrimaryScrollController .maybeOf (context);
15541557 if (status == AnimationStatus .dismissed) {
15551558 assert (_fadeoutOpacityAnimation.value == 0.0 );
15561559 // We do not check for a valid scroll position if the scrollbar is not
15571560 // visible, because it cannot be interacted with.
1558- } else if (scrollController != null && enableGestures) {
1561+ } else if (_effectiveScrollController != null && enableGestures) {
15591562 // Interactive scrollbars need to be properly configured. If it is visible
15601563 // for interaction, ensure we are set up properly.
15611564 assert (_debugCheckHasValidScrollPosition ());
@@ -1566,7 +1569,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
15661569 if (! mounted) {
15671570 return true ;
15681571 }
1569- final ScrollController ? scrollController = widget.controller ?? PrimaryScrollController . maybeOf (context) ;
1572+ final ScrollController ? scrollController = _effectiveScrollController ;
15701573 final bool tryPrimary = widget.controller == null ;
15711574 final String controllerForError = tryPrimary
15721575 ? 'PrimaryScrollController'
@@ -1698,11 +1701,11 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
16981701 }
16991702
17001703 void _updateScrollPosition (Offset updatedOffset) {
1701- assert (_currentController != null );
1704+ assert (_cachedController != null );
17021705 assert (_startDragScrollbarAxisOffset != null );
17031706 assert (_startDragThumbOffset != null );
17041707
1705- final ScrollPosition position = _currentController ! .position;
1708+ final ScrollPosition position = _cachedController ! .position;
17061709 late double primaryDelta;
17071710 switch (position.axisDirection) {
17081711 case AxisDirection .up:
@@ -1761,9 +1764,9 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
17611764 /// current scroll controller does not have any attached positions.
17621765 @protected
17631766 Axis ? getScrollbarDirection () {
1764- assert (_currentController != null );
1765- if (_currentController ! .hasClients) {
1766- return _currentController ! .position.axis;
1767+ assert (_cachedController != null );
1768+ if (_cachedController ! .hasClients) {
1769+ return _cachedController ! .position.axis;
17671770 }
17681771 return null ;
17691772 }
@@ -1788,7 +1791,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
17881791 @mustCallSuper
17891792 void handleThumbPressStart (Offset localPosition) {
17901793 assert (_debugCheckHasValidScrollPosition ());
1791- _currentController = widget.controller ?? PrimaryScrollController . maybeOf (context) ;
1794+ _cachedController = _effectiveScrollController ;
17921795 final Axis ? direction = getScrollbarDirection ();
17931796 if (direction == null ) {
17941797 return ;
@@ -1797,6 +1800,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
17971800 _fadeoutAnimationController.forward ();
17981801 _startDragScrollbarAxisOffset = localPosition;
17991802 _startDragThumbOffset = scrollbarPainter.getThumbScrollOffset ();
1803+ _thumbDragging = true ;
18001804 }
18011805
18021806 /// Handler called when a currently active long press gesture moves.
@@ -1806,7 +1810,11 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
18061810 @mustCallSuper
18071811 void handleThumbPressUpdate (Offset localPosition) {
18081812 assert (_debugCheckHasValidScrollPosition ());
1809- final ScrollPosition position = _currentController! .position;
1813+ if (_lastDragUpdateOffset == localPosition) {
1814+ return ;
1815+ }
1816+ _lastDragUpdateOffset = localPosition;
1817+ final ScrollPosition position = _cachedController! .position;
18101818 if (! position.physics.shouldAcceptUserOffset (position)) {
18111819 return ;
18121820 }
@@ -1822,45 +1830,47 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
18221830 @mustCallSuper
18231831 void handleThumbPressEnd (Offset localPosition, Velocity velocity) {
18241832 assert (_debugCheckHasValidScrollPosition ());
1833+ _thumbDragging = false ;
18251834 final Axis ? direction = getScrollbarDirection ();
18261835 if (direction == null ) {
18271836 return ;
18281837 }
18291838 _maybeStartFadeoutTimer ();
18301839 _startDragScrollbarAxisOffset = null ;
1840+ _lastDragUpdateOffset = null ;
18311841 _startDragThumbOffset = null ;
1832- _currentController = null ;
1842+ _cachedController = null ;
18331843 }
18341844
18351845 void _handleTrackTapDown (TapDownDetails details) {
18361846 // The Scrollbar should page towards the position of the tap on the track.
18371847 assert (_debugCheckHasValidScrollPosition ());
1838- _currentController = widget.controller ?? PrimaryScrollController . maybeOf (context) ;
1848+ _cachedController = _effectiveScrollController ;
18391849
1840- final ScrollPosition position = _currentController ! .position;
1850+ final ScrollPosition position = _cachedController ! .position;
18411851 if (! position.physics.shouldAcceptUserOffset (position)) {
18421852 return ;
18431853 }
18441854
18451855 double scrollIncrement;
18461856 // Is an increment calculator available?
18471857 final ScrollIncrementCalculator ? calculator = Scrollable .maybeOf (
1848- _currentController ! .position.context.notificationContext! ,
1858+ _cachedController ! .position.context.notificationContext! ,
18491859 )? .widget.incrementCalculator;
18501860 if (calculator != null ) {
18511861 scrollIncrement = calculator (
18521862 ScrollIncrementDetails (
18531863 type: ScrollIncrementType .page,
1854- metrics: _currentController ! .position,
1864+ metrics: _cachedController ! .position,
18551865 ),
18561866 );
18571867 } else {
18581868 // Default page increment
1859- scrollIncrement = 0.8 * _currentController ! .position.viewportDimension;
1869+ scrollIncrement = 0.8 * _cachedController ! .position.viewportDimension;
18601870 }
18611871
18621872 // Adjust scrollIncrement for direction
1863- switch (_currentController ! .position.axisDirection) {
1873+ switch (_cachedController ! .position.axisDirection) {
18641874 case AxisDirection .up:
18651875 if (details.localPosition.dy > scrollbarPainter._thumbOffset) {
18661876 scrollIncrement = - scrollIncrement;
@@ -1883,17 +1893,16 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
18831893 break ;
18841894 }
18851895
1886- _currentController ! .position.moveTo (
1887- _currentController ! .position.pixels + scrollIncrement,
1896+ _cachedController ! .position.moveTo (
1897+ _cachedController ! .position.pixels + scrollIncrement,
18881898 duration: const Duration (milliseconds: 100 ),
18891899 curve: Curves .easeInOut,
18901900 );
18911901 }
18921902
18931903 // ScrollController takes precedence over ScrollNotification
18941904 bool _shouldUpdatePainter (Axis notificationAxis) {
1895- final ScrollController ? scrollController = widget.controller ??
1896- PrimaryScrollController .maybeOf (context);
1905+ final ScrollController ? scrollController = _effectiveScrollController;
18971906 // Only update the painter of this scrollbar if the notification
18981907 // metrics do not conflict with the information we have from the scroll
18991908 // controller.
@@ -1979,8 +1988,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
19791988
19801989 Map <Type , GestureRecognizerFactory > get _gestures {
19811990 final Map <Type , GestureRecognizerFactory > gestures = < Type , GestureRecognizerFactory > {};
1982- final ScrollController ? controller = widget.controller ?? PrimaryScrollController .maybeOf (context);
1983- if (controller == null || ! enableGestures) {
1991+ if (_effectiveScrollController == null || ! enableGestures) {
19841992 return gestures;
19851993 }
19861994
@@ -2086,6 +2094,64 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
20862094 _maybeStartFadeoutTimer ();
20872095 }
20882096
2097+ // Returns the delta that should result from applying [event] with axis and
2098+ // direction taken into account.
2099+ double _pointerSignalEventDelta (PointerScrollEvent event) {
2100+ assert (_cachedController != null );
2101+ double delta = _cachedController! .position.axis == Axis .horizontal
2102+ ? event.scrollDelta.dx
2103+ : event.scrollDelta.dy;
2104+
2105+ if (axisDirectionIsReversed (_cachedController! .position.axisDirection)) {
2106+ delta *= - 1 ;
2107+ }
2108+ return delta;
2109+ }
2110+
2111+ // Returns the offset that should result from applying [event] to the current
2112+ // position, taking min/max scroll extent into account.
2113+ double _targetScrollOffsetForPointerScroll (double delta) {
2114+ assert (_cachedController != null );
2115+ return math.min (
2116+ math.max (_cachedController! .position.pixels + delta, _cachedController! .position.minScrollExtent),
2117+ _cachedController! .position.maxScrollExtent,
2118+ );
2119+ }
2120+
2121+ void _handlePointerScroll (PointerEvent event) {
2122+ assert (event is PointerScrollEvent );
2123+ _cachedController = _effectiveScrollController;
2124+ final double delta = _pointerSignalEventDelta (event as PointerScrollEvent );
2125+ final double targetScrollOffset = _targetScrollOffsetForPointerScroll (delta);
2126+ if (delta != 0.0 && targetScrollOffset != _cachedController! .position.pixels) {
2127+ _cachedController! .position.pointerScroll (delta);
2128+ }
2129+ }
2130+
2131+ void _receivedPointerSignal (PointerSignalEvent event) {
2132+ _cachedController = _effectiveScrollController;
2133+ // Only try to scroll if the bar absorb the hit test.
2134+ if ((scrollbarPainter.hitTest (event.localPosition) ?? false ) &&
2135+ _cachedController != null &&
2136+ _cachedController! .hasClients &&
2137+ (! _thumbDragging || kIsWeb)) {
2138+ final ScrollPosition position = _cachedController! .position;
2139+ if (event is PointerScrollEvent && position != null ) {
2140+ if (! position.physics.shouldAcceptUserOffset (position)) {
2141+ return ;
2142+ }
2143+ final double delta = _pointerSignalEventDelta (event);
2144+ final double targetScrollOffset = _targetScrollOffsetForPointerScroll (delta);
2145+ if (delta != 0.0 && targetScrollOffset != position.pixels) {
2146+ GestureBinding .instance.pointerSignalResolver.register (event, _handlePointerScroll);
2147+ }
2148+ } else if (event is PointerScrollInertiaCancelEvent ) {
2149+ position.jumpTo (position.pixels);
2150+ // Don't use the pointer signal resolver, all hit-tested scrollables should stop.
2151+ }
2152+ }
2153+ }
2154+
20892155 @override
20902156 void dispose () {
20912157 _fadeoutAnimationController.dispose ();
@@ -2103,43 +2169,46 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
21032169 child: NotificationListener <ScrollNotification >(
21042170 onNotification: _handleScrollNotification,
21052171 child: RepaintBoundary (
2106- child: RawGestureDetector (
2107- gestures: _gestures,
2108- child: MouseRegion (
2109- onExit: (PointerExitEvent event) {
2110- switch (event.kind) {
2111- case PointerDeviceKind .mouse:
2112- case PointerDeviceKind .trackpad:
2113- if (enableGestures) {
2114- handleHoverExit (event);
2115- }
2116- break ;
2117- case PointerDeviceKind .stylus:
2118- case PointerDeviceKind .invertedStylus:
2119- case PointerDeviceKind .unknown:
2120- case PointerDeviceKind .touch:
2121- break ;
2122- }
2123- },
2124- onHover: (PointerHoverEvent event) {
2125- switch (event.kind) {
2126- case PointerDeviceKind .mouse:
2127- case PointerDeviceKind .trackpad:
2128- if (enableGestures) {
2129- handleHover (event);
2130- }
2131- break ;
2132- case PointerDeviceKind .stylus:
2133- case PointerDeviceKind .invertedStylus:
2134- case PointerDeviceKind .unknown:
2135- case PointerDeviceKind .touch:
2136- break ;
2137- }
2138- },
2139- child: CustomPaint (
2140- key: _scrollbarPainterKey,
2141- foregroundPainter: scrollbarPainter,
2142- child: RepaintBoundary (child: widget.child),
2172+ child: Listener (
2173+ onPointerSignal: _receivedPointerSignal,
2174+ child: RawGestureDetector (
2175+ gestures: _gestures,
2176+ child: MouseRegion (
2177+ onExit: (PointerExitEvent event) {
2178+ switch (event.kind) {
2179+ case PointerDeviceKind .mouse:
2180+ case PointerDeviceKind .trackpad:
2181+ if (enableGestures) {
2182+ handleHoverExit (event);
2183+ }
2184+ break ;
2185+ case PointerDeviceKind .stylus:
2186+ case PointerDeviceKind .invertedStylus:
2187+ case PointerDeviceKind .unknown:
2188+ case PointerDeviceKind .touch:
2189+ break ;
2190+ }
2191+ },
2192+ onHover: (PointerHoverEvent event) {
2193+ switch (event.kind) {
2194+ case PointerDeviceKind .mouse:
2195+ case PointerDeviceKind .trackpad:
2196+ if (enableGestures) {
2197+ handleHover (event);
2198+ }
2199+ break ;
2200+ case PointerDeviceKind .stylus:
2201+ case PointerDeviceKind .invertedStylus:
2202+ case PointerDeviceKind .unknown:
2203+ case PointerDeviceKind .touch:
2204+ break ;
2205+ }
2206+ },
2207+ child: CustomPaint (
2208+ key: _scrollbarPainterKey,
2209+ foregroundPainter: scrollbarPainter,
2210+ child: RepaintBoundary (child: widget.child),
2211+ ),
21432212 ),
21442213 ),
21452214 ),
0 commit comments