mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-10-25 06:51:51 +00:00
feat: customize animation for popover (#6507)
* feat: customize animation for popover * chore: code refactor * feat: using popover direction calculate the popover animation translate direction * feat: integrate the animated popover in appflowy_popover and popover_action * fix: close popover assertion * chore: format code * chore: code refactor * feat: optimize the popover listener * feat: clear popover when hot-reloading * chore: refactor code * fix: integration test * fix: icon test
This commit is contained in:
parent
580a23f3f5
commit
8cf683eb50
@ -11,7 +11,7 @@ void main() {
|
|||||||
|
|
||||||
const emoji = '😁';
|
const emoji = '😁';
|
||||||
|
|
||||||
group('Icon', () {
|
group('Icon:', () {
|
||||||
testWidgets('Update page icon in sidebar', (tester) async {
|
testWidgets('Update page icon in sidebar', (tester) async {
|
||||||
await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
await tester.tapAnonymousSignInButton();
|
await tester.tapAnonymousSignInButton();
|
||||||
@ -52,6 +52,7 @@ void main() {
|
|||||||
if (value == ViewLayoutPB.Chat) {
|
if (value == ViewLayoutPB.Chat) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.createNewPageWithNameUnderParent(
|
await tester.createNewPageWithNameUnderParent(
|
||||||
name: value.name,
|
name: value.name,
|
||||||
parentName: gettingStarted,
|
parentName: gettingStarted,
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import 'package:scaled_app/scaled_app.dart';
|
|||||||
import 'startup/startup.dart';
|
import 'startup/startup.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0);
|
ScaledWidgetsFlutterBinding.ensureInitialized(
|
||||||
|
scaleFactor: (_) => 1.0,
|
||||||
|
);
|
||||||
|
|
||||||
await runAppFlowy();
|
await runAppFlowy();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -629,23 +629,23 @@ class FieldNameTextField extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||||
FocusNode focusNode = FocusNode();
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
focusNode.addListener(() {
|
focusNode.addListener(_onFocusChanged);
|
||||||
if (focusNode.hasFocus) {
|
widget.popoverMutex?.addPopoverListener(_onPopoverChanged);
|
||||||
widget.popoverMutex?.close();
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
widget.popoverMutex?.listenOnPopoverChanged(() {
|
@override
|
||||||
if (focusNode.hasFocus) {
|
void dispose() {
|
||||||
focusNode.unfocus();
|
widget.popoverMutex?.removePopoverListener(_onPopoverChanged);
|
||||||
}
|
focusNode.removeListener(_onFocusChanged);
|
||||||
});
|
focusNode.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -662,15 +662,16 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _onFocusChanged() {
|
||||||
void dispose() {
|
if (focusNode.hasFocus) {
|
||||||
focusNode.removeListener(() {
|
widget.popoverMutex?.close();
|
||||||
if (focusNode.hasFocus) {
|
}
|
||||||
widget.popoverMutex?.close();
|
}
|
||||||
}
|
|
||||||
});
|
void _onPopoverChanged() {
|
||||||
focusNode.dispose();
|
if (focusNode.hasFocus) {
|
||||||
super.dispose();
|
focusNode.unfocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -206,22 +206,23 @@ class CreateOptionTextField extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
||||||
late final FocusNode _focusNode;
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_focusNode = FocusNode()
|
|
||||||
..addListener(() {
|
focusNode.addListener(_onFocusChanged);
|
||||||
if (_focusNode.hasFocus) {
|
widget.popoverMutex?.addPopoverListener(_onPopoverChanged);
|
||||||
widget.popoverMutex?.close();
|
}
|
||||||
}
|
|
||||||
});
|
@override
|
||||||
widget.popoverMutex?.listenOnPopoverChanged(() {
|
void dispose() {
|
||||||
if (_focusNode.hasFocus) {
|
widget.popoverMutex?.removePopoverListener(_onPopoverChanged);
|
||||||
_focusNode.unfocus();
|
focusNode.removeListener(_onFocusChanged);
|
||||||
}
|
focusNode.dispose();
|
||||||
});
|
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -234,7 +235,7 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
|||||||
child: FlowyTextField(
|
child: FlowyTextField(
|
||||||
autoClearWhenDone: true,
|
autoClearWhenDone: true,
|
||||||
text: text,
|
text: text,
|
||||||
focusNode: _focusNode,
|
focusNode: focusNode,
|
||||||
onCanceled: () {
|
onCanceled: () {
|
||||||
context
|
context
|
||||||
.read<SelectOptionTypeOptionBloc>()
|
.read<SelectOptionTypeOptionBloc>()
|
||||||
@ -252,15 +253,16 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _onFocusChanged() {
|
||||||
void dispose() {
|
if (focusNode.hasFocus) {
|
||||||
_focusNode.removeListener(() {
|
widget.popoverMutex?.close();
|
||||||
if (_focusNode.hasFocus) {
|
}
|
||||||
widget.popoverMutex?.close();
|
}
|
||||||
}
|
|
||||||
});
|
void _onPopoverChanged() {
|
||||||
_focusNode.dispose();
|
if (focusNode.hasFocus) {
|
||||||
super.dispose();
|
focusNode.unfocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -70,12 +70,12 @@ class _BlockOptionButtonState extends State<BlockOptionButton> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopoverActionList<PopoverAction>(
|
return PopoverActionList<PopoverAction>(
|
||||||
popoverMutex: PopoverMutex(),
|
popoverMutex: PopoverMutex(),
|
||||||
|
actions: popoverActions,
|
||||||
direction:
|
direction:
|
||||||
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
|
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
|
||||||
LayoutDirection.rtlLayout
|
LayoutDirection.rtlLayout
|
||||||
? PopoverDirection.rightWithCenterAligned
|
? PopoverDirection.rightWithCenterAligned
|
||||||
: PopoverDirection.leftWithCenterAligned,
|
: PopoverDirection.leftWithCenterAligned,
|
||||||
actions: popoverActions,
|
|
||||||
onPopupBuilder: () {
|
onPopupBuilder: () {
|
||||||
keepEditorFocusNotifier.increase();
|
keepEditorFocusNotifier.increase();
|
||||||
widget.blockComponentState.alwaysShowActions = true;
|
widget.blockComponentState.alwaysShowActions = true;
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
const _maxLengthTwelveHour = 8;
|
const _maxLengthTwelveHour = 8;
|
||||||
@ -64,7 +63,7 @@ class _TimeTextFieldState extends State<TimeTextField> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_focusNode.addListener(_focusNodeListener);
|
_focusNode.addListener(_focusNodeListener);
|
||||||
widget.popoverMutex?.listenOnPopoverChanged(_popoverListener);
|
widget.popoverMutex?.addPopoverListener(_popoverListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -17,6 +17,8 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
|
|||||||
this.direction = PopoverDirection.rightWithTopAligned,
|
this.direction = PopoverDirection.rightWithTopAligned,
|
||||||
this.asBarrier = false,
|
this.asBarrier = false,
|
||||||
this.offset = Offset.zero,
|
this.offset = Offset.zero,
|
||||||
|
this.animationDuration = const Duration(),
|
||||||
|
this.slideDistance = 20,
|
||||||
this.constraints = const BoxConstraints(
|
this.constraints = const BoxConstraints(
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
maxWidth: 460,
|
maxWidth: 460,
|
||||||
@ -35,6 +37,8 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
|
|||||||
final bool asBarrier;
|
final bool asBarrier;
|
||||||
final Offset offset;
|
final Offset offset;
|
||||||
final BoxConstraints constraints;
|
final BoxConstraints constraints;
|
||||||
|
final Duration animationDuration;
|
||||||
|
final double slideDistance;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PopoverActionList<T>> createState() => _PopoverActionListState<T>();
|
State<PopoverActionList<T>> createState() => _PopoverActionListState<T>();
|
||||||
@ -55,6 +59,8 @@ class _PopoverActionListState<T extends PopoverAction>
|
|||||||
final child = widget.buildChild(popoverController);
|
final child = widget.buildChild(popoverController);
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
asBarrier: widget.asBarrier,
|
asBarrier: widget.asBarrier,
|
||||||
|
animationDuration: widget.animationDuration,
|
||||||
|
slideDistance: widget.slideDistance,
|
||||||
controller: popoverController,
|
controller: popoverController,
|
||||||
constraints: widget.constraints,
|
constraints: widget.constraints,
|
||||||
direction: widget.direction,
|
direction: widget.direction,
|
||||||
|
|||||||
@ -1,4 +1,60 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- "**/*.g.dart"
|
||||||
|
- "**/*.freezed.dart"
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at
|
||||||
|
# https://dart-lang.github.io/linter/lints/index.html.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
- require_trailing_commas
|
||||||
|
|
||||||
|
- prefer_collection_literals
|
||||||
|
- prefer_final_fields
|
||||||
|
- prefer_final_in_for_each
|
||||||
|
- prefer_final_locals
|
||||||
|
|
||||||
|
- sized_box_for_whitespace
|
||||||
|
- use_decorated_box
|
||||||
|
|
||||||
|
- unnecessary_parenthesis
|
||||||
|
- unnecessary_await_in_return
|
||||||
|
- unnecessary_raw_strings
|
||||||
|
|
||||||
|
- avoid_unnecessary_containers
|
||||||
|
- avoid_redundant_argument_values
|
||||||
|
- avoid_unused_constructor_parameters
|
||||||
|
|
||||||
|
- always_declare_return_types
|
||||||
|
|
||||||
|
- sort_constructors_first
|
||||||
|
- unawaited_futures
|
||||||
|
|
||||||
|
- prefer_single_quotes
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|
||||||
|
errors:
|
||||||
|
invalid_annotation_target: ignore
|
||||||
|
|||||||
@ -7,8 +7,14 @@
|
|||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- "**/*.g.dart"
|
||||||
|
- "**/*.freezed.dart"
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
@ -22,8 +28,33 @@ linter:
|
|||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
# producing the lint.
|
# producing the lint.
|
||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
- require_trailing_commas
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
|
||||||
|
- prefer_collection_literals
|
||||||
|
- prefer_final_fields
|
||||||
|
- prefer_final_in_for_each
|
||||||
|
- prefer_final_locals
|
||||||
|
|
||||||
|
- sized_box_for_whitespace
|
||||||
|
- use_decorated_box
|
||||||
|
|
||||||
|
- unnecessary_parenthesis
|
||||||
|
- unnecessary_await_in_return
|
||||||
|
- unnecessary_raw_strings
|
||||||
|
|
||||||
|
- avoid_unnecessary_containers
|
||||||
|
- avoid_redundant_argument_values
|
||||||
|
- avoid_unused_constructor_parameters
|
||||||
|
|
||||||
|
- always_declare_return_types
|
||||||
|
|
||||||
|
- sort_constructors_first
|
||||||
|
- unawaited_futures
|
||||||
|
|
||||||
|
- prefer_single_quotes
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|
||||||
|
errors:
|
||||||
|
invalid_annotation_target: ignore
|
||||||
|
|||||||
@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>9.0</string>
|
<string>12.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 50;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@ -127,7 +127,7 @@
|
|||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastUpgradeCheck = 1300;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
97C146ED1CF9000F007C117D = {
|
97C146ED1CF9000F007C117D = {
|
||||||
@ -171,10 +171,12 @@
|
|||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
);
|
);
|
||||||
name = "Thin Binary";
|
name = "Thin Binary";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
@ -185,6 +187,7 @@
|
|||||||
};
|
};
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
@ -272,7 +275,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@ -349,7 +352,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@ -398,7 +401,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1300"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@ -45,5 +45,7 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class PopoverMenu extends StatefulWidget {
|
class PopoverMenu extends StatefulWidget {
|
||||||
const PopoverMenu({super.key});
|
const PopoverMenu({super.key});
|
||||||
@ -14,43 +14,32 @@ class _PopoverMenuState extends State<PopoverMenu> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.withOpacity(0.5),
|
color: Colors.grey.withOpacity(0.5),
|
||||||
spreadRadius: 5,
|
spreadRadius: 5,
|
||||||
blurRadius: 7,
|
blurRadius: 7,
|
||||||
offset: const Offset(0, 3), // changes position of shadow
|
offset: const Offset(0, 3), // changes position of shadow
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: ListView(children: [
|
child: ListView(
|
||||||
|
children: [
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.all(8),
|
margin: const EdgeInsets.all(8),
|
||||||
child: const Text("Popover",
|
child: const Text(
|
||||||
style: TextStyle(
|
'Popover',
|
||||||
fontSize: 14,
|
style: TextStyle(
|
||||||
color: Colors.black,
|
fontSize: 14,
|
||||||
fontStyle: null,
|
color: Colors.black,
|
||||||
decoration: null)),
|
),
|
||||||
),
|
|
||||||
Popover(
|
|
||||||
triggerActions:
|
|
||||||
PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
|
||||||
mutex: popOverMutex,
|
|
||||||
offset: const Offset(10, 0),
|
|
||||||
popupBuilder: (BuildContext context) {
|
|
||||||
return const PopoverMenu();
|
|
||||||
},
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {},
|
|
||||||
child: const Text("First"),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Popover(
|
Popover(
|
||||||
@ -63,33 +52,52 @@ class _PopoverMenuState extends State<PopoverMenu> {
|
|||||||
},
|
},
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
child: const Text("Second"),
|
child: const Text('First'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
Popover(
|
||||||
));
|
triggerActions:
|
||||||
|
PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||||
|
mutex: popOverMutex,
|
||||||
|
offset: const Offset(10, 0),
|
||||||
|
popupBuilder: (BuildContext context) {
|
||||||
|
return const PopoverMenu();
|
||||||
|
},
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text('Second'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExampleButton extends StatelessWidget {
|
class ExampleButton extends StatelessWidget {
|
||||||
final String label;
|
|
||||||
final Offset? offset;
|
|
||||||
final PopoverDirection? direction;
|
|
||||||
|
|
||||||
const ExampleButton({
|
const ExampleButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.label,
|
required this.label,
|
||||||
this.direction,
|
required this.direction,
|
||||||
this.offset = Offset.zero,
|
this.offset = Offset.zero,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final Offset? offset;
|
||||||
|
final PopoverDirection direction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Popover(
|
return Popover(
|
||||||
triggerActions: PopoverTriggerFlags.click,
|
triggerActions: PopoverTriggerFlags.click,
|
||||||
|
animationDuration: Durations.medium1,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
direction: direction ?? PopoverDirection.rightWithTopAligned,
|
direction: direction,
|
||||||
child: TextButton(child: Text(label), onPressed: () {}),
|
child: TextButton(
|
||||||
|
child: Text(label),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
popupBuilder: (BuildContext context) {
|
popupBuilder: (BuildContext context) {
|
||||||
return const PopoverMenu();
|
return const PopoverMenu();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import "./example_button.dart";
|
|
||||||
|
import './example_button.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
@ -9,21 +10,11 @@ void main() {
|
|||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
// This widget is the root of your application.
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Flutter Demo',
|
title: 'Flutter Demo',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
// This is the theme of your application.
|
|
||||||
//
|
|
||||||
// Try running your application with "flutter run". You'll see the
|
|
||||||
// application has a blue toolbar. Then, without quitting the app, try
|
|
||||||
// changing the primarySwatch below to Colors.green and then invoke
|
|
||||||
// "hot reload" (press "r" in the console where you ran "flutter run",
|
|
||||||
// or simply save your changes to "hot reload" in a Flutter IDE).
|
|
||||||
// Notice that the counter didn't reset back to zero; the application
|
|
||||||
// is not restarted.
|
|
||||||
primarySwatch: Colors.blue,
|
primarySwatch: Colors.blue,
|
||||||
),
|
),
|
||||||
home: const MyHomePage(title: 'AppFlowy Popover Example'),
|
home: const MyHomePage(title: 'AppFlowy Popover Example'),
|
||||||
@ -34,15 +25,6 @@ class MyApp extends StatelessWidget {
|
|||||||
class MyHomePage extends StatefulWidget {
|
class MyHomePage extends StatefulWidget {
|
||||||
const MyHomePage({super.key, required this.title});
|
const MyHomePage({super.key, required this.title});
|
||||||
|
|
||||||
// This widget is the home page of your application. It is stateful, meaning
|
|
||||||
// that it has a State object (defined below) that contains fields that affect
|
|
||||||
// how it looks.
|
|
||||||
|
|
||||||
// This class is the configuration for the state. It holds the values (in this
|
|
||||||
// case the title) provided by the parent (in this case the App widget) and
|
|
||||||
// used by the build method of the State. Fields in a Widget subclass are
|
|
||||||
// always marked "final".
|
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -52,79 +34,82 @@ class MyHomePage extends StatefulWidget {
|
|||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// This method is rerun every time setState is called, for instance as done
|
|
||||||
// by the _incrementCounter method above.
|
|
||||||
//
|
|
||||||
// The Flutter framework has been optimized to make rerunning build methods
|
|
||||||
// fast, so that you can just rebuild anything that needs updating rather
|
|
||||||
// than having to individually change instances of widgets.
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
// Here we take the value from the MyHomePage object that was created by
|
|
||||||
// the App.build method, and use it to set our appbar title.
|
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
),
|
),
|
||||||
body: const Row(
|
body: const Padding(
|
||||||
children: [
|
padding: EdgeInsets.symmetric(horizontal: 48.0, vertical: 24.0),
|
||||||
Column(children: [
|
child: Row(
|
||||||
ExampleButton(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
label: "Left top",
|
children: [
|
||||||
offset: Offset(0, 10),
|
Column(
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
),
|
|
||||||
Expanded(child: SizedBox.shrink()),
|
|
||||||
ExampleButton(
|
|
||||||
label: "Left bottom",
|
|
||||||
offset: Offset(0, -10),
|
|
||||||
direction: PopoverDirection.topWithLeftAligned,
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
ExampleButton(
|
ExampleButton(
|
||||||
label: "Top",
|
label: 'Left top',
|
||||||
|
offset: Offset(0, 10),
|
||||||
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
|
),
|
||||||
|
ExampleButton(
|
||||||
|
label: 'Left Center',
|
||||||
|
offset: Offset(0, -10),
|
||||||
|
direction: PopoverDirection.rightWithCenterAligned,
|
||||||
|
),
|
||||||
|
ExampleButton(
|
||||||
|
label: 'Left bottom',
|
||||||
|
offset: Offset(0, -10),
|
||||||
|
direction: PopoverDirection.topWithLeftAligned,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ExampleButton(
|
||||||
|
label: 'Top',
|
||||||
offset: Offset(0, 10),
|
offset: Offset(0, 10),
|
||||||
direction: PopoverDirection.bottomWithCenterAligned,
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
),
|
),
|
||||||
Expanded(
|
Column(
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
ExampleButton(
|
||||||
children: [
|
label: 'Central',
|
||||||
ExampleButton(
|
offset: Offset(0, 10),
|
||||||
label: "Central",
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
offset: Offset(0, 10),
|
),
|
||||||
direction: PopoverDirection.bottomWithCenterAligned,
|
],
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ExampleButton(
|
ExampleButton(
|
||||||
label: "Bottom",
|
label: 'Bottom',
|
||||||
offset: Offset(0, -10),
|
offset: Offset(0, -10),
|
||||||
direction: PopoverDirection.topWithCenterAligned,
|
direction: PopoverDirection.topWithCenterAligned,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
Column(
|
||||||
Column(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
ExampleButton(
|
ExampleButton(
|
||||||
label: "Right top",
|
label: 'Right top',
|
||||||
offset: Offset(0, 10),
|
offset: Offset(0, 10),
|
||||||
direction: PopoverDirection.bottomWithRightAligned,
|
direction: PopoverDirection.bottomWithRightAligned,
|
||||||
),
|
),
|
||||||
Expanded(child: SizedBox.shrink()),
|
ExampleButton(
|
||||||
ExampleButton(
|
label: 'Right Center',
|
||||||
label: "Right bottom",
|
offset: Offset(0, 10),
|
||||||
offset: Offset(0, -10),
|
direction: PopoverDirection.leftWithCenterAligned,
|
||||||
direction: PopoverDirection.topWithRightAligned,
|
),
|
||||||
),
|
ExampleButton(
|
||||||
],
|
label: 'Right bottom',
|
||||||
)
|
offset: Offset(0, -10),
|
||||||
],
|
direction: PopoverDirection.topWithRightAligned,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 51;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXAggregateTarget section */
|
/* Begin PBXAggregateTarget section */
|
||||||
@ -182,7 +182,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 0920;
|
LastSwiftUpdateCheck = 0920;
|
||||||
LastUpgradeCheck = 1300;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
33CC10EC2044A3C60003C045 = {
|
33CC10EC2044A3C60003C045 = {
|
||||||
@ -235,6 +235,7 @@
|
|||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
@ -344,7 +345,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
@ -423,7 +424,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@ -470,7 +471,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1300"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
|||||||
@ -27,7 +27,9 @@ class PopoverCompositedTransformFollower extends CompositedTransformFollower {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void updateRenderObject(
|
void updateRenderObject(
|
||||||
BuildContext context, PopoverRenderFollowerLayer renderObject) {
|
BuildContext context,
|
||||||
|
PopoverRenderFollowerLayer renderObject,
|
||||||
|
) {
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
renderObject
|
renderObject
|
||||||
..screenSize = screenSize
|
..screenSize = screenSize
|
||||||
@ -40,8 +42,6 @@ class PopoverCompositedTransformFollower extends CompositedTransformFollower {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PopoverRenderFollowerLayer extends RenderFollowerLayer {
|
class PopoverRenderFollowerLayer extends RenderFollowerLayer {
|
||||||
Size screenSize;
|
|
||||||
|
|
||||||
PopoverRenderFollowerLayer({
|
PopoverRenderFollowerLayer({
|
||||||
required super.link,
|
required super.link,
|
||||||
super.showWhenUnlinked = true,
|
super.showWhenUnlinked = true,
|
||||||
@ -52,6 +52,8 @@ class PopoverRenderFollowerLayer extends RenderFollowerLayer {
|
|||||||
required this.screenSize,
|
required this.screenSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Size screenSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(PaintingContext context, Offset offset) {
|
void paint(PaintingContext context, Offset offset) {
|
||||||
super.paint(context, offset);
|
super.paint(context, offset);
|
||||||
@ -59,13 +61,6 @@ class PopoverRenderFollowerLayer extends RenderFollowerLayer {
|
|||||||
if (link.leader == null) {
|
if (link.leader == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (link.leader!.offset.dx + link.leaderSize!.width + size.width >
|
|
||||||
screenSize.width) {
|
|
||||||
debugPrint("over flow");
|
|
||||||
}
|
|
||||||
debugPrint(
|
|
||||||
"right: ${link.leader!.offset.dx + link.leaderSize!.width + size.width}, screen with: ${screenSize.width}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
import './popover.dart';
|
import './popover.dart';
|
||||||
|
|
||||||
class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
|
class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
|
||||||
PopoverLink link;
|
|
||||||
PopoverDirection direction;
|
|
||||||
final Offset offset;
|
|
||||||
final EdgeInsets windowPadding;
|
|
||||||
|
|
||||||
PopoverLayoutDelegate({
|
PopoverLayoutDelegate({
|
||||||
required this.link,
|
required this.link,
|
||||||
required this.direction,
|
required this.direction,
|
||||||
@ -16,6 +13,11 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
|
|||||||
required this.windowPadding,
|
required this.windowPadding,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
PopoverLink link;
|
||||||
|
PopoverDirection direction;
|
||||||
|
final Offset offset;
|
||||||
|
final EdgeInsets windowPadding;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRelayout(PopoverLayoutDelegate oldDelegate) {
|
bool shouldRelayout(PopoverLayoutDelegate oldDelegate) {
|
||||||
if (direction != oldDelegate.direction) {
|
if (direction != oldDelegate.direction) {
|
||||||
@ -52,141 +54,26 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
|
|||||||
maxHeight:
|
maxHeight:
|
||||||
constraints.maxHeight - windowPadding.top - windowPadding.bottom,
|
constraints.maxHeight - windowPadding.top - windowPadding.bottom,
|
||||||
);
|
);
|
||||||
// assert(link.leaderSize != null);
|
|
||||||
// // if (link.leaderSize == null) {
|
|
||||||
// // return constraints.loosen();
|
|
||||||
// // }
|
|
||||||
// final anchorRect = Rect.fromLTWH(
|
|
||||||
// link.leaderOffset!.dx,
|
|
||||||
// link.leaderOffset!.dy,
|
|
||||||
// link.leaderSize!.width,
|
|
||||||
// link.leaderSize!.height,
|
|
||||||
// );
|
|
||||||
// BoxConstraints childConstraints;
|
|
||||||
// switch (direction) {
|
|
||||||
// case PopoverDirection.topLeft:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// anchorRect.left,
|
|
||||||
// anchorRect.top,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.topRight:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// constraints.maxWidth - anchorRect.right,
|
|
||||||
// anchorRect.top,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.bottomLeft:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// anchorRect.left,
|
|
||||||
// constraints.maxHeight - anchorRect.bottom,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.bottomRight:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// constraints.maxWidth - anchorRect.right,
|
|
||||||
// constraints.maxHeight - anchorRect.bottom,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.center:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// constraints.maxWidth,
|
|
||||||
// constraints.maxHeight,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.topWithLeftAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// constraints.maxWidth - anchorRect.left,
|
|
||||||
// anchorRect.top,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.topWithCenterAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// constraints.maxWidth,
|
|
||||||
// anchorRect.top,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.topWithRightAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// anchorRect.right,
|
|
||||||
// anchorRect.top,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.rightWithTopAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// constraints.maxWidth - anchorRect.right,
|
|
||||||
// constraints.maxHeight - anchorRect.top,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.rightWithCenterAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// constraints.maxWidth - anchorRect.right,
|
|
||||||
// constraints.maxHeight,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.rightWithBottomAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// constraints.maxWidth - anchorRect.right,
|
|
||||||
// anchorRect.bottom,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.bottomWithLeftAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// anchorRect.left,
|
|
||||||
// constraints.maxHeight - anchorRect.bottom,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.bottomWithCenterAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// constraints.maxWidth,
|
|
||||||
// constraints.maxHeight - anchorRect.bottom,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.bottomWithRightAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// anchorRect.right,
|
|
||||||
// constraints.maxHeight - anchorRect.bottom,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.leftWithTopAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// anchorRect.left,
|
|
||||||
// constraints.maxHeight - anchorRect.top,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.leftWithCenterAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// anchorRect.left,
|
|
||||||
// constraints.maxHeight,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.leftWithBottomAligned:
|
|
||||||
// childConstraints = BoxConstraints.loose(Size(
|
|
||||||
// anchorRect.left,
|
|
||||||
// anchorRect.bottom,
|
|
||||||
// ));
|
|
||||||
// break;
|
|
||||||
// case PopoverDirection.custom:
|
|
||||||
// childConstraints = constraints.loosen();
|
|
||||||
// break;
|
|
||||||
// default:
|
|
||||||
// throw UnimplementedError();
|
|
||||||
// }
|
|
||||||
// return childConstraints;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset getPositionForChild(Size size, Size childSize) {
|
Offset getPositionForChild(Size size, Size childSize) {
|
||||||
if (link.leaderSize == null) {
|
final leaderOffset = link.leaderOffset;
|
||||||
|
final leaderSize = link.leaderSize;
|
||||||
|
|
||||||
|
if (leaderOffset == null || leaderSize == null) {
|
||||||
return Offset.zero;
|
return Offset.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
final anchorRect = Rect.fromLTWH(
|
final anchorRect = Rect.fromLTWH(
|
||||||
link.leaderOffset!.dx + offset.dx,
|
leaderOffset.dx + offset.dx,
|
||||||
link.leaderOffset!.dy + offset.dy,
|
leaderOffset.dy + offset.dy,
|
||||||
link.leaderSize!.width,
|
leaderSize.width,
|
||||||
link.leaderSize!.height,
|
leaderSize.height,
|
||||||
);
|
);
|
||||||
|
|
||||||
Offset position;
|
Offset position;
|
||||||
|
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case PopoverDirection.topLeft:
|
case PopoverDirection.topLeft:
|
||||||
position = Offset(
|
position = Offset(
|
||||||
@ -287,27 +174,35 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
|
|||||||
default:
|
default:
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Offset(
|
return Offset(
|
||||||
math.max(
|
math.max(
|
||||||
windowPadding.left,
|
windowPadding.left,
|
||||||
math.min(
|
math.min(
|
||||||
windowPadding.left + size.width - childSize.width, position.dx)),
|
windowPadding.left + size.width - childSize.width,
|
||||||
|
position.dx,
|
||||||
|
),
|
||||||
|
),
|
||||||
math.max(
|
math.max(
|
||||||
windowPadding.top,
|
windowPadding.top,
|
||||||
math.min(
|
math.min(
|
||||||
windowPadding.top + size.height - childSize.height, position.dy)),
|
windowPadding.top + size.height - childSize.height,
|
||||||
|
position.dy,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopoverTarget extends SingleChildRenderObjectWidget {
|
class PopoverTarget extends SingleChildRenderObjectWidget {
|
||||||
final PopoverLink link;
|
|
||||||
const PopoverTarget({
|
const PopoverTarget({
|
||||||
super.key,
|
super.key,
|
||||||
super.child,
|
super.child,
|
||||||
required this.link,
|
required this.link,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final PopoverLink link;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PopoverTargetRenderBox createRenderObject(BuildContext context) {
|
PopoverTargetRenderBox createRenderObject(BuildContext context) {
|
||||||
return PopoverTargetRenderBox(
|
return PopoverTargetRenderBox(
|
||||||
@ -317,14 +212,20 @@ class PopoverTarget extends SingleChildRenderObjectWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void updateRenderObject(
|
void updateRenderObject(
|
||||||
BuildContext context, PopoverTargetRenderBox renderObject) {
|
BuildContext context,
|
||||||
|
PopoverTargetRenderBox renderObject,
|
||||||
|
) {
|
||||||
renderObject.link = link;
|
renderObject.link = link;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopoverTargetRenderBox extends RenderProxyBox {
|
class PopoverTargetRenderBox extends RenderProxyBox {
|
||||||
|
PopoverTargetRenderBox({
|
||||||
|
required this.link,
|
||||||
|
RenderBox? child,
|
||||||
|
}) : super(child);
|
||||||
|
|
||||||
PopoverLink link;
|
PopoverLink link;
|
||||||
PopoverTargetRenderBox({required this.link, RenderBox? child}) : super(child);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get alwaysNeedsCompositing => true;
|
bool get alwaysNeedsCompositing => true;
|
||||||
|
|||||||
@ -3,11 +3,15 @@ import 'dart:collection';
|
|||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
typedef EntryMap = LinkedHashMap<PopoverState, OverlayEntryContext>;
|
typedef _EntryMap = LinkedHashMap<PopoverState, OverlayEntryContext>;
|
||||||
|
|
||||||
class RootOverlayEntry {
|
class RootOverlayEntry {
|
||||||
final EntryMap _entries = EntryMap();
|
final _EntryMap _entries = _EntryMap();
|
||||||
RootOverlayEntry();
|
|
||||||
|
bool contains(PopoverState state) => _entries.containsKey(state);
|
||||||
|
|
||||||
|
bool get isEmpty => _entries.isEmpty;
|
||||||
|
bool get isNotEmpty => _entries.isNotEmpty;
|
||||||
|
|
||||||
void addEntry(
|
void addEntry(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -15,62 +19,54 @@ class RootOverlayEntry {
|
|||||||
OverlayEntry entry,
|
OverlayEntry entry,
|
||||||
bool asBarrier,
|
bool asBarrier,
|
||||||
) {
|
) {
|
||||||
_entries[newState] = OverlayEntryContext(entry, newState, asBarrier);
|
_entries[newState] = OverlayEntryContext(
|
||||||
|
entry,
|
||||||
|
newState,
|
||||||
|
asBarrier,
|
||||||
|
);
|
||||||
Overlay.of(context).insert(entry);
|
Overlay.of(context).insert(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool contains(PopoverState oldState) {
|
void removeEntry(PopoverState state) {
|
||||||
return _entries.containsKey(oldState);
|
final removedEntry = _entries.remove(state);
|
||||||
}
|
|
||||||
|
|
||||||
void removeEntry(PopoverState oldState) {
|
|
||||||
if (_entries.isEmpty) return;
|
|
||||||
|
|
||||||
final removedEntry = _entries.remove(oldState);
|
|
||||||
removedEntry?.overlayEntry.remove();
|
removedEntry?.overlayEntry.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isEmpty => _entries.isEmpty;
|
|
||||||
|
|
||||||
bool get isNotEmpty => _entries.isNotEmpty;
|
|
||||||
|
|
||||||
bool hasEntry() {
|
|
||||||
return _entries.isNotEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
PopoverState? popEntry() {
|
PopoverState? popEntry() {
|
||||||
if (_entries.isEmpty) return null;
|
if (isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final lastEntry = _entries.values.last;
|
final lastEntry = _entries.values.last;
|
||||||
_entries.remove(lastEntry.popoverState);
|
_entries.remove(lastEntry.popoverState);
|
||||||
lastEntry.overlayEntry.remove();
|
lastEntry.overlayEntry.remove();
|
||||||
lastEntry.popoverState.widget.onClose?.call();
|
lastEntry.popoverState.widget.onClose?.call();
|
||||||
|
|
||||||
if (lastEntry.asBarrier) {
|
return lastEntry.asBarrier ? lastEntry.popoverState : popEntry();
|
||||||
return lastEntry.popoverState;
|
|
||||||
} else {
|
|
||||||
return popEntry();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OverlayEntryContext {
|
class OverlayEntryContext {
|
||||||
final bool asBarrier;
|
|
||||||
final PopoverState popoverState;
|
|
||||||
final OverlayEntry overlayEntry;
|
|
||||||
|
|
||||||
OverlayEntryContext(
|
OverlayEntryContext(
|
||||||
this.overlayEntry,
|
this.overlayEntry,
|
||||||
this.popoverState,
|
this.popoverState,
|
||||||
this.asBarrier,
|
this.asBarrier,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final OverlayEntry overlayEntry;
|
||||||
|
final PopoverState popoverState;
|
||||||
|
final bool asBarrier;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopoverMask extends StatelessWidget {
|
class PopoverMask extends StatelessWidget {
|
||||||
final void Function() onTap;
|
const PopoverMask({
|
||||||
final Decoration? decoration;
|
super.key,
|
||||||
|
required this.onTap,
|
||||||
|
this.decoration,
|
||||||
|
});
|
||||||
|
|
||||||
const PopoverMask({super.key, required this.onTap, this.decoration});
|
final VoidCallback onTap;
|
||||||
|
final Decoration? decoration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -5,22 +5,18 @@ import 'popover.dart';
|
|||||||
/// If multiple popovers are exclusive,
|
/// If multiple popovers are exclusive,
|
||||||
/// pass the same mutex to them.
|
/// pass the same mutex to them.
|
||||||
class PopoverMutex {
|
class PopoverMutex {
|
||||||
final _PopoverStateNotifier _stateNotifier = _PopoverStateNotifier();
|
|
||||||
PopoverMutex();
|
PopoverMutex();
|
||||||
|
|
||||||
|
final _PopoverStateNotifier _stateNotifier = _PopoverStateNotifier();
|
||||||
|
|
||||||
|
void addPopoverListener(VoidCallback listener) {
|
||||||
|
_stateNotifier.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
void removePopoverListener(VoidCallback listener) {
|
void removePopoverListener(VoidCallback listener) {
|
||||||
_stateNotifier.removeListener(listener);
|
_stateNotifier.removeListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
VoidCallback listenOnPopoverChanged(VoidCallback callback) {
|
|
||||||
listenerCallback() {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
_stateNotifier.addListener(listenerCallback);
|
|
||||||
return listenerCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void close() => _stateNotifier.state?.close();
|
void close() => _stateNotifier.state?.close();
|
||||||
|
|
||||||
PopoverState? get state => _stateNotifier.state;
|
PopoverState? get state => _stateNotifier.state;
|
||||||
|
|||||||
@ -54,6 +54,33 @@ enum PopoverClickHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Popover extends StatefulWidget {
|
class Popover extends StatefulWidget {
|
||||||
|
const Popover({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
required this.popupBuilder,
|
||||||
|
this.controller,
|
||||||
|
this.offset,
|
||||||
|
this.triggerActions = 0,
|
||||||
|
this.direction = PopoverDirection.rightWithTopAligned,
|
||||||
|
this.mutex,
|
||||||
|
this.windowPadding,
|
||||||
|
this.onOpen,
|
||||||
|
this.onClose,
|
||||||
|
this.canClose,
|
||||||
|
this.asBarrier = false,
|
||||||
|
this.clickHandler = PopoverClickHandler.listener,
|
||||||
|
this.skipTraversal = false,
|
||||||
|
this.animationDuration = const Duration(milliseconds: 200),
|
||||||
|
this.beginOpacity = 0.0,
|
||||||
|
this.endOpacity = 1.0,
|
||||||
|
this.beginScaleFactor = 0.95,
|
||||||
|
this.endScaleFactor = 1.0,
|
||||||
|
this.slideDistance = 20.0,
|
||||||
|
this.maskDecoration = const BoxDecoration(
|
||||||
|
color: Color.fromARGB(0, 244, 67, 54),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
final PopoverController? controller;
|
final PopoverController? controller;
|
||||||
|
|
||||||
/// The offset from the [child] where the popover will be drawn
|
/// The offset from the [child] where the popover will be drawn
|
||||||
@ -93,113 +120,69 @@ class Popover extends StatefulWidget {
|
|||||||
|
|
||||||
final bool skipTraversal;
|
final bool skipTraversal;
|
||||||
|
|
||||||
|
/// Animation time of the popover.
|
||||||
|
final Duration? animationDuration;
|
||||||
|
|
||||||
|
/// The distance of the popover's slide animation.
|
||||||
|
final double slideDistance;
|
||||||
|
|
||||||
|
/// The scale factor of the popover's scale animation.
|
||||||
|
final double beginScaleFactor;
|
||||||
|
final double endScaleFactor;
|
||||||
|
|
||||||
|
/// The opacity of the popover's fade animation.
|
||||||
|
final double beginOpacity;
|
||||||
|
final double endOpacity;
|
||||||
|
|
||||||
/// The content area of the popover.
|
/// The content area of the popover.
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const Popover({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
required this.popupBuilder,
|
|
||||||
this.controller,
|
|
||||||
this.offset,
|
|
||||||
this.maskDecoration = const BoxDecoration(
|
|
||||||
color: Color.fromARGB(0, 244, 67, 54),
|
|
||||||
),
|
|
||||||
this.triggerActions = 0,
|
|
||||||
this.direction = PopoverDirection.rightWithTopAligned,
|
|
||||||
this.mutex,
|
|
||||||
this.windowPadding,
|
|
||||||
this.onOpen,
|
|
||||||
this.onClose,
|
|
||||||
this.canClose,
|
|
||||||
this.asBarrier = false,
|
|
||||||
this.clickHandler = PopoverClickHandler.listener,
|
|
||||||
this.skipTraversal = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<Popover> createState() => PopoverState();
|
State<Popover> createState() => PopoverState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopoverState extends State<Popover> {
|
class PopoverState extends State<Popover> with SingleTickerProviderStateMixin {
|
||||||
static final RootOverlayEntry _rootEntry = RootOverlayEntry();
|
static final RootOverlayEntry rootEntry = RootOverlayEntry();
|
||||||
|
|
||||||
final PopoverLink popoverLink = PopoverLink();
|
final PopoverLink popoverLink = PopoverLink();
|
||||||
|
late final layoutDelegate = PopoverLayoutDelegate(
|
||||||
|
direction: widget.direction,
|
||||||
|
link: popoverLink,
|
||||||
|
offset: widget.offset ?? Offset.zero,
|
||||||
|
windowPadding: widget.windowPadding ?? EdgeInsets.zero,
|
||||||
|
);
|
||||||
|
|
||||||
|
late AnimationController animationController;
|
||||||
|
late Animation<double> fadeAnimation;
|
||||||
|
late Animation<double> scaleAnimation;
|
||||||
|
late Animation<Offset> slideAnimation;
|
||||||
|
|
||||||
|
// If the widget is disposed, prevent the animation from being called.
|
||||||
|
bool isDisposed = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
widget.controller?._state = this;
|
widget.controller?._state = this;
|
||||||
}
|
_buildAnimations();
|
||||||
|
|
||||||
void showOverlay() {
|
|
||||||
close();
|
|
||||||
|
|
||||||
if (widget.mutex != null) {
|
|
||||||
widget.mutex?.state = this;
|
|
||||||
}
|
|
||||||
final shouldAddMask = _rootEntry.isEmpty;
|
|
||||||
final newEntry = OverlayEntry(builder: (context) {
|
|
||||||
final children = <Widget>[];
|
|
||||||
if (shouldAddMask) {
|
|
||||||
children.add(
|
|
||||||
PopoverMask(
|
|
||||||
decoration: widget.maskDecoration,
|
|
||||||
onTap: () async {
|
|
||||||
if (!(await widget.canClose?.call() ?? true)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_removeRootOverlay();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
children.add(
|
|
||||||
PopoverContainer(
|
|
||||||
direction: widget.direction,
|
|
||||||
popoverLink: popoverLink,
|
|
||||||
offset: widget.offset ?? Offset.zero,
|
|
||||||
windowPadding: widget.windowPadding ?? EdgeInsets.zero,
|
|
||||||
popupBuilder: widget.popupBuilder,
|
|
||||||
onClose: close,
|
|
||||||
onCloseAll: _removeRootOverlay,
|
|
||||||
skipTraversal: widget.skipTraversal,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return CallbackShortcuts(
|
|
||||||
bindings: {
|
|
||||||
const SingleActivator(LogicalKeyboardKey.escape): _removeRootOverlay,
|
|
||||||
},
|
|
||||||
child: FocusScope(child: Stack(children: children)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
_rootEntry.addEntry(context, this, newEntry, widget.asBarrier);
|
|
||||||
}
|
|
||||||
|
|
||||||
void close({bool notify = true}) {
|
|
||||||
if (_rootEntry.contains(this)) {
|
|
||||||
_rootEntry.removeEntry(this);
|
|
||||||
if (notify) {
|
|
||||||
widget.onClose?.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _removeRootOverlay() {
|
|
||||||
_rootEntry.popEntry();
|
|
||||||
|
|
||||||
if (widget.mutex?.state == this) {
|
|
||||||
widget.mutex?.removeState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void deactivate() {
|
void deactivate() {
|
||||||
close(notify: false);
|
close(notify: false);
|
||||||
|
|
||||||
super.deactivate();
|
super.deactivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
isDisposed = true;
|
||||||
|
animationController.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopoverTarget(
|
return PopoverTarget(
|
||||||
@ -208,6 +191,66 @@ class PopoverState extends State<Popover> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reassemble() {
|
||||||
|
// clear the overlay
|
||||||
|
while (rootEntry.isNotEmpty) {
|
||||||
|
rootEntry.popEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.reassemble();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showOverlay() {
|
||||||
|
close();
|
||||||
|
|
||||||
|
if (widget.mutex != null) {
|
||||||
|
widget.mutex?.state = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
final shouldAddMask = rootEntry.isEmpty;
|
||||||
|
rootEntry.addEntry(
|
||||||
|
context,
|
||||||
|
this,
|
||||||
|
OverlayEntry(
|
||||||
|
builder: (context) => _buildOverlayContent(shouldAddMask),
|
||||||
|
),
|
||||||
|
widget.asBarrier,
|
||||||
|
);
|
||||||
|
|
||||||
|
animationController.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
void close({
|
||||||
|
bool notify = true,
|
||||||
|
bool withAnimation = false,
|
||||||
|
}) {
|
||||||
|
if (rootEntry.contains(this)) {
|
||||||
|
void callback() {
|
||||||
|
rootEntry.removeEntry(this);
|
||||||
|
if (notify) {
|
||||||
|
widget.onClose?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDisposed || !withAnimation) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
animationController.reverse().then((_) => callback());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeRootOverlay() {
|
||||||
|
animationController.reverse().then((_) {
|
||||||
|
rootEntry.popEntry();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (widget.mutex?.state == this) {
|
||||||
|
widget.mutex?.removeState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildChild(BuildContext context) {
|
Widget _buildChild(BuildContext context) {
|
||||||
if (widget.triggerActions == 0) {
|
if (widget.triggerActions == 0) {
|
||||||
return widget.child;
|
return widget.child;
|
||||||
@ -247,36 +290,182 @@ class PopoverState extends State<Popover> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _callHandler(VoidCallback handler) {
|
void _callHandler(VoidCallback handler) {
|
||||||
if (_rootEntry.contains(this)) {
|
if (rootEntry.contains(this)) {
|
||||||
close();
|
close();
|
||||||
} else {
|
} else {
|
||||||
handler();
|
handler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildOverlayContent(bool shouldAddMask) {
|
||||||
|
return CallbackShortcuts(
|
||||||
|
bindings: {
|
||||||
|
const SingleActivator(LogicalKeyboardKey.escape): _removeRootOverlay,
|
||||||
|
},
|
||||||
|
child: FocusScope(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (shouldAddMask) _buildMask(),
|
||||||
|
_buildPopoverContainer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMask() {
|
||||||
|
return PopoverMask(
|
||||||
|
decoration: widget.maskDecoration,
|
||||||
|
onTap: () async {
|
||||||
|
if (await widget.canClose?.call() ?? true) {
|
||||||
|
_removeRootOverlay();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPopoverContainer() {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: animationController,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: fadeAnimation.value,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: scaleAnimation.value,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: slideAnimation.value,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: PopoverContainer(
|
||||||
|
delegate: layoutDelegate,
|
||||||
|
popupBuilder: widget.popupBuilder,
|
||||||
|
skipTraversal: widget.skipTraversal,
|
||||||
|
onClose: close,
|
||||||
|
onCloseAll: _removeRootOverlay,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _buildAnimations() {
|
||||||
|
animationController = AnimationController(
|
||||||
|
duration: widget.animationDuration,
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
fadeAnimation = _buildFadeAnimation();
|
||||||
|
scaleAnimation = _buildScaleAnimation();
|
||||||
|
slideAnimation = _buildSlideAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation<double> _buildFadeAnimation() {
|
||||||
|
return Tween<double>(
|
||||||
|
begin: widget.beginOpacity,
|
||||||
|
end: widget.endOpacity,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: animationController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation<double> _buildScaleAnimation() {
|
||||||
|
return Tween<double>(
|
||||||
|
begin: widget.beginScaleFactor,
|
||||||
|
end: widget.endScaleFactor,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: animationController,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation<Offset> _buildSlideAnimation() {
|
||||||
|
final values = _getSlideAnimationValues();
|
||||||
|
return Tween<Offset>(
|
||||||
|
begin: values.$1,
|
||||||
|
end: values.$2,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: animationController,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(Offset, Offset) _getSlideAnimationValues() {
|
||||||
|
final slideDistance = widget.slideDistance;
|
||||||
|
|
||||||
|
switch (widget.direction) {
|
||||||
|
case PopoverDirection.bottomWithLeftAligned:
|
||||||
|
return (
|
||||||
|
Offset(-slideDistance, -slideDistance),
|
||||||
|
Offset.zero,
|
||||||
|
);
|
||||||
|
case PopoverDirection.bottomWithCenterAligned:
|
||||||
|
return (
|
||||||
|
Offset(0, -slideDistance),
|
||||||
|
Offset.zero,
|
||||||
|
);
|
||||||
|
case PopoverDirection.bottomWithRightAligned:
|
||||||
|
return (
|
||||||
|
Offset(slideDistance, -slideDistance),
|
||||||
|
Offset.zero,
|
||||||
|
);
|
||||||
|
case PopoverDirection.topWithLeftAligned:
|
||||||
|
return (
|
||||||
|
Offset(-slideDistance, slideDistance),
|
||||||
|
Offset.zero,
|
||||||
|
);
|
||||||
|
case PopoverDirection.topWithCenterAligned:
|
||||||
|
return (
|
||||||
|
Offset(0, slideDistance),
|
||||||
|
Offset.zero,
|
||||||
|
);
|
||||||
|
case PopoverDirection.topWithRightAligned:
|
||||||
|
return (
|
||||||
|
Offset(slideDistance, slideDistance),
|
||||||
|
Offset.zero,
|
||||||
|
);
|
||||||
|
case PopoverDirection.leftWithTopAligned:
|
||||||
|
case PopoverDirection.leftWithCenterAligned:
|
||||||
|
case PopoverDirection.leftWithBottomAligned:
|
||||||
|
return (
|
||||||
|
Offset(slideDistance, 0),
|
||||||
|
Offset.zero,
|
||||||
|
);
|
||||||
|
case PopoverDirection.rightWithTopAligned:
|
||||||
|
case PopoverDirection.rightWithCenterAligned:
|
||||||
|
case PopoverDirection.rightWithBottomAligned:
|
||||||
|
return (
|
||||||
|
Offset(-slideDistance, 0),
|
||||||
|
Offset.zero,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (Offset.zero, Offset.zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopoverContainer extends StatefulWidget {
|
class PopoverContainer extends StatefulWidget {
|
||||||
final Widget? Function(BuildContext context) popupBuilder;
|
|
||||||
final PopoverDirection direction;
|
|
||||||
final PopoverLink popoverLink;
|
|
||||||
final Offset offset;
|
|
||||||
final EdgeInsets windowPadding;
|
|
||||||
final void Function() onClose;
|
|
||||||
final void Function() onCloseAll;
|
|
||||||
final bool skipTraversal;
|
|
||||||
|
|
||||||
const PopoverContainer({
|
const PopoverContainer({
|
||||||
super.key,
|
super.key,
|
||||||
required this.popupBuilder,
|
required this.popupBuilder,
|
||||||
required this.direction,
|
required this.delegate,
|
||||||
required this.popoverLink,
|
|
||||||
required this.offset,
|
|
||||||
required this.windowPadding,
|
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
required this.onCloseAll,
|
required this.onCloseAll,
|
||||||
required this.skipTraversal,
|
required this.skipTraversal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final Widget? Function(BuildContext context) popupBuilder;
|
||||||
|
final void Function() onClose;
|
||||||
|
final void Function() onCloseAll;
|
||||||
|
final bool skipTraversal;
|
||||||
|
final PopoverLayoutDelegate delegate;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => PopoverContainerState();
|
State<StatefulWidget> createState() => PopoverContainerState();
|
||||||
|
|
||||||
@ -302,12 +491,7 @@ class PopoverContainerState extends State<PopoverContainer> {
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
skipTraversal: widget.skipTraversal,
|
skipTraversal: widget.skipTraversal,
|
||||||
child: CustomSingleChildLayout(
|
child: CustomSingleChildLayout(
|
||||||
delegate: PopoverLayoutDelegate(
|
delegate: widget.delegate,
|
||||||
direction: widget.direction,
|
|
||||||
link: widget.popoverLink,
|
|
||||||
offset: widget.offset,
|
|
||||||
windowPadding: widget.windowPadding,
|
|
||||||
),
|
|
||||||
child: widget.popupBuilder(context),
|
child: widget.popupBuilder(context),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,8 +4,8 @@ version: 0.0.1
|
|||||||
homepage: https://appflowy.io
|
homepage: https://appflowy.io
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
flutter: ">=3.22.0"
|
||||||
flutter: ">=3.10.1"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@ -14,41 +14,4 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.1
|
flutter_lints: ^4.0.0
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
|
||||||
flutter:
|
|
||||||
|
|
||||||
# To add assets to your package, add an assets section, like this:
|
|
||||||
# assets:
|
|
||||||
# - images/a_dot_burr.jpeg
|
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
#
|
|
||||||
# For details regarding assets in packages, see
|
|
||||||
# https://flutter.dev/assets-and-images/#from-packages
|
|
||||||
#
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
|
||||||
|
|
||||||
# To add custom fonts to your package, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts in packages, see
|
|
||||||
# https://flutter.dev/custom-fonts/#from-packages
|
|
||||||
|
|||||||
@ -1,37 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra/colorscheme/default_colorscheme.dart';
|
import 'package:flowy_infra/colorscheme/default_colorscheme.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AppFlowyPopover extends StatelessWidget {
|
class AppFlowyPopover extends StatelessWidget {
|
||||||
final Widget child;
|
|
||||||
final PopoverController? controller;
|
|
||||||
final Widget Function(BuildContext context) popupBuilder;
|
|
||||||
final PopoverDirection direction;
|
|
||||||
final int triggerActions;
|
|
||||||
final BoxConstraints constraints;
|
|
||||||
final VoidCallback? onOpen;
|
|
||||||
final VoidCallback? onClose;
|
|
||||||
final Future<bool> Function()? canClose;
|
|
||||||
final PopoverMutex? mutex;
|
|
||||||
final Offset? offset;
|
|
||||||
final bool asBarrier;
|
|
||||||
final EdgeInsets margin;
|
|
||||||
final EdgeInsets windowPadding;
|
|
||||||
final Color? decorationColor;
|
|
||||||
final BorderRadius? borderRadius;
|
|
||||||
|
|
||||||
/// The widget that will be used to trigger the popover.
|
|
||||||
///
|
|
||||||
/// Why do we need this?
|
|
||||||
/// Because if the parent widget of the popover is GestureDetector,
|
|
||||||
/// the conflict won't be resolve by using Listener, we want these two gestures exclusive.
|
|
||||||
final PopoverClickHandler clickHandler;
|
|
||||||
|
|
||||||
/// If true the popover will not participate in focus traversal.
|
|
||||||
///
|
|
||||||
final bool skipTraversal;
|
|
||||||
|
|
||||||
const AppFlowyPopover({
|
const AppFlowyPopover({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
@ -52,12 +23,46 @@ class AppFlowyPopover extends StatelessWidget {
|
|||||||
this.skipTraversal = false,
|
this.skipTraversal = false,
|
||||||
this.decorationColor,
|
this.decorationColor,
|
||||||
this.borderRadius,
|
this.borderRadius,
|
||||||
|
this.animationDuration = const Duration(),
|
||||||
|
this.slideDistance = 20.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final PopoverController? controller;
|
||||||
|
final Widget Function(BuildContext context) popupBuilder;
|
||||||
|
final PopoverDirection direction;
|
||||||
|
final int triggerActions;
|
||||||
|
final BoxConstraints constraints;
|
||||||
|
final VoidCallback? onOpen;
|
||||||
|
final VoidCallback? onClose;
|
||||||
|
final Future<bool> Function()? canClose;
|
||||||
|
final PopoverMutex? mutex;
|
||||||
|
final Offset? offset;
|
||||||
|
final bool asBarrier;
|
||||||
|
final EdgeInsets margin;
|
||||||
|
final EdgeInsets windowPadding;
|
||||||
|
final Color? decorationColor;
|
||||||
|
final BorderRadius? borderRadius;
|
||||||
|
final Duration animationDuration;
|
||||||
|
final double slideDistance;
|
||||||
|
|
||||||
|
/// The widget that will be used to trigger the popover.
|
||||||
|
///
|
||||||
|
/// Why do we need this?
|
||||||
|
/// Because if the parent widget of the popover is GestureDetector,
|
||||||
|
/// the conflict won't be resolve by using Listener, we want these two gestures exclusive.
|
||||||
|
final PopoverClickHandler clickHandler;
|
||||||
|
|
||||||
|
/// If true the popover will not participate in focus traversal.
|
||||||
|
///
|
||||||
|
final bool skipTraversal;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Popover(
|
return Popover(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
animationDuration: animationDuration,
|
||||||
|
slideDistance: slideDistance,
|
||||||
onOpen: onOpen,
|
onOpen: onOpen,
|
||||||
onClose: onClose,
|
onClose: onClose,
|
||||||
canClose: canClose,
|
canClose: canClose,
|
||||||
|
|||||||
@ -1535,10 +1535,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -1933,10 +1933,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
string_validator:
|
string_validator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -2238,10 +2238,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.1"
|
version: "14.2.5"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user