Skip to content

Commit

Permalink
feat: optimize editing experience on mobile (#592)
Browse files Browse the repository at this point in the history
* feat: optimize mobile editing

* feat: refactot the mobile toolbar

* feat: don't reattch text input if the selection doesn't change

* chore: update mobile toolbar layout
  • Loading branch information
LucasXu0 committed Nov 15, 2023
1 parent 784fde1 commit 4345105
Show file tree
Hide file tree
Showing 31 changed files with 824 additions and 142 deletions.
3 changes: 2 additions & 1 deletion example/lib/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,14 @@ class _HomePageState extends State<HomePage> {
key: _scaffoldKey,
extendBodyBehindAppBar: PlatformExtension.isDesktopOrWeb,
drawer: _buildDrawer(context),
resizeToAvoidBottomInset: false,
appBar: AppBar(
backgroundColor: const Color.fromARGB(255, 134, 46, 247),
foregroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
title: const Text('AppFlowy Editor'),
),
body: SafeArea(child: _buildBody(context)),
body: _buildBody(context),
);
}

Expand Down
10 changes: 3 additions & 7 deletions example/lib/pages/mobile_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,14 @@ class _MobileEditorState extends State<MobileEditor> {
),
),
// build mobile toolbar
MobileToolbar(
MobileToolbarV2(
editorState: editorState,
toolbarItems: [
textDecorationMobileToolbarItem,
textDecorationMobileToolbarItemV2,
buildTextAndBackgroundColorMobileToolbarItem(),
headingMobileToolbarItem,
todoListMobileToolbarItem,
listMobileToolbarItem,
blocksMobileToolbarItem,
linkMobileToolbarItem,
quoteMobileToolbarItem,
dividerMobileToolbarItem,
codeMobileToolbarItem,
],
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
late final TextInputService textInputService;
late final FocusNode focusNode;

// previous selection
Selection? previousSelection;

// use for IME only
bool enableShortcuts = true;

Expand Down Expand Up @@ -187,9 +190,17 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
if (doNotAttach == true) {
return;
}
enableShortcuts = true;

// attach the delta text input service if needed
final selection = editorState.selection;

if (PlatformExtension.isMobile && previousSelection == selection) {
// no need to attach the text input service if the selection is not changed.
return;
}

enableShortcuts = true;

if (selection == null) {
textInputService.close();
} else {
Expand All @@ -202,6 +213,8 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
Log.editor.debug('keyboard service - request focus');
}
}

previousSelection = selection;
}

void _attachTextInputService(Selection selection) {
Expand Down
1 change: 1 addition & 0 deletions lib/src/editor/toolbar/mobile/mobile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export 'mobile_floating_toolbar/mobile_floating_toolbar.dart';
export 'mobile_toolbar.dart';
export 'mobile_toolbar_item.dart';
export 'mobile_toolbar_style.dart';
export 'mobile_toolbar_v2.dart';
export 'toolbar_items/toolbar_items.dart';
export 'utils/utils.dart';
65 changes: 48 additions & 17 deletions lib/src/editor/toolbar/mobile/mobile_toolbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ class MobileToolbar extends StatelessWidget {
return const SizedBox.shrink();
}
return RepaintBoundary(
child: MobileToolbarStyle(
child: MobileToolbarTheme(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
clearDiagonalLineColor: clearDiagonalLineColor,
itemHighlightColor: itemHighlightColor,
itemOutlineColor: itemOutlineColor,
tabbarSelectedBackgroundColor: tabbarSelectedBackgroundColor,
tabbarSelectedForegroundColor: tabbarSelectedForegroundColor,
tabBarSelectedBackgroundColor: tabbarSelectedBackgroundColor,
tabBarSelectedForegroundColor: tabbarSelectedForegroundColor,
primaryColor: primaryColor,
onPrimaryColor: onPrimaryColor,
outlineColor: outlineColor,
Expand Down Expand Up @@ -90,7 +90,7 @@ abstract class MobileToolbarWidgetService {
void closeItemMenu();
}

class MobileToolbarWidget extends StatefulWidget {
class MobileToolbarWidget extends StatefulWidget with WidgetsBindingObserver {
const MobileToolbarWidget({
super.key,
required this.editorState,
Expand All @@ -111,6 +111,9 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
bool _showItemMenu = false;
int? _selectedToolbarItemIndex;

double previousKeyboardHeight = 0.0;
bool updateKeyboardHeight = true;

@override
void closeItemMenu() {
if (_showItemMenu) {
Expand All @@ -123,7 +126,14 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final style = MobileToolbarStyle.of(context);
final style = MobileToolbarTheme.of(context);

final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
if (updateKeyboardHeight) {
previousKeyboardHeight = keyboardHeight;
}
// print('object $keyboardHeight');

return Column(
children: [
Container(
Expand All @@ -147,15 +157,27 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
toolbarItems: widget.toolbarItems,
toolbarWidgetService: this,
itemWithMenuOnPressed: (selectedItemIndex) {
updateKeyboardHeight = false;
setState(() {
// If last selected item is selected again, toggle item menu
if (_selectedToolbarItemIndex == selectedItemIndex) {
_showItemMenu = !_showItemMenu;

if (!_showItemMenu) {
// updateKeyboardHeight = true;
widget.editorState.service.keyboardService
?.enableKeyBoard(widget.selection);
} else {
updateKeyboardHeight = false;
widget.editorState.service.keyboardService
?.closeKeyboard();
}
} else {
_selectedToolbarItemIndex = selectedItemIndex;
// If not, show item menu
_showItemMenu = true;
// close keyboard when menu pop up

widget.editorState.service.keyboardService
?.closeKeyboard();
}
Expand All @@ -174,8 +196,8 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
onPressed: () {
setState(() {
_showItemMenu = false;
widget.editorState.service.keyboardService!
.enableKeyBoard(widget.selection);
widget.editorState.service.keyboardService
?.enableKeyBoard(widget.selection);
});
},
icon: const AFMobileIcon(
Expand All @@ -187,14 +209,22 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
),
),
// only for MobileToolbarItem.withMenu
if (_showItemMenu && _selectedToolbarItemIndex != null)
MobileToolbarItemMenu(
editorState: widget.editorState,
itemMenuBuilder: () => widget
.toolbarItems[_selectedToolbarItemIndex!]
// pass current [MobileToolbarWidgetState] to be used to closeItemMenu
.itemMenuBuilder!(widget.editorState, widget.selection, this),
),
(_showItemMenu && _selectedToolbarItemIndex != null)
? Container(
constraints: BoxConstraints(
minHeight: previousKeyboardHeight,
),
child: MobileToolbarItemMenu(
editorState: widget.editorState,
itemMenuBuilder: () => widget
.toolbarItems[_selectedToolbarItemIndex!]
// pass current [MobileToolbarWidgetState] to be used to closeItemMenu
.itemMenuBuilder!(context, widget.editorState, this),
),
)
: SizedBox(
height: previousKeyboardHeight,
),
],
);
}
Expand Down Expand Up @@ -239,9 +269,10 @@ class _ToolbarItemListView extends StatelessWidget {
return ListView.builder(
itemBuilder: (context, index) {
final toolbarItem = toolbarItems[index];
final icon = toolbarItem.itemIconBuilder(
final icon = toolbarItem.itemIconBuilder?.call(
context,
editorState,
toolbarWidgetService,
);
if (icon == null) {
return const SizedBox.shrink();
Expand All @@ -256,8 +287,8 @@ class _ToolbarItemListView extends StatelessWidget {
// close menu if other item's menu is still on the screen
toolbarWidgetService.closeItemMenu();
toolbarItems[index].actionHandler?.call(
context,
editorState,
selection,
);
}
},
Expand Down
23 changes: 12 additions & 11 deletions lib/src/editor/toolbar/mobile/mobile_toolbar_item.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

typedef MobileToolbarItemMenuBuilder = Widget Function(
typedef MobileToolbarItemIconBuilder = Widget Function(
BuildContext context,
EditorState editorState,
Selection selection,
// To access to the state of MobileToolbarWidget
MobileToolbarWidgetService service,
);

typedef MobileToolbarItemActionHandler = void Function(
BuildContext context,
EditorState editorState,
Selection selection,
);

class MobileToolbarItem {
Expand All @@ -19,20 +19,21 @@ class MobileToolbarItem {
required this.itemIconBuilder,
required this.actionHandler,
}) : hasMenu = false,
itemMenuBuilder = null;
itemMenuBuilder = null,
assert(itemIconBuilder != null && actionHandler != null);

/// Tool bar item that opens a menu to show options
const MobileToolbarItem.withMenu({
required this.itemIconBuilder,
required this.itemMenuBuilder,
}) : hasMenu = true,
actionHandler = null;
actionHandler = null,
assert(itemMenuBuilder != null && itemIconBuilder != null);

final bool hasMenu;
final Widget? Function(
BuildContext context,
EditorState editorState,
) itemIconBuilder;
final MobileToolbarItemMenuBuilder? itemMenuBuilder;
// if the result is null, the item will be hidden
final MobileToolbarItemIconBuilder? itemIconBuilder;
final MobileToolbarItemActionHandler? actionHandler;

final bool hasMenu;
final MobileToolbarItemIconBuilder? itemMenuBuilder;
}
66 changes: 34 additions & 32 deletions lib/src/editor/toolbar/mobile/mobile_toolbar_style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,35 @@ import 'package:flutter/material.dart';
/// itemHighlightColor -> selected item border color
///
/// itemOutlineColor -> item border color
class MobileToolbarStyle extends InheritedWidget {
class MobileToolbarTheme extends InheritedWidget {
const MobileToolbarTheme({
super.key,
this.backgroundColor = Colors.white,
this.foregroundColor = const Color(0xff676666),
this.clearDiagonalLineColor = const Color(0xffB3261E),
this.itemHighlightColor = const Color(0xff1F71AC),
this.itemOutlineColor = const Color(0xFFE3E3E3),
this.tabBarSelectedBackgroundColor = const Color(0x23808080),
this.tabBarSelectedForegroundColor = Colors.black,
this.primaryColor = const Color(0xff1F71AC),
this.onPrimaryColor = Colors.white,
this.outlineColor = const Color(0xFFE3E3E3),
this.toolbarHeight = 50.0,
this.borderRadius = 6.0,
this.buttonHeight = 40.0,
this.buttonSpacing = 8.0,
this.buttonBorderWidth = 1.0,
this.buttonSelectedBorderWidth = 2.0,
required super.child,
});

final Color backgroundColor;
final Color foregroundColor;
final Color clearDiagonalLineColor;
final Color itemHighlightColor;
final Color itemOutlineColor;
final Color tabbarSelectedBackgroundColor;
final Color tabbarSelectedForegroundColor;
final Color tabBarSelectedBackgroundColor;
final Color tabBarSelectedForegroundColor;
final Color primaryColor;
final Color onPrimaryColor;
final Color outlineColor;
Expand All @@ -25,44 +46,25 @@ class MobileToolbarStyle extends InheritedWidget {
final double buttonBorderWidth;
final double buttonSelectedBorderWidth;

const MobileToolbarStyle({
super.key,
required this.backgroundColor,
required this.foregroundColor,
required this.clearDiagonalLineColor,
required this.itemHighlightColor,
required this.itemOutlineColor,
required this.tabbarSelectedBackgroundColor,
required this.tabbarSelectedForegroundColor,
required this.primaryColor,
required this.onPrimaryColor,
required this.outlineColor,
required this.toolbarHeight,
required this.borderRadius,
required this.buttonHeight,
required this.buttonSpacing,
required this.buttonBorderWidth,
required this.buttonSelectedBorderWidth,
required super.child,
});
static MobileToolbarTheme of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MobileToolbarTheme>()!;
}

static MobileToolbarStyle of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MobileToolbarStyle>()!;
static MobileToolbarTheme? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MobileToolbarTheme>();
}

@override
bool updateShouldNotify(covariant MobileToolbarStyle oldWidget) {
// This function is called whenever the inherited widget is rebuilt.
// It should return true if the new widget's values are different from the old widget's values.
bool updateShouldNotify(covariant MobileToolbarTheme oldWidget) {
return backgroundColor != oldWidget.backgroundColor ||
foregroundColor != oldWidget.foregroundColor ||
clearDiagonalLineColor != oldWidget.clearDiagonalLineColor ||
itemHighlightColor != oldWidget.itemHighlightColor ||
itemOutlineColor != oldWidget.itemOutlineColor ||
tabbarSelectedBackgroundColor !=
oldWidget.tabbarSelectedBackgroundColor ||
tabbarSelectedForegroundColor !=
oldWidget.tabbarSelectedForegroundColor ||
tabBarSelectedBackgroundColor !=
oldWidget.tabBarSelectedBackgroundColor ||
tabBarSelectedForegroundColor !=
oldWidget.tabBarSelectedForegroundColor ||
primaryColor != oldWidget.primaryColor ||
onPrimaryColor != oldWidget.onPrimaryColor ||
outlineColor != oldWidget.outlineColor ||
Expand Down
Loading

0 comments on commit 4345105

Please sign in to comment.