mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: implement autoscrolling on edge touch
This commit is contained in:
parent
90fa1312f2
commit
e5787090d2
@ -241,6 +241,62 @@
|
||||
"subtype": "number-list",
|
||||
"number": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EditorEntryWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
@ -31,28 +32,26 @@ class EditorNodeWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: node.children
|
||||
.map(
|
||||
(child) =>
|
||||
editorState.service.renderPluginService.buildPluginWidget(
|
||||
child is TextNode
|
||||
? NodeWidgetContext<TextNode>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: editorState,
|
||||
)
|
||||
: NodeWidgetContext<Node>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: editorState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: node.children
|
||||
.map(
|
||||
(child) =>
|
||||
editorState.service.renderPluginService.buildPluginWidget(
|
||||
child is TextNode
|
||||
? NodeWidgetContext<TextNode>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: editorState,
|
||||
)
|
||||
: NodeWidgetContext<Node>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: editorState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/document/path.dart';
|
||||
import 'package:flowy_editor/document/position.dart';
|
||||
import 'package:flowy_editor/document/selection.dart';
|
||||
import 'package:flowy_editor/document/text_delta.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/document/path.dart';
|
||||
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
||||
import 'package:flowy_editor/render/selection/selectable.dart';
|
||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class RichTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<TextNode> context) {
|
||||
@ -173,11 +173,12 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
||||
}
|
||||
|
||||
TextSpan get _textSpan => TextSpan(
|
||||
children: widget.textNode.delta.operations
|
||||
.whereType<TextInsert>()
|
||||
.map((insert) => RichTextStyle(
|
||||
attributes: insert.attributes ?? {},
|
||||
text: insert.content,
|
||||
).toTextSpan())
|
||||
.toList(growable: false));
|
||||
children: widget.textNode.delta.operations
|
||||
.whereType<TextInsert>()
|
||||
.map((insert) => RichTextStyle(
|
||||
attributes: insert.attributes ?? {},
|
||||
text: insert.content,
|
||||
).toTextSpan())
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
@ -13,10 +12,13 @@ import 'package:flowy_editor/render/rich_text/quoted_text.dart';
|
||||
import 'package:flowy_editor/service/input_service.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart';
|
||||
import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||
import 'package:flowy_editor/service/scroll_service.dart';
|
||||
import 'package:flowy_editor/service/selection_service.dart';
|
||||
import 'package:flowy_editor/service/toolbar_service.dart';
|
||||
|
||||
@ -60,15 +62,25 @@ class FlowyEditor extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FlowyEditorState extends State<FlowyEditor> {
|
||||
late ScrollController _scrollController;
|
||||
|
||||
EditorState get editorState => widget.editorState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_scrollController = ScrollController()..addListener(_scrollCallback);
|
||||
editorState.service.renderPluginService = _createRenderPlugin();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyEditor oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@ -80,33 +92,36 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowySelection(
|
||||
key: editorState.service.selectionServiceKey,
|
||||
editorState: editorState,
|
||||
child: FlowyInput(
|
||||
key: editorState.service.inputServiceKey,
|
||||
editorState: editorState,
|
||||
child: FlowyKeyboard(
|
||||
key: editorState.service.keyboardServiceKey,
|
||||
handlers: [
|
||||
...defaultKeyEventHandler,
|
||||
...widget.keyEventHandlers,
|
||||
],
|
||||
return FlowyScroll(
|
||||
key: editorState.service.scrollServiceKey,
|
||||
child: FlowySelection(
|
||||
key: editorState.service.selectionServiceKey,
|
||||
editorState: editorState,
|
||||
child: FlowyToolbar(
|
||||
key: editorState.service.toolbarServiceKey,
|
||||
child: FlowyInput(
|
||||
key: editorState.service.inputServiceKey,
|
||||
editorState: editorState,
|
||||
child: editorState.service.renderPluginService.buildPluginWidget(
|
||||
NodeWidgetContext(
|
||||
context: context,
|
||||
node: editorState.document.root,
|
||||
child: FlowyKeyboard(
|
||||
key: editorState.service.keyboardServiceKey,
|
||||
handlers: [
|
||||
...defaultKeyEventHandler,
|
||||
...widget.keyEventHandlers,
|
||||
],
|
||||
editorState: editorState,
|
||||
child: FlowyToolbar(
|
||||
key: editorState.service.toolbarServiceKey,
|
||||
editorState: editorState,
|
||||
child:
|
||||
editorState.service.renderPluginService.buildPluginWidget(
|
||||
NodeWidgetContext(
|
||||
context: context,
|
||||
node: editorState.document.root,
|
||||
editorState: editorState,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin(
|
||||
@ -116,4 +131,8 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
||||
...widget.customBuilders,
|
||||
},
|
||||
);
|
||||
|
||||
void _scrollCallback() {
|
||||
debugPrint('scrolling');
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
mixin FlowyScrollService<T extends StatefulWidget> on State<T> {
|
||||
double get dy;
|
||||
|
||||
void scrollTo(double dy);
|
||||
|
||||
RenderObject? scrollRenderObject();
|
||||
}
|
||||
|
||||
class FlowyScroll extends StatefulWidget {
|
||||
const FlowyScroll({
|
||||
Key? key,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<FlowyScroll> createState() => _FlowyScrollState();
|
||||
}
|
||||
|
||||
class _FlowyScrollState extends State<FlowyScroll> with FlowyScrollService {
|
||||
final _scrollController = ScrollController();
|
||||
final _scrollViewKey = GlobalKey();
|
||||
|
||||
@override
|
||||
double get dy => _scrollController.position.pixels;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerSignal: _onPointerSignal,
|
||||
child: SingleChildScrollView(
|
||||
key: _scrollViewKey,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void scrollTo(double dy) {
|
||||
_scrollController.position.jumpTo(
|
||||
dy.clamp(
|
||||
_scrollController.position.minScrollExtent,
|
||||
_scrollController.position.maxScrollExtent,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPointerSignal(PointerSignalEvent event) {
|
||||
if (event is PointerScrollEvent) {
|
||||
final dy = (_scrollController.position.pixels + event.scrollDelta.dy);
|
||||
scrollTo(dy);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
RenderObject? scrollRenderObject() {
|
||||
return _scrollViewKey.currentContext?.findRenderObject();
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -99,102 +100,18 @@ class FlowySelection extends StatefulWidget {
|
||||
State<FlowySelection> createState() => _FlowySelectionState();
|
||||
}
|
||||
|
||||
/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]
|
||||
/// for a while. So we need to implement our own GestureDetector.
|
||||
@immutable
|
||||
class _SelectionGestureDetector extends StatefulWidget {
|
||||
const _SelectionGestureDetector(
|
||||
{Key? key,
|
||||
this.child,
|
||||
this.onTapDown,
|
||||
this.onDoubleTapDown,
|
||||
this.onPanStart,
|
||||
this.onPanUpdate,
|
||||
this.onPanEnd})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<_SelectionGestureDetector> createState() =>
|
||||
_SelectionGestureDetectorState();
|
||||
|
||||
final Widget? child;
|
||||
|
||||
final GestureTapDownCallback? onTapDown;
|
||||
final GestureTapDownCallback? onDoubleTapDown;
|
||||
final GestureDragStartCallback? onPanStart;
|
||||
final GestureDragUpdateCallback? onPanUpdate;
|
||||
final GestureDragEndCallback? onPanEnd;
|
||||
}
|
||||
|
||||
class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> {
|
||||
bool _isDoubleTap = false;
|
||||
Timer? _doubleTapTimer;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawGestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
gestures: {
|
||||
PanGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
|
||||
() => PanGestureRecognizer(),
|
||||
(recognizer) {
|
||||
recognizer
|
||||
..onStart = widget.onPanStart
|
||||
..onUpdate = widget.onPanUpdate
|
||||
..onEnd = widget.onPanEnd;
|
||||
},
|
||||
),
|
||||
TapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(),
|
||||
(recognizer) {
|
||||
recognizer.onTapDown = _tapDownDelegate;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
_tapDownDelegate(TapDownDetails tapDownDetails) {
|
||||
if (_isDoubleTap) {
|
||||
_isDoubleTap = false;
|
||||
_doubleTapTimer?.cancel();
|
||||
_doubleTapTimer = null;
|
||||
if (widget.onDoubleTapDown != null) {
|
||||
widget.onDoubleTapDown!(tapDownDetails);
|
||||
}
|
||||
} else {
|
||||
if (widget.onTapDown != null) {
|
||||
widget.onTapDown!(tapDownDetails);
|
||||
}
|
||||
|
||||
_isDoubleTap = true;
|
||||
_doubleTapTimer?.cancel();
|
||||
_doubleTapTimer = Timer(kDoubleTapTimeout, () {
|
||||
_isDoubleTap = false;
|
||||
_doubleTapTimer = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_doubleTapTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _FlowySelectionState extends State<FlowySelection>
|
||||
with FlowySelectionService, WidgetsBindingObserver {
|
||||
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
||||
|
||||
final List<OverlayEntry> _selectionOverlays = [];
|
||||
final List<OverlayEntry> _cursorOverlays = [];
|
||||
OverlayEntry? _debugOverlay;
|
||||
|
||||
/// [Pan] and [Tap] must be mutually exclusive.
|
||||
/// Pan
|
||||
Offset? panStartOffset;
|
||||
double? panStartScrollDy;
|
||||
Offset? panEndOffset;
|
||||
|
||||
/// Tap
|
||||
@ -259,7 +176,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
@override
|
||||
void updateSelection(Selection selection) {
|
||||
_rects.clear();
|
||||
_clearSelection();
|
||||
clearSelection();
|
||||
|
||||
// cursor
|
||||
if (selection.isCollapsed) {
|
||||
@ -273,7 +190,19 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
|
||||
@override
|
||||
void clearSelection() {
|
||||
_clearSelection();
|
||||
currentSelection = null;
|
||||
currentSelectedNodes.value = [];
|
||||
|
||||
// clear selection
|
||||
_selectionOverlays
|
||||
..forEach((overlay) => overlay.remove())
|
||||
..clear();
|
||||
// clear cursors
|
||||
_cursorOverlays
|
||||
..forEach((overlay) => overlay.remove())
|
||||
..clear();
|
||||
// clear toolbar
|
||||
editorState.service.toolbarService?.hide();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -325,7 +254,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
}
|
||||
}
|
||||
for (final child in node.children) {
|
||||
result.addAll(computeNodesInRange(child, start, end));
|
||||
result.addAll(_computeNodesInRange(child, start, end));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -411,12 +340,24 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
clearSelection();
|
||||
|
||||
panStartOffset = details.globalPosition;
|
||||
panStartScrollDy = editorState.service.scrollService?.dy;
|
||||
|
||||
debugPrint('[_onPanStart] panStartOffset = $panStartOffset');
|
||||
}
|
||||
|
||||
void _onPanUpdate(DragUpdateDetails details) {
|
||||
panEndOffset = details.globalPosition;
|
||||
if (panStartOffset == null || panStartScrollDy == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final nodes = getNodesInRange(panStartOffset!, panEndOffset!);
|
||||
panEndOffset = details.globalPosition;
|
||||
final dy = editorState.service.scrollService?.dy;
|
||||
var panStartOffsetWithScrollDyGap = panStartOffset!;
|
||||
if (dy != null) {
|
||||
panStartOffsetWithScrollDyGap =
|
||||
panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy);
|
||||
}
|
||||
final nodes = getNodesInRange(panStartOffsetWithScrollDyGap, panEndOffset!);
|
||||
if (nodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@ -427,40 +368,30 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
if (first != null && last != null) {
|
||||
bool isDownward;
|
||||
if (first == last) {
|
||||
isDownward = panStartOffset!.dx < panEndOffset!.dx;
|
||||
isDownward = panStartOffsetWithScrollDyGap.dx < panEndOffset!.dx;
|
||||
} else {
|
||||
isDownward = panStartOffset!.dy < panEndOffset!.dy;
|
||||
isDownward = panStartOffsetWithScrollDyGap.dy < panEndOffset!.dy;
|
||||
}
|
||||
final start =
|
||||
first.getSelectionInRange(panStartOffset!, panEndOffset!).start;
|
||||
final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end;
|
||||
final start = first
|
||||
.getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
|
||||
.start;
|
||||
final end = last
|
||||
.getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
|
||||
.end;
|
||||
final selection = Selection(
|
||||
start: isDownward ? start : end, end: isDownward ? end : start);
|
||||
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
||||
editorState.service.selectionService.updateSelection(selection);
|
||||
}
|
||||
|
||||
_scrollUpOrDownIfNeeded(panEndOffset!);
|
||||
_showDebugLayerIfNeeded();
|
||||
}
|
||||
|
||||
void _onPanEnd(DragEndDetails details) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
void _clearSelection() {
|
||||
currentSelection = null;
|
||||
currentSelectedNodes.value = [];
|
||||
|
||||
// clear selection
|
||||
_selectionOverlays
|
||||
..forEach((overlay) => overlay.remove())
|
||||
..clear();
|
||||
// clear cursors
|
||||
_cursorOverlays
|
||||
..forEach((overlay) => overlay.remove())
|
||||
..clear();
|
||||
// clear toolbar
|
||||
editorState.service.toolbarService?.hide();
|
||||
}
|
||||
|
||||
void _updateSelection(Selection selection) {
|
||||
final nodes =
|
||||
_selectedNodesInSelection(editorState.document.root, selection);
|
||||
@ -554,12 +485,12 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
if (rect != null) {
|
||||
_rects.add(_transformRectToGlobal(selectable!, rect));
|
||||
final cursor = OverlayEntry(
|
||||
builder: ((context) => CursorWidget(
|
||||
key: _cursorKey,
|
||||
rect: rect,
|
||||
color: widget.cursorColor,
|
||||
layerLink: node.layerLink,
|
||||
)),
|
||||
builder: (context) => CursorWidget(
|
||||
key: _cursorKey,
|
||||
rect: rect,
|
||||
color: widget.cursorColor,
|
||||
layerLink: node.layerLink,
|
||||
),
|
||||
);
|
||||
_cursorOverlays.add(cursor);
|
||||
Overlay.of(context)?.insertAll(_cursorOverlays);
|
||||
@ -584,4 +515,139 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void _scrollUpOrDownIfNeeded(Offset offset) {
|
||||
final dy = editorState.service.scrollService?.dy;
|
||||
if (dy == null) {
|
||||
assert(false, 'Dy could not be null');
|
||||
return;
|
||||
}
|
||||
final topLimit = MediaQuery.of(context).size.height * 0.2;
|
||||
final bottomLimit = MediaQuery.of(context).size.height * 0.8;
|
||||
|
||||
/// TODO: It is necessary to calculate the relative speed
|
||||
/// according to the gap and move forward more gently.
|
||||
final distance = 10.0;
|
||||
if (offset.dy <= topLimit) {
|
||||
// up
|
||||
editorState.service.scrollService?.scrollTo(dy - distance);
|
||||
} else if (offset.dy >= bottomLimit) {
|
||||
//down
|
||||
editorState.service.scrollService?.scrollTo(dy + distance);
|
||||
}
|
||||
}
|
||||
|
||||
void _showDebugLayerIfNeeded() {
|
||||
// remove false to show debug overlay.
|
||||
if (kDebugMode && false) {
|
||||
_debugOverlay?.remove();
|
||||
if (panStartOffset != null) {
|
||||
_debugOverlay = OverlayEntry(
|
||||
builder: (context) => Positioned.fromRect(
|
||||
rect: Rect.fromPoints(
|
||||
panStartOffset?.translate(
|
||||
0,
|
||||
-(editorState.service.scrollService!.dy -
|
||||
panStartScrollDy!),
|
||||
) ??
|
||||
Offset.zero,
|
||||
panEndOffset ?? Offset.zero)
|
||||
.translate(0, 0),
|
||||
child: Container(
|
||||
color: Colors.red.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
);
|
||||
Overlay.of(context)?.insert(_debugOverlay!);
|
||||
} else {
|
||||
_debugOverlay = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]
|
||||
/// for a while. So we need to implement our own GestureDetector.
|
||||
@immutable
|
||||
class _SelectionGestureDetector extends StatefulWidget {
|
||||
const _SelectionGestureDetector(
|
||||
{Key? key,
|
||||
this.child,
|
||||
this.onTapDown,
|
||||
this.onDoubleTapDown,
|
||||
this.onPanStart,
|
||||
this.onPanUpdate,
|
||||
this.onPanEnd})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<_SelectionGestureDetector> createState() =>
|
||||
_SelectionGestureDetectorState();
|
||||
|
||||
final Widget? child;
|
||||
|
||||
final GestureTapDownCallback? onTapDown;
|
||||
final GestureTapDownCallback? onDoubleTapDown;
|
||||
final GestureDragStartCallback? onPanStart;
|
||||
final GestureDragUpdateCallback? onPanUpdate;
|
||||
final GestureDragEndCallback? onPanEnd;
|
||||
}
|
||||
|
||||
class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> {
|
||||
bool _isDoubleTap = false;
|
||||
Timer? _doubleTapTimer;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawGestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
gestures: {
|
||||
PanGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
|
||||
() => PanGestureRecognizer(),
|
||||
(recognizer) {
|
||||
recognizer
|
||||
..onStart = widget.onPanStart
|
||||
..onUpdate = widget.onPanUpdate
|
||||
..onEnd = widget.onPanEnd;
|
||||
},
|
||||
),
|
||||
TapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(),
|
||||
(recognizer) {
|
||||
recognizer.onTapDown = _tapDownDelegate;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
_tapDownDelegate(TapDownDetails tapDownDetails) {
|
||||
if (_isDoubleTap) {
|
||||
_isDoubleTap = false;
|
||||
_doubleTapTimer?.cancel();
|
||||
_doubleTapTimer = null;
|
||||
if (widget.onDoubleTapDown != null) {
|
||||
widget.onDoubleTapDown!(tapDownDetails);
|
||||
}
|
||||
} else {
|
||||
if (widget.onTapDown != null) {
|
||||
widget.onTapDown!(tapDownDetails);
|
||||
}
|
||||
|
||||
_isDoubleTap = true;
|
||||
_doubleTapTimer?.cancel();
|
||||
_doubleTapTimer = Timer(kDoubleTapTimeout, () {
|
||||
_isDoubleTap = false;
|
||||
_doubleTapTimer = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_doubleTapTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||
import 'package:flowy_editor/service/toolbar_service.dart';
|
||||
import 'package:flowy_editor/service/scroll_service.dart';
|
||||
import 'package:flowy_editor/service/selection_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_editor/service/toolbar_service.dart';
|
||||
|
||||
class FlowyService {
|
||||
// selection service
|
||||
@ -31,10 +33,20 @@ class FlowyService {
|
||||
|
||||
// toolbar service
|
||||
final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service');
|
||||
ToolbarService? get toolbarService {
|
||||
FlowyToolbarService? get toolbarService {
|
||||
if (toolbarServiceKey.currentState != null &&
|
||||
toolbarServiceKey.currentState is ToolbarService) {
|
||||
return toolbarServiceKey.currentState! as ToolbarService;
|
||||
toolbarServiceKey.currentState is FlowyToolbarService) {
|
||||
return toolbarServiceKey.currentState! as FlowyToolbarService;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// scroll service
|
||||
final scrollServiceKey = GlobalKey(debugLabel: 'flowy_scroll_service');
|
||||
FlowyScrollService? get scrollService {
|
||||
if (scrollServiceKey.currentState != null &&
|
||||
scrollServiceKey.currentState is FlowyScrollService) {
|
||||
return scrollServiceKey.currentState! as FlowyScrollService;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flowy_editor/render/selection/toolbar_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
mixin ToolbarService {
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flowy_editor/render/selection/toolbar_widget.dart';
|
||||
|
||||
mixin FlowyToolbarService {
|
||||
/// Show the toolbar widget beside the offset.
|
||||
void showInOffset(Offset offset, LayerLink layerLink);
|
||||
|
||||
@ -24,7 +25,7 @@ class FlowyToolbar extends StatefulWidget {
|
||||
State<FlowyToolbar> createState() => _FlowyToolbarState();
|
||||
}
|
||||
|
||||
class _FlowyToolbarState extends State<FlowyToolbar> with ToolbarService {
|
||||
class _FlowyToolbarState extends State<FlowyToolbar> with FlowyToolbarService {
|
||||
OverlayEntry? _toolbarOverlay;
|
||||
|
||||
@override
|
||||
|
Loading…
Reference in New Issue
Block a user