diff --git a/example/lib/pages/mobile_editor.dart b/example/lib/pages/mobile_editor.dart index 935e8dbda..fb0869d73 100644 --- a/example/lib/pages/mobile_editor.dart +++ b/example/lib/pages/mobile_editor.dart @@ -86,6 +86,7 @@ class _MobileEditorState extends State { editorState: editorState, editorScrollController: editorScrollController, blockComponentBuilders: blockComponentBuilders, + showMagnifier: true, // showcase 3: customize the header and footer. header: Padding( padding: const EdgeInsets.only(bottom: 10.0), diff --git a/lib/src/editor/editor_component/entry/page_block_component.dart b/lib/src/editor/editor_component/entry/page_block_component.dart index 627fcc1ba..7ac9d6f46 100644 --- a/lib/src/editor/editor_component/entry/page_block_component.dart +++ b/lib/src/editor/editor_component/entry/page_block_component.dart @@ -54,25 +54,27 @@ class PageBlockComponent extends BlockComponentStatelessWidget { if (scrollController == null || scrollController.shrinkWrap || !editorState.editable) { - return Builder( - builder: (context) { - final scroller = Scrollable.maybeOf(context); - if (scroller != null) { - editorState.updateAutoScroller(scroller); - } - return Column( - children: [ - if (header != null) header!, - ...items.map( - (e) => Padding( - padding: editorState.editorStyle.padding, - child: editorState.renderer.build(context, e), + return SingleChildScrollView( + child: Builder( + builder: (context) { + final scroller = Scrollable.maybeOf(context); + if (scroller != null) { + editorState.updateAutoScroller(scroller); + } + return Column( + children: [ + if (header != null) header!, + ...items.map( + (e) => Padding( + padding: editorState.editorStyle.padding, + child: editorState.renderer.build(context, e), + ), ), - ), - if (footer != null) footer!, - ], - ); - }, + if (footer != null) footer!, + ], + ); + }, + ), ); } else { int extentCount = 0; diff --git a/lib/src/editor/editor_component/service/editor.dart b/lib/src/editor/editor_component/service/editor.dart index e5a37c1af..1735052f5 100644 --- a/lib/src/editor/editor_component/service/editor.dart +++ b/lib/src/editor/editor_component/service/editor.dart @@ -27,6 +27,7 @@ class AppFlowyEditor extends StatefulWidget { this.autoFocus = false, this.focusedSelection, this.shrinkWrap = false, + this.showMagnifier = true, this.editorScrollController, this.editorStyle = const EditorStyle.desktop(), this.header, @@ -151,6 +152,11 @@ class AppFlowyEditor extends StatefulWidget { /// Notes: Must provide a scrollController when shrinkWrap is true. final bool shrinkWrap; + /// Show the magnifier or not. + /// + /// only works on iOS or Android. + final bool showMagnifier; + @override State createState() => _AppFlowyEditorState(); } @@ -247,6 +253,7 @@ class _AppFlowyEditorState extends State { key: editorState.service.selectionServiceKey, cursorColor: widget.editorStyle.cursorColor, selectionColor: widget.editorStyle.selectionColor, + showMagnifier: widget.showMagnifier, contextMenuItems: widget.contextMenuItems, child: KeyboardServiceWidget( key: editorState.service.keyboardServiceKey, diff --git a/lib/src/editor/editor_component/service/selection/mobile_magnifiter.dart b/lib/src/editor/editor_component/service/selection/mobile_magnifiter.dart new file mode 100644 index 000000000..5d79eb08f --- /dev/null +++ b/lib/src/editor/editor_component/service/selection/mobile_magnifiter.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class MobileMagnifier extends StatelessWidget { + const MobileMagnifier({ + super.key, + required this.size, + required this.offset, + }); + + final Size size; + final Offset offset; + + @override + Widget build(BuildContext context) { + return Positioned.fromRect( + rect: Rect.fromCenter( + center: offset.translate(0, -size.height), + width: size.width, + height: size.height, + ), + child: IgnorePointer( + child: Magnifier( + size: size, + ), + ), + ); + } +} diff --git a/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart b/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart index aa1285320..dc76c2335 100644 --- a/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart +++ b/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_magnifiter.dart'; import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/render/selection/mobile_selection_widget.dart'; import 'package:appflowy_editor/src/service/selection/mobile_selection_gesture.dart'; @@ -23,6 +24,7 @@ class MobileSelectionServiceWidget extends StatefulWidget { super.key, this.cursorColor = const Color(0xFF00BCF0), this.selectionColor = const Color.fromARGB(53, 111, 201, 231), + this.showMagnifier = true, required this.child, }); @@ -30,6 +32,11 @@ class MobileSelectionServiceWidget extends StatefulWidget { final Color cursorColor; final Color selectionColor; + /// Show the magnifier or not. + /// + /// only works on iOS or Android. + final bool showMagnifier; + @override State createState() => _MobileSelectionServiceWidgetState(); @@ -51,6 +58,7 @@ class _MobileSelectionServiceWidgetState List currentSelectedNodes = []; final List _interceptors = []; + final ValueNotifier _lastPanOffset = ValueNotifier(null); /// Pan Offset? _panStartOffset; @@ -90,7 +98,29 @@ class _MobileSelectionServiceWidgetState onTapUp: _onTapUp, onDoubleTapUp: _onDoubleTapUp, onTripleTapUp: _onTripleTapUp, - child: widget.child, + child: Stack( + children: [ + widget.child, + if (widget.showMagnifier) _buildMagnifier(), + ], + ), + ); + } + + Widget _buildMagnifier() { + return ValueListenableBuilder( + valueListenable: _lastPanOffset, + builder: (_, offset, __) { + if (offset == null) { + return const SizedBox.shrink(); + } + final renderBox = context.findRenderObject() as RenderBox; + final local = renderBox.globalToLocal(offset); + return MobileMagnifier( + size: const Size(72, 48), + offset: local, + ); + }, ); } @@ -337,11 +367,14 @@ class _MobileSelectionServiceWidgetState Selection.collapsed(end), ); } + + _lastPanOffset.value = panEndOffset; } } void _onPanEnd(DragEndDetails details) { // do nothing + _lastPanOffset.value = null; } void _updateSelectionAreas(Selection selection) { diff --git a/lib/src/editor/editor_component/service/selection_service_widget.dart b/lib/src/editor/editor_component/service/selection_service_widget.dart index c312b2517..31b404020 100644 --- a/lib/src/editor/editor_component/service/selection_service_widget.dart +++ b/lib/src/editor/editor_component/service/selection_service_widget.dart @@ -8,6 +8,7 @@ class SelectionServiceWidget extends StatefulWidget { super.key, this.cursorColor = const Color(0xFF00BCF0), this.selectionColor = const Color.fromARGB(53, 111, 201, 231), + this.showMagnifier = true, required this.contextMenuItems, required this.child, }); @@ -17,6 +18,11 @@ class SelectionServiceWidget extends StatefulWidget { final Color selectionColor; final List> contextMenuItems; + /// Show the magnifier or not. + /// + /// only works on iOS or Android. + final bool showMagnifier; + @override State createState() => _SelectionServiceWidgetState(); } @@ -46,6 +52,7 @@ class _SelectionServiceWidgetState extends State key: forwardKey, cursorColor: widget.cursorColor, selectionColor: widget.selectionColor, + showMagnifier: widget.showMagnifier, child: widget.child, ); } diff --git a/lib/src/editor/selection_menu/selection_menu_service.dart b/lib/src/editor/selection_menu/selection_menu_service.dart index 17afc6868..a486915d9 100644 --- a/lib/src/editor/selection_menu/selection_menu_service.dart +++ b/lib/src/editor/selection_menu/selection_menu_service.dart @@ -192,8 +192,8 @@ class SelectionMenu extends SelectionMenuService { void calculateSelectionMenuOffset(Rect rect) { // Workaround: We can customize the padding through the [EditorStyle], - // but the coordinates of overlay are not properly converted currently. - // Just subtract the padding here as a result. + // but the coordinates of overlay are not properly converted currently. + // Just subtract the padding here as a result. const menuHeight = 200.0; const menuOffset = Offset(0, 10); final editorOffset = @@ -223,13 +223,13 @@ class SelectionMenu extends SelectionMenuService { } // show on left - if (_offset.dx > editorWidth / 2) { + if (_offset.dx - editorOffset.dx > editorWidth / 2) { _alignment = _alignment == Alignment.topLeft ? Alignment.topRight : Alignment.bottomRight; _offset = Offset( - editorWidth - _offset.dx, + editorWidth - _offset.dx + editorOffset.dx, _offset.dy, ); } diff --git a/lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart b/lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart index 53726a97d..53e941b68 100644 --- a/lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart +++ b/lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart @@ -105,6 +105,7 @@ class _MobileFloatingToolbarState extends State prevSelection = selection; } else { // uses debounce to avoid the computing the rects too frequently. + _clear(); _showAfterDelay(const Duration(milliseconds: 400)); } }