Add horizontal gesture support for CupertinoScrollbar (#69063)
This commit is contained in:
parent
d3a8b03574
commit
b86fa13235
@ -241,7 +241,7 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
|
||||
late Animation<double> _fadeoutOpacityAnimation;
|
||||
late AnimationController _thicknessAnimationController;
|
||||
Timer? _fadeoutTimer;
|
||||
double? _dragScrollbarPositionY;
|
||||
double? _dragScrollbarAxisPosition;
|
||||
Drag? _drag;
|
||||
|
||||
double get _thickness {
|
||||
@ -342,18 +342,25 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
|
||||
// position, and create/update the drag event with that position.
|
||||
final double scrollOffsetLocal = _painter!.getTrackToScroll(primaryDelta);
|
||||
final double scrollOffsetGlobal = scrollOffsetLocal + _currentController!.position.pixels;
|
||||
final Axis direction = _currentController!.position.axis;
|
||||
|
||||
if (_drag == null) {
|
||||
_drag = _currentController!.position.drag(
|
||||
DragStartDetails(
|
||||
globalPosition: Offset(0.0, scrollOffsetGlobal),
|
||||
globalPosition: direction == Axis.vertical
|
||||
? Offset(0.0, scrollOffsetGlobal)
|
||||
: Offset(scrollOffsetGlobal, 0.0),
|
||||
),
|
||||
() {},
|
||||
);
|
||||
} else {
|
||||
_drag!.update(DragUpdateDetails(
|
||||
globalPosition: Offset(0.0, scrollOffsetGlobal),
|
||||
delta: Offset(0.0, -scrollOffsetLocal),
|
||||
globalPosition: direction == Axis.vertical
|
||||
? Offset(0.0, scrollOffsetGlobal)
|
||||
: Offset(scrollOffsetGlobal, 0.0),
|
||||
delta: direction == Axis.vertical
|
||||
? Offset(0.0, -scrollOffsetLocal)
|
||||
: Offset(-scrollOffsetLocal, 0.0),
|
||||
primaryDelta: -scrollOffsetLocal,
|
||||
));
|
||||
}
|
||||
@ -369,33 +376,43 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
|
||||
}
|
||||
}
|
||||
|
||||
bool _checkVertical() {
|
||||
Axis? _getDirection() {
|
||||
try {
|
||||
return _currentController!.position.axis == Axis.vertical;
|
||||
return _currentController!.position.axis;
|
||||
} catch (_) {
|
||||
// Ignore the gesture if we cannot determine the direction.
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
double _pressStartY = 0.0;
|
||||
double _pressStartAxisPosition = 0.0;
|
||||
|
||||
// Long press event callbacks handle the gesture where the user long presses
|
||||
// on the scrollbar thumb and then drags the scrollbar without releasing.
|
||||
void _handleLongPressStart(LongPressStartDetails details) {
|
||||
_currentController = _controller;
|
||||
if (!_checkVertical()) {
|
||||
final Axis? direction = _getDirection();
|
||||
if (direction == null) {
|
||||
return;
|
||||
}
|
||||
_pressStartY = details.localPosition.dy;
|
||||
_fadeoutTimer?.cancel();
|
||||
_fadeoutAnimationController.forward();
|
||||
_dragScrollbar(details.localPosition.dy);
|
||||
_dragScrollbarPositionY = details.localPosition.dy;
|
||||
switch (direction) {
|
||||
case Axis.vertical:
|
||||
_pressStartAxisPosition = details.localPosition.dy;
|
||||
_dragScrollbar(details.localPosition.dy);
|
||||
_dragScrollbarAxisPosition = details.localPosition.dy;
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
_pressStartAxisPosition = details.localPosition.dx;
|
||||
_dragScrollbar(details.localPosition.dx);
|
||||
_dragScrollbarAxisPosition = details.localPosition.dx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLongPress() {
|
||||
if (!_checkVertical()) {
|
||||
if (_getDirection() == null) {
|
||||
return;
|
||||
}
|
||||
_fadeoutTimer?.cancel();
|
||||
@ -405,37 +422,57 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
|
||||
}
|
||||
|
||||
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||
if (!_checkVertical()) {
|
||||
final Axis? direction = _getDirection();
|
||||
if (direction == null) {
|
||||
return;
|
||||
}
|
||||
_dragScrollbar(details.localPosition.dy - _dragScrollbarPositionY!);
|
||||
_dragScrollbarPositionY = details.localPosition.dy;
|
||||
switch(direction) {
|
||||
case Axis.vertical:
|
||||
_dragScrollbar(details.localPosition.dy - _dragScrollbarAxisPosition!);
|
||||
_dragScrollbarAxisPosition = details.localPosition.dy;
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
_dragScrollbar(details.localPosition.dx - _dragScrollbarAxisPosition!);
|
||||
_dragScrollbarAxisPosition = details.localPosition.dx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLongPressEnd(LongPressEndDetails details) {
|
||||
if (!_checkVertical()) {
|
||||
final Axis? direction = _getDirection();
|
||||
if (direction == null) {
|
||||
return;
|
||||
}
|
||||
_handleDragScrollEnd(details.velocity.pixelsPerSecond.dy);
|
||||
if (details.velocity.pixelsPerSecond.dy.abs() < 10 &&
|
||||
(details.localPosition.dy - _pressStartY).abs() > 0) {
|
||||
HapticFeedback.mediumImpact();
|
||||
switch(direction) {
|
||||
case Axis.vertical:
|
||||
_handleDragScrollEnd(details.velocity.pixelsPerSecond.dy, direction);
|
||||
if (details.velocity.pixelsPerSecond.dy.abs() < 10 &&
|
||||
(details.localPosition.dy - _pressStartAxisPosition).abs() > 0) {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
_handleDragScrollEnd(details.velocity.pixelsPerSecond.dx, direction);
|
||||
if (details.velocity.pixelsPerSecond.dx.abs() < 10 &&
|
||||
(details.localPosition.dx - _pressStartAxisPosition).abs() > 0) {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
break;
|
||||
}
|
||||
_currentController = null;
|
||||
}
|
||||
|
||||
void _handleDragScrollEnd(double trackVelocityY) {
|
||||
void _handleDragScrollEnd(double trackVelocity, Axis direction) {
|
||||
_startFadeoutTimer();
|
||||
_thicknessAnimationController.reverse();
|
||||
_dragScrollbarPositionY = null;
|
||||
final double scrollVelocityY = _painter!.getTrackToScroll(trackVelocityY);
|
||||
_dragScrollbarAxisPosition = null;
|
||||
final double scrollVelocity = _painter!.getTrackToScroll(trackVelocity);
|
||||
_drag?.end(DragEndDetails(
|
||||
primaryVelocity: -scrollVelocityY,
|
||||
primaryVelocity: -scrollVelocity,
|
||||
velocity: Velocity(
|
||||
pixelsPerSecond: Offset(
|
||||
0.0,
|
||||
-scrollVelocityY,
|
||||
),
|
||||
pixelsPerSecond: direction == Axis.vertical
|
||||
? Offset(0.0, -scrollVelocity)
|
||||
: Offset(-scrollVelocity, 0.0),
|
||||
),
|
||||
));
|
||||
_drag = null;
|
||||
@ -458,7 +495,7 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
|
||||
_painter!.update(notification.metrics, notification.metrics.axisDirection);
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
// On iOS, the scrollbar can only go away once the user lifted the finger.
|
||||
if (_dragScrollbarPositionY == null) {
|
||||
if (_dragScrollbarAxisPosition == null) {
|
||||
_startFadeoutTimer();
|
||||
}
|
||||
}
|
||||
|
@ -607,4 +607,77 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
|
||||
});
|
||||
|
||||
testWidgets('Scrollbar thumb can be dragged with long press - horizontal axis', (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(
|
||||
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 left.
|
||||
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(50.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 down to scroll back to the left.
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user