chore: optimize auto scroller speed (#1147)
* chore: bump version 6.0.0 * chore: bump version 6.0.0 * feat: optimize the auto scroller speed * chore: code format
This commit is contained in:
parent
4c1d2eca35
commit
bcafa80b78
10 changed files with 335 additions and 71 deletions
|
|
@ -1,3 +1,9 @@
|
|||
## 6.0.0
|
||||
* fix: paste single line text will remove the children block by @LucasXu0 in https://github.com/AppFlowy-IO/appflowy-editor/pull/1100
|
||||
* feat: support block wrapper by @LucasXu0 in https://github.com/AppFlowy-IO/appflowy-editor/pull/1101
|
||||
* Adding offline collaboration example using appflowy_editor_sync_plugin by @Musta-Pollo in https://github.com/AppFlowy-IO/appflowy-editor/pull/1103
|
||||
* chore: upgrade to flutter 3.32.4 by @LucasXu0 in https://github.com/AppFlowy-IO/appflowy-editor/pull/1123
|
||||
|
||||
## 5.2.0
|
||||
* feat: support customizing link button by @LucasXu0 in https://github.com/AppFlowy-IO/appflowy-editor/pull/1048
|
||||
* feat: add flag to disable keyboard shortcuts by @LucasXu0 in https://github.com/AppFlowy-IO/appflowy-editor/pull/1049
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ class _EditorState extends State<Editor> {
|
|||
|
||||
editorState.logConfiguration
|
||||
..handler = debugPrint
|
||||
..level = AppFlowyEditorLogLevel.all;
|
||||
..level = AppFlowyEditorLogLevel.off;
|
||||
|
||||
editorState.transactionStream.listen((event) {
|
||||
if (event.$1 == TransactionTime.after) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/editor/util/platform_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const _selectionDragModeKey = 'selection_drag_mode';
|
||||
|
||||
class AutoScrollableWidget extends StatefulWidget {
|
||||
const AutoScrollableWidget({
|
||||
|
|
@ -51,11 +55,28 @@ class _AutoScrollableWidgetState extends State<AutoScrollableWidget> {
|
|||
}
|
||||
|
||||
void _initAutoScroller() {
|
||||
final bool isDesktopOrWeb = PlatformExtension.isDesktopOrWeb;
|
||||
_autoScroller = AutoScroller(
|
||||
_scrollableState,
|
||||
velocityScalar: PlatformExtension.isDesktopOrWeb ? 25 : 100,
|
||||
velocityScalar: isDesktopOrWeb ? 0.125 : 0.02,
|
||||
minimumAutoScrollDelta: isDesktopOrWeb ? 0.07 : 0.004,
|
||||
maxAutoScrollDelta: isDesktopOrWeb ? 2.75 : 0.053,
|
||||
onScrollViewScrolled: () {
|
||||
// _autoScroller.continueToAutoScroll();
|
||||
if (!isDesktopOrWeb) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
final editorState = context.read<EditorState?>();
|
||||
final dynamic dragMode =
|
||||
editorState?.selectionExtraInfo?[_selectionDragModeKey];
|
||||
final bool isDraggingSelection = dragMode != null &&
|
||||
dragMode.toString() != 'MobileSelectionDragMode.none';
|
||||
if (!isDraggingSelection) {
|
||||
return;
|
||||
}
|
||||
_autoScroller.continueToAutoScroll();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,16 @@ class AutoScroller extends EdgeDraggingAutoScroller
|
|||
super.scrollable, {
|
||||
super.onScrollViewScrolled,
|
||||
super.velocityScalar = _kDefaultAutoScrollVelocityScalar,
|
||||
super.minimumAutoScrollDelta = _kDefaultMinAutoScrollDelta,
|
||||
super.maxAutoScrollDelta = _kDefaultMaxAutoScrollDelta,
|
||||
});
|
||||
|
||||
static const double _kDefaultAutoScrollVelocityScalar = 7;
|
||||
static const double _kDefaultMinAutoScrollDelta = 1.0;
|
||||
static const double _kDefaultMaxAutoScrollDelta = 20.0;
|
||||
|
||||
Offset? lastOffset;
|
||||
Duration? lastDuration;
|
||||
|
||||
@override
|
||||
void startAutoScroll(
|
||||
|
|
@ -31,13 +36,14 @@ class AutoScroller extends EdgeDraggingAutoScroller
|
|||
AxisDirection? direction,
|
||||
Duration? duration,
|
||||
}) {
|
||||
lastOffset = offset;
|
||||
lastDuration = duration;
|
||||
if (direction != null && direction == AxisDirection.up) {
|
||||
return startAutoScrollIfNecessary(
|
||||
offset & Size(1, edgeOffset),
|
||||
);
|
||||
}
|
||||
|
||||
lastOffset = offset;
|
||||
final dragTarget = Rect.fromCenter(
|
||||
center: offset,
|
||||
width: edgeOffset,
|
||||
|
|
@ -52,12 +58,16 @@ class AutoScroller extends EdgeDraggingAutoScroller
|
|||
@override
|
||||
void stopAutoScroll() {
|
||||
lastOffset = null;
|
||||
lastDuration = null;
|
||||
super.stopAutoScroll();
|
||||
}
|
||||
|
||||
void continueToAutoScroll() {
|
||||
if (lastOffset != null) {
|
||||
startAutoScroll(lastOffset!);
|
||||
startAutoScroll(
|
||||
lastOffset!,
|
||||
duration: lastDuration,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const Duration _kDesktopAutoScrollTickDuration = Duration(milliseconds: 80);
|
||||
|
||||
class DesktopScrollService extends StatefulWidget {
|
||||
const DesktopScrollService({
|
||||
super.key,
|
||||
|
|
@ -108,7 +110,7 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
|
|||
offset,
|
||||
edgeOffset: edgeOffset,
|
||||
direction: direction,
|
||||
duration: duration,
|
||||
duration: duration ?? _kDesktopAutoScrollTickDuration,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,8 +92,27 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
|
|||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final selectionRect = editorState.selectionRects();
|
||||
final endTouchPoint = selectionRect.lastOrNull?.centerRight;
|
||||
final selectionRects = editorState.selectionRects();
|
||||
if (selectionRects.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rect targetRect;
|
||||
AxisDirection? direction;
|
||||
final dynamic dragMode =
|
||||
editorState.selectionExtraInfo?['selection_drag_mode'];
|
||||
switch (dragMode?.toString()) {
|
||||
case 'MobileSelectionDragMode.leftSelectionHandle':
|
||||
targetRect = selectionRects.first;
|
||||
direction = AxisDirection.up;
|
||||
case 'MobileSelectionDragMode.rightSelectionHandle':
|
||||
targetRect = selectionRects.last;
|
||||
direction = AxisDirection.down;
|
||||
default:
|
||||
targetRect = selectionRects.last;
|
||||
}
|
||||
|
||||
final endTouchPoint = targetRect.centerRight;
|
||||
|
||||
if (PlatformExtension.isMobile) {
|
||||
// soft keyboard
|
||||
|
|
@ -106,34 +125,23 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
|
|||
if (_forwardKey.currentContext == null) {
|
||||
return;
|
||||
}
|
||||
if (endTouchPoint == null) {
|
||||
jumpTo(selection.end.path.first);
|
||||
} else {
|
||||
startAutoScroll(
|
||||
endTouchPoint,
|
||||
edgeOffset: editorState.autoScrollEdgeOffset,
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
startAutoScroll(
|
||||
endTouchPoint,
|
||||
edgeOffset: editorState.autoScrollEdgeOffset,
|
||||
direction: direction,
|
||||
duration: Duration.zero,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
if (_forwardKey.currentContext == null) {
|
||||
return;
|
||||
}
|
||||
if (endTouchPoint == null) {
|
||||
// check if the selection is valid
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
jumpTo(selection.end.path.first);
|
||||
} else {
|
||||
startAutoScroll(
|
||||
endTouchPoint,
|
||||
edgeOffset: editorState.autoScrollEdgeOffset,
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
startAutoScroll(
|
||||
endTouchPoint,
|
||||
edgeOffset: editorState.autoScrollEdgeOffset,
|
||||
direction: direction,
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ class _DesktopSelectionServiceWidgetState
|
|||
|
||||
Position? _panStartPosition;
|
||||
|
||||
bool _isDraggingSelection = false;
|
||||
Offset? _lastPanOffset;
|
||||
|
||||
OverlayEntry? _dropTargetEntry;
|
||||
|
||||
late EditorState editorState = Provider.of<EditorState>(
|
||||
|
|
@ -64,6 +67,7 @@ class _DesktopSelectionServiceWidgetState
|
|||
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
editorState.selectionNotifier.addListener(_updateSelection);
|
||||
editorState.addScrollViewScrolledListener(_handleAutoScrollWhileDragging);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -85,6 +89,9 @@ class _DesktopSelectionServiceWidgetState
|
|||
clearSelection();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
editorState.selectionNotifier.removeListener(_updateSelection);
|
||||
editorState.removeScrollViewScrolledListener(
|
||||
_handleAutoScrollWhileDragging,
|
||||
);
|
||||
currentSelection.dispose();
|
||||
removeDropTarget();
|
||||
super.dispose();
|
||||
|
|
@ -116,6 +123,7 @@ class _DesktopSelectionServiceWidgetState
|
|||
@override
|
||||
void clearSelection() {
|
||||
// currentSelectedNodes = [];
|
||||
_resetPanState();
|
||||
currentSelection.value = null;
|
||||
|
||||
_clearSelection();
|
||||
|
|
@ -140,6 +148,14 @@ class _DesktopSelectionServiceWidgetState
|
|||
..clear();
|
||||
}
|
||||
|
||||
void _resetPanState() {
|
||||
_isDraggingSelection = false;
|
||||
_panStartOffset = null;
|
||||
_panStartScrollDy = null;
|
||||
_panStartPosition = null;
|
||||
_lastPanOffset = null;
|
||||
}
|
||||
|
||||
void _clearContextMenu() {
|
||||
_contextMenuAreas
|
||||
..forEach((overlay) => overlay.remove())
|
||||
|
|
@ -300,6 +316,13 @@ class _DesktopSelectionServiceWidgetState
|
|||
_panStartPosition = getNodeInOffset(_panStartOffset!)
|
||||
?.selectable
|
||||
?.getPositionInOffset(_panStartOffset!);
|
||||
if (_panStartPosition == null) {
|
||||
_resetPanState();
|
||||
return;
|
||||
}
|
||||
|
||||
_lastPanOffset = _panStartOffset;
|
||||
_isDraggingSelection = true;
|
||||
}
|
||||
|
||||
void _onPanUpdate(DragUpdateDetails details) {
|
||||
|
|
@ -311,35 +334,18 @@ class _DesktopSelectionServiceWidgetState
|
|||
return;
|
||||
}
|
||||
|
||||
if (_panStartOffset == null ||
|
||||
_panStartScrollDy == null ||
|
||||
if (!_isDraggingSelection ||
|
||||
_panStartOffset == null ||
|
||||
_panStartPosition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final panEndOffset = details.globalPosition;
|
||||
final dy = editorState.service.scrollService?.dy;
|
||||
final panStartOffset = dy == null
|
||||
? _panStartOffset!
|
||||
: _panStartOffset!.translate(0, _panStartScrollDy! - dy);
|
||||
|
||||
// this one maybe redundant.
|
||||
final last = getNodeInOffset(panEndOffset)?.selectable;
|
||||
|
||||
// compute the selection in range.
|
||||
if (last != null) {
|
||||
final start = _panStartPosition!;
|
||||
final end = last.getSelectionInRange(panStartOffset, panEndOffset).end;
|
||||
final selection = Selection(start: start, end: end);
|
||||
|
||||
if (selection != currentSelection.value) {
|
||||
updateSelection(selection);
|
||||
}
|
||||
}
|
||||
_lastPanOffset = details.globalPosition;
|
||||
_updateSelectionDuringDrag(_lastPanOffset!);
|
||||
|
||||
editorState.service.scrollService?.startAutoScroll(
|
||||
panEndOffset,
|
||||
edgeOffset: 100,
|
||||
_lastPanOffset!,
|
||||
edgeOffset: 80,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -351,9 +357,62 @@ class _DesktopSelectionServiceWidgetState
|
|||
return;
|
||||
}
|
||||
|
||||
_panStartPosition = null;
|
||||
|
||||
editorState.service.scrollService?.stopAutoScroll();
|
||||
_resetPanState();
|
||||
}
|
||||
|
||||
void _updateSelectionDuringDrag(Offset panEndOffset) {
|
||||
if (!_isDraggingSelection ||
|
||||
_panStartPosition == null ||
|
||||
_panStartOffset == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final double? currentDy = editorState.service.scrollService?.dy;
|
||||
final Offset panStartOffset = currentDy == null || _panStartScrollDy == null
|
||||
? _panStartOffset!
|
||||
: _panStartOffset!.translate(
|
||||
0,
|
||||
_panStartScrollDy! - currentDy,
|
||||
);
|
||||
|
||||
final selectable = getNodeInOffset(panEndOffset)?.selectable;
|
||||
if (selectable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Selection selection = Selection(
|
||||
start: _panStartPosition!,
|
||||
end: selectable
|
||||
.getSelectionInRange(
|
||||
panStartOffset,
|
||||
panEndOffset,
|
||||
)
|
||||
.end,
|
||||
);
|
||||
|
||||
if (selection != currentSelection.value) {
|
||||
updateSelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleAutoScrollWhileDragging() {
|
||||
if (!mounted || !_isDraggingSelection || _lastPanOffset == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted ||
|
||||
!_isDraggingSelection ||
|
||||
_lastPanOffset == null ||
|
||||
_panStartOffset == null ||
|
||||
_panStartPosition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_updateSelectionDuringDrag(_lastPanOffset!);
|
||||
editorState.autoScroller?.continueToAutoScroll();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateSelection() {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ class _MobileSelectionServiceWidgetState
|
|||
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
editorState.selectionNotifier.addListener(_updateSelection);
|
||||
editorState.addScrollViewScrolledListener(_handleAutoScrollWhileDragging);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -117,6 +118,9 @@ class _MobileSelectionServiceWidgetState
|
|||
WidgetsBinding.instance.removeObserver(this);
|
||||
selectionNotifierAfterLayout.dispose();
|
||||
editorState.selectionNotifier.removeListener(_updateSelection);
|
||||
editorState.removeScrollViewScrolledListener(
|
||||
_handleAutoScrollWhileDragging,
|
||||
);
|
||||
collapsedHandleTimer?.cancel();
|
||||
|
||||
super.dispose();
|
||||
|
|
@ -394,6 +398,79 @@ class _MobileSelectionServiceWidgetState
|
|||
selectionRects.clear();
|
||||
}
|
||||
|
||||
void _handleAutoScrollWhileDragging() {
|
||||
if (!mounted || dragMode == MobileSelectionDragMode.none) {
|
||||
return;
|
||||
}
|
||||
if (_panStartOffset == null ||
|
||||
_panStartSelection == null ||
|
||||
_lastPanOffset.value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted ||
|
||||
dragMode == MobileSelectionDragMode.none ||
|
||||
_panStartOffset == null ||
|
||||
_panStartSelection == null) {
|
||||
return;
|
||||
}
|
||||
final offset = _lastPanOffset.value;
|
||||
if (offset == null) {
|
||||
return;
|
||||
}
|
||||
_updateSelectionDuringDrag(offset);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateSelectionDuringDrag(Offset panEndOffset) {
|
||||
if (_panStartOffset == null || _panStartSelection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final double? dy = editorState.service.scrollService?.dy;
|
||||
final Offset panStartOffset;
|
||||
if (dy == null || _panStartScrollDy == null) {
|
||||
panStartOffset = _panStartOffset!;
|
||||
} else {
|
||||
panStartOffset = _panStartOffset!.translate(
|
||||
0,
|
||||
_panStartScrollDy! - dy,
|
||||
);
|
||||
}
|
||||
|
||||
final selectionInRange = getNodeInOffset(panEndOffset)
|
||||
?.selectable
|
||||
?.getSelectionInRange(panStartOffset, panEndOffset);
|
||||
final end = selectionInRange?.end;
|
||||
if (end == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
late final Selection newSelection;
|
||||
switch (dragMode) {
|
||||
case MobileSelectionDragMode.leftSelectionHandle:
|
||||
newSelection = Selection(
|
||||
start: _panStartSelection!.normalized.end,
|
||||
end: end,
|
||||
).normalized;
|
||||
break;
|
||||
case MobileSelectionDragMode.rightSelectionHandle:
|
||||
newSelection = Selection(
|
||||
start: _panStartSelection!.normalized.start,
|
||||
end: end,
|
||||
).normalized;
|
||||
break;
|
||||
case MobileSelectionDragMode.cursor:
|
||||
newSelection = Selection.collapsed(end);
|
||||
break;
|
||||
case MobileSelectionDragMode.none:
|
||||
return;
|
||||
}
|
||||
|
||||
updateSelection(newSelection);
|
||||
}
|
||||
|
||||
@override
|
||||
Node? getNodeInOffset(Offset offset) {
|
||||
final List<Node> sortedNodes = editorState.getVisibleNodes(
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class EditorStateDebugInfo {
|
|||
/// set true to this key to prevent attaching the text service when selection is changed.
|
||||
const selectionExtraInfoDoNotAttachTextService =
|
||||
'selectionExtraInfoDoNotAttachTextService';
|
||||
const _selectionDragModeKey = 'selection_drag_mode';
|
||||
|
||||
class ApplyOptions {
|
||||
const ApplyOptions({
|
||||
|
|
@ -631,11 +632,31 @@ class EditorState {
|
|||
) {
|
||||
if (this.scrollableState != scrollableState) {
|
||||
autoScroller?.stopAutoScroll();
|
||||
autoScroller = AutoScroller(
|
||||
final bool isDesktopOrWeb = PlatformExtension.isDesktopOrWeb;
|
||||
late AutoScroller scroller;
|
||||
scroller = AutoScroller(
|
||||
scrollableState,
|
||||
velocityScalar: PlatformExtension.isDesktopOrWeb ? 50 : 100,
|
||||
onScrollViewScrolled: _notifyScrollViewScrolledListeners,
|
||||
velocityScalar: isDesktopOrWeb ? 0.125 : 0.02,
|
||||
minimumAutoScrollDelta: isDesktopOrWeb ? 0.07 : 0.004,
|
||||
maxAutoScrollDelta: isDesktopOrWeb ? 2.75 : 0.053,
|
||||
onScrollViewScrolled: () {
|
||||
_notifyScrollViewScrolledListeners();
|
||||
if (!isDesktopOrWeb) {
|
||||
final dynamic dragMode = selectionExtraInfo?[_selectionDragModeKey];
|
||||
final bool isDraggingSelection = dragMode != null &&
|
||||
dragMode.toString() != 'MobileSelectionDragMode.none';
|
||||
if (!isDraggingSelection) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (autoScroller == scroller) {
|
||||
scroller.continueToAutoScroll();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
autoScroller = scroller;
|
||||
this.scrollableState = scrollableState;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -147,7 +148,12 @@ class EdgeDraggingAutoScroller {
|
|||
this.scrollable, {
|
||||
this.onScrollViewScrolled,
|
||||
required this.velocityScalar,
|
||||
});
|
||||
double minimumAutoScrollDelta = 1.0,
|
||||
double maxAutoScrollDelta = 20.0,
|
||||
}) : assert(minimumAutoScrollDelta >= 0),
|
||||
assert(maxAutoScrollDelta >= minimumAutoScrollDelta),
|
||||
_minimumAutoScrollDelta = minimumAutoScrollDelta,
|
||||
_maxAutoScrollDelta = maxAutoScrollDelta;
|
||||
|
||||
/// The [Scrollable] this auto scroller is scrolling.
|
||||
final ScrollableState scrollable;
|
||||
|
|
@ -167,6 +173,15 @@ class EdgeDraggingAutoScroller {
|
|||
/// {@endtemplate}
|
||||
final double velocityScalar;
|
||||
|
||||
/// The least amount of scroll delta applied per auto scroll tick.
|
||||
///
|
||||
/// When the calculated scroll distance is smaller than this value (but still
|
||||
/// non-zero), the auto scroller will nudge by this minimum to keep the view
|
||||
/// moving rather than treat it as too small to scroll.
|
||||
final double _minimumAutoScrollDelta;
|
||||
final double _maxAutoScrollDelta;
|
||||
double? _previousScrollDelta;
|
||||
|
||||
late Rect _dragTargetRelatedToScrollOrigin;
|
||||
|
||||
/// Whether the auto scroll is in progress.
|
||||
|
|
@ -212,9 +227,10 @@ class EdgeDraggingAutoScroller {
|
|||
/// Stop any ongoing auto scrolling.
|
||||
void stopAutoScroll() {
|
||||
_scrolling = false;
|
||||
_previousScrollDelta = null;
|
||||
}
|
||||
|
||||
Future<void> _scroll() async {
|
||||
Future<void> _scroll({Duration? duration}) async {
|
||||
try {
|
||||
final RenderBox scrollRenderBox =
|
||||
scrollable.context.findRenderObject()! as RenderBox;
|
||||
|
|
@ -268,18 +284,20 @@ class EdgeDraggingAutoScroller {
|
|||
scrollable.position.minScrollExtent) {
|
||||
final double overDrag =
|
||||
math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
final double delta = _smoothScrollDelta(overDrag);
|
||||
newOffset = math.max(
|
||||
scrollable.position.minScrollExtent,
|
||||
scrollable.position.pixels - overDrag,
|
||||
scrollable.position.pixels - delta,
|
||||
);
|
||||
} else if (proxyStart < viewportStart &&
|
||||
scrollable.position.pixels <
|
||||
scrollable.position.maxScrollExtent) {
|
||||
final double overDrag =
|
||||
math.min(viewportStart - proxyStart, overDragMax);
|
||||
final double delta = _smoothScrollDelta(overDrag);
|
||||
newOffset = math.min(
|
||||
scrollable.position.maxScrollExtent,
|
||||
scrollable.position.pixels + overDrag,
|
||||
scrollable.position.pixels + delta,
|
||||
);
|
||||
}
|
||||
case AxisDirection.right:
|
||||
|
|
@ -289,35 +307,58 @@ class EdgeDraggingAutoScroller {
|
|||
scrollable.position.minScrollExtent) {
|
||||
final double overDrag =
|
||||
math.min(viewportStart - proxyStart, overDragMax);
|
||||
final double delta = _smoothScrollDelta(overDrag);
|
||||
newOffset = math.max(
|
||||
scrollable.position.minScrollExtent,
|
||||
scrollable.position.pixels - overDrag,
|
||||
scrollable.position.pixels - delta,
|
||||
);
|
||||
} else if (proxyEnd > viewportEnd &&
|
||||
scrollable.position.pixels <
|
||||
scrollable.position.maxScrollExtent) {
|
||||
final double overDrag =
|
||||
math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
final double delta = _smoothScrollDelta(overDrag);
|
||||
newOffset = math.min(
|
||||
scrollable.position.maxScrollExtent,
|
||||
scrollable.position.pixels + overDrag,
|
||||
scrollable.position.pixels + delta,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (newOffset == null ||
|
||||
(newOffset - scrollable.position.pixels).abs() < 1.0) {
|
||||
final double currentPixels = scrollable.position.pixels;
|
||||
if (newOffset == null) {
|
||||
// Drag should not trigger scroll.
|
||||
_scrolling = false;
|
||||
return;
|
||||
}
|
||||
final Duration duration =
|
||||
Duration(milliseconds: (1000 / velocityScalar).round());
|
||||
await scrollable.position
|
||||
.animateTo(newOffset, duration: duration, curve: Curves.linear);
|
||||
double delta = newOffset - currentPixels;
|
||||
if (delta.abs() < _minimumAutoScrollDelta) {
|
||||
if (delta.abs() <= precisionErrorTolerance) {
|
||||
_scrolling = false;
|
||||
return;
|
||||
}
|
||||
final double direction = delta.sign;
|
||||
final double target =
|
||||
(currentPixels + direction * _minimumAutoScrollDelta).clamp(
|
||||
scrollable.position.minScrollExtent,
|
||||
scrollable.position.maxScrollExtent,
|
||||
);
|
||||
newOffset = target.toDouble();
|
||||
delta = newOffset - currentPixels;
|
||||
if (delta.abs() <= precisionErrorTolerance) {
|
||||
_scrolling = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
await scrollable.position.moveTo(
|
||||
newOffset,
|
||||
duration: duration,
|
||||
curve: Curves.linear,
|
||||
clamp: true,
|
||||
);
|
||||
onScrollViewScrolled?.call();
|
||||
if (_scrolling) {
|
||||
await _scroll();
|
||||
await _scroll(duration: duration);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
|
|
@ -325,6 +366,25 @@ class EdgeDraggingAutoScroller {
|
|||
_scrolling = false;
|
||||
}
|
||||
}
|
||||
|
||||
double _smoothScrollDelta(double overDrag) {
|
||||
if (overDrag <= precisionErrorTolerance) {
|
||||
return 0;
|
||||
}
|
||||
final double desiredDelta = overDrag * velocityScalar;
|
||||
final double clampedDelta = desiredDelta.clamp(
|
||||
_minimumAutoScrollDelta,
|
||||
_maxAutoScrollDelta,
|
||||
);
|
||||
if (_previousScrollDelta == null) {
|
||||
_previousScrollDelta = clampedDelta;
|
||||
return clampedDelta;
|
||||
}
|
||||
final double smoothed =
|
||||
lerpDouble(_previousScrollDelta!, clampedDelta, 0.35)!;
|
||||
_previousScrollDelta = smoothed;
|
||||
return smoothed;
|
||||
}
|
||||
}
|
||||
|
||||
/// A typedef for a function that can calculate the offset for a type of scroll
|
||||
|
|
|
|||
Loading…
Reference in a new issue