Fix scrollbar drag gestures for reversed scrollables (#82764)
This commit is contained in:
parent
6728cf34cc
commit
8603ed995d
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user