[feature] : quill add magnifier (#2026)

* [feature] : quill add magnifier

* [feature] : fix ci issue

* [feature] : fix ci issue

* [feature] : desktop not show magnifier

* [feature] : empty line case exception

---------

Co-authored-by: xuyang <xuyang@qimao.com>
pull/2032/head v9.6.0
License name 9 months ago committed by GitHub
parent 0dbe4c4685
commit af691e69ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      lib/src/models/config/raw_editor/raw_editor_configurations.dart
  2. 26
      lib/src/widgets/editor/editor.dart
  3. 152
      lib/src/widgets/others/text_selection.dart
  4. 4
      lib/src/widgets/quill/text_line.dart
  5. 8
      lib/src/widgets/raw_editor/raw_editor.dart
  6. 68
      lib/src/widgets/raw_editor/raw_editor_state.dart

@ -5,7 +5,8 @@ import 'package:flutter/material.dart'
AdaptiveTextSelectionToolbar, AdaptiveTextSelectionToolbar,
PointerDownEvent, PointerDownEvent,
TextCapitalization, TextCapitalization,
TextInputAction; TextInputAction,
TextMagnifierConfiguration;
import 'package:flutter/widgets.dart' import 'package:flutter/widgets.dart'
show show
Action, Action,
@ -87,6 +88,7 @@ class QuillRawEditorConfigurations extends Equatable {
this.onScribbleActivated, this.onScribbleActivated,
this.scribbleAreaInsets, this.scribbleAreaInsets,
this.readOnlyMouseCursor = SystemMouseCursors.text, this.readOnlyMouseCursor = SystemMouseCursors.text,
this.magnifierConfiguration,
}); });
/// Controls the document being edited. /// Controls the document being edited.
@ -337,6 +339,8 @@ class QuillRawEditorConfigurations extends Equatable {
/// Optional insets for the scribble area. /// Optional insets for the scribble area.
final EdgeInsets? scribbleAreaInsets; final EdgeInsets? scribbleAreaInsets;
final TextMagnifierConfiguration? magnifierConfiguration;
@override @override
List<Object?> get props => [ List<Object?> get props => [
readOnly, readOnly,

@ -2,7 +2,8 @@ import 'dart:math' as math;
import 'package:flutter/cupertino.dart' import 'package:flutter/cupertino.dart'
show CupertinoTheme, cupertinoTextSelectionControls; show CupertinoTheme, cupertinoTextSelectionControls;
import 'package:flutter/foundation.dart' show ValueListenable; import 'package:flutter/foundation.dart'
show ValueListenable, defaultTargetPlatform;
import 'package:flutter/gestures.dart' show PointerDeviceKind; import 'package:flutter/gestures.dart' show PointerDeviceKind;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
@ -297,6 +298,7 @@ class QuillEditorState extends State<QuillEditor>
onScribbleActivated: configurations.onScribbleActivated, onScribbleActivated: configurations.onScribbleActivated,
scribbleAreaInsets: configurations.scribbleAreaInsets, scribbleAreaInsets: configurations.scribbleAreaInsets,
readOnlyMouseCursor: configurations.readOnlyMouseCursor, readOnlyMouseCursor: configurations.readOnlyMouseCursor,
magnifierConfiguration: configurations.magnifierConfiguration,
), ),
), ),
), ),
@ -423,6 +425,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
SelectionChangedCause.longPress, SelectionChangedCause.longPress,
); );
} }
editor?.updateMagnifier(details.globalPosition);
} }
bool _isPositionSelected(TapUpDetails details) { bool _isPositionSelected(TapUpDetails details) {
@ -562,6 +565,8 @@ class _QuillEditorSelectionGestureDetectorBuilder
Feedback.forLongPress(_state.context); Feedback.forLongPress(_state.context);
} }
} }
_showMagnifierIfSupportedByPlatform(details.globalPosition);
} }
@override @override
@ -580,8 +585,27 @@ class _QuillEditorSelectionGestureDetectorBuilder
} }
} }
} }
_hideMagnifierIfSupportedByPlatform();
super.onSingleLongTapEnd(details); super.onSingleLongTapEnd(details);
} }
void _showMagnifierIfSupportedByPlatform(Offset positionToShow) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editor?.showMagnifier(positionToShow);
default:
}
}
void _hideMagnifierIfSupportedByPlatform() {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editor?.hideMagnifier();
default:
}
}
} }
/// Signature for the callback that reports when the user changes the selection /// Signature for the callback that reports when the user changes the selection

@ -75,6 +75,7 @@ class EditorTextSelectionOverlay {
this.onSelectionHandleTapped, this.onSelectionHandleTapped,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.handlesVisible = false, this.handlesVisible = false,
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
}) { }) {
// Clipboard status is only checked on first instance of // Clipboard status is only checked on first instance of
// ClipboardStatusNotifier // ClipboardStatusNotifier
@ -183,6 +184,13 @@ class EditorTextSelectionOverlay {
TextSelection get _selection => value.selection; TextSelection get _selection => value.selection;
final MagnifierController _magnifierController = MagnifierController();
final TextMagnifierConfiguration magnifierConfiguration;
final ValueNotifier<MagnifierInfo> _magnifierInfo =
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
void setHandlesVisible(bool visible) { void setHandlesVisible(bool visible) {
if (handlesVisible == visible) { if (handlesVisible == visible) {
return; return;
@ -237,7 +245,7 @@ class EditorTextSelectionOverlay {
BuildContext context, _TextSelectionHandlePosition position) { BuildContext context, _TextSelectionHandlePosition position) {
if (_selection.isCollapsed && if (_selection.isCollapsed &&
position == _TextSelectionHandlePosition.end) { position == _TextSelectionHandlePosition.end) {
return Container(); return const SizedBox.shrink();
} }
return Visibility( return Visibility(
visible: handlesVisible, visible: handlesVisible,
@ -252,6 +260,9 @@ class EditorTextSelectionOverlay {
selection: _selection, selection: _selection,
selectionControls: selectionCtrls, selectionControls: selectionCtrls,
position: position, position: position,
onHandleDragStart: _onHandleDragStart,
onHandleDragUpdate: _onHandleDragUpdate,
onHandleDragEnd: _onHandleDragEnd,
dragStartBehavior: dragStartBehavior, dragStartBehavior: dragStartBehavior,
)); ));
} }
@ -341,11 +352,12 @@ class EditorTextSelectionOverlay {
/// Final cleanup. /// Final cleanup.
void dispose() { void dispose() {
hide(); hide();
_magnifierInfo.dispose();
} }
/// Builds the handles by inserting them into the [context]'s overlay. /// Builds the handles by inserting them into the [context]'s overlay.
void showHandles() { void showHandles() {
assert(_handles == null); if (_handles != null) return;
_handles = <OverlayEntry>[ _handles = <OverlayEntry>[
OverlayEntry( OverlayEntry(
builder: (context) => builder: (context) =>
@ -366,8 +378,123 @@ class EditorTextSelectionOverlay {
void updateForScroll() { void updateForScroll() {
markNeedsBuild(); markNeedsBuild();
} }
void _onHandleDragStart(DragStartDetails details, TextPosition position) {
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.android) return;
showMagnifier(position, details.globalPosition, renderObject);
}
void _onHandleDragUpdate(DragUpdateDetails details, TextPosition position) {
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.android) return;
updateMagnifier(position, details.globalPosition, renderObject);
}
void _onHandleDragEnd(DragEndDetails details) {
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.android) return;
hideMagnifier();
}
void showMagnifier(
TextPosition position, Offset offset, RenderEditor editor) {
_showMagnifier(
_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: offset,
renderEditable: editor,
),
);
}
void _showMagnifier(MagnifierInfo initialMagnifierInfo) {
// toolbar
if (toolbar != null) {
hideToolbar();
}
// magnifierInfo
_magnifierInfo.value = initialMagnifierInfo;
final builtMagnifier = magnifierConfiguration.magnifierBuilder(
context,
_magnifierController,
_magnifierInfo,
);
if (builtMagnifier == null) return;
_magnifierController.show(
context: context,
below: magnifierConfiguration.shouldDisplayHandlesInMagnifier
? null
: _handles![0],
builder: (_) => builtMagnifier,
);
}
void updateMagnifier(
TextPosition position, Offset offset, RenderEditor editor) {
_updateMagnifier(
_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: offset,
renderEditable: editor,
),
);
}
void _updateMagnifier(MagnifierInfo magnifierInfo) {
if (_magnifierController.overlayEntry == null) {
return;
}
_magnifierInfo.value = magnifierInfo;
}
void hideMagnifier() {
if (_magnifierController.overlayEntry == null) {
return;
}
_magnifierController.hide();
}
// build magnifier info
MagnifierInfo _buildMagnifier(
{required RenderEditor renderEditable,
required Offset globalGesturePosition,
required TextPosition currentTextPosition}) {
final globalRenderEditableTopLeft =
renderEditable.localToGlobal(Offset.zero);
final localCaretRect =
renderEditable.getLocalRectForCaret(currentTextPosition);
final lineAtOffset = renderEditable.getLineAtOffset(currentTextPosition);
final positionAtEndOfLine = TextPosition(
offset: lineAtOffset.extentOffset,
affinity: TextAffinity.upstream,
);
// Default affinity is downstream.
final positionAtBeginningOfLine = TextPosition(
offset: lineAtOffset.baseOffset,
);
final lineBoundaries = Rect.fromPoints(
renderEditable.getLocalRectForCaret(positionAtBeginningOfLine).topCenter,
renderEditable.getLocalRectForCaret(positionAtEndOfLine).bottomCenter,
);
return MagnifierInfo(
fieldBounds: globalRenderEditableTopLeft & renderEditable.size,
globalGesturePosition: globalGesturePosition,
caretRect: localCaretRect.shift(globalRenderEditableTopLeft),
currentLineBoundaries: lineBoundaries.shift(globalRenderEditableTopLeft),
);
}
} }
typedef DargHandleCallback<T> = void Function(T details, TextPosition position);
/// This widget represents a single draggable text selection handle. /// This widget represents a single draggable text selection handle.
class _TextSelectionHandleOverlay extends StatefulWidget { class _TextSelectionHandleOverlay extends StatefulWidget {
const _TextSelectionHandleOverlay({ const _TextSelectionHandleOverlay({
@ -379,6 +506,9 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
required this.onSelectionHandleChanged, required this.onSelectionHandleChanged,
required this.onSelectionHandleTapped, required this.onSelectionHandleTapped,
required this.selectionControls, required this.selectionControls,
required this.onHandleDragStart,
required this.onHandleDragUpdate,
required this.onHandleDragEnd,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
}); });
@ -388,6 +518,9 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
final LayerLink endHandleLayerLink; final LayerLink endHandleLayerLink;
final RenderEditor renderObject; final RenderEditor renderObject;
final ValueChanged<TextSelection?> onSelectionHandleChanged; final ValueChanged<TextSelection?> onSelectionHandleChanged;
final DargHandleCallback<DragStartDetails>? onHandleDragStart;
final DargHandleCallback<DragUpdateDetails>? onHandleDragUpdate;
final ValueChanged<DragEndDetails> onHandleDragEnd;
final VoidCallback? onSelectionHandleTapped; final VoidCallback? onSelectionHandleTapped;
final TextSelectionControls selectionControls; final TextSelectionControls selectionControls;
final DragStartBehavior dragStartBehavior; final DragStartBehavior dragStartBehavior;
@ -453,15 +586,18 @@ class _TextSelectionHandleOverlayState
} }
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
if (!widget.renderObject.attached) return;
final textPosition = widget.position == _TextSelectionHandlePosition.start final textPosition = widget.position == _TextSelectionHandlePosition.start
? widget.selection.base ? widget.selection.base
: widget.selection.extent; : widget.selection.extent;
final lineHeight = widget.renderObject.preferredLineHeight(textPosition); final lineHeight = widget.renderObject.preferredLineHeight(textPosition);
final handleSize = widget.selectionControls.getHandleSize(lineHeight); final handleSize = widget.selectionControls.getHandleSize(lineHeight);
_dragPosition = details.globalPosition + Offset(0, -handleSize.height); _dragPosition = details.globalPosition + Offset(0, -handleSize.height);
widget.onHandleDragStart?.call(details, textPosition);
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
if (!widget.renderObject.attached) return;
_dragPosition += details.delta; _dragPosition += details.delta;
final position = final position =
widget.renderObject.getPositionForOffset(details.globalPosition); widget.renderObject.getPositionForOffset(details.globalPosition);
@ -497,8 +633,17 @@ class _TextSelectionHandleOverlayState
if (newSelection.baseOffset >= newSelection.extentOffset) { if (newSelection.baseOffset >= newSelection.extentOffset) {
return; // don't allow order swapping. return; // don't allow order swapping.
} }
widget.onSelectionHandleChanged(newSelection); widget.onSelectionHandleChanged(newSelection);
if (widget.position == _TextSelectionHandlePosition.start) {
widget.onHandleDragUpdate?.call(details, newSelection.base);
} else if (widget.position == _TextSelectionHandlePosition.end) {
widget.onHandleDragUpdate?.call(details, newSelection.extent);
}
}
void _handleDragEnd(DragEndDetails details) {
if (!widget.renderObject.attached) return;
widget.onHandleDragEnd.call(details);
} }
void _handleTap() { void _handleTap() {
@ -579,6 +724,7 @@ class _TextSelectionHandleOverlayState
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
onPanStart: _handleDragStart, onPanStart: _handleDragStart,
onPanUpdate: _handleDragUpdate, onPanUpdate: _handleDragUpdate,
onPanEnd: _handleDragEnd,
onTap: _handleTap, onTap: _handleTap,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(

@ -907,6 +907,10 @@ class RenderEditableTextLine extends RenderEditableBox {
_getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1)) _getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1))
.where((element) => element.top < lineDy && element.bottom > lineDy) .where((element) => element.top < lineDy && element.bottom > lineDy)
.toList(growable: false); .toList(growable: false);
if (lineBoxes.isEmpty) {
// Empty line, line box is empty
return TextRange.collapsed(position.offset);
}
return TextRange( return TextRange(
start: getPositionForOffset( start: getPositionForOffset(
Offset(lineBoxes.first.left, lineDy), Offset(lineBoxes.first.left, lineDy),

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/widgets.dart' import 'package:flutter/widgets.dart'
show show
AnimationController, AnimationController,
@ -84,4 +86,10 @@ abstract class EditorState extends State<QuillRawEditor>
bool showToolbar(); bool showToolbar();
void requestKeyboard(); void requestKeyboard();
void showMagnifier(Offset positionToShow);
void updateMagnifier(Offset positionToShow);
void hideMagnifier();
} }

@ -918,7 +918,13 @@ class QuillRawEditorState extends EditorState
final oldSelection = controller.selection; final oldSelection = controller.selection;
controller.updateSelection(selection, ChangeSource.local); controller.updateSelection(selection, ChangeSource.local);
if (_selectionOverlay == null) {
_selectionOverlay = _createSelectionOverlay();
} else {
_selectionOverlay!.update(textEditingValue);
}
_selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles();
_selectionOverlay?.showHandles();
if (!_keyboardVisible) { if (!_keyboardVisible) {
// This will show the keyboard for all selection changes on the // This will show the keyboard for all selection changes on the
@ -1279,6 +1285,7 @@ class QuillRawEditorState extends EditorState
@override @override
void dispose() { void dispose() {
hideMagnifier();
closeConnectionIfNeeded(); closeConnectionIfNeeded();
_keyboardVisibilitySubscription?.cancel(); _keyboardVisibilitySubscription?.cancel();
HardwareKeyboard.instance.removeHandler(_hardwareKeyboardEvent); HardwareKeyboard.instance.removeHandler(_hardwareKeyboardEvent);
@ -1374,33 +1381,36 @@ class QuillRawEditorState extends EditorState
void _updateOrDisposeSelectionOverlayIfNeeded() { void _updateOrDisposeSelectionOverlayIfNeeded() {
if (_selectionOverlay != null) { if (_selectionOverlay != null) {
if (!_hasFocus || textEditingValue.selection.isCollapsed) { if (_hasFocus) {
_selectionOverlay!.dispose();
_selectionOverlay = null;
} else {
_selectionOverlay!.update(textEditingValue); _selectionOverlay!.update(textEditingValue);
} }
} else if (_hasFocus) { } else if (_hasFocus) {
_selectionOverlay = EditorTextSelectionOverlay( _selectionOverlay = _createSelectionOverlay();
value: textEditingValue,
context: context,
debugRequiredFor: widget,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
renderObject: renderEditor,
selectionCtrls: widget.configurations.selectionCtrls,
selectionDelegate: this,
clipboardStatus: _clipboardStatus,
contextMenuBuilder: widget.configurations.contextMenuBuilder == null
? null
: (context) =>
widget.configurations.contextMenuBuilder!(context, this),
);
_selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles();
_selectionOverlay!.showHandles(); _selectionOverlay!.showHandles();
} }
} }
EditorTextSelectionOverlay _createSelectionOverlay() {
return EditorTextSelectionOverlay(
value: textEditingValue,
context: context,
debugRequiredFor: widget,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
renderObject: renderEditor,
selectionCtrls: widget.configurations.selectionCtrls,
selectionDelegate: this,
clipboardStatus: _clipboardStatus,
contextMenuBuilder: widget.configurations.contextMenuBuilder == null
? null
: (context) =>
widget.configurations.contextMenuBuilder!(context, this),
magnifierConfiguration: widget.configurations.magnifierConfiguration ??
TextMagnifier.adaptiveMagnifierConfiguration,
);
}
void _handleFocusChanged() { void _handleFocusChanged() {
if (dirty) { if (dirty) {
requestKeyboard(); requestKeyboard();
@ -1777,4 +1787,24 @@ class QuillRawEditorState extends EditorState
@override @override
bool get shareEnabled => false; bool get shareEnabled => false;
@override
void hideMagnifier() {
if (_selectionOverlay == null) return;
_selectionOverlay?.hideMagnifier();
}
@override
void showMagnifier(ui.Offset positionToShow) {
if (_selectionOverlay == null) return;
final position = renderEditor.getPositionForOffset(positionToShow);
_selectionOverlay?.showMagnifier(position, positionToShow, renderEditor);
}
@override
void updateMagnifier(ui.Offset positionToShow) {
_updateOrDisposeSelectionOverlayIfNeeded();
final position = renderEditor.getPositionForOffset(positionToShow);
_selectionOverlay?.updateMagnifier(position, positionToShow, renderEditor);
}
} }

Loading…
Cancel
Save