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
|
## 5.2.0
|
||||||
* feat: support customizing link button by @LucasXu0 in https://github.com/AppFlowy-IO/appflowy-editor/pull/1048
|
* 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
|
* 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
|
editorState.logConfiguration
|
||||||
..handler = debugPrint
|
..handler = debugPrint
|
||||||
..level = AppFlowyEditorLogLevel.all;
|
..level = AppFlowyEditorLogLevel.off;
|
||||||
|
|
||||||
editorState.transactionStream.listen((event) {
|
editorState.transactionStream.listen((event) {
|
||||||
if (event.$1 == TransactionTime.after) {
|
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/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:appflowy_editor/src/editor/util/platform_extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
const _selectionDragModeKey = 'selection_drag_mode';
|
||||||
|
|
||||||
class AutoScrollableWidget extends StatefulWidget {
|
class AutoScrollableWidget extends StatefulWidget {
|
||||||
const AutoScrollableWidget({
|
const AutoScrollableWidget({
|
||||||
|
|
@ -51,11 +55,28 @@ class _AutoScrollableWidgetState extends State<AutoScrollableWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initAutoScroller() {
|
void _initAutoScroller() {
|
||||||
|
final bool isDesktopOrWeb = PlatformExtension.isDesktopOrWeb;
|
||||||
_autoScroller = AutoScroller(
|
_autoScroller = AutoScroller(
|
||||||
_scrollableState,
|
_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: () {
|
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.scrollable, {
|
||||||
super.onScrollViewScrolled,
|
super.onScrollViewScrolled,
|
||||||
super.velocityScalar = _kDefaultAutoScrollVelocityScalar,
|
super.velocityScalar = _kDefaultAutoScrollVelocityScalar,
|
||||||
|
super.minimumAutoScrollDelta = _kDefaultMinAutoScrollDelta,
|
||||||
|
super.maxAutoScrollDelta = _kDefaultMaxAutoScrollDelta,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const double _kDefaultAutoScrollVelocityScalar = 7;
|
static const double _kDefaultAutoScrollVelocityScalar = 7;
|
||||||
|
static const double _kDefaultMinAutoScrollDelta = 1.0;
|
||||||
|
static const double _kDefaultMaxAutoScrollDelta = 20.0;
|
||||||
|
|
||||||
Offset? lastOffset;
|
Offset? lastOffset;
|
||||||
|
Duration? lastDuration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void startAutoScroll(
|
void startAutoScroll(
|
||||||
|
|
@ -31,13 +36,14 @@ class AutoScroller extends EdgeDraggingAutoScroller
|
||||||
AxisDirection? direction,
|
AxisDirection? direction,
|
||||||
Duration? duration,
|
Duration? duration,
|
||||||
}) {
|
}) {
|
||||||
|
lastOffset = offset;
|
||||||
|
lastDuration = duration;
|
||||||
if (direction != null && direction == AxisDirection.up) {
|
if (direction != null && direction == AxisDirection.up) {
|
||||||
return startAutoScrollIfNecessary(
|
return startAutoScrollIfNecessary(
|
||||||
offset & Size(1, edgeOffset),
|
offset & Size(1, edgeOffset),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastOffset = offset;
|
|
||||||
final dragTarget = Rect.fromCenter(
|
final dragTarget = Rect.fromCenter(
|
||||||
center: offset,
|
center: offset,
|
||||||
width: edgeOffset,
|
width: edgeOffset,
|
||||||
|
|
@ -52,12 +58,16 @@ class AutoScroller extends EdgeDraggingAutoScroller
|
||||||
@override
|
@override
|
||||||
void stopAutoScroll() {
|
void stopAutoScroll() {
|
||||||
lastOffset = null;
|
lastOffset = null;
|
||||||
|
lastDuration = null;
|
||||||
super.stopAutoScroll();
|
super.stopAutoScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
void continueToAutoScroll() {
|
void continueToAutoScroll() {
|
||||||
if (lastOffset != null) {
|
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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
const Duration _kDesktopAutoScrollTickDuration = Duration(milliseconds: 80);
|
||||||
|
|
||||||
class DesktopScrollService extends StatefulWidget {
|
class DesktopScrollService extends StatefulWidget {
|
||||||
const DesktopScrollService({
|
const DesktopScrollService({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -108,7 +110,7 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
|
||||||
offset,
|
offset,
|
||||||
edgeOffset: edgeOffset,
|
edgeOffset: edgeOffset,
|
||||||
direction: direction,
|
direction: direction,
|
||||||
duration: duration,
|
duration: duration ?? _kDesktopAutoScrollTickDuration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,27 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final selectionRect = editorState.selectionRects();
|
final selectionRects = editorState.selectionRects();
|
||||||
final endTouchPoint = selectionRect.lastOrNull?.centerRight;
|
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) {
|
if (PlatformExtension.isMobile) {
|
||||||
// soft keyboard
|
// soft keyboard
|
||||||
|
|
@ -106,34 +125,23 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
|
||||||
if (_forwardKey.currentContext == null) {
|
if (_forwardKey.currentContext == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (endTouchPoint == null) {
|
startAutoScroll(
|
||||||
jumpTo(selection.end.path.first);
|
endTouchPoint,
|
||||||
} else {
|
edgeOffset: editorState.autoScrollEdgeOffset,
|
||||||
startAutoScroll(
|
direction: direction,
|
||||||
endTouchPoint,
|
duration: Duration.zero,
|
||||||
edgeOffset: editorState.autoScrollEdgeOffset,
|
);
|
||||||
duration: Duration.zero,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (_forwardKey.currentContext == null) {
|
if (_forwardKey.currentContext == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (endTouchPoint == null) {
|
startAutoScroll(
|
||||||
// check if the selection is valid
|
endTouchPoint,
|
||||||
final node = editorState.getNodeAtPath(selection.end.path);
|
edgeOffset: editorState.autoScrollEdgeOffset,
|
||||||
if (node == null) {
|
direction: direction,
|
||||||
return;
|
duration: Duration.zero,
|
||||||
}
|
);
|
||||||
jumpTo(selection.end.path.first);
|
|
||||||
} else {
|
|
||||||
startAutoScroll(
|
|
||||||
endTouchPoint,
|
|
||||||
edgeOffset: editorState.autoScrollEdgeOffset,
|
|
||||||
duration: Duration.zero,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ class _DesktopSelectionServiceWidgetState
|
||||||
|
|
||||||
Position? _panStartPosition;
|
Position? _panStartPosition;
|
||||||
|
|
||||||
|
bool _isDraggingSelection = false;
|
||||||
|
Offset? _lastPanOffset;
|
||||||
|
|
||||||
OverlayEntry? _dropTargetEntry;
|
OverlayEntry? _dropTargetEntry;
|
||||||
|
|
||||||
late EditorState editorState = Provider.of<EditorState>(
|
late EditorState editorState = Provider.of<EditorState>(
|
||||||
|
|
@ -64,6 +67,7 @@ class _DesktopSelectionServiceWidgetState
|
||||||
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
editorState.selectionNotifier.addListener(_updateSelection);
|
editorState.selectionNotifier.addListener(_updateSelection);
|
||||||
|
editorState.addScrollViewScrolledListener(_handleAutoScrollWhileDragging);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -85,6 +89,9 @@ class _DesktopSelectionServiceWidgetState
|
||||||
clearSelection();
|
clearSelection();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
editorState.selectionNotifier.removeListener(_updateSelection);
|
editorState.selectionNotifier.removeListener(_updateSelection);
|
||||||
|
editorState.removeScrollViewScrolledListener(
|
||||||
|
_handleAutoScrollWhileDragging,
|
||||||
|
);
|
||||||
currentSelection.dispose();
|
currentSelection.dispose();
|
||||||
removeDropTarget();
|
removeDropTarget();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
@ -116,6 +123,7 @@ class _DesktopSelectionServiceWidgetState
|
||||||
@override
|
@override
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
// currentSelectedNodes = [];
|
// currentSelectedNodes = [];
|
||||||
|
_resetPanState();
|
||||||
currentSelection.value = null;
|
currentSelection.value = null;
|
||||||
|
|
||||||
_clearSelection();
|
_clearSelection();
|
||||||
|
|
@ -140,6 +148,14 @@ class _DesktopSelectionServiceWidgetState
|
||||||
..clear();
|
..clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _resetPanState() {
|
||||||
|
_isDraggingSelection = false;
|
||||||
|
_panStartOffset = null;
|
||||||
|
_panStartScrollDy = null;
|
||||||
|
_panStartPosition = null;
|
||||||
|
_lastPanOffset = null;
|
||||||
|
}
|
||||||
|
|
||||||
void _clearContextMenu() {
|
void _clearContextMenu() {
|
||||||
_contextMenuAreas
|
_contextMenuAreas
|
||||||
..forEach((overlay) => overlay.remove())
|
..forEach((overlay) => overlay.remove())
|
||||||
|
|
@ -300,6 +316,13 @@ class _DesktopSelectionServiceWidgetState
|
||||||
_panStartPosition = getNodeInOffset(_panStartOffset!)
|
_panStartPosition = getNodeInOffset(_panStartOffset!)
|
||||||
?.selectable
|
?.selectable
|
||||||
?.getPositionInOffset(_panStartOffset!);
|
?.getPositionInOffset(_panStartOffset!);
|
||||||
|
if (_panStartPosition == null) {
|
||||||
|
_resetPanState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastPanOffset = _panStartOffset;
|
||||||
|
_isDraggingSelection = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanUpdate(DragUpdateDetails details) {
|
void _onPanUpdate(DragUpdateDetails details) {
|
||||||
|
|
@ -311,35 +334,18 @@ class _DesktopSelectionServiceWidgetState
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_panStartOffset == null ||
|
if (!_isDraggingSelection ||
|
||||||
_panStartScrollDy == null ||
|
_panStartOffset == null ||
|
||||||
_panStartPosition == null) {
|
_panStartPosition == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final panEndOffset = details.globalPosition;
|
_lastPanOffset = details.globalPosition;
|
||||||
final dy = editorState.service.scrollService?.dy;
|
_updateSelectionDuringDrag(_lastPanOffset!);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editorState.service.scrollService?.startAutoScroll(
|
editorState.service.scrollService?.startAutoScroll(
|
||||||
panEndOffset,
|
_lastPanOffset!,
|
||||||
edgeOffset: 100,
|
edgeOffset: 80,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,9 +357,62 @@ class _DesktopSelectionServiceWidgetState
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_panStartPosition = null;
|
|
||||||
|
|
||||||
editorState.service.scrollService?.stopAutoScroll();
|
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() {
|
void _updateSelection() {
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ class _MobileSelectionServiceWidgetState
|
||||||
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
editorState.selectionNotifier.addListener(_updateSelection);
|
editorState.selectionNotifier.addListener(_updateSelection);
|
||||||
|
editorState.addScrollViewScrolledListener(_handleAutoScrollWhileDragging);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -117,6 +118,9 @@ class _MobileSelectionServiceWidgetState
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
selectionNotifierAfterLayout.dispose();
|
selectionNotifierAfterLayout.dispose();
|
||||||
editorState.selectionNotifier.removeListener(_updateSelection);
|
editorState.selectionNotifier.removeListener(_updateSelection);
|
||||||
|
editorState.removeScrollViewScrolledListener(
|
||||||
|
_handleAutoScrollWhileDragging,
|
||||||
|
);
|
||||||
collapsedHandleTimer?.cancel();
|
collapsedHandleTimer?.cancel();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
@ -394,6 +398,79 @@ class _MobileSelectionServiceWidgetState
|
||||||
selectionRects.clear();
|
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
|
@override
|
||||||
Node? getNodeInOffset(Offset offset) {
|
Node? getNodeInOffset(Offset offset) {
|
||||||
final List<Node> sortedNodes = editorState.getVisibleNodes(
|
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.
|
/// set true to this key to prevent attaching the text service when selection is changed.
|
||||||
const selectionExtraInfoDoNotAttachTextService =
|
const selectionExtraInfoDoNotAttachTextService =
|
||||||
'selectionExtraInfoDoNotAttachTextService';
|
'selectionExtraInfoDoNotAttachTextService';
|
||||||
|
const _selectionDragModeKey = 'selection_drag_mode';
|
||||||
|
|
||||||
class ApplyOptions {
|
class ApplyOptions {
|
||||||
const ApplyOptions({
|
const ApplyOptions({
|
||||||
|
|
@ -631,11 +632,31 @@ class EditorState {
|
||||||
) {
|
) {
|
||||||
if (this.scrollableState != scrollableState) {
|
if (this.scrollableState != scrollableState) {
|
||||||
autoScroller?.stopAutoScroll();
|
autoScroller?.stopAutoScroll();
|
||||||
autoScroller = AutoScroller(
|
final bool isDesktopOrWeb = PlatformExtension.isDesktopOrWeb;
|
||||||
|
late AutoScroller scroller;
|
||||||
|
scroller = AutoScroller(
|
||||||
scrollableState,
|
scrollableState,
|
||||||
velocityScalar: PlatformExtension.isDesktopOrWeb ? 50 : 100,
|
velocityScalar: isDesktopOrWeb ? 0.125 : 0.02,
|
||||||
onScrollViewScrolled: _notifyScrollViewScrolledListeners,
|
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;
|
this.scrollableState = scrollableState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -147,7 +148,12 @@ class EdgeDraggingAutoScroller {
|
||||||
this.scrollable, {
|
this.scrollable, {
|
||||||
this.onScrollViewScrolled,
|
this.onScrollViewScrolled,
|
||||||
required this.velocityScalar,
|
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.
|
/// The [Scrollable] this auto scroller is scrolling.
|
||||||
final ScrollableState scrollable;
|
final ScrollableState scrollable;
|
||||||
|
|
@ -167,6 +173,15 @@ class EdgeDraggingAutoScroller {
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final double velocityScalar;
|
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;
|
late Rect _dragTargetRelatedToScrollOrigin;
|
||||||
|
|
||||||
/// Whether the auto scroll is in progress.
|
/// Whether the auto scroll is in progress.
|
||||||
|
|
@ -212,9 +227,10 @@ class EdgeDraggingAutoScroller {
|
||||||
/// Stop any ongoing auto scrolling.
|
/// Stop any ongoing auto scrolling.
|
||||||
void stopAutoScroll() {
|
void stopAutoScroll() {
|
||||||
_scrolling = false;
|
_scrolling = false;
|
||||||
|
_previousScrollDelta = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _scroll() async {
|
Future<void> _scroll({Duration? duration}) async {
|
||||||
try {
|
try {
|
||||||
final RenderBox scrollRenderBox =
|
final RenderBox scrollRenderBox =
|
||||||
scrollable.context.findRenderObject()! as RenderBox;
|
scrollable.context.findRenderObject()! as RenderBox;
|
||||||
|
|
@ -268,18 +284,20 @@ class EdgeDraggingAutoScroller {
|
||||||
scrollable.position.minScrollExtent) {
|
scrollable.position.minScrollExtent) {
|
||||||
final double overDrag =
|
final double overDrag =
|
||||||
math.min(proxyEnd - viewportEnd, overDragMax);
|
math.min(proxyEnd - viewportEnd, overDragMax);
|
||||||
|
final double delta = _smoothScrollDelta(overDrag);
|
||||||
newOffset = math.max(
|
newOffset = math.max(
|
||||||
scrollable.position.minScrollExtent,
|
scrollable.position.minScrollExtent,
|
||||||
scrollable.position.pixels - overDrag,
|
scrollable.position.pixels - delta,
|
||||||
);
|
);
|
||||||
} else if (proxyStart < viewportStart &&
|
} else if (proxyStart < viewportStart &&
|
||||||
scrollable.position.pixels <
|
scrollable.position.pixels <
|
||||||
scrollable.position.maxScrollExtent) {
|
scrollable.position.maxScrollExtent) {
|
||||||
final double overDrag =
|
final double overDrag =
|
||||||
math.min(viewportStart - proxyStart, overDragMax);
|
math.min(viewportStart - proxyStart, overDragMax);
|
||||||
|
final double delta = _smoothScrollDelta(overDrag);
|
||||||
newOffset = math.min(
|
newOffset = math.min(
|
||||||
scrollable.position.maxScrollExtent,
|
scrollable.position.maxScrollExtent,
|
||||||
scrollable.position.pixels + overDrag,
|
scrollable.position.pixels + delta,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case AxisDirection.right:
|
case AxisDirection.right:
|
||||||
|
|
@ -289,35 +307,58 @@ class EdgeDraggingAutoScroller {
|
||||||
scrollable.position.minScrollExtent) {
|
scrollable.position.minScrollExtent) {
|
||||||
final double overDrag =
|
final double overDrag =
|
||||||
math.min(viewportStart - proxyStart, overDragMax);
|
math.min(viewportStart - proxyStart, overDragMax);
|
||||||
|
final double delta = _smoothScrollDelta(overDrag);
|
||||||
newOffset = math.max(
|
newOffset = math.max(
|
||||||
scrollable.position.minScrollExtent,
|
scrollable.position.minScrollExtent,
|
||||||
scrollable.position.pixels - overDrag,
|
scrollable.position.pixels - delta,
|
||||||
);
|
);
|
||||||
} else if (proxyEnd > viewportEnd &&
|
} else if (proxyEnd > viewportEnd &&
|
||||||
scrollable.position.pixels <
|
scrollable.position.pixels <
|
||||||
scrollable.position.maxScrollExtent) {
|
scrollable.position.maxScrollExtent) {
|
||||||
final double overDrag =
|
final double overDrag =
|
||||||
math.min(proxyEnd - viewportEnd, overDragMax);
|
math.min(proxyEnd - viewportEnd, overDragMax);
|
||||||
|
final double delta = _smoothScrollDelta(overDrag);
|
||||||
newOffset = math.min(
|
newOffset = math.min(
|
||||||
scrollable.position.maxScrollExtent,
|
scrollable.position.maxScrollExtent,
|
||||||
scrollable.position.pixels + overDrag,
|
scrollable.position.pixels + delta,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newOffset == null ||
|
final double currentPixels = scrollable.position.pixels;
|
||||||
(newOffset - scrollable.position.pixels).abs() < 1.0) {
|
if (newOffset == null) {
|
||||||
// Drag should not trigger scroll.
|
// Drag should not trigger scroll.
|
||||||
_scrolling = false;
|
_scrolling = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Duration duration =
|
double delta = newOffset - currentPixels;
|
||||||
Duration(milliseconds: (1000 / velocityScalar).round());
|
if (delta.abs() < _minimumAutoScrollDelta) {
|
||||||
await scrollable.position
|
if (delta.abs() <= precisionErrorTolerance) {
|
||||||
.animateTo(newOffset, duration: duration, curve: Curves.linear);
|
_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();
|
onScrollViewScrolled?.call();
|
||||||
if (_scrolling) {
|
if (_scrolling) {
|
||||||
await _scroll();
|
await _scroll(duration: duration);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint(e.toString());
|
debugPrint(e.toString());
|
||||||
|
|
@ -325,6 +366,25 @@ class EdgeDraggingAutoScroller {
|
||||||
_scrolling = false;
|
_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
|
/// A typedef for a function that can calculate the offset for a type of scroll
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue