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