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:
Lucas.Xu 2023-12-19 18:15:56 +08:00 • committed by GitHub
commit 82f3baf3f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 209 additions and 111 deletions

View file

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

View file

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

View file

@ -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,
); );

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
),
), ),
], ],
); );

View file

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

View file

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

View file

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