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
6 changed files with 335 additions and 71 deletions
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue