[feature] : quill add magnifier

pull/2026/head
xuyang 9 months ago
parent 53731f4d92
commit 101dbebdc7
  1. 4
      lib/src/models/config/raw_editor/raw_editor_configurations.dart
  2. 4
      lib/src/widgets/editor/editor.dart
  3. 152
      lib/src/widgets/others/text_selection.dart
  4. 8
      lib/src/widgets/raw_editor/raw_editor.dart
  5. 47
      lib/src/widgets/raw_editor/raw_editor_state.dart
  6. 2
      lib/src/widgets/toolbar/buttons/link_style2_button.dart

@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show Brightness, Uint8List; import 'package:flutter/foundation.dart' show Brightness, Uint8List;
import 'package:flutter/material.dart' import 'package:flutter/material.dart'
show show
@ -86,6 +87,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.
@ -334,6 +336,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,

@ -292,6 +292,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,
), ),
), ),
), ),
@ -418,6 +419,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
SelectionChangedCause.longPress, SelectionChangedCause.longPress,
); );
} }
editor?.updateMagnifier(details.globalPosition);
} }
bool _isPositionSelected(TapUpDetails details) { bool _isPositionSelected(TapUpDetails details) {
@ -557,6 +559,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
Feedback.forLongPress(_state.context); Feedback.forLongPress(_state.context);
} }
} }
editor?.showMagnifier(details.globalPosition);
} }
@override @override
@ -575,6 +578,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
} }
} }
} }
editor?.hideMagnifier();
super.onSingleLongTapEnd(details); super.onSingleLongTapEnd(details);
} }
} }

@ -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(

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

@ -884,7 +884,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
@ -1245,11 +1251,13 @@ 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);
assert(!hasConnection); assert(!hasConnection);
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
print('selection overlay disposed');
_selectionOverlay = null; _selectionOverlay = null;
controller.removeListener(_didChangeTextEditingValueListener); controller.removeListener(_didChangeTextEditingValueListener);
widget.configurations.focusNode.removeListener(_handleFocusChanged); widget.configurations.focusNode.removeListener(_handleFocusChanged);
@ -1340,14 +1348,18 @@ 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();
_selectionOverlay!.handlesVisible = _shouldShowSelectionHandles();
_selectionOverlay!.showHandles();
}
}
EditorTextSelectionOverlay _createSelectionOverlay() {
return EditorTextSelectionOverlay(
value: textEditingValue, value: textEditingValue,
context: context, context: context,
debugRequiredFor: widget, debugRequiredFor: widget,
@ -1361,10 +1373,9 @@ class QuillRawEditorState extends EditorState
? null ? null
: (context) => : (context) =>
widget.configurations.contextMenuBuilder!(context, this), widget.configurations.contextMenuBuilder!(context, this),
magnifierConfiguration: widget.configurations.magnifierConfiguration ??
TextMagnifier.adaptiveMagnifierConfiguration,
); );
_selectionOverlay!.handlesVisible = _shouldShowSelectionHandles();
_selectionOverlay!.showHandles();
}
} }
void _handleFocusChanged() { void _handleFocusChanged() {
@ -1734,4 +1745,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);
}
} }

@ -288,7 +288,7 @@ class _LinkStyleDialogState extends State<LinkStyleDialog> {
? Theme.of(context) ? Theme.of(context)
.elevatedButtonTheme .elevatedButtonTheme
.style .style
?.copyWith(fixedSize: WidgetStatePropertyAll(widget.buttonSize)) ?.copyWith(fixedSize: MaterialStatePropertyAll(widget.buttonSize))
: widget.dialogTheme?.buttonStyle; : widget.dialogTheme?.buttonStyle;
final isWrappable = widget.dialogTheme?.isWrappable ?? false; final isWrappable = widget.dialogTheme?.isWrappable ?? false;

Loading…
Cancel
Save