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 = '😁'; 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,

View File

@ -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();
} }

View File

@ -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();
}
} }
} }

View File

@ -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();
}
} }
} }

View File

@ -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;

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_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

View File

@ -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,

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 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

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -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"

View File

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

View File

@ -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();
}, },

View File

@ -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,
),
],
),
],
),
), ),
); );
} }

View File

@ -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;

View File

@ -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"

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 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

View File

@ -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}");
} }
} }

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -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),
), ),
); );

View File

@ -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

View File

@ -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,

View File

@ -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: