feat: double tap on text

This commit is contained in:
Vincent Chan 2022-07-28 18:06:54 +08:00
parent 583d838344
commit 2a09f69bec
3 changed files with 125 additions and 22 deletions

View File

@ -108,6 +108,16 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
return Position(path: _textNode.path, offset: baseOffset);
}
@override
Selection? getWorldBoundaryInOffset(Offset offset) {
final localOffset = _renderParagraph.globalToLocal(offset);
final textPosition = _renderParagraph.getPositionForOffset(localOffset);
final textRange = _renderParagraph.getWordBoundary(textPosition);
final start = Position(path: _textNode.path, offset: textRange.start);
final end = Position(path: _textNode.path, offset: textRange.end);
return Selection(start: start, end: end);
}
@override
List<Rect> getRectsInSelection(Selection selection) {
assert(pathEquals(selection.start.path, selection.end.path) &&

View File

@ -21,6 +21,10 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
///
/// The return result must be an offset of the local coordinate system.
Position getPositionInOffset(Offset start);
Selection? getWorldBoundaryInOffset(Offset start) {
return null;
}
Rect getCursorRectInPosition(Position position);
Offset localToGlobal(Offset offset);

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/document/selection.dart';
@ -6,10 +8,10 @@ import 'package:flowy_editor/render/selection/cursor_widget.dart';
import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
import 'package:flowy_editor/extensions/object_extensions.dart';
import 'package:flowy_editor/extensions/node_extensions.dart';
import 'package:flutter/gestures.dart';
import 'package:flowy_editor/service/shortcut_service.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// Process selection and cursor
@ -99,6 +101,92 @@ 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');
@ -152,27 +240,12 @@ class _FlowySelectionState extends State<FlowySelection>
@override
Widget build(BuildContext context) {
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: {
PanGestureRecognizer:
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(),
(recognizer) {
recognizer
..onStart = _onPanStart
..onUpdate = _onPanUpdate
..onEnd = _onPanEnd;
},
),
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(),
(recognizer) {
recognizer.onTapDown = _onTapDown;
},
)
},
return _SelectionGestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
onTapDown: _onTapDown,
onDoubleTapDown: _onDoubleTapDown,
child: widget.child,
);
}
@ -278,6 +351,22 @@ class _FlowySelectionState extends State<FlowySelection>
return false;
}
void _onDoubleTapDown(TapDownDetails details) {
final offset = details.globalPosition;
final nodes = getNodesInRange(offset);
if (nodes.isEmpty) {
editorState.updateCursorSelection(null);
return;
}
final selectable = nodes.first.selectable;
if (selectable == null) {
editorState.updateCursorSelection(null);
return;
}
editorState
.updateCursorSelection(selectable.getWorldBoundaryInOffset(offset));
}
void _onTapDown(TapDownDetails details) {
// clear old state.
panStartOffset = null;