fix: the new mobile toolbar doesn't work if resizeToAvoidBottomInset is true (#593)
* fix: toolbar issues * fix: adjust the mobile toolbar when resizeToAvoidBottomInset is true
This commit is contained in:
parent
4345105ebe
commit
e665b67c0d
8 changed files with 158 additions and 73 deletions
|
|
@ -87,14 +87,15 @@ class _HomePageState extends State<HomePage> {
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
extendBodyBehindAppBar: PlatformExtension.isDesktopOrWeb,
|
extendBodyBehindAppBar: PlatformExtension.isDesktopOrWeb,
|
||||||
drawer: _buildDrawer(context),
|
drawer: _buildDrawer(context),
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: const Color.fromARGB(255, 134, 46, 247),
|
backgroundColor: const Color.fromARGB(255, 134, 46, 247),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
title: const Text('AppFlowy Editor'),
|
title: const Text('AppFlowy Editor'),
|
||||||
),
|
),
|
||||||
body: _buildBody(context),
|
body: SafeArea(
|
||||||
|
child: _buildBody(context),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,59 +48,58 @@ class _MobileEditorState extends State<MobileEditor> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(PlatformExtension.isMobile);
|
assert(PlatformExtension.isMobile);
|
||||||
return Column(
|
return MobileToolbarV2(
|
||||||
children: [
|
toolbarItems: [
|
||||||
// build appflowy editor
|
textDecorationMobileToolbarItemV2,
|
||||||
Expanded(
|
buildTextAndBackgroundColorMobileToolbarItem(),
|
||||||
child: MobileFloatingToolbar(
|
blocksMobileToolbarItem,
|
||||||
editorState: editorState,
|
linkMobileToolbarItem,
|
||||||
editorScrollController: editorScrollController,
|
dividerMobileToolbarItem,
|
||||||
toolbarBuilder: (context, anchor, closeToolbar) {
|
],
|
||||||
return AdaptiveTextSelectionToolbar.editable(
|
editorState: editorState,
|
||||||
clipboardStatus: ClipboardStatus.pasteable,
|
child: Column(
|
||||||
onCopy: () {
|
children: [
|
||||||
copyCommand.execute(editorState);
|
// build appflowy editor
|
||||||
closeToolbar();
|
Expanded(
|
||||||
},
|
child: MobileFloatingToolbar(
|
||||||
onCut: () => cutCommand.execute(editorState),
|
|
||||||
onPaste: () => pasteCommand.execute(editorState),
|
|
||||||
onSelectAll: () => selectAllCommand.execute(editorState),
|
|
||||||
onLiveTextInput: null,
|
|
||||||
anchors: TextSelectionToolbarAnchors(
|
|
||||||
primaryAnchor: anchor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: AppFlowyEditor(
|
|
||||||
editorStyle: editorStyle,
|
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
editorScrollController: editorScrollController,
|
editorScrollController: editorScrollController,
|
||||||
blockComponentBuilders: blockComponentBuilders,
|
toolbarBuilder: (context, anchor, closeToolbar) {
|
||||||
// showcase 3: customize the header and footer.
|
return AdaptiveTextSelectionToolbar.editable(
|
||||||
header: Padding(
|
clipboardStatus: ClipboardStatus.pasteable,
|
||||||
padding: const EdgeInsets.only(bottom: 10.0),
|
onCopy: () {
|
||||||
child: Image.asset(
|
copyCommand.execute(editorState);
|
||||||
'assets/images/header.png',
|
closeToolbar();
|
||||||
|
},
|
||||||
|
onCut: () => cutCommand.execute(editorState),
|
||||||
|
onPaste: () => pasteCommand.execute(editorState),
|
||||||
|
onSelectAll: () => selectAllCommand.execute(editorState),
|
||||||
|
onLiveTextInput: null,
|
||||||
|
anchors: TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: anchor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: AppFlowyEditor(
|
||||||
|
editorStyle: editorStyle,
|
||||||
|
editorState: editorState,
|
||||||
|
editorScrollController: editorScrollController,
|
||||||
|
blockComponentBuilders: blockComponentBuilders,
|
||||||
|
// showcase 3: customize the header and footer.
|
||||||
|
header: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/header.png',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
footer: const SizedBox(
|
||||||
|
height: 100,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
footer: const SizedBox(
|
|
||||||
height: 100,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
// build mobile toolbar
|
),
|
||||||
MobileToolbarV2(
|
|
||||||
editorState: editorState,
|
|
||||||
toolbarItems: [
|
|
||||||
textDecorationMobileToolbarItemV2,
|
|
||||||
buildTextAndBackgroundColorMobileToolbarItem(),
|
|
||||||
blocksMobileToolbarItem,
|
|
||||||
linkMobileToolbarItem,
|
|
||||||
dividerMobileToolbarItem,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,10 +216,12 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
|
||||||
),
|
),
|
||||||
child: MobileToolbarItemMenu(
|
child: MobileToolbarItemMenu(
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
itemMenuBuilder: () => widget
|
itemMenuBuilder: () =>
|
||||||
.toolbarItems[_selectedToolbarItemIndex!]
|
widget
|
||||||
// pass current [MobileToolbarWidgetState] to be used to closeItemMenu
|
.toolbarItems[_selectedToolbarItemIndex!]
|
||||||
.itemMenuBuilder!(context, widget.editorState, this),
|
// pass current [MobileToolbarWidgetState] to be used to closeItemMenu
|
||||||
|
.itemMenuBuilder!(context, widget.editorState, this) ??
|
||||||
|
const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: SizedBox(
|
: SizedBox(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
typedef MobileToolbarItemIconBuilder = Widget Function(
|
// if the result is null, the item will be hidden
|
||||||
|
typedef MobileToolbarItemIconBuilder = Widget? Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
EditorState editorState,
|
EditorState editorState,
|
||||||
// To access to the state of MobileToolbarWidget
|
// To access to the state of MobileToolbarWidget
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,58 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:keyboard_height_plugin/keyboard_height_plugin.dart';
|
||||||
|
|
||||||
class MobileToolbarV2 extends StatelessWidget {
|
class MobileToolbarV2 extends StatefulWidget {
|
||||||
const MobileToolbarV2({
|
const MobileToolbarV2({
|
||||||
super.key,
|
super.key,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.toolbarItems,
|
required this.toolbarItems,
|
||||||
|
this.bottomOffset = 0.0,
|
||||||
|
required this.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final List<MobileToolbarItem> toolbarItems;
|
final List<MobileToolbarItem> toolbarItems;
|
||||||
|
final double bottomOffset;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileToolbarV2> createState() => _MobileToolbarV2State();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileToolbarV2State extends State<MobileToolbarV2> {
|
||||||
|
OverlayEntry? toolbarOverlay;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_insertKeyboardToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_removeKeyboardToolbar();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder<Selection?>(
|
return widget.child;
|
||||||
valueListenable: editorState.selectionNotifier,
|
}
|
||||||
|
|
||||||
|
void _removeKeyboardToolbar() {
|
||||||
|
toolbarOverlay?.remove();
|
||||||
|
toolbarOverlay?.dispose();
|
||||||
|
toolbarOverlay = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _insertKeyboardToolbar() {
|
||||||
|
_removeKeyboardToolbar();
|
||||||
|
|
||||||
|
Widget child = ValueListenableBuilder<Selection?>(
|
||||||
|
valueListenable: widget.editorState.selectionNotifier,
|
||||||
builder: (_, Selection? selection, child) {
|
builder: (_, Selection? selection, child) {
|
||||||
// if the selection is null, hide the toolbar
|
// if the selection is null, hide the toolbar
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
|
|
@ -33,10 +71,34 @@ class MobileToolbarV2 extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: _MobileToolbar(
|
child: _MobileToolbar(
|
||||||
editorState: editorState,
|
editorState: widget.editorState,
|
||||||
toolbarItems: toolbarItems,
|
toolbarItems: widget.toolbarItems,
|
||||||
|
bottomOffset: widget.bottomOffset,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
child = Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Material(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
toolbarOverlay = OverlayEntry(
|
||||||
|
builder: (context) {
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
Overlay.of(context, rootOverlay: true).insert(toolbarOverlay!);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,10 +106,12 @@ class _MobileToolbar extends StatefulWidget {
|
||||||
const _MobileToolbar({
|
const _MobileToolbar({
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.toolbarItems,
|
required this.toolbarItems,
|
||||||
|
required this.bottomOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final List<MobileToolbarItem> toolbarItems;
|
final List<MobileToolbarItem> toolbarItems;
|
||||||
|
final double bottomOffset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_MobileToolbar> createState() => _MobileToolbarState();
|
State<_MobileToolbar> createState() => _MobileToolbarState();
|
||||||
|
|
@ -64,6 +128,9 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||||
bool canUpdateCachedKeyboardHeight = true;
|
bool canUpdateCachedKeyboardHeight = true;
|
||||||
ValueNotifier<double> cachedKeyboardHeight = ValueNotifier(0.0);
|
ValueNotifier<double> cachedKeyboardHeight = ValueNotifier(0.0);
|
||||||
double get keyboardHeight => MediaQuery.of(context).viewInsets.bottom;
|
double get keyboardHeight => MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
double get safeAreaBottomPadding => MediaQuery.of(context).viewPadding.bottom;
|
||||||
|
|
||||||
|
final keyboardHeightPlugin = KeyboardHeightPlugin();
|
||||||
|
|
||||||
// used to check if click the same item again
|
// used to check if click the same item again
|
||||||
int? selectedMenuIndex;
|
int? selectedMenuIndex;
|
||||||
|
|
@ -75,6 +142,11 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
currentSelection = widget.editorState.selection;
|
currentSelection = widget.editorState.selection;
|
||||||
|
keyboardHeightPlugin.onKeyboardHeightChanged((height) {
|
||||||
|
if (canUpdateCachedKeyboardHeight) {
|
||||||
|
cachedKeyboardHeight.value = height;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -91,6 +163,7 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||||
void dispose() {
|
void dispose() {
|
||||||
showMenuNotifier.dispose();
|
showMenuNotifier.dispose();
|
||||||
cachedKeyboardHeight.dispose();
|
cachedKeyboardHeight.dispose();
|
||||||
|
keyboardHeightPlugin.dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +182,11 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||||
// update the keyboard height here.
|
// update the keyboard height here.
|
||||||
// try to get the height in `didChangeMetrics`, but it's not accurate.
|
// try to get the height in `didChangeMetrics`, but it's not accurate.
|
||||||
if (canUpdateCachedKeyboardHeight) {
|
if (canUpdateCachedKeyboardHeight) {
|
||||||
cachedKeyboardHeight.value = keyboardHeight;
|
// cachedKeyboardHeight.value = max(
|
||||||
|
// 0,
|
||||||
|
// keyboardHeight - safeAreaBottomPadding - widget.bottomOffset,
|
||||||
|
// );
|
||||||
|
// print('cachedKeyboardHeight: ${cachedKeyboardHeight.value}');
|
||||||
}
|
}
|
||||||
// toolbar
|
// toolbar
|
||||||
// - if the menu is shown, the toolbar will be pushed up by the height of the menu
|
// - if the menu is shown, the toolbar will be pushed up by the height of the menu
|
||||||
|
|
@ -233,18 +310,20 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable: showMenuNotifier,
|
valueListenable: showMenuNotifier,
|
||||||
builder: (_, showingMenu, __) {
|
builder: (_, showingMenu, __) {
|
||||||
return ConstrainedBox(
|
return SizedBox(
|
||||||
constraints: BoxConstraints(minHeight: height),
|
height: height,
|
||||||
child: (showingMenu && selectedMenuIndex != null)
|
child: (showingMenu && selectedMenuIndex != null)
|
||||||
? MobileToolbarItemMenu(
|
? MobileToolbarItemMenu(
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
itemMenuBuilder: () => widget
|
itemMenuBuilder: () =>
|
||||||
.toolbarItems[selectedMenuIndex!].itemMenuBuilder!
|
widget
|
||||||
.call(
|
.toolbarItems[selectedMenuIndex!].itemMenuBuilder!
|
||||||
context,
|
.call(
|
||||||
widget.editorState,
|
context,
|
||||||
this,
|
widget.editorState,
|
||||||
),
|
this,
|
||||||
|
) ??
|
||||||
|
const SizedBox.shrink(),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ class _BlocksMenuState extends State<_BlocksMenu> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final style = MobileToolbarTheme.of(context);
|
||||||
|
|
||||||
final children = lists.map((list) {
|
final children = lists.map((list) {
|
||||||
// Check if current node is list and its type
|
// Check if current node is list and its type
|
||||||
final node = widget.editorState.getNodeAtPath(
|
final node = widget.editorState.getNodeAtPath(
|
||||||
|
|
@ -111,11 +113,10 @@ class _BlocksMenuState extends State<_BlocksMenu> {
|
||||||
|
|
||||||
return GridView(
|
return GridView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
padding: EdgeInsets.zero,
|
||||||
|
gridDelegate: buildMobileToolbarMenuGridDelegate(
|
||||||
|
mobileToolbarStyle: style,
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
mainAxisSpacing: 8,
|
|
||||||
crossAxisSpacing: 8,
|
|
||||||
childAspectRatio: 5,
|
|
||||||
),
|
),
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ class _TextDecorationMenuState extends State<_TextDecorationMenu> {
|
||||||
|
|
||||||
return GridView(
|
return GridView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
gridDelegate: buildMobileToolbarMenuGridDelegate(
|
gridDelegate: buildMobileToolbarMenuGridDelegate(
|
||||||
mobileToolbarStyle: style,
|
mobileToolbarStyle: style,
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ dependencies:
|
||||||
diff_match_patch: ^0.4.1
|
diff_match_patch: ^0.4.1
|
||||||
string_validator: ^1.0.0
|
string_validator: ^1.0.0
|
||||||
universal_html: ^2.2.4
|
universal_html: ^2.2.4
|
||||||
|
keyboard_height_plugin: ^0.0.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue