Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ abstract class AutoScrollerService {
double edgeOffset = 200,
AxisDirection? direction,
Duration? duration,
bool repeat = true,
});

void stopAutoScroll();
Expand Down Expand Up @@ -38,6 +39,7 @@ class AutoScroller extends EdgeDraggingAutoScroller
double edgeOffset = 200,
AxisDirection? direction,
Duration? duration,
bool repeat = true,
}) {
lastOffset = offset;
lastDuration = duration;
Expand All @@ -47,13 +49,15 @@ class AutoScroller extends EdgeDraggingAutoScroller
return startAutoScrollIfNecessary(
Rect.fromLTWH(offset.dx, offset.dy - edgeOffset, 1, edgeOffset),
duration: duration,
repeat: repeat,
);
}

if (direction != null && direction == AxisDirection.down) {
return startAutoScrollIfNecessary(
Rect.fromLTWH(offset.dx, offset.dy, 1, edgeOffset),
duration: duration,
repeat: repeat,
);
}

Expand All @@ -66,6 +70,7 @@ class AutoScroller extends EdgeDraggingAutoScroller
startAutoScrollIfNecessary(
dragTarget,
duration: duration,
repeat: repeat,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
double edgeOffset = 200,
AxisDirection? direction,
Duration? duration,
bool repeat = true,
}) {
if (editorState.disableAutoScroll) {
return;
Expand All @@ -114,6 +115,7 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
edgeOffset: edgeOffset,
direction: direction,
duration: duration ?? _kDesktopAutoScrollTickDuration,
repeat: repeat,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,14 @@ class _MobileScrollServiceState extends State<MobileScrollService>
double edgeOffset = 200,
AxisDirection? direction,
Duration? duration,
bool repeat = true,
}) {
autoScroller?.startAutoScroll(
offset,
edgeOffset: edgeOffset,
direction: direction,
duration: duration,
repeat: repeat,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
edgeOffset: editorState.autoScrollEdgeOffset,
direction: direction,
duration: scrollDuration,
repeat: isDragOperation,
);
});
} else {
Expand All @@ -191,6 +192,7 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
edgeOffset: editorState.autoScrollEdgeOffset,
direction: direction,
duration: Duration.zero,
repeat: false,
);
}
});
Expand Down Expand Up @@ -243,12 +245,14 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
double edgeOffset = 100,
AxisDirection? direction,
Duration? duration,
bool repeat = true,
}) {
forward.startAutoScroll(
offset,
edgeOffset: edgeOffset,
direction: direction,
duration: duration,
repeat: repeat,
);
}

Expand Down
66 changes: 56 additions & 10 deletions lib/src/flutter/scrollable_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

export 'package:flutter/physics.dart' show Tolerance;

Expand Down Expand Up @@ -188,12 +189,14 @@ class EdgeDraggingAutoScroller {
final Duration _animationDuration;
Duration? _currentDuration;
double? _previousScrollDelta;
bool _repeatScroll = true;

late Rect _dragTargetRelatedToScrollOrigin;

/// Whether the auto scroll is in progress.
bool get scrolling => _scrolling;
bool _scrolling = false;
bool _scrollScheduled = false;

double _offsetExtent(Offset offset, Axis scrollDirection) {
return switch (scrollDirection) {
Expand All @@ -220,28 +223,54 @@ class EdgeDraggingAutoScroller {
///
/// If the scrollable is already scrolling, calling this method updates the
/// previous dragTarget to the new value and continues scrolling if necessary.
void startAutoScrollIfNecessary(Rect dragTarget, {Duration? duration}) {
void startAutoScrollIfNecessary(
Rect dragTarget, {
Duration? duration,
bool repeat = true,
}) {
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
_dragTargetRelatedToScrollOrigin =
dragTarget.translate(deltaToOrigin.dx, deltaToOrigin.dy);
_currentDuration = duration;
_repeatScroll = repeat;
if (_scrolling) {
// The change will be picked up in the next scroll.
return;
}
assert(!_scrolling);
_scroll();
_scrolling = true;
_scheduleScroll();
}

/// Stop any ongoing auto scrolling.
void stopAutoScroll() {
_scrolling = false;
_scrollScheduled = false;
_previousScrollDelta = null;
_currentDuration = null;
_repeatScroll = true;
}

void _scheduleScroll() {
if (!_scrolling || _scrollScheduled) {
return;
}
_scrollScheduled = true;
SchedulerBinding.instance.scheduleFrameCallback((_) {
_scrollScheduled = false;
if (!_scrolling) {
return;
}
unawaited(_scroll());
});
SchedulerBinding.instance.ensureVisualUpdate();
}

Future<void> _scroll() async {
try {
if (!_scrolling) {
return;
}
final RenderBox scrollRenderBox =
scrollable.context.findRenderObject()! as RenderBox;
final Matrix4 transform = scrollRenderBox.getTransformTo(null);
Expand All @@ -266,7 +295,6 @@ class EdgeDraggingAutoScroller {
// do nothing.
}

_scrolling = true;
double? newOffset;
const double overDragMax = 20.0;

Expand Down Expand Up @@ -340,14 +368,14 @@ class EdgeDraggingAutoScroller {
final double currentPixels = scrollable.position.pixels;
if (newOffset == null) {
// Drag should not trigger scroll.
_scrolling = false;
stopAutoScroll();

return;
}
double delta = newOffset - currentPixels;
if (delta.abs() < _minimumAutoScrollDelta) {
if (delta.abs() <= precisionErrorTolerance) {
_scrolling = false;
stopAutoScroll();

return;
}
Expand All @@ -360,25 +388,43 @@ class EdgeDraggingAutoScroller {
newOffset = target.toDouble();
delta = newOffset - currentPixels;
if (delta.abs() <= precisionErrorTolerance) {
_scrolling = false;
stopAutoScroll();

return;
}
}
final double pixelsBeforeMove = scrollable.position.pixels;
await scrollable.position.moveTo(
newOffset,
duration: _currentDuration ?? _animationDuration,
curve: Curves.linear,
// clamp: true,
);
onScrollViewScrolled?.call();
if (_scrolling) {
await _scroll();
// Termination guard against an unbounded auto-scroll loop.
//
// `_scroll` re-invokes itself via `await _scroll()` as long as
// `_scrolling` is true. `moveTo` can complete synchronously when the
// position cannot actually advance (already at min/maxScrollExtent, a
// zero scroll range, or a pinned position), in which case the
// self-recursion degenerates into a tight microtask loop that pins the
// platform/UI thread with no frames produced — observed as an ANR on
// mobile when leaving a table cell. Requiring real forward progress
// bounds the loop: each tick must move the scroll offset by at least
// the precision tolerance, and the scroll extent is finite.
if ((scrollable.position.pixels - pixelsBeforeMove).abs() <=
precisionErrorTolerance) {
stopAutoScroll();
return;
}
if (_repeatScroll) {
_scheduleScroll();
} else {
stopAutoScroll();
}
} catch (e) {
debugPrint(e.toString());
} finally {
_scrolling = false;
stopAutoScroll();
}
}

Expand Down
6 changes: 5 additions & 1 deletion test/editor/editor_component/service/auto_scroller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ class _CapturingAutoScroller extends AutoScroller {
Rect? capturedRect;

@override
void startAutoScrollIfNecessary(Rect dragTarget, {Duration? duration}) {
void startAutoScrollIfNecessary(
Rect dragTarget, {
Duration? duration,
bool repeat = true,
}) {
capturedRect = dragTarget;
// intentionally do not call super — we only want the rect
}
Expand Down