Skip to content

Commit c0e748f

Browse files
authored
fix a scroll bar stutter bug (#121786)
Fix a scrolling stutter caused by dragging scrollbar
1 parent a5061bf commit c0e748f

File tree

2 files changed

+85
-9
lines changed

2 files changed

+85
-9
lines changed

packages/flutter/lib/src/widgets/scrollbar.dart

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,29 +1676,41 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
16761676
void _updateScrollPosition(Offset updatedOffset) {
16771677
assert(_cachedController != null);
16781678
assert(_startDragScrollbarAxisOffset != null);
1679+
assert(_lastDragUpdateOffset != null);
16791680
assert(_startDragThumbOffset != null);
16801681

16811682
final ScrollPosition position = _cachedController!.position;
1682-
late double primaryDelta;
1683+
late double primaryDeltaFromDragStart;
1684+
late double primaryDeltaFromLastDragUpdate;
16831685
switch (position.axisDirection) {
16841686
case AxisDirection.up:
1685-
primaryDelta = _startDragScrollbarAxisOffset!.dy - updatedOffset.dy;
1687+
primaryDeltaFromDragStart = _startDragScrollbarAxisOffset!.dy - updatedOffset.dy;
1688+
primaryDeltaFromLastDragUpdate = _lastDragUpdateOffset!.dy - updatedOffset.dy;
16861689
break;
16871690
case AxisDirection.right:
1688-
primaryDelta = updatedOffset.dx -_startDragScrollbarAxisOffset!.dx;
1691+
primaryDeltaFromDragStart = updatedOffset.dx -_startDragScrollbarAxisOffset!.dx;
1692+
primaryDeltaFromLastDragUpdate = updatedOffset.dx -_lastDragUpdateOffset!.dx;
16891693
break;
16901694
case AxisDirection.down:
1691-
primaryDelta = updatedOffset.dy -_startDragScrollbarAxisOffset!.dy;
1695+
primaryDeltaFromDragStart = updatedOffset.dy -_startDragScrollbarAxisOffset!.dy;
1696+
primaryDeltaFromLastDragUpdate = updatedOffset.dy -_lastDragUpdateOffset!.dy;
16921697
break;
16931698
case AxisDirection.left:
1694-
primaryDelta = _startDragScrollbarAxisOffset!.dx - updatedOffset.dx;
1699+
primaryDeltaFromDragStart = _startDragScrollbarAxisOffset!.dx - updatedOffset.dx;
1700+
primaryDeltaFromLastDragUpdate = _lastDragUpdateOffset!.dx - updatedOffset.dx;
16951701
break;
16961702
}
16971703

16981704
// Convert primaryDelta, the amount that the scrollbar moved since the last
1699-
// time when drag started, into the coordinate space of the scroll
1705+
// time when drag started or last updated, into the coordinate space of the scroll
17001706
// position, and jump to that position.
1701-
final double scrollOffsetGlobal = scrollbarPainter.getTrackToScroll(primaryDelta + _startDragThumbOffset!);
1707+
double scrollOffsetGlobal = scrollbarPainter.getTrackToScroll(primaryDeltaFromDragStart + _startDragThumbOffset!);
1708+
if (primaryDeltaFromDragStart > 0 && scrollOffsetGlobal < position.pixels
1709+
|| primaryDeltaFromDragStart < 0 && scrollOffsetGlobal > position.pixels) {
1710+
// Adjust the position value if the scrolling direction conflicts with
1711+
// the dragging direction due to scroll metrics shrink.
1712+
scrollOffsetGlobal = position.pixels + scrollbarPainter.getTrackToScroll(primaryDeltaFromLastDragUpdate);
1713+
}
17021714
if (scrollOffsetGlobal != position.pixels) {
17031715
// Ensure we don't drag into overscroll if the physics do not allow it.
17041716
final double physicsAdjustment = position.physics.applyBoundaryConditions(position, scrollOffsetGlobal);
@@ -1772,6 +1784,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
17721784
_fadeoutTimer?.cancel();
17731785
_fadeoutAnimationController.forward();
17741786
_startDragScrollbarAxisOffset = localPosition;
1787+
_lastDragUpdateOffset = localPosition;
17751788
_startDragThumbOffset = scrollbarPainter.getThumbScrollOffset();
17761789
_thumbDragging = true;
17771790
}
@@ -1786,7 +1799,6 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
17861799
if (_lastDragUpdateOffset == localPosition) {
17871800
return;
17881801
}
1789-
_lastDragUpdateOffset = localPosition;
17901802
final ScrollPosition position = _cachedController!.position;
17911803
if (!position.physics.shouldAcceptUserOffset(position)) {
17921804
return;
@@ -1796,6 +1808,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
17961808
return;
17971809
}
17981810
_updateScrollPosition(localPosition);
1811+
_lastDragUpdateOffset = localPosition;
17991812
}
18001813

18011814
/// Handler called when a long press has ended.

packages/flutter/test/widgets/scrollbar_test.dart

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2740,7 +2740,6 @@ void main() {
27402740
controller: scrollController,
27412741
child: CustomScrollView(
27422742
controller: scrollController,
2743-
// cacheExtent: double.maxFinite,
27442743
slivers: <Widget>[
27452744
SliverList(
27462745
delegate: SliverChildBuilderDelegate(
@@ -2796,6 +2795,70 @@ void main() {
27962795
);
27972796
});
27982797

2798+
testWidgets('The scrollable should not stutter when the scroll metrics shrink during dragging', (WidgetTester tester) async {
2799+
// Regressing test for https://github.com/flutter/flutter/issues/121574
2800+
final ScrollController scrollController = ScrollController();
2801+
await tester.pumpWidget(
2802+
Directionality(
2803+
textDirection: TextDirection.ltr,
2804+
child: MediaQuery(
2805+
data: const MediaQueryData(),
2806+
child: PrimaryScrollController(
2807+
controller: scrollController,
2808+
child: RawScrollbar(
2809+
isAlwaysShown: true,
2810+
controller: scrollController,
2811+
child: CustomScrollView(
2812+
controller: scrollController,
2813+
slivers: <Widget>[
2814+
SliverList(
2815+
delegate: SliverChildBuilderDelegate(
2816+
(BuildContext context, int index) {
2817+
final double height;
2818+
if (index < 10) {
2819+
height = 500;
2820+
} else {
2821+
height = 100;
2822+
}
2823+
return SizedBox(
2824+
height: height,
2825+
child: Text('$index'),
2826+
);
2827+
},
2828+
childCount: 100,
2829+
),
2830+
),
2831+
],
2832+
),
2833+
),
2834+
),
2835+
),
2836+
),
2837+
);
2838+
await tester.pumpAndSettle();
2839+
expect(scrollController.offset, 0.0);
2840+
2841+
// Drag the thumb down to scroll down.
2842+
const double scrollAmount = 100;
2843+
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 5.0));
2844+
await tester.pumpAndSettle();
2845+
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
2846+
await tester.pumpAndSettle();
2847+
2848+
final double lastPosition = scrollController.offset;
2849+
// The view has scrolled more than it would have by a swipe gesture of the
2850+
// same distance.
2851+
expect(lastPosition, greaterThan((100.0 * 10 + 500.0 * 90) / 6));
2852+
2853+
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
2854+
await tester.pumpAndSettle();
2855+
2856+
await dragScrollbarGesture.up();
2857+
await tester.pumpAndSettle();
2858+
2859+
expect(scrollController.offset, greaterThan(lastPosition));
2860+
});
2861+
27992862
testWidgets('The bar support mouse wheel event', (WidgetTester tester) async {
28002863
// Regression test for https://github.com/flutter/flutter/pull/109659
28012864
final ScrollController scrollController = ScrollController();

0 commit comments

Comments
 (0)