feat: implement autoscrolling on edge touch

This commit is contained in:
Lucas.Xu 2022-08-05 10:59:51 +08:00
parent 90fa1312f2
commit e5787090d2
8 changed files with 403 additions and 184 deletions

View File

@ -241,6 +241,62 @@
"subtype": "number-list", "subtype": "number-list",
"number": 3 "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. AppFlowys 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. AppFlowys 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. AppFlowys 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. AppFlowys 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. AppFlowys 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. AppFlowys 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. AppFlowys 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."
}
]
} }
] ]
} }

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flowy_editor/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
class EditorEntryWidgetBuilder extends NodeWidgetBuilder<Node> { class EditorEntryWidgetBuilder extends NodeWidgetBuilder<Node> {
@override @override
@ -31,28 +32,26 @@ class EditorNodeWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: node.children
children: node.children .map(
.map( (child) =>
(child) => editorState.service.renderPluginService.buildPluginWidget(
editorState.service.renderPluginService.buildPluginWidget( child is TextNode
child is TextNode ? NodeWidgetContext<TextNode>(
? NodeWidgetContext<TextNode>( context: context,
context: context, node: child,
node: child, editorState: editorState,
editorState: editorState, )
) : NodeWidgetContext<Node>(
: NodeWidgetContext<Node>( context: context,
context: context, node: child,
node: child, editorState: editorState,
editorState: editorState, ),
), ),
), )
) .toList(),
.toList(),
),
); );
} }
} }

View File

@ -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/node.dart';
import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/document/text_delta.dart';
import 'package:flowy_editor/editor_state.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/rich_text/rich_text_style.dart';
import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/render/selection/selectable.dart';
import 'package:flowy_editor/service/render_plugin_service.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> { class RichTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@override @override
Widget build(NodeWidgetContext<TextNode> context) { Widget build(NodeWidgetContext<TextNode> context) {
@ -173,11 +173,12 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
} }
TextSpan get _textSpan => TextSpan( TextSpan get _textSpan => TextSpan(
children: widget.textNode.delta.operations children: widget.textNode.delta.operations
.whereType<TextInsert>() .whereType<TextInsert>()
.map((insert) => RichTextStyle( .map((insert) => RichTextStyle(
attributes: insert.attributes ?? {}, attributes: insert.attributes ?? {},
text: insert.content, text: insert.content,
).toTextSpan()) ).toTextSpan())
.toList(growable: false)); .toList(growable: false),
);
} }

View File

@ -1,5 +1,4 @@
import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; import 'package:flutter/gestures.dart';
import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flowy_editor/editor_state.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/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/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_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/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/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/keyboard_service.dart';
import 'package:flowy_editor/service/render_plugin_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/selection_service.dart';
import 'package:flowy_editor/service/toolbar_service.dart'; import 'package:flowy_editor/service/toolbar_service.dart';
@ -60,15 +62,25 @@ class FlowyEditor extends StatefulWidget {
} }
class _FlowyEditorState extends State<FlowyEditor> { class _FlowyEditorState extends State<FlowyEditor> {
late ScrollController _scrollController;
EditorState get editorState => widget.editorState; EditorState get editorState => widget.editorState;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_scrollController = ScrollController()..addListener(_scrollCallback);
editorState.service.renderPluginService = _createRenderPlugin(); editorState.service.renderPluginService = _createRenderPlugin();
} }
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override @override
void didUpdateWidget(covariant FlowyEditor oldWidget) { void didUpdateWidget(covariant FlowyEditor oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
@ -80,33 +92,36 @@ class _FlowyEditorState extends State<FlowyEditor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlowySelection( return FlowyScroll(
key: editorState.service.selectionServiceKey, key: editorState.service.scrollServiceKey,
editorState: editorState, child: FlowySelection(
child: FlowyInput( key: editorState.service.selectionServiceKey,
key: editorState.service.inputServiceKey,
editorState: editorState,
child: FlowyKeyboard(
key: editorState.service.keyboardServiceKey,
handlers: [
...defaultKeyEventHandler,
...widget.keyEventHandlers,
],
editorState: editorState, editorState: editorState,
child: FlowyToolbar( child: FlowyInput(
key: editorState.service.toolbarServiceKey, key: editorState.service.inputServiceKey,
editorState: editorState, editorState: editorState,
child: editorState.service.renderPluginService.buildPluginWidget( child: FlowyKeyboard(
NodeWidgetContext( key: editorState.service.keyboardServiceKey,
context: context, handlers: [
node: editorState.document.root, ...defaultKeyEventHandler,
...widget.keyEventHandlers,
],
editorState: editorState,
child: FlowyToolbar(
key: editorState.service.toolbarServiceKey,
editorState: editorState, editorState: editorState,
child:
editorState.service.renderPluginService.buildPluginWidget(
NodeWidgetContext(
context: context,
node: editorState.document.root,
editorState: editorState,
),
),
), ),
), ),
), ),
), ));
),
);
} }
FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin( FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin(
@ -116,4 +131,8 @@ class _FlowyEditorState extends State<FlowyEditor> {
...widget.customBuilders, ...widget.customBuilders,
}, },
); );
void _scrollCallback() {
debugPrint('scrolling');
}
} }

View File

@ -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();
}
}

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -99,102 +100,18 @@ class FlowySelection extends StatefulWidget {
State<FlowySelection> createState() => _FlowySelectionState(); 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> class _FlowySelectionState extends State<FlowySelection>
with FlowySelectionService, WidgetsBindingObserver { with FlowySelectionService, WidgetsBindingObserver {
final _cursorKey = GlobalKey(debugLabel: 'cursor'); final _cursorKey = GlobalKey(debugLabel: 'cursor');
final List<OverlayEntry> _selectionOverlays = []; final List<OverlayEntry> _selectionOverlays = [];
final List<OverlayEntry> _cursorOverlays = []; final List<OverlayEntry> _cursorOverlays = [];
OverlayEntry? _debugOverlay;
/// [Pan] and [Tap] must be mutually exclusive. /// [Pan] and [Tap] must be mutually exclusive.
/// Pan /// Pan
Offset? panStartOffset; Offset? panStartOffset;
double? panStartScrollDy;
Offset? panEndOffset; Offset? panEndOffset;
/// Tap /// Tap
@ -259,7 +176,7 @@ class _FlowySelectionState extends State<FlowySelection>
@override @override
void updateSelection(Selection selection) { void updateSelection(Selection selection) {
_rects.clear(); _rects.clear();
_clearSelection(); clearSelection();
// cursor // cursor
if (selection.isCollapsed) { if (selection.isCollapsed) {
@ -273,7 +190,19 @@ class _FlowySelectionState extends State<FlowySelection>
@override @override
void clearSelection() { 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 @override
@ -325,7 +254,7 @@ class _FlowySelectionState extends State<FlowySelection>
} }
} }
for (final child in node.children) { for (final child in node.children) {
result.addAll(computeNodesInRange(child, start, end)); result.addAll(_computeNodesInRange(child, start, end));
} }
return result; return result;
} }
@ -411,12 +340,24 @@ class _FlowySelectionState extends State<FlowySelection>
clearSelection(); clearSelection();
panStartOffset = details.globalPosition; panStartOffset = details.globalPosition;
panStartScrollDy = editorState.service.scrollService?.dy;
debugPrint('[_onPanStart] panStartOffset = $panStartOffset');
} }
void _onPanUpdate(DragUpdateDetails details) { 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) { if (nodes.isEmpty) {
return; return;
} }
@ -427,40 +368,30 @@ class _FlowySelectionState extends State<FlowySelection>
if (first != null && last != null) { if (first != null && last != null) {
bool isDownward; bool isDownward;
if (first == last) { if (first == last) {
isDownward = panStartOffset!.dx < panEndOffset!.dx; isDownward = panStartOffsetWithScrollDyGap.dx < panEndOffset!.dx;
} else { } else {
isDownward = panStartOffset!.dy < panEndOffset!.dy; isDownward = panStartOffsetWithScrollDyGap.dy < panEndOffset!.dy;
} }
final start = final start = first
first.getSelectionInRange(panStartOffset!, panEndOffset!).start; .getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end; .start;
final end = last
.getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
.end;
final selection = Selection( final selection = Selection(
start: isDownward ? start : end, end: isDownward ? end : start); start: isDownward ? start : end, end: isDownward ? end : start);
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection'); debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
editorState.service.selectionService.updateSelection(selection); editorState.service.selectionService.updateSelection(selection);
} }
_scrollUpOrDownIfNeeded(panEndOffset!);
_showDebugLayerIfNeeded();
} }
void _onPanEnd(DragEndDetails details) { void _onPanEnd(DragEndDetails details) {
// do nothing // 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) { void _updateSelection(Selection selection) {
final nodes = final nodes =
_selectedNodesInSelection(editorState.document.root, selection); _selectedNodesInSelection(editorState.document.root, selection);
@ -554,12 +485,12 @@ class _FlowySelectionState extends State<FlowySelection>
if (rect != null) { if (rect != null) {
_rects.add(_transformRectToGlobal(selectable!, rect)); _rects.add(_transformRectToGlobal(selectable!, rect));
final cursor = OverlayEntry( final cursor = OverlayEntry(
builder: ((context) => CursorWidget( builder: (context) => CursorWidget(
key: _cursorKey, key: _cursorKey,
rect: rect, rect: rect,
color: widget.cursorColor, color: widget.cursorColor,
layerLink: node.layerLink, layerLink: node.layerLink,
)), ),
); );
_cursorOverlays.add(cursor); _cursorOverlays.add(cursor);
Overlay.of(context)?.insertAll(_cursorOverlays); Overlay.of(context)?.insertAll(_cursorOverlays);
@ -584,4 +515,139 @@ class _FlowySelectionState extends State<FlowySelection>
} }
return result; 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();
}
} }

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flowy_editor/service/render_plugin_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:flowy_editor/service/selection_service.dart';
import 'package:flutter/material.dart'; import 'package:flowy_editor/service/toolbar_service.dart';
class FlowyService { class FlowyService {
// selection service // selection service
@ -31,10 +33,20 @@ class FlowyService {
// toolbar service // toolbar service
final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service'); final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service');
ToolbarService? get toolbarService { FlowyToolbarService? get toolbarService {
if (toolbarServiceKey.currentState != null && if (toolbarServiceKey.currentState != null &&
toolbarServiceKey.currentState is ToolbarService) { toolbarServiceKey.currentState is FlowyToolbarService) {
return toolbarServiceKey.currentState! as ToolbarService; 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; return null;
} }

View File

@ -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'; 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. /// Show the toolbar widget beside the offset.
void showInOffset(Offset offset, LayerLink layerLink); void showInOffset(Offset offset, LayerLink layerLink);
@ -24,7 +25,7 @@ class FlowyToolbar extends StatefulWidget {
State<FlowyToolbar> createState() => _FlowyToolbarState(); State<FlowyToolbar> createState() => _FlowyToolbarState();
} }
class _FlowyToolbarState extends State<FlowyToolbar> with ToolbarService { class _FlowyToolbarState extends State<FlowyToolbar> with FlowyToolbarService {
OverlayEntry? _toolbarOverlay; OverlayEntry? _toolbarOverlay;
@override @override