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:
Lucas 2024-10-09 15:10:05 +08:00 committed by GitHub
parent 580a23f3f5
commit 8cf683eb50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 700 additions and 563 deletions

View File

@ -11,7 +11,7 @@ void main() {
const emoji = '😁';
group('Icon', () {
group('Icon:', () {
testWidgets('Update page icon in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@ -52,6 +52,7 @@ void main() {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,

View File

@ -3,7 +3,9 @@ import 'package:scaled_app/scaled_app.dart';
import 'startup/startup.dart';
Future<void> main() async {
ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0);
ScaledWidgetsFlutterBinding.ensureInitialized(
scaleFactor: (_) => 1.0,
);
await runAppFlowy();
}

View File

@ -629,23 +629,23 @@ class FieldNameTextField extends StatefulWidget {
}
class _FieldNameTextFieldState extends State<FieldNameTextField> {
FocusNode focusNode = FocusNode();
final focusNode = FocusNode();
@override
void initState() {
super.initState();
focusNode.addListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
focusNode.addListener(_onFocusChanged);
widget.popoverMutex?.addPopoverListener(_onPopoverChanged);
}
widget.popoverMutex?.listenOnPopoverChanged(() {
if (focusNode.hasFocus) {
focusNode.unfocus();
}
});
@override
void dispose() {
widget.popoverMutex?.removePopoverListener(_onPopoverChanged);
focusNode.removeListener(_onFocusChanged);
focusNode.dispose();
super.dispose();
}
@override
@ -662,15 +662,16 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
);
}
@override
void dispose() {
focusNode.removeListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
focusNode.dispose();
super.dispose();
void _onFocusChanged() {
if (focusNode.hasFocus) {
widget.popoverMutex?.close();
}
}
void _onPopoverChanged() {
if (focusNode.hasFocus) {
focusNode.unfocus();
}
}
}

View File

@ -206,22 +206,23 @@ class CreateOptionTextField extends StatefulWidget {
}
class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
late final FocusNode _focusNode;
final focusNode = FocusNode();
@override
void initState() {
super.initState();
_focusNode = FocusNode()
..addListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
widget.popoverMutex?.listenOnPopoverChanged(() {
if (_focusNode.hasFocus) {
_focusNode.unfocus();
}
});
focusNode.addListener(_onFocusChanged);
widget.popoverMutex?.addPopoverListener(_onPopoverChanged);
}
@override
void dispose() {
widget.popoverMutex?.removePopoverListener(_onPopoverChanged);
focusNode.removeListener(_onFocusChanged);
focusNode.dispose();
super.dispose();
}
@override
@ -234,7 +235,7 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
child: FlowyTextField(
autoClearWhenDone: true,
text: text,
focusNode: _focusNode,
focusNode: focusNode,
onCanceled: () {
context
.read<SelectOptionTypeOptionBloc>()
@ -252,15 +253,16 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
);
}
@override
void dispose() {
_focusNode.removeListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
_focusNode.dispose();
super.dispose();
void _onFocusChanged() {
if (focusNode.hasFocus) {
widget.popoverMutex?.close();
}
}
void _onPopoverChanged() {
if (focusNode.hasFocus) {
focusNode.unfocus();
}
}
}

View File

@ -70,12 +70,12 @@ class _BlockOptionButtonState extends State<BlockOptionButton> {
Widget build(BuildContext context) {
return PopoverActionList<PopoverAction>(
popoverMutex: PopoverMutex(),
actions: popoverActions,
direction:
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
LayoutDirection.rtlLayout
? PopoverDirection.rightWithCenterAligned
: PopoverDirection.leftWithCenterAligned,
actions: popoverActions,
onPopupBuilder: () {
keepEditorFocusNotifier.increase();
widget.blockComponentState.alwaysShowActions = true;

View File

@ -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_popover/appflowy_popover.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';
const _maxLengthTwelveHour = 8;
@ -64,7 +63,7 @@ class _TimeTextFieldState extends State<TimeTextField> {
}
_focusNode.addListener(_focusNodeListener);
widget.popoverMutex?.listenOnPopoverChanged(_popoverListener);
widget.popoverMutex?.addPopoverListener(_popoverListener);
}
@override

View File

@ -17,6 +17,8 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
this.direction = PopoverDirection.rightWithTopAligned,
this.asBarrier = false,
this.offset = Offset.zero,
this.animationDuration = const Duration(),
this.slideDistance = 20,
this.constraints = const BoxConstraints(
minWidth: 120,
maxWidth: 460,
@ -35,6 +37,8 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
final bool asBarrier;
final Offset offset;
final BoxConstraints constraints;
final Duration animationDuration;
final double slideDistance;
@override
State<PopoverActionList<T>> createState() => _PopoverActionListState<T>();
@ -55,6 +59,8 @@ class _PopoverActionListState<T extends PopoverAction>
final child = widget.buildChild(popoverController);
return AppFlowyPopover(
asBarrier: widget.asBarrier,
animationDuration: widget.animationDuration,
slideDistance: widget.slideDistance,
controller: popoverController,
constraints: widget.constraints,
direction: widget.direction,

View File

@ -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
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
# https://dart.dev/guides/language/analysis-options
errors:
invalid_annotation_target: ignore

View File

@ -7,8 +7,14 @@
# 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
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`
@ -22,8 +28,33 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
- 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
# https://dart.dev/guides/language/analysis-options
errors:
invalid_annotation_target: ignore

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>12.0</string>
</dict>
</plist>

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -127,7 +127,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@ -171,10 +171,12 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@ -185,6 +187,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@ -272,7 +275,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -349,7 +352,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -398,7 +401,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -45,5 +45,7 @@
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
class PopoverMenu extends StatefulWidget {
const PopoverMenu({super.key});
@ -14,43 +14,32 @@ class _PopoverMenuState extends State<PopoverMenu> {
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(8)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
child: ListView(children: [
type: MaterialType.transparency,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(8)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
child: ListView(
children: [
Container(
margin: const EdgeInsets.all(8),
child: const Text("Popover",
style: TextStyle(
fontSize: 14,
color: Colors.black,
fontStyle: null,
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"),
child: const Text(
'Popover',
style: TextStyle(
fontSize: 14,
color: Colors.black,
),
),
),
Popover(
@ -63,33 +52,52 @@ class _PopoverMenuState extends State<PopoverMenu> {
},
child: TextButton(
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 {
final String label;
final Offset? offset;
final PopoverDirection? direction;
const ExampleButton({
super.key,
required this.label,
this.direction,
required this.direction,
this.offset = Offset.zero,
});
final String label;
final Offset? offset;
final PopoverDirection direction;
@override
Widget build(BuildContext context) {
return Popover(
triggerActions: PopoverTriggerFlags.click,
animationDuration: Durations.medium1,
offset: offset,
direction: direction ?? PopoverDirection.rightWithTopAligned,
child: TextButton(child: Text(label), onPressed: () {}),
direction: direction,
child: TextButton(
child: Text(label),
onPressed: () {},
),
popupBuilder: (BuildContext context) {
return const PopoverMenu();
},

View File

@ -1,6 +1,7 @@
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import "./example_button.dart";
import './example_button.dart';
void main() {
runApp(const MyApp());
@ -9,21 +10,11 @@ void main() {
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
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,
),
home: const MyHomePage(title: 'AppFlowy Popover Example'),
@ -34,15 +25,6 @@ class MyApp extends StatelessWidget {
class MyHomePage extends StatefulWidget {
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;
@override
@ -52,79 +34,82 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> {
@override
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(
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),
),
body: const Row(
children: [
Column(children: [
ExampleButton(
label: "Left top",
offset: Offset(0, 10),
direction: PopoverDirection.bottomWithLeftAligned,
),
Expanded(child: SizedBox.shrink()),
ExampleButton(
label: "Left bottom",
offset: Offset(0, -10),
direction: PopoverDirection.topWithLeftAligned,
),
]),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
body: const Padding(
padding: EdgeInsets.symmetric(horizontal: 48.0, vertical: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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),
direction: PopoverDirection.bottomWithCenterAligned,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ExampleButton(
label: "Central",
offset: Offset(0, 10),
direction: PopoverDirection.bottomWithCenterAligned,
),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ExampleButton(
label: 'Central',
offset: Offset(0, 10),
direction: PopoverDirection.bottomWithCenterAligned,
),
],
),
ExampleButton(
label: "Bottom",
label: 'Bottom',
offset: Offset(0, -10),
direction: PopoverDirection.topWithCenterAligned,
),
],
),
),
Column(
children: [
ExampleButton(
label: "Right top",
offset: Offset(0, 10),
direction: PopoverDirection.bottomWithRightAligned,
),
Expanded(child: SizedBox.shrink()),
ExampleButton(
label: "Right bottom",
offset: Offset(0, -10),
direction: PopoverDirection.topWithRightAligned,
),
],
)
],
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ExampleButton(
label: 'Right top',
offset: Offset(0, 10),
direction: PopoverDirection.bottomWithRightAligned,
),
ExampleButton(
label: 'Right Center',
offset: Offset(0, 10),
direction: PopoverDirection.leftWithCenterAligned,
),
ExampleButton(
label: 'Right bottom',
offset: Offset(0, -10),
direction: PopoverDirection.topWithRightAligned,
),
],
),
],
),
),
);
}

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
@ -182,7 +182,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
33CC10EC2044A3C60003C045 = {
@ -235,6 +235,7 @@
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@ -344,7 +345,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@ -423,7 +424,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@ -470,7 +471,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
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.
# To automatically upgrade your package dependencies to the latest versions

View File

@ -27,7 +27,9 @@ class PopoverCompositedTransformFollower extends CompositedTransformFollower {
@override
void updateRenderObject(
BuildContext context, PopoverRenderFollowerLayer renderObject) {
BuildContext context,
PopoverRenderFollowerLayer renderObject,
) {
final screenSize = MediaQuery.of(context).size;
renderObject
..screenSize = screenSize
@ -40,8 +42,6 @@ class PopoverCompositedTransformFollower extends CompositedTransformFollower {
}
class PopoverRenderFollowerLayer extends RenderFollowerLayer {
Size screenSize;
PopoverRenderFollowerLayer({
required super.link,
super.showWhenUnlinked = true,
@ -52,6 +52,8 @@ class PopoverRenderFollowerLayer extends RenderFollowerLayer {
required this.screenSize,
});
Size screenSize;
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
@ -59,13 +61,6 @@ class PopoverRenderFollowerLayer extends RenderFollowerLayer {
if (link.leader == null) {
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}");
}
}

View File

@ -1,14 +1,11 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import './popover.dart';
class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
PopoverLink link;
PopoverDirection direction;
final Offset offset;
final EdgeInsets windowPadding;
PopoverLayoutDelegate({
required this.link,
required this.direction,
@ -16,6 +13,11 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
required this.windowPadding,
});
PopoverLink link;
PopoverDirection direction;
final Offset offset;
final EdgeInsets windowPadding;
@override
bool shouldRelayout(PopoverLayoutDelegate oldDelegate) {
if (direction != oldDelegate.direction) {
@ -52,141 +54,26 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
maxHeight:
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
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;
}
final anchorRect = Rect.fromLTWH(
link.leaderOffset!.dx + offset.dx,
link.leaderOffset!.dy + offset.dy,
link.leaderSize!.width,
link.leaderSize!.height,
leaderOffset.dx + offset.dx,
leaderOffset.dy + offset.dy,
leaderSize.width,
leaderSize.height,
);
Offset position;
switch (direction) {
case PopoverDirection.topLeft:
position = Offset(
@ -287,27 +174,35 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
default:
throw UnimplementedError();
}
return Offset(
math.max(
windowPadding.left,
math.min(
windowPadding.left + size.width - childSize.width, position.dx)),
windowPadding.left,
math.min(
windowPadding.left + size.width - childSize.width,
position.dx,
),
),
math.max(
windowPadding.top,
math.min(
windowPadding.top + size.height - childSize.height, position.dy)),
windowPadding.top,
math.min(
windowPadding.top + size.height - childSize.height,
position.dy,
),
),
);
}
}
class PopoverTarget extends SingleChildRenderObjectWidget {
final PopoverLink link;
const PopoverTarget({
super.key,
super.child,
required this.link,
});
final PopoverLink link;
@override
PopoverTargetRenderBox createRenderObject(BuildContext context) {
return PopoverTargetRenderBox(
@ -317,14 +212,20 @@ class PopoverTarget extends SingleChildRenderObjectWidget {
@override
void updateRenderObject(
BuildContext context, PopoverTargetRenderBox renderObject) {
BuildContext context,
PopoverTargetRenderBox renderObject,
) {
renderObject.link = link;
}
}
class PopoverTargetRenderBox extends RenderProxyBox {
PopoverTargetRenderBox({
required this.link,
RenderBox? child,
}) : super(child);
PopoverLink link;
PopoverTargetRenderBox({required this.link, RenderBox? child}) : super(child);
@override
bool get alwaysNeedsCompositing => true;

View File

@ -3,11 +3,15 @@ import 'dart:collection';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
typedef EntryMap = LinkedHashMap<PopoverState, OverlayEntryContext>;
typedef _EntryMap = LinkedHashMap<PopoverState, OverlayEntryContext>;
class RootOverlayEntry {
final EntryMap _entries = EntryMap();
RootOverlayEntry();
final _EntryMap _entries = _EntryMap();
bool contains(PopoverState state) => _entries.containsKey(state);
bool get isEmpty => _entries.isEmpty;
bool get isNotEmpty => _entries.isNotEmpty;
void addEntry(
BuildContext context,
@ -15,62 +19,54 @@ class RootOverlayEntry {
OverlayEntry entry,
bool asBarrier,
) {
_entries[newState] = OverlayEntryContext(entry, newState, asBarrier);
_entries[newState] = OverlayEntryContext(
entry,
newState,
asBarrier,
);
Overlay.of(context).insert(entry);
}
bool contains(PopoverState oldState) {
return _entries.containsKey(oldState);
}
void removeEntry(PopoverState oldState) {
if (_entries.isEmpty) return;
final removedEntry = _entries.remove(oldState);
void removeEntry(PopoverState state) {
final removedEntry = _entries.remove(state);
removedEntry?.overlayEntry.remove();
}
bool get isEmpty => _entries.isEmpty;
bool get isNotEmpty => _entries.isNotEmpty;
bool hasEntry() {
return _entries.isNotEmpty;
}
PopoverState? popEntry() {
if (_entries.isEmpty) return null;
if (isEmpty) {
return null;
}
final lastEntry = _entries.values.last;
_entries.remove(lastEntry.popoverState);
lastEntry.overlayEntry.remove();
lastEntry.popoverState.widget.onClose?.call();
if (lastEntry.asBarrier) {
return lastEntry.popoverState;
} else {
return popEntry();
}
return lastEntry.asBarrier ? lastEntry.popoverState : popEntry();
}
}
class OverlayEntryContext {
final bool asBarrier;
final PopoverState popoverState;
final OverlayEntry overlayEntry;
OverlayEntryContext(
this.overlayEntry,
this.popoverState,
this.asBarrier,
);
final OverlayEntry overlayEntry;
final PopoverState popoverState;
final bool asBarrier;
}
class PopoverMask extends StatelessWidget {
final void Function() onTap;
final Decoration? decoration;
const PopoverMask({
super.key,
required this.onTap,
this.decoration,
});
const PopoverMask({super.key, required this.onTap, this.decoration});
final VoidCallback onTap;
final Decoration? decoration;
@override
Widget build(BuildContext context) {

View File

@ -5,22 +5,18 @@ import 'popover.dart';
/// If multiple popovers are exclusive,
/// pass the same mutex to them.
class PopoverMutex {
final _PopoverStateNotifier _stateNotifier = _PopoverStateNotifier();
PopoverMutex();
final _PopoverStateNotifier _stateNotifier = _PopoverStateNotifier();
void addPopoverListener(VoidCallback listener) {
_stateNotifier.addListener(listener);
}
void removePopoverListener(VoidCallback listener) {
_stateNotifier.removeListener(listener);
}
VoidCallback listenOnPopoverChanged(VoidCallback callback) {
listenerCallback() {
callback();
}
_stateNotifier.addListener(listenerCallback);
return listenerCallback;
}
void close() => _stateNotifier.state?.close();
PopoverState? get state => _stateNotifier.state;

View File

@ -54,6 +54,33 @@ enum PopoverClickHandler {
}
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;
/// The offset from the [child] where the popover will be drawn
@ -93,113 +120,69 @@ class Popover extends StatefulWidget {
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.
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
State<Popover> createState() => PopoverState();
}
class PopoverState extends State<Popover> {
static final RootOverlayEntry _rootEntry = RootOverlayEntry();
class PopoverState extends State<Popover> with SingleTickerProviderStateMixin {
static final RootOverlayEntry rootEntry = RootOverlayEntry();
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
void initState() {
super.initState();
widget.controller?._state = this;
}
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();
}
_buildAnimations();
}
@override
void deactivate() {
close(notify: false);
super.deactivate();
}
@override
void dispose() {
isDisposed = true;
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
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) {
if (widget.triggerActions == 0) {
return widget.child;
@ -247,36 +290,182 @@ class PopoverState extends State<Popover> {
}
void _callHandler(VoidCallback handler) {
if (_rootEntry.contains(this)) {
if (rootEntry.contains(this)) {
close();
} else {
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 {
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({
super.key,
required this.popupBuilder,
required this.direction,
required this.popoverLink,
required this.offset,
required this.windowPadding,
required this.delegate,
required this.onClose,
required this.onCloseAll,
required this.skipTraversal,
});
final Widget? Function(BuildContext context) popupBuilder;
final void Function() onClose;
final void Function() onCloseAll;
final bool skipTraversal;
final PopoverLayoutDelegate delegate;
@override
State<StatefulWidget> createState() => PopoverContainerState();
@ -302,12 +491,7 @@ class PopoverContainerState extends State<PopoverContainer> {
autofocus: true,
skipTraversal: widget.skipTraversal,
child: CustomSingleChildLayout(
delegate: PopoverLayoutDelegate(
direction: widget.direction,
link: widget.popoverLink,
offset: widget.offset,
windowPadding: widget.windowPadding,
),
delegate: widget.delegate,
child: widget.popupBuilder(context),
),
);

View File

@ -4,8 +4,8 @@ version: 0.0.1
homepage: https://appflowy.io
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.10.1"
flutter: ">=3.22.0"
sdk: ">=3.3.0 <4.0.0"
dependencies:
flutter:
@ -14,41 +14,4 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
# 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
flutter_lints: ^4.0.0

View File

@ -1,37 +1,8 @@
import 'package:flutter/material.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/colorscheme/default_colorscheme.dart';
import 'package:flutter/material.dart';
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({
super.key,
required this.child,
@ -52,12 +23,46 @@ class AppFlowyPopover extends StatelessWidget {
this.skipTraversal = false,
this.decorationColor,
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
Widget build(BuildContext context) {
return Popover(
controller: controller,
animationDuration: animationDuration,
slideDistance: slideDistance,
onOpen: onOpen,
onClose: onClose,
canClose: canClose,

View File

@ -1535,10 +1535,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.5"
plugin_platform_interface:
dependency: "direct dev"
description:
@ -1933,10 +1933,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
string_validator:
dependency: "direct main"
description:
@ -2238,10 +2238,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.1"
version: "14.2.5"
watcher:
dependency: transitive
description: