Unverified Commit 8603ed99 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Fix scrollbar drag gestures for reversed scrollables (#82764)

parent 6728cf34
......@@ -951,7 +951,7 @@ class RawScrollbar extends StatefulWidget {
/// Provides defaults gestures for dragging the scrollbar thumb and tapping on the
/// scrollbar track.
class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProviderStateMixin<T> {
double? _dragScrollbarAxisPosition;
Offset? _dragScrollbarAxisOffset;
ScrollController? _currentController;
Timer? _fadeoutTimer;
late AnimationController _fadeoutAnimationController;
......@@ -1133,9 +1133,25 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
}
}
void _updateScrollPosition(double primaryDelta) {
void _updateScrollPosition(Offset updatedOffset) {
assert(_currentController != null);
assert(_dragScrollbarAxisOffset != null);
final ScrollPosition position = _currentController!.position;
late double primaryDelta;
switch (position.axisDirection) {
case AxisDirection.up:
primaryDelta = _dragScrollbarAxisOffset!.dy - updatedOffset.dy;
break;
case AxisDirection.right:
primaryDelta = updatedOffset.dx -_dragScrollbarAxisOffset!.dx;
break;
case AxisDirection.down:
primaryDelta = updatedOffset.dy -_dragScrollbarAxisOffset!.dy;
break;
case AxisDirection.left:
primaryDelta = _dragScrollbarAxisOffset!.dx - updatedOffset.dx;
break;
}
// Convert primaryDelta, the amount that the scrollbar moved since the last
// time _updateScrollPosition was called, into the coordinate space of the scroll
......@@ -1159,8 +1175,8 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
}
}
/// Returns the [Axis] of the child scroll view, or null if the current scroll
/// controller does not have any attached positions.
/// Returns the [Axis] of the child scroll view, or null if the
/// current scroll controller does not have any attached positions.
@protected
Axis? getScrollbarDirection() {
assert(_currentController != null);
......@@ -1194,14 +1210,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
}
_fadeoutTimer?.cancel();
_fadeoutAnimationController.forward();
switch (direction) {
case Axis.vertical:
_dragScrollbarAxisPosition = localPosition.dy;
break;
case Axis.horizontal:
_dragScrollbarAxisPosition = localPosition.dx;
break;
}
_dragScrollbarAxisOffset = localPosition;
}
/// Handler called when a currently active long press gesture moves.
......@@ -1214,16 +1223,8 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
if (direction == null) {
return;
}
switch(direction) {
case Axis.vertical:
_updateScrollPosition(localPosition.dy - _dragScrollbarAxisPosition!);
_dragScrollbarAxisPosition = localPosition.dy;
break;
case Axis.horizontal:
_updateScrollPosition(localPosition.dx - _dragScrollbarAxisPosition!);
_dragScrollbarAxisPosition = localPosition.dx;
break;
}
_updateScrollPosition(localPosition);
_dragScrollbarAxisOffset = localPosition;
}
/// Handler called when a long press has ended.
......@@ -1234,7 +1235,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
if (direction == null)
return;
_maybeStartFadeoutTimer();
_dragScrollbarAxisPosition = null;
_dragScrollbarAxisOffset = null;
_currentController = null;
}
......@@ -1303,7 +1304,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
_fadeoutTimer?.cancel();
scrollbarPainter.update(notification.metrics, notification.metrics.axisDirection);
} else if (notification is ScrollEndNotification) {
if (_dragScrollbarAxisPosition == null)
if (_dragScrollbarAxisOffset == null)
_maybeStartFadeoutTimer();
}
return false;
......
......@@ -167,6 +167,80 @@ void main() {
await tester.pump(_kScrollbarFadeDuration);
});
testWidgets('Scrollbar thumb can be dragged with long press - reverse', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: scrollController,
child: const CupertinoScrollbar(
child: SingleChildScrollView(
reverse: true,
child: SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
),
);
expect(scrollController.offset, 0.0);
// Scroll a bit.
const double scrollAmount = 10.0;
final TestGesture scrollGesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
// Scroll up by swiping down.
await scrollGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
// Scrollbar thumb is fully showing and scroll offset has moved by
// scrollAmount.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
expect(scrollController.offset, scrollAmount);
await scrollGesture.up();
await tester.pump();
int hapticFeedbackCalls = 0;
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'HapticFeedback.vibrate') {
hapticFeedbackCalls++;
}
});
// Long press on the scrollbar thumb and expect a vibration after it resizes.
expect(hapticFeedbackCalls, 0);
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(796.0, 550.0));
await tester.pump(_kLongPressDuration);
expect(hapticFeedbackCalls, 0);
await tester.pump(_kScrollbarResizeDuration);
// Allow the haptic feedback some slack.
await tester.pump(const Duration(milliseconds: 1));
expect(hapticFeedbackCalls, 1);
// Drag the thumb up to scroll up.
await dragScrollbarGesture.moveBy(const Offset(0.0, -scrollAmount));
await tester.pump(const Duration(milliseconds: 100));
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect(scrollController.offset, greaterThan(scrollAmount * 2));
// The scrollbar thumb is still fully visible.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
// Let the thumb fade out so all timers have resolved.
await tester.pump(_kScrollbarTimeToFade);
await tester.pump(_kScrollbarFadeDuration);
});
testWidgets('Scrollbar changes thickness and radius when dragged', (WidgetTester tester) async {
const double thickness = 20;
const double thicknessWhileDragging = 40;
......@@ -727,6 +801,80 @@ void main() {
await tester.pump(_kScrollbarFadeDuration);
});
testWidgets('Scrollbar thumb can be dragged with long press - horizontal axis, reverse', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CupertinoScrollbar(
controller: scrollController,
child: SingleChildScrollView(
reverse: true,
controller: scrollController,
scrollDirection: Axis.horizontal,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
);
expect(scrollController.offset, 0.0);
// Scroll a bit.
const double scrollAmount = 10.0;
final TestGesture scrollGesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
// Scroll right by swiping right.
await scrollGesture.moveBy(const Offset(scrollAmount, 0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
// Scrollbar thumb is fully showing and scroll offset has moved by
// scrollAmount.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
expect(scrollController.offset, scrollAmount);
await scrollGesture.up();
await tester.pump();
int hapticFeedbackCalls = 0;
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'HapticFeedback.vibrate') {
hapticFeedbackCalls++;
}
});
// Long press on the scrollbar thumb and expect a vibration after it resizes.
expect(hapticFeedbackCalls, 0);
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(750.0, 596.0));
await tester.pump(_kLongPressDuration);
expect(hapticFeedbackCalls, 0);
await tester.pump(_kScrollbarResizeDuration);
// Allow the haptic feedback some slack.
await tester.pump(const Duration(milliseconds: 1));
expect(hapticFeedbackCalls, 1);
// Drag the thumb to scroll back to the right.
await dragScrollbarGesture.moveBy(const Offset(-scrollAmount, 0.0));
await tester.pump(const Duration(milliseconds: 100));
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect(scrollController.offset, greaterThan(scrollAmount * 2));
// The scrollbar thumb is still fully visible.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
// Let the thumb fade out so all timers have resolved.
await tester.pump(_kScrollbarTimeToFade);
await tester.pump(_kScrollbarFadeDuration);
});
testWidgets('Tapping the track area pages the Scroll View', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
......
......@@ -1339,6 +1339,61 @@ void main() {
);
});
testWidgets('Scrollbar thumb can be dragged in reverse', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: scrollController,
child: RawScrollbar(
isAlwaysShown: true,
controller: scrollController,
child: const SingleChildScrollView(
reverse: true,
child: SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
),
);
await tester.pumpAndSettle();
expect(scrollController.offset, 0.0);
expect(
find.byType(RawScrollbar),
paints
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
..rect(
rect: const Rect.fromLTRB(794.0, 510.0, 800.0, 600.0),
color: const Color(0x66BCBCBC),
),
);
// Drag the thumb up to scroll up.
const double scrollAmount = 10.0;
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 550.0));
await tester.pumpAndSettle();
await dragScrollbarGesture.moveBy(const Offset(0.0, -scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect(scrollController.offset, greaterThan(scrollAmount * 2));
expect(
find.byType(RawScrollbar),
paints
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
..rect(
rect: const Rect.fromLTRB(794.0, 500.0, 800.0, 590.0),
color: const Color(0x66BCBCBC),
),
);
});
testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async {
final ScrollbarPainter painter = ScrollbarPainter(
color: _kScrollbarColor,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment