@@ -1458,13 +1458,17 @@ class RawScrollbar extends StatefulWidget {
1458
1458
/// scrollbar track.
1459
1459
class RawScrollbarState <T extends RawScrollbar > extends State <T > with TickerProviderStateMixin <T > {
1460
1460
Offset ? _startDragScrollbarAxisOffset;
1461
+ Offset ? _lastDragUpdateOffset;
1461
1462
double ? _startDragThumbOffset;
1462
- ScrollController ? _currentController ;
1463
+ ScrollController ? _cachedController ;
1463
1464
Timer ? _fadeoutTimer;
1464
1465
late AnimationController _fadeoutAnimationController;
1465
1466
late Animation <double > _fadeoutOpacityAnimation;
1466
1467
final GlobalKey _scrollbarPainterKey = GlobalKey ();
1467
1468
bool _hoverIsActive = false ;
1469
+ bool _thumbDragging = false ;
1470
+
1471
+ ScrollController ? get _effectiveScrollController => widget.controller ?? PrimaryScrollController .maybeOf (context);
1468
1472
1469
1473
/// Used to paint the scrollbar.
1470
1474
///
@@ -1550,12 +1554,11 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1550
1554
}
1551
1555
1552
1556
void _validateInteractions (AnimationStatus status) {
1553
- final ScrollController ? scrollController = widget.controller ?? PrimaryScrollController .maybeOf (context);
1554
1557
if (status == AnimationStatus .dismissed) {
1555
1558
assert (_fadeoutOpacityAnimation.value == 0.0 );
1556
1559
// We do not check for a valid scroll position if the scrollbar is not
1557
1560
// visible, because it cannot be interacted with.
1558
- } else if (scrollController != null && enableGestures) {
1561
+ } else if (_effectiveScrollController != null && enableGestures) {
1559
1562
// Interactive scrollbars need to be properly configured. If it is visible
1560
1563
// for interaction, ensure we are set up properly.
1561
1564
assert (_debugCheckHasValidScrollPosition ());
@@ -1566,7 +1569,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1566
1569
if (! mounted) {
1567
1570
return true ;
1568
1571
}
1569
- final ScrollController ? scrollController = widget.controller ?? PrimaryScrollController . maybeOf (context) ;
1572
+ final ScrollController ? scrollController = _effectiveScrollController ;
1570
1573
final bool tryPrimary = widget.controller == null ;
1571
1574
final String controllerForError = tryPrimary
1572
1575
? 'PrimaryScrollController'
@@ -1698,11 +1701,11 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1698
1701
}
1699
1702
1700
1703
void _updateScrollPosition (Offset updatedOffset) {
1701
- assert (_currentController != null );
1704
+ assert (_cachedController != null );
1702
1705
assert (_startDragScrollbarAxisOffset != null );
1703
1706
assert (_startDragThumbOffset != null );
1704
1707
1705
- final ScrollPosition position = _currentController ! .position;
1708
+ final ScrollPosition position = _cachedController ! .position;
1706
1709
late double primaryDelta;
1707
1710
switch (position.axisDirection) {
1708
1711
case AxisDirection .up:
@@ -1761,9 +1764,9 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1761
1764
/// current scroll controller does not have any attached positions.
1762
1765
@protected
1763
1766
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;
1767
1770
}
1768
1771
return null ;
1769
1772
}
@@ -1788,7 +1791,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1788
1791
@mustCallSuper
1789
1792
void handleThumbPressStart (Offset localPosition) {
1790
1793
assert (_debugCheckHasValidScrollPosition ());
1791
- _currentController = widget.controller ?? PrimaryScrollController . maybeOf (context) ;
1794
+ _cachedController = _effectiveScrollController ;
1792
1795
final Axis ? direction = getScrollbarDirection ();
1793
1796
if (direction == null ) {
1794
1797
return ;
@@ -1797,6 +1800,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1797
1800
_fadeoutAnimationController.forward ();
1798
1801
_startDragScrollbarAxisOffset = localPosition;
1799
1802
_startDragThumbOffset = scrollbarPainter.getThumbScrollOffset ();
1803
+ _thumbDragging = true ;
1800
1804
}
1801
1805
1802
1806
/// Handler called when a currently active long press gesture moves.
@@ -1806,7 +1810,11 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1806
1810
@mustCallSuper
1807
1811
void handleThumbPressUpdate (Offset localPosition) {
1808
1812
assert (_debugCheckHasValidScrollPosition ());
1809
- final ScrollPosition position = _currentController! .position;
1813
+ if (_lastDragUpdateOffset == localPosition) {
1814
+ return ;
1815
+ }
1816
+ _lastDragUpdateOffset = localPosition;
1817
+ final ScrollPosition position = _cachedController! .position;
1810
1818
if (! position.physics.shouldAcceptUserOffset (position)) {
1811
1819
return ;
1812
1820
}
@@ -1822,45 +1830,47 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1822
1830
@mustCallSuper
1823
1831
void handleThumbPressEnd (Offset localPosition, Velocity velocity) {
1824
1832
assert (_debugCheckHasValidScrollPosition ());
1833
+ _thumbDragging = false ;
1825
1834
final Axis ? direction = getScrollbarDirection ();
1826
1835
if (direction == null ) {
1827
1836
return ;
1828
1837
}
1829
1838
_maybeStartFadeoutTimer ();
1830
1839
_startDragScrollbarAxisOffset = null ;
1840
+ _lastDragUpdateOffset = null ;
1831
1841
_startDragThumbOffset = null ;
1832
- _currentController = null ;
1842
+ _cachedController = null ;
1833
1843
}
1834
1844
1835
1845
void _handleTrackTapDown (TapDownDetails details) {
1836
1846
// The Scrollbar should page towards the position of the tap on the track.
1837
1847
assert (_debugCheckHasValidScrollPosition ());
1838
- _currentController = widget.controller ?? PrimaryScrollController . maybeOf (context) ;
1848
+ _cachedController = _effectiveScrollController ;
1839
1849
1840
- final ScrollPosition position = _currentController ! .position;
1850
+ final ScrollPosition position = _cachedController ! .position;
1841
1851
if (! position.physics.shouldAcceptUserOffset (position)) {
1842
1852
return ;
1843
1853
}
1844
1854
1845
1855
double scrollIncrement;
1846
1856
// Is an increment calculator available?
1847
1857
final ScrollIncrementCalculator ? calculator = Scrollable .maybeOf (
1848
- _currentController ! .position.context.notificationContext! ,
1858
+ _cachedController ! .position.context.notificationContext! ,
1849
1859
)? .widget.incrementCalculator;
1850
1860
if (calculator != null ) {
1851
1861
scrollIncrement = calculator (
1852
1862
ScrollIncrementDetails (
1853
1863
type: ScrollIncrementType .page,
1854
- metrics: _currentController ! .position,
1864
+ metrics: _cachedController ! .position,
1855
1865
),
1856
1866
);
1857
1867
} else {
1858
1868
// Default page increment
1859
- scrollIncrement = 0.8 * _currentController ! .position.viewportDimension;
1869
+ scrollIncrement = 0.8 * _cachedController ! .position.viewportDimension;
1860
1870
}
1861
1871
1862
1872
// Adjust scrollIncrement for direction
1863
- switch (_currentController ! .position.axisDirection) {
1873
+ switch (_cachedController ! .position.axisDirection) {
1864
1874
case AxisDirection .up:
1865
1875
if (details.localPosition.dy > scrollbarPainter._thumbOffset) {
1866
1876
scrollIncrement = - scrollIncrement;
@@ -1883,17 +1893,16 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1883
1893
break ;
1884
1894
}
1885
1895
1886
- _currentController ! .position.moveTo (
1887
- _currentController ! .position.pixels + scrollIncrement,
1896
+ _cachedController ! .position.moveTo (
1897
+ _cachedController ! .position.pixels + scrollIncrement,
1888
1898
duration: const Duration (milliseconds: 100 ),
1889
1899
curve: Curves .easeInOut,
1890
1900
);
1891
1901
}
1892
1902
1893
1903
// ScrollController takes precedence over ScrollNotification
1894
1904
bool _shouldUpdatePainter (Axis notificationAxis) {
1895
- final ScrollController ? scrollController = widget.controller ??
1896
- PrimaryScrollController .maybeOf (context);
1905
+ final ScrollController ? scrollController = _effectiveScrollController;
1897
1906
// Only update the painter of this scrollbar if the notification
1898
1907
// metrics do not conflict with the information we have from the scroll
1899
1908
// controller.
@@ -1979,8 +1988,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
1979
1988
1980
1989
Map <Type , GestureRecognizerFactory > get _gestures {
1981
1990
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) {
1984
1992
return gestures;
1985
1993
}
1986
1994
@@ -2086,6 +2094,64 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
2086
2094
_maybeStartFadeoutTimer ();
2087
2095
}
2088
2096
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
+
2089
2155
@override
2090
2156
void dispose () {
2091
2157
_fadeoutAnimationController.dispose ();
@@ -2103,43 +2169,46 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
2103
2169
child: NotificationListener <ScrollNotification >(
2104
2170
onNotification: _handleScrollNotification,
2105
2171
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
+ ),
2143
2212
),
2144
2213
),
2145
2214
),
0 commit comments