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
|
/// Provides defaults gestures for dragging the scrollbar thumb and tapping on the
|
||||||
/// scrollbar track.
|
/// scrollbar track.
|
||||||
class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProviderStateMixin<T> {
|
class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProviderStateMixin<T> {
|
||||||
double? _dragScrollbarAxisPosition;
|
Offset? _dragScrollbarAxisOffset;
|
||||||
ScrollController? _currentController;
|
ScrollController? _currentController;
|
||||||
Timer? _fadeoutTimer;
|
Timer? _fadeoutTimer;
|
||||||
late AnimationController _fadeoutAnimationController;
|
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(_currentController != null);
|
||||||
|
assert(_dragScrollbarAxisOffset != null);
|
||||||
final ScrollPosition position = _currentController!.position;
|
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
|
// Convert primaryDelta, the amount that the scrollbar moved since the last
|
||||||
// time _updateScrollPosition was called, into the coordinate space of the scroll
|
// 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
|
/// Returns the [Axis] of the child scroll view, or null if the
|
||||||
/// controller does not have any attached positions.
|
/// current scroll controller does not have any attached positions.
|
||||||
@protected
|
@protected
|
||||||
Axis? getScrollbarDirection() {
|
Axis? getScrollbarDirection() {
|
||||||
assert(_currentController != null);
|
assert(_currentController != null);
|
||||||
@ -1194,14 +1210,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
}
|
}
|
||||||
_fadeoutTimer?.cancel();
|
_fadeoutTimer?.cancel();
|
||||||
_fadeoutAnimationController.forward();
|
_fadeoutAnimationController.forward();
|
||||||
switch (direction) {
|
_dragScrollbarAxisOffset = localPosition;
|
||||||
case Axis.vertical:
|
|
||||||
_dragScrollbarAxisPosition = localPosition.dy;
|
|
||||||
break;
|
|
||||||
case Axis.horizontal:
|
|
||||||
_dragScrollbarAxisPosition = localPosition.dx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler called when a currently active long press gesture moves.
|
/// 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) {
|
if (direction == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch(direction) {
|
_updateScrollPosition(localPosition);
|
||||||
case Axis.vertical:
|
_dragScrollbarAxisOffset = localPosition;
|
||||||
_updateScrollPosition(localPosition.dy - _dragScrollbarAxisPosition!);
|
|
||||||
_dragScrollbarAxisPosition = localPosition.dy;
|
|
||||||
break;
|
|
||||||
case Axis.horizontal:
|
|
||||||
_updateScrollPosition(localPosition.dx - _dragScrollbarAxisPosition!);
|
|
||||||
_dragScrollbarAxisPosition = localPosition.dx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler called when a long press has ended.
|
/// 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)
|
if (direction == null)
|
||||||
return;
|
return;
|
||||||
_maybeStartFadeoutTimer();
|
_maybeStartFadeoutTimer();
|
||||||
_dragScrollbarAxisPosition = null;
|
_dragScrollbarAxisOffset = null;
|
||||||
_currentController = null;
|
_currentController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1303,7 +1304,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
_fadeoutTimer?.cancel();
|
_fadeoutTimer?.cancel();
|
||||||
scrollbarPainter.update(notification.metrics, notification.metrics.axisDirection);
|
scrollbarPainter.update(notification.metrics, notification.metrics.axisDirection);
|
||||||
} else if (notification is ScrollEndNotification) {
|
} else if (notification is ScrollEndNotification) {
|
||||||
if (_dragScrollbarAxisPosition == null)
|
if (_dragScrollbarAxisOffset == null)
|
||||||
_maybeStartFadeoutTimer();
|
_maybeStartFadeoutTimer();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -167,6 +167,80 @@ void main() {
|
|||||||
await tester.pump(_kScrollbarFadeDuration);
|
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 {
|
testWidgets('Scrollbar changes thickness and radius when dragged', (WidgetTester tester) async {
|
||||||
const double thickness = 20;
|
const double thickness = 20;
|
||||||
const double thicknessWhileDragging = 40;
|
const double thicknessWhileDragging = 40;
|
||||||
@ -727,6 +801,80 @@ void main() {
|
|||||||
await tester.pump(_kScrollbarFadeDuration);
|
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 {
|
testWidgets('Tapping the track area pages the Scroll View', (WidgetTester tester) async {
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
await tester.pumpWidget(
|
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 {
|
testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async {
|
||||||
final ScrollbarPainter painter = ScrollbarPainter(
|
final ScrollbarPainter painter = ScrollbarPainter(
|
||||||
color: _kScrollbarColor,
|
color: _kScrollbarColor,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user