feat: implement mobile toolbar
This commit is contained in:
parent
e8280bd2b4
commit
2487408920
10 changed files with 205 additions and 52 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -33,19 +34,12 @@ class SimpleEditor extends StatelessWidget {
|
|||
..handler = debugPrint
|
||||
..level = LogLevel.all;
|
||||
onEditorStateChange(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(padding: const EdgeInsets.all(0)),
|
||||
},
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(child: _buildEditor(context, editorState)),
|
||||
if (Platform.isIOS || Platform.isAndroid)
|
||||
_buildMobileToolbar(context, editorState),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,3 +54,4 @@ export 'src/infra/html_converter.dart';
|
|||
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||
|
||||
export 'src/editor/block_component/block_component.dart';
|
||||
export 'src/editor/editor_component/editor_component.dart';
|
||||
|
|
|
|||
1
lib/src/editor/editor_component/editor_component.dart
Normal file
1
lib/src/editor/editor_component/editor_component.dart
Normal file
|
|
@ -0,0 +1 @@
|
|||
export 'toolbar/mobile_toolbar.dart';
|
||||
|
|
@ -86,6 +86,7 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
|||
'attach text editing value: $textEditingValue',
|
||||
);
|
||||
textInputService.attach(textEditingValue);
|
||||
isAttached = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@ class DesktopScrollService extends StatefulWidget {
|
|||
|
||||
class _DesktopScrollServiceState extends State<DesktopScrollService>
|
||||
implements AppFlowyScrollService {
|
||||
bool forward = false;
|
||||
double _panStartDy = 0.0;
|
||||
AxisDirection _direction = AxisDirection.down;
|
||||
|
||||
bool _scrollEnabled = true;
|
||||
|
||||
|
|
@ -57,14 +56,8 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
|
|||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerSignal: _onPointerSignal,
|
||||
onPointerPanZoomStart: (event) {
|
||||
_panStartDy = event.localPosition.dy;
|
||||
},
|
||||
onPointerPanZoomUpdate: _onPointerPanZoomUpdate,
|
||||
onPointerPanZoomEnd: (event) {
|
||||
final forward = this.forward ? 1.0 : -1.0;
|
||||
goBallistic(-1000 * forward);
|
||||
},
|
||||
onPointerPanZoomEnd: _onPointerPanZoomEnd,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
|
@ -91,6 +84,24 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
|
|||
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) {
|
||||
if (event is PointerScrollEvent && _scrollEnabled) {
|
||||
final dy =
|
||||
|
|
@ -101,27 +112,20 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
|
|||
|
||||
void _onPointerPanZoomUpdate(PointerPanZoomUpdateEvent event) {
|
||||
if (_scrollEnabled) {
|
||||
forward = event.panDelta.dy > 0;
|
||||
final dy = (widget.scrollController.position.pixels - event.panDelta.dy);
|
||||
scrollTo(dy);
|
||||
|
||||
_direction =
|
||||
event.panDelta.dy > 0 ? AxisDirection.down : AxisDirection.up;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void startAutoScroll(Offset offset) =>
|
||||
widget.autoScroller.startAutoScroll(offset);
|
||||
|
||||
@override
|
||||
void stopAutoScroll() {
|
||||
// TODO: implement stopAutoScroll
|
||||
}
|
||||
|
||||
@override
|
||||
void goBallistic(double velocity) {
|
||||
final position = widget.scrollController.position;
|
||||
if (position is ScrollPositionWithSingleContext) {
|
||||
position.goBallistic(velocity);
|
||||
position.context.setIgnorePointer(false);
|
||||
}
|
||||
void _onPointerPanZoomEnd(PointerPanZoomEndEvent event) {
|
||||
// TODO: calculate the pixelsPerSecond
|
||||
// var dyPerSecond = -1000.0;
|
||||
// if (_direction == AxisDirection.up) {
|
||||
// dyPerSecond *= -1.0;
|
||||
// }
|
||||
// goBallistic(dyPerSecond);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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_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/mobile_scroll_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -83,7 +84,7 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
|
|||
BuildContext context,
|
||||
AutoScroller autoScroller,
|
||||
) {
|
||||
return DesktopScrollService(
|
||||
return MobileScrollService(
|
||||
key: _forwardKey,
|
||||
scrollController: _scrollController,
|
||||
autoScroller: autoScroller,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
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/service/context_menu/built_in_context_menu_item.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.
|
||||
if (currentSelection.value != null) {
|
||||
Debounce.debounce(
|
||||
'didChangeMetrics - update selection ',
|
||||
const Duration(milliseconds: 100),
|
||||
() => updateSelection(currentSelection.value!),
|
||||
);
|
||||
// Debounce.debounce(
|
||||
// 'didChangeMetrics - update selection ',
|
||||
// const Duration(milliseconds: 100),
|
||||
// () => updateSelection(currentSelection.value!),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
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/service/context_menu/built_in_context_menu_item.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.
|
||||
if (currentSelection.value != null) {
|
||||
Debounce.debounce(
|
||||
'didChangeMetrics - update selection ',
|
||||
const Duration(milliseconds: 100),
|
||||
() => updateSelection(currentSelection.value!),
|
||||
);
|
||||
// Debounce.debounce(
|
||||
// 'didChangeMetrics - update selection ',
|
||||
// const Duration(milliseconds: 100),
|
||||
// () => updateSelection(currentSelection.value!),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
42
lib/src/editor/editor_component/toolbar/mobile_toolbar.dart
Normal file
42
lib/src/editor/editor_component/toolbar/mobile_toolbar.dart
Normal 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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue