feat: implement mobile toolbar

This commit is contained in:
Lucas.Xu 2023-04-20 13:45:46 +08:00
commit 2487408920
10 changed files with 205 additions and 52 deletions

View file

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -33,19 +34,12 @@ class SimpleEditor extends StatelessWidget {
..handler = debugPrint ..handler = debugPrint
..level = LogLevel.all; ..level = LogLevel.all;
onEditorStateChange(editorState); onEditorStateChange(editorState);
return Column(
return AppFlowyEditor( children: [
editorState: editorState, Expanded(child: _buildEditor(context, editorState)),
themeData: themeData, if (Platform.isIOS || Platform.isAndroid)
autoFocus: editorState.document.isEmpty, _buildMobileToolbar(context, editorState),
customBuilders: { ],
'paragraph': TextBlockComponentBuilder(),
'todo_list': TodoListBlockComponentBuilder(),
'bulleted_list': BulletedListBlockComponentBuilder(),
'numbered_list': NumberedListBlockComponentBuilder(),
'quote':
QuoteBlockComponentBuilder(padding: const EdgeInsets.all(0)),
},
); );
} else { } else {
return const Center( return const Center(
@ -55,4 +49,23 @@ class SimpleEditor extends StatelessWidget {
}, },
); );
} }
Widget _buildEditor(BuildContext context, EditorState editorState) {
return AppFlowyEditor(
editorState: editorState,
themeData: themeData,
autoFocus: editorState.document.isEmpty,
customBuilders: {
'paragraph': TextBlockComponentBuilder(),
'todo_list': TodoListBlockComponentBuilder(),
'bulleted_list': BulletedListBlockComponentBuilder(),
'numbered_list': NumberedListBlockComponentBuilder(),
'quote': QuoteBlockComponentBuilder(),
},
);
}
Widget _buildMobileToolbar(BuildContext context, EditorState editorState) {
return MobileToolbar(editorState: editorState);
}
} }

View file

@ -54,3 +54,4 @@ export 'src/infra/html_converter.dart';
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart'; export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
export 'src/editor/block_component/block_component.dart'; export 'src/editor/block_component/block_component.dart';
export 'src/editor/editor_component/editor_component.dart';

View file

@ -0,0 +1 @@
export 'toolbar/mobile_toolbar.dart';

View file

@ -86,6 +86,7 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
'attach text editing value: $textEditingValue', 'attach text editing value: $textEditingValue',
); );
textInputService.attach(textEditingValue); textInputService.attach(textEditingValue);
isAttached = true;
} }
} }

View file

@ -22,8 +22,7 @@ class DesktopScrollService extends StatefulWidget {
class _DesktopScrollServiceState extends State<DesktopScrollService> class _DesktopScrollServiceState extends State<DesktopScrollService>
implements AppFlowyScrollService { implements AppFlowyScrollService {
bool forward = false; AxisDirection _direction = AxisDirection.down;
double _panStartDy = 0.0;
bool _scrollEnabled = true; bool _scrollEnabled = true;
@ -57,14 +56,8 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Listener( return Listener(
onPointerSignal: _onPointerSignal, onPointerSignal: _onPointerSignal,
onPointerPanZoomStart: (event) {
_panStartDy = event.localPosition.dy;
},
onPointerPanZoomUpdate: _onPointerPanZoomUpdate, onPointerPanZoomUpdate: _onPointerPanZoomUpdate,
onPointerPanZoomEnd: (event) { onPointerPanZoomEnd: _onPointerPanZoomEnd,
final forward = this.forward ? 1.0 : -1.0;
goBallistic(-1000 * forward);
},
child: widget.child, child: widget.child,
); );
} }
@ -91,6 +84,24 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
Log.scroll.debug('enable scroll service'); Log.scroll.debug('enable scroll service');
} }
@override
void startAutoScroll(Offset offset) {
widget.autoScroller.startAutoScroll(offset);
}
@override
void stopAutoScroll() {
widget.autoScroller.stopAutoScroll();
}
@override
void goBallistic(double velocity) {
final position = widget.scrollController.position;
if (position is ScrollPositionWithSingleContext) {
position.goBallistic(velocity);
}
}
void _onPointerSignal(PointerSignalEvent event) { void _onPointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent && _scrollEnabled) { if (event is PointerScrollEvent && _scrollEnabled) {
final dy = final dy =
@ -101,27 +112,20 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
void _onPointerPanZoomUpdate(PointerPanZoomUpdateEvent event) { void _onPointerPanZoomUpdate(PointerPanZoomUpdateEvent event) {
if (_scrollEnabled) { if (_scrollEnabled) {
forward = event.panDelta.dy > 0;
final dy = (widget.scrollController.position.pixels - event.panDelta.dy); final dy = (widget.scrollController.position.pixels - event.panDelta.dy);
scrollTo(dy); scrollTo(dy);
_direction =
event.panDelta.dy > 0 ? AxisDirection.down : AxisDirection.up;
} }
} }
@override void _onPointerPanZoomEnd(PointerPanZoomEndEvent event) {
void startAutoScroll(Offset offset) => // TODO: calculate the pixelsPerSecond
widget.autoScroller.startAutoScroll(offset); // var dyPerSecond = -1000.0;
// if (_direction == AxisDirection.up) {
@override // dyPerSecond *= -1.0;
void stopAutoScroll() { // }
// TODO: implement stopAutoScroll // goBallistic(dyPerSecond);
}
@override
void goBallistic(double velocity) {
final position = widget.scrollController.position;
if (position is ScrollPositionWithSingleContext) {
position.goBallistic(velocity);
position.context.setIgnorePointer(false);
}
} }
} }

View file

@ -0,0 +1,92 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart';
import 'package:flutter/material.dart';
class MobileScrollService extends StatefulWidget {
const MobileScrollService({
Key? key,
required this.scrollController,
required this.autoScroller,
required this.child,
}) : super(key: key);
final ScrollController scrollController;
final AutoScroller autoScroller;
final Widget child;
@override
State<MobileScrollService> createState() => _MobileScrollServiceState();
}
class _MobileScrollServiceState extends State<MobileScrollService>
implements AppFlowyScrollService {
@override
double get dy => widget.scrollController.position.pixels;
@override
double? get onePageHeight {
final renderBox = context.findRenderObject()?.unwrapOrNull<RenderBox>();
return renderBox?.size.height;
}
@override
double get maxScrollExtent =>
widget.scrollController.position.maxScrollExtent;
@override
double get minScrollExtent =>
widget.scrollController.position.minScrollExtent;
@override
int? get page {
if (onePageHeight != null) {
final scrollExtent = maxScrollExtent - minScrollExtent;
return (scrollExtent / onePageHeight!).ceil();
}
return null;
}
@override
Widget build(BuildContext context) {
return widget.child;
}
@override
void scrollTo(double dy) {
widget.scrollController.position.jumpTo(
dy.clamp(
widget.scrollController.position.minScrollExtent,
widget.scrollController.position.maxScrollExtent,
),
);
}
@override
void disable() {
Log.scroll.debug('disable scroll service');
}
@override
void enable() {
Log.scroll.debug('enable scroll service');
}
@override
void startAutoScroll(Offset offset) {
widget.autoScroller.startAutoScroll(offset);
}
@override
void stopAutoScroll() {
widget.autoScroller.stopAutoScroll();
}
@override
void goBallistic(double velocity) {
final position = widget.scrollController.position;
if (position is ScrollPositionWithSingleContext) {
position.goBallistic(velocity);
}
}
}

View file

@ -4,6 +4,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scrollable_widget.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scrollable_widget.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.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:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -83,7 +84,7 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
BuildContext context, BuildContext context,
AutoScroller autoScroller, AutoScroller autoScroller,
) { ) {
return DesktopScrollService( return MobileScrollService(
key: _forwardKey, key: _forwardKey,
scrollController: _scrollController, scrollController: _scrollController,
autoScroller: autoScroller, autoScroller: autoScroller,

View file

@ -1,5 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/util/debounce.dart';
import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/flutter/overlay.dart';
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart'; import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart'; import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
@ -67,11 +66,11 @@ class _DesktopSelectionServiceWidgetState
// Need to refresh the selection when the metrics changed. // Need to refresh the selection when the metrics changed.
if (currentSelection.value != null) { if (currentSelection.value != null) {
Debounce.debounce( // Debounce.debounce(
'didChangeMetrics - update selection ', // 'didChangeMetrics - update selection ',
const Duration(milliseconds: 100), // const Duration(milliseconds: 100),
() => updateSelection(currentSelection.value!), // () => updateSelection(currentSelection.value!),
); // );
} }
} }

View file

@ -1,5 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/util/debounce.dart';
import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/flutter/overlay.dart';
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart'; import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart'; import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
@ -67,11 +66,11 @@ class _MobileSelectionServiceWidgetState
// Need to refresh the selection when the metrics changed. // Need to refresh the selection when the metrics changed.
if (currentSelection.value != null) { if (currentSelection.value != null) {
Debounce.debounce( // Debounce.debounce(
'didChangeMetrics - update selection ', // 'didChangeMetrics - update selection ',
const Duration(milliseconds: 100), // const Duration(milliseconds: 100),
() => updateSelection(currentSelection.value!), // () => updateSelection(currentSelection.value!),
); // );
} }
} }

View file

@ -0,0 +1,42 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
class MobileToolbar extends StatelessWidget {
const MobileToolbar({
super.key,
required this.editorState,
});
final EditorState editorState;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Selection?>(
valueListenable: editorState.service.selectionService.currentSelection,
builder: (_, Selection? selection, __) {
if (selection == null) {
return const SizedBox.shrink();
}
final width = MediaQuery.of(context).size.width;
return SizedBox(
width: width,
height: 30,
child: Container(
color: Colors.grey.withOpacity(0.3),
child: Row(
children: [
IconButton(
onPressed: () {
editorState.selection.updateSelection(null);
},
icon: const Icon(Icons.keyboard_hide),
),
const Text('FIXME: Mobile Toolbar'),
],
),
),
);
},
);
}
}