feat: support customizing the mobile magnifiter (#625)
* feat: support customizing the mobile magnifiter * chore: remove unused test * feat: expaned the padding between cursor and toolbar * feat: support long pressing to update selection * chore: revert code sign
This commit is contained in:
parent
e6b1336041
commit
82f3baf3f1
14 changed files with 209 additions and 111 deletions
|
|
@ -358,7 +358,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = KD48NC6JW7;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|
@ -487,7 +487,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = KD48NC6JW7;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|
@ -510,7 +510,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = KD48NC6JW7;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,8 @@ class _MobileEditorState extends State<MobileEditor> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
|
magnifierSize: const Size(144, 96),
|
||||||
|
mobileDragHandleBallSize: const Size(12, 12),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/editor/block_component/base_component/selection/selection_area_painter.dart';
|
import 'package:appflowy_editor/src/editor/block_component/base_component/selection/selection_area_painter.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart';
|
||||||
import 'package:appflowy_editor/src/render/selection/cursor.dart';
|
import 'package:appflowy_editor/src/render/selection/cursor.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
@ -123,10 +124,15 @@ class _BlockSelectionAreaState extends State<BlockSelectionArea> {
|
||||||
prevCursorRect == null) {
|
prevCursorRect == null) {
|
||||||
return sizedBox;
|
return sizedBox;
|
||||||
}
|
}
|
||||||
|
final editorState = context.read<EditorState>();
|
||||||
|
final dragMode =
|
||||||
|
editorState.selectionExtraInfo?[selectionDragModeKey];
|
||||||
|
final shouldBlink = widget.delegate.shouldCursorBlink &&
|
||||||
|
dragMode != MobileSelectionDragMode.cursor;
|
||||||
final cursor = Cursor(
|
final cursor = Cursor(
|
||||||
key: cursorKey,
|
key: cursorKey,
|
||||||
rect: prevCursorRect!,
|
rect: prevCursorRect!,
|
||||||
shouldBlink: widget.delegate.shouldCursorBlink,
|
shouldBlink: shouldBlink,
|
||||||
cursorStyle: widget.delegate.cursorStyle,
|
cursorStyle: widget.delegate.cursorStyle,
|
||||||
color: widget.cursorColor,
|
color: widget.cursorColor,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/desktop_scroll_service.dart';
|
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/desktop_scroll_service.dart';
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/mobile_scroll_service.dart';
|
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/mobile_scroll_service.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/toolbar/mobile/utils/keyboard_height_observer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
@ -100,10 +101,14 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
|
||||||
if (PlatformExtension.isMobile) {
|
if (PlatformExtension.isMobile) {
|
||||||
// soft keyboard
|
// soft keyboard
|
||||||
// workaround: wait for the soft keyboard to show up
|
// workaround: wait for the soft keyboard to show up
|
||||||
return Future.delayed(const Duration(milliseconds: 300), () {
|
return Future.delayed(
|
||||||
|
Duration(
|
||||||
|
milliseconds:
|
||||||
|
KeyboardHeightObserver.currentKeyboardHeight == 0 ? 250 : 0,
|
||||||
|
), () {
|
||||||
startAutoScroll(
|
startAutoScroll(
|
||||||
endTouchPoint,
|
endTouchPoint,
|
||||||
edgeOffset: 100,
|
edgeOffset: 150,
|
||||||
duration: Duration.zero,
|
duration: Duration.zero,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,55 @@ class MobileMagnifier extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// the magnifier will blink if the center is the same as the offset.
|
||||||
|
final magicOffset = Offset(0, size.height - 22);
|
||||||
return Positioned.fromRect(
|
return Positioned.fromRect(
|
||||||
rect: Rect.fromCenter(
|
rect: Rect.fromCenter(
|
||||||
center: offset.translate(0, -size.height),
|
center: offset - magicOffset,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
),
|
),
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Magnifier(
|
child: _CustomMagnifier(
|
||||||
size: size,
|
size: size,
|
||||||
|
additionalFocalPointOffset: magicOffset,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CustomMagnifier extends StatelessWidget {
|
||||||
|
const _CustomMagnifier({
|
||||||
|
this.additionalFocalPointOffset = Offset.zero,
|
||||||
|
required this.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Size size;
|
||||||
|
final Offset additionalFocalPointOffset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RawMagnifier(
|
||||||
|
decoration: const MagnifierDecoration(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(40)),
|
||||||
|
),
|
||||||
|
shadows: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 1.5,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
spreadRadius: 0.75,
|
||||||
|
color: Color.fromARGB(25, 0, 0, 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
magnificationScale: 1.25,
|
||||||
|
focalPointOffset: additionalFocalPointOffset,
|
||||||
|
size: size,
|
||||||
|
child: const ColoredBox(
|
||||||
|
color: Color.fromARGB(8, 158, 158, 158),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,16 @@ enum MobileSelectionHandlerType {
|
||||||
cursorHandler,
|
cursorHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the value type is MobileSelectionDragMode
|
||||||
|
const String selectionDragModeKey = 'selection_drag_mode';
|
||||||
|
|
||||||
class MobileSelectionServiceWidget extends StatefulWidget {
|
class MobileSelectionServiceWidget extends StatefulWidget {
|
||||||
const MobileSelectionServiceWidget({
|
const MobileSelectionServiceWidget({
|
||||||
super.key,
|
super.key,
|
||||||
this.cursorColor = const Color(0xFF00BCF0),
|
this.cursorColor = const Color(0xFF00BCF0),
|
||||||
this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
|
this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
|
||||||
this.showMagnifier = true,
|
this.showMagnifier = true,
|
||||||
|
this.magnifierSize = const Size(72, 48),
|
||||||
required this.child,
|
required this.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -37,6 +41,8 @@ class MobileSelectionServiceWidget extends StatefulWidget {
|
||||||
/// only works on iOS or Android.
|
/// only works on iOS or Android.
|
||||||
final bool showMagnifier;
|
final bool showMagnifier;
|
||||||
|
|
||||||
|
final Size magnifierSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MobileSelectionServiceWidget> createState() =>
|
State<MobileSelectionServiceWidget> createState() =>
|
||||||
_MobileSelectionServiceWidgetState();
|
_MobileSelectionServiceWidgetState();
|
||||||
|
|
@ -98,6 +104,9 @@ class _MobileSelectionServiceWidgetState
|
||||||
onTapUp: _onTapUp,
|
onTapUp: _onTapUp,
|
||||||
onDoubleTapUp: _onDoubleTapUp,
|
onDoubleTapUp: _onDoubleTapUp,
|
||||||
onTripleTapUp: _onTripleTapUp,
|
onTripleTapUp: _onTripleTapUp,
|
||||||
|
onLongPressStart: _onLongPressStart,
|
||||||
|
onLongPressMoveUpdate: _onLongPressMoveUpdate,
|
||||||
|
onLongPressEnd: _onLongPressEnd,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
widget.child,
|
widget.child,
|
||||||
|
|
@ -117,7 +126,7 @@ class _MobileSelectionServiceWidgetState
|
||||||
final renderBox = context.findRenderObject() as RenderBox;
|
final renderBox = context.findRenderObject() as RenderBox;
|
||||||
final local = renderBox.globalToLocal(offset);
|
final local = renderBox.globalToLocal(offset);
|
||||||
return MobileMagnifier(
|
return MobileMagnifier(
|
||||||
size: const Size(72, 48),
|
size: widget.magnifierSize,
|
||||||
offset: local,
|
offset: local,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -145,6 +154,9 @@ class _MobileSelectionServiceWidgetState
|
||||||
editorState.updateSelectionWithReason(
|
editorState.updateSelectionWithReason(
|
||||||
selection,
|
selection,
|
||||||
reason: SelectionUpdateReason.uiEvent,
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
extraInfo: {
|
||||||
|
selectionDragModeKey: dragMode,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,7 +278,10 @@ class _MobileSelectionServiceWidgetState
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
editorState.selection = Selection.collapsed(position);
|
editorState.updateSelectionWithReason(
|
||||||
|
Selection.collapsed(position),
|
||||||
|
extraInfo: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDoubleTapUp(TapUpDetails details) {
|
void _onDoubleTapUp(TapUpDetails details) {
|
||||||
|
|
@ -375,6 +390,58 @@ class _MobileSelectionServiceWidgetState
|
||||||
void _onPanEnd(DragEndDetails details) {
|
void _onPanEnd(DragEndDetails details) {
|
||||||
// do nothing
|
// do nothing
|
||||||
_lastPanOffset.value = null;
|
_lastPanOffset.value = null;
|
||||||
|
|
||||||
|
// clear the status
|
||||||
|
dragMode = MobileSelectionDragMode.none;
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
editorState.selection,
|
||||||
|
extraInfo: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressStart(LongPressStartDetails details) {
|
||||||
|
clearSelection();
|
||||||
|
|
||||||
|
// clear old state.
|
||||||
|
_panStartOffset = null;
|
||||||
|
|
||||||
|
final position = getPositionInOffset(details.globalPosition);
|
||||||
|
if (position == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastPanOffset.value = details.globalPosition;
|
||||||
|
|
||||||
|
dragMode = MobileSelectionDragMode.cursor;
|
||||||
|
updateSelection(
|
||||||
|
Selection.collapsed(position),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||||
|
final panEndOffset = details.globalPosition;
|
||||||
|
final position = getNodeInOffset(panEndOffset)
|
||||||
|
?.selectable
|
||||||
|
?.getPositionInOffset(panEndOffset);
|
||||||
|
|
||||||
|
if (position == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastPanOffset.value = panEndOffset;
|
||||||
|
updateSelection(
|
||||||
|
Selection.collapsed(position),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressEnd(LongPressEndDetails details) {
|
||||||
|
_lastPanOffset.value = null;
|
||||||
|
|
||||||
|
dragMode = MobileSelectionDragMode.none;
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
editorState.selection,
|
||||||
|
extraInfo: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateSelectionAreas(Selection selection) {
|
void _updateSelectionAreas(Selection selection) {
|
||||||
|
|
@ -444,6 +511,9 @@ class _MobileSelectionServiceWidgetState
|
||||||
showLeftHandler: showLeftHandler,
|
showLeftHandler: showLeftHandler,
|
||||||
showRightHandler: showRightHandler,
|
showRightHandler: showRightHandler,
|
||||||
handlerColor: editorState.editorStyle.cursorColor,
|
handlerColor: editorState.editorStyle.cursorColor,
|
||||||
|
handlerWidth: editorState.editorStyle.mobileDragHandleWidth,
|
||||||
|
handlerBallWidth:
|
||||||
|
editorState.editorStyle.mobileDragHandleBallSize.width,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_selectionAreas.add(overlay);
|
_selectionAreas.add(overlay);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/selection/desktop_selection_service.dart';
|
import 'package:appflowy_editor/src/editor/editor_component/service/selection/desktop_selection_service.dart';
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart';
|
import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart';
|
||||||
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SelectionServiceWidget extends StatefulWidget {
|
class SelectionServiceWidget extends StatefulWidget {
|
||||||
const SelectionServiceWidget({
|
const SelectionServiceWidget({
|
||||||
|
|
@ -48,11 +49,13 @@ class _SelectionServiceWidgetState extends State<SelectionServiceWidget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final editorState = context.read<EditorState>();
|
||||||
return MobileSelectionServiceWidget(
|
return MobileSelectionServiceWidget(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
cursorColor: widget.cursorColor,
|
cursorColor: widget.cursorColor,
|
||||||
selectionColor: widget.selectionColor,
|
selectionColor: widget.selectionColor,
|
||||||
showMagnifier: widget.showMagnifier,
|
showMagnifier: widget.showMagnifier,
|
||||||
|
magnifierSize: editorState.editorStyle.magnifierSize,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// The style of the editor.
|
/// The style of the editor.
|
||||||
|
|
@ -14,6 +13,9 @@ class EditorStyle {
|
||||||
required this.selectionColor,
|
required this.selectionColor,
|
||||||
required this.textStyleConfiguration,
|
required this.textStyleConfiguration,
|
||||||
required this.textSpanDecorator,
|
required this.textSpanDecorator,
|
||||||
|
this.magnifierSize = const Size(72, 48),
|
||||||
|
this.mobileDragHandleBallSize = const Size(8, 8),
|
||||||
|
this.mobileDragHandleWidth = 2.0,
|
||||||
this.defaultTextDirection,
|
this.defaultTextDirection,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -43,6 +45,16 @@ class EditorStyle {
|
||||||
|
|
||||||
final String? defaultTextDirection;
|
final String? defaultTextDirection;
|
||||||
|
|
||||||
|
/// The size of the magnifier.
|
||||||
|
/// Only works on mobile.
|
||||||
|
final Size magnifierSize;
|
||||||
|
|
||||||
|
/// mobile drag handler size.
|
||||||
|
/// /// Only works on mobile.
|
||||||
|
final Size mobileDragHandleBallSize;
|
||||||
|
|
||||||
|
final double mobileDragHandleWidth;
|
||||||
|
|
||||||
const EditorStyle.desktop({
|
const EditorStyle.desktop({
|
||||||
EdgeInsets? padding,
|
EdgeInsets? padding,
|
||||||
Color? cursorColor,
|
Color? cursorColor,
|
||||||
|
|
@ -59,7 +71,10 @@ class EditorStyle {
|
||||||
text: TextStyle(fontSize: 16, color: Colors.black),
|
text: TextStyle(fontSize: 16, color: Colors.black),
|
||||||
),
|
),
|
||||||
textSpanDecorator =
|
textSpanDecorator =
|
||||||
textSpanDecorator ?? defaultTextSpanDecoratorForAttribute;
|
textSpanDecorator ?? defaultTextSpanDecoratorForAttribute,
|
||||||
|
magnifierSize = Size.zero,
|
||||||
|
mobileDragHandleBallSize = Size.zero,
|
||||||
|
mobileDragHandleWidth = 0.0;
|
||||||
|
|
||||||
const EditorStyle.mobile({
|
const EditorStyle.mobile({
|
||||||
EdgeInsets? padding,
|
EdgeInsets? padding,
|
||||||
|
|
@ -68,6 +83,9 @@ class EditorStyle {
|
||||||
TextStyleConfiguration? textStyleConfiguration,
|
TextStyleConfiguration? textStyleConfiguration,
|
||||||
TextSpanDecoratorForAttribute? textSpanDecorator,
|
TextSpanDecoratorForAttribute? textSpanDecorator,
|
||||||
this.defaultTextDirection,
|
this.defaultTextDirection,
|
||||||
|
this.magnifierSize = const Size(72, 48),
|
||||||
|
this.mobileDragHandleBallSize = const Size(8, 8),
|
||||||
|
this.mobileDragHandleWidth = 2.0,
|
||||||
}) : padding = padding ?? const EdgeInsets.symmetric(horizontal: 20),
|
}) : padding = padding ?? const EdgeInsets.symmetric(horizontal: 20),
|
||||||
cursorColor = cursorColor ?? const Color(0xFF00BCF0),
|
cursorColor = cursorColor ?? const Color(0xFF00BCF0),
|
||||||
selectionColor =
|
selectionColor =
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class MobileFloatingToolbarItem {
|
class MobileFloatingToolbarItem {
|
||||||
|
|
@ -104,8 +105,15 @@ class _MobileFloatingToolbarState extends State<MobileFloatingToolbar>
|
||||||
}
|
}
|
||||||
prevSelection = selection;
|
prevSelection = selection;
|
||||||
} else {
|
} else {
|
||||||
// uses debounce to avoid the computing the rects too frequently.
|
|
||||||
_clear();
|
_clear();
|
||||||
|
final dragMode = editorState.selectionExtraInfo?[selectionDragModeKey];
|
||||||
|
if ([
|
||||||
|
MobileSelectionDragMode.leftSelectionHandler,
|
||||||
|
MobileSelectionDragMode.rightSelectionHandler,
|
||||||
|
].contains(dragMode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// uses debounce to avoid the computing the rects too frequently.
|
||||||
_showAfterDelay(const Duration(milliseconds: 400));
|
_showAfterDelay(const Duration(milliseconds: 400));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@ typedef KeyboardHeightCallback = void Function(double height);
|
||||||
class KeyboardHeightObserver {
|
class KeyboardHeightObserver {
|
||||||
KeyboardHeightObserver._() {
|
KeyboardHeightObserver._() {
|
||||||
_keyboardHeightPlugin.onKeyboardHeightChanged((height) {
|
_keyboardHeightPlugin.onKeyboardHeightChanged((height) {
|
||||||
|
currentKeyboardHeight = height;
|
||||||
notify(height);
|
notify(height);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static final KeyboardHeightObserver instance = KeyboardHeightObserver._();
|
static final KeyboardHeightObserver instance = KeyboardHeightObserver._();
|
||||||
|
static double currentKeyboardHeight = 0;
|
||||||
|
|
||||||
final List<KeyboardHeightCallback> _listeners = [];
|
final List<KeyboardHeightCallback> _listeners = [];
|
||||||
final KeyboardHeightPlugin _keyboardHeightPlugin = KeyboardHeightPlugin();
|
final KeyboardHeightPlugin _keyboardHeightPlugin = KeyboardHeightPlugin();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ class MobileSelectionWidget extends StatelessWidget {
|
||||||
this.showLeftHandler = false,
|
this.showLeftHandler = false,
|
||||||
this.showRightHandler = false,
|
this.showRightHandler = false,
|
||||||
this.handlerColor = Colors.black,
|
this.handlerColor = Colors.black,
|
||||||
|
this.handlerBallWidth = 6.0,
|
||||||
|
this.handlerWidth = 2.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
@ -19,11 +21,11 @@ class MobileSelectionWidget extends StatelessWidget {
|
||||||
final bool showLeftHandler;
|
final bool showLeftHandler;
|
||||||
final bool showRightHandler;
|
final bool showRightHandler;
|
||||||
final Color handlerColor;
|
final Color handlerColor;
|
||||||
|
final double handlerWidth;
|
||||||
|
final double handlerBallWidth;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const handlerWidth = 2.0;
|
|
||||||
const handlerBallWidth = 6.0;
|
|
||||||
// to avoid row overflow
|
// to avoid row overflow
|
||||||
const threshold = 0.25;
|
const threshold = 0.25;
|
||||||
// left and right add 2px to avoid the selection area from being too narrow
|
// left and right add 2px to avoid the selection area from being too narrow
|
||||||
|
|
@ -91,27 +93,33 @@ class MobileSelectionWithHandler extends StatelessWidget {
|
||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
);
|
);
|
||||||
if (showLeftHandler || showRightHandler) {
|
if (showLeftHandler || showRightHandler) {
|
||||||
child = Row(
|
child = Stack(
|
||||||
mainAxisSize: MainAxisSize.min,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
if (showLeftHandler)
|
if (showLeftHandler)
|
||||||
_DragHandler(
|
Positioned(
|
||||||
handlerColor: handlerColor,
|
left: -handlerWidth,
|
||||||
handlerWidth: handlerWidth,
|
child: _DragHandler(
|
||||||
handlerBallWidth: handlerBallWidth,
|
handlerColor: handlerColor,
|
||||||
handlerHeight: handlerHeight,
|
handlerWidth: handlerWidth,
|
||||||
showLeftHandler: true,
|
handlerBallWidth: handlerBallWidth,
|
||||||
showRightHandler: false,
|
handlerHeight: handlerHeight,
|
||||||
|
showLeftHandler: true,
|
||||||
|
showRightHandler: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Expanded(child: child),
|
child,
|
||||||
if (showRightHandler)
|
if (showRightHandler)
|
||||||
_DragHandler(
|
Positioned(
|
||||||
handlerColor: handlerColor,
|
right: -handlerWidth,
|
||||||
handlerWidth: handlerWidth,
|
child: _DragHandler(
|
||||||
handlerBallWidth: handlerBallWidth,
|
handlerColor: handlerColor,
|
||||||
handlerHeight: handlerHeight,
|
handlerWidth: handlerWidth,
|
||||||
showRightHandler: true,
|
handlerBallWidth: handlerBallWidth,
|
||||||
showLeftHandler: false,
|
handlerHeight: handlerHeight,
|
||||||
|
showRightHandler: true,
|
||||||
|
showLeftHandler: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ class MobileSelectionGestureDetector extends StatefulWidget {
|
||||||
this.onPanStart,
|
this.onPanStart,
|
||||||
this.onPanUpdate,
|
this.onPanUpdate,
|
||||||
this.onPanEnd,
|
this.onPanEnd,
|
||||||
|
this.onLongPressStart,
|
||||||
|
this.onLongPressEnd,
|
||||||
|
this.onLongPressMoveUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -31,6 +34,9 @@ class MobileSelectionGestureDetector extends StatefulWidget {
|
||||||
final GestureDragStartCallback? onPanStart;
|
final GestureDragStartCallback? onPanStart;
|
||||||
final GestureDragUpdateCallback? onPanUpdate;
|
final GestureDragUpdateCallback? onPanUpdate;
|
||||||
final GestureDragEndCallback? onPanEnd;
|
final GestureDragEndCallback? onPanEnd;
|
||||||
|
final GestureLongPressStartCallback? onLongPressStart;
|
||||||
|
final GestureLongPressEndCallback? onLongPressEnd;
|
||||||
|
final GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MobileSelectionGestureDetectorState
|
class MobileSelectionGestureDetectorState
|
||||||
|
|
@ -67,6 +73,16 @@ class MobileSelectionGestureDetectorState
|
||||||
..onEnd = widget.onPanEnd;
|
..onEnd = widget.onPanEnd;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
LongPressGestureRecognizer:
|
||||||
|
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||||
|
() => LongPressGestureRecognizer(),
|
||||||
|
(recognizer) {
|
||||||
|
recognizer
|
||||||
|
..onLongPressStart = widget.onLongPressStart
|
||||||
|
..onLongPressEnd = widget.onLongPressEnd
|
||||||
|
..onLongPressMoveUpdate = widget.onLongPressMoveUpdate;
|
||||||
|
},
|
||||||
|
),
|
||||||
TapGestureRecognizer:
|
TapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||||
() => TapGestureRecognizer(),
|
() => TapGestureRecognizer(),
|
||||||
|
|
|
||||||
|
|
@ -49,15 +49,13 @@ dependencies:
|
||||||
keyboard_height_plugin: ^0.0.4
|
keyboard_height_plugin: ^0.0.4
|
||||||
numerus: ^2.1.2
|
numerus: ^2.1.2
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.1
|
flutter_lints: ^3.0.1
|
||||||
network_image_mock: ^2.1.1
|
network_image_mock: ^2.1.1
|
||||||
mockito: ^5.4.1
|
mockito: ^5.4.1
|
||||||
leak_tracker_flutter_testing: ^1.0.10
|
leak_tracker: 9.0.16
|
||||||
leak_tracker: '>=9.0.0 <11.0.0'
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:leak_tracker/leak_tracker.dart';
|
|
||||||
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
testWidgetsWithLeakTracking(
|
|
||||||
'memory leak',
|
|
||||||
(tester) async {
|
|
||||||
await tester.pumpWidget(const _NoMemoryLeaks());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
},
|
|
||||||
leakTesting: LeakTesting.settings.copyWith(
|
|
||||||
ignore: false,
|
|
||||||
leakDiagnosticConfig: const LeakDiagnosticConfig(
|
|
||||||
collectStackTraceOnStart: true,
|
|
||||||
collectRetainingPathForNotGCed: true,
|
|
||||||
),
|
|
||||||
onLeaks: (leaks) {
|
|
||||||
// dump the leaks
|
|
||||||
for (final leak in leaks.all) {
|
|
||||||
final stack =
|
|
||||||
leak.context![ContextKeys.startCallstack]! as StackTrace;
|
|
||||||
final stackInEditor = stack
|
|
||||||
.toString()
|
|
||||||
.split('\n')
|
|
||||||
.where((stack) => stack.contains('package:appflowy_editor'))
|
|
||||||
.join('\n');
|
|
||||||
debugPrint('''
|
|
||||||
${leak.type} => ${leak.trackedClass}
|
|
||||||
$stackInEditor
|
|
||||||
-------------------------------------------------------------------------------
|
|
||||||
''');
|
|
||||||
}
|
|
||||||
expect(leaks.all, isEmpty);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NoMemoryLeaks extends StatefulWidget {
|
|
||||||
const _NoMemoryLeaks();
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_NoMemoryLeaks> createState() => __NoMemoryLeaksState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class __NoMemoryLeaksState extends State<_NoMemoryLeaks> {
|
|
||||||
final editorState = EditorState.blank();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
editorState.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
home: Scaffold(
|
|
||||||
body: SafeArea(
|
|
||||||
child: Container(
|
|
||||||
width: 500,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.blue),
|
|
||||||
),
|
|
||||||
child: AppFlowyEditor(
|
|
||||||
editorState: editorState,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue