From 08f8d894d15fd1d6f8ed0bbcb1664e7be576d979 Mon Sep 17 00:00:00 2001 From: Fedor Blagodyr Date: Fri, 31 Oct 2025 23:58:07 +0300 Subject: [PATCH 1/2] issue(40): Add draggable bounding --- example/.gitignore | 2 + example/ios/Flutter/AppFrameworkInfo.plist | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/xcschemes/Runner.xcscheme | 3 + example/ios/Runner/AppDelegate.swift | 2 +- .../positioned_dialog_basic_usage.dart | 55 ++++++-- example/pubspec.lock | 10 +- lib/src/core/easy_dialogs_controller.dart | 7 +- lib/src/core/widget/free_positioned.dart | 122 +++++++++++++----- .../positioned/dialog/positioned_dialog.dart | 29 ++++- 10 files changed, 178 insertions(+), 60 deletions(-) diff --git a/example/.gitignore b/example/.gitignore index a8e938c..221b422 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 2d5f6c6..cd7ed99 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -275,7 +275,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -352,7 +352,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -401,7 +401,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d..9c12df5 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4..b636303 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/lib/positioned/screens/positioned_dialog_basic_usage.dart b/example/lib/positioned/screens/positioned_dialog_basic_usage.dart index d0b8970..c78ff4a 100644 --- a/example/lib/positioned/screens/positioned_dialog_basic_usage.dart +++ b/example/lib/positioned/screens/positioned_dialog_basic_usage.dart @@ -40,6 +40,7 @@ class _PositionedDialogManagerBasicUsageScreenState _dismissibleTap: EasyDialogDismiss.tap( onDismissed: () => _count++, willDismiss: () => true, + behavior: HitTestBehavior.deferToChild, ), _dismissibleHorizontalSwipe: EasyDialogDismiss.swipe( onDismissed: () => _count++, @@ -53,6 +54,7 @@ class _PositionedDialogManagerBasicUsageScreenState _dismissibleAnimatedTap: EasyDialogDismiss.animatedTap( onDismissed: () => _count++, willDismiss: () => true, + behavior: HitTestBehavior.translucent, ), }; final _animatorsDropDownItems = _animations.entries @@ -88,6 +90,7 @@ class _PositionedDialogManagerBasicUsageScreenState EasyDialogPosition _selectedPosition = EasyDialogPosition.top; late var _selectedDismissible = _dismissibles.values.first; var _isAutoHide = false; + var _isDraggable = false; var _autoHideDuration = 300.0; @override @@ -158,6 +161,15 @@ class _PositionedDialogManagerBasicUsageScreenState onChanged: (value) => setState(() => _autoHideDuration = value), ), ], + CheckboxListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 20.0, + vertical: 5.0, + ), + title: const Text('Draggable'), + value: _isDraggable, + onChanged: (value) => setState(() => _isDraggable = value!), + ), ElevatedButton( onPressed: _show, child: const Text('Show'), @@ -182,30 +194,45 @@ class _PositionedDialogManagerBasicUsageScreenState Future _show() async { final messenger = ScaffoldMessenger.of(context); - final content = Container( - height: 150.0, - color: Colors.blue[900], - alignment: Alignment.center, - child: Text( - _selectedPosition.name, - style: const TextStyle( - color: Colors.white, - fontSize: 30.0, + final content = ColoredBox( + color: Colors.blue[900]!, + child: SizedBox( + height: 150.0, + width: 50, + child: Text( + _selectedPosition.name, + style: const TextStyle( + color: Colors.white, + fontSize: 30.0, + ), ), ), ); - final result = await content + var dialog = content .positioned( position: _selectedPosition, autoHideDuration: _isAutoHide ? Duration(milliseconds: _autoHideDuration.toInt()) : null, ) - .decorate(const PositionedShell.banner()) - .decorate(_selectedAnimation) - .decorate(_selectedDismissible) - .show(); + .decorate(const PositionedShell.banner()); + + if (_isDraggable) { + final screenSize = MediaQuery.sizeOf(context); + dialog = dialog.draggable( + bounds: Rect.fromLTWH( + 0, + 0, + screenSize.width, + screenSize.height, + ), + ); + } + + dialog = dialog.decorate(_selectedAnimation).decorate(_selectedDismissible); + + final result = await dialog.show(); if (result == null) return; messenger diff --git a/example/pubspec.lock b/example/pubspec.lock index 3cd213b..5950eff 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -134,10 +134,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -195,10 +195,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" vector_math: dependency: transitive description: @@ -216,5 +216,5 @@ packages: source: hosted version: "15.0.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/src/core/easy_dialogs_controller.dart b/lib/src/core/easy_dialogs_controller.dart index e79ba0b..c256409 100644 --- a/lib/src/core/easy_dialogs_controller.dart +++ b/lib/src/core/easy_dialogs_controller.dart @@ -760,11 +760,14 @@ extension EasyDialogsX on EasyDialog { ); } - EasyDialog draggable() { + EasyDialog draggable({ + Rect? bounds, + }) { return decorate( EasyDialogDecoration.builder( - (context, dialog) => FreePositioned( + (_, dialog) => FreePositioned( child: dialog.content, + bounds: bounds, ), ), ); diff --git a/lib/src/core/widget/free_positioned.dart b/lib/src/core/widget/free_positioned.dart index 54a403d..dc1ccf7 100644 --- a/lib/src/core/widget/free_positioned.dart +++ b/lib/src/core/widget/free_positioned.dart @@ -1,59 +1,119 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easy_dialogs/src/positioned/positioned.dart' + show PositionedDialog; class FreePositioned extends StatefulWidget { final Widget child; + final Rect? bounds; - const FreePositioned({required this.child}); + const FreePositioned({ + required this.child, + this.bounds, + super.key, + }); @override State createState() => _FreePositionedState(); } class _FreePositionedState extends State { - var _x = 0.0; - var _y = 0.0; - AlignmentGeometry? _alignment; + var _offset = Offset(0.0, 0.0); + Offset? _globalOffset; @override Widget build(BuildContext context) { final screenSize = MediaQuery.sizeOf(context); + final result = Builder( + builder: (context) => GestureDetector( + child: widget.child, + onPanStart: (details) => _onPanStart(details, context), + onPanUpdate: (details) => _onPanUpdate(details, context), + ), + ); + + final alignment = PositionedDialog.maybeOf(context)?.alignment; + return Stack( children: [ Positioned( - left: _x, - top: _y, - child: GestureDetector( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: screenSize.width, - maxHeight: screenSize.height, - ), - // Not the best approach to apply alignment but it works for now, - // need to be reworked in the future - child: _alignment == null - ? widget.child - : Align( - alignment: _alignment!, - child: widget.child, - ), - ), - onPanUpdate: (details) => setState( - () { - _x += details.delta.dx; - _y += details.delta.dy; - }, + left: _offset.dx, + top: _offset.dy, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: screenSize.width, + maxHeight: screenSize.height, ), + child: alignment == null + ? result + : Align( + alignment: alignment, + child: result, + ), ), ), ], ); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); + void _onPanStart(DragStartDetails details, BuildContext context) { + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) return; + + _globalOffset = renderBox.localToGlobal(Offset.zero); + } + + void _onPanUpdate(DragUpdateDetails details, BuildContext context) { + final globalOffset = _globalOffset; + + if (globalOffset == null) { + return; + } + + final deltaX = details.delta.dx; + final deltaY = details.delta.dy; + final newGlobalX = globalOffset.dx + deltaX; + final newGlobalY = globalOffset.dy + deltaY; + final oldOffset = _offset; + + final bounds = widget.bounds; + + if (bounds == null) { + setState(() { + _offset = Offset(_offset.dx + deltaX, _offset.dy + deltaY); + _globalOffset = Offset(newGlobalX, newGlobalY); + }); + + return; + } + + final RenderBox? renderBox = context.findRenderObject() as RenderBox?; + + if (renderBox == null) { + return; + } + + final childSize = renderBox.size; + + final inBoundsX = newGlobalX >= bounds.left && + newGlobalX + childSize.width <= bounds.right; + final inBoundsY = newGlobalY >= bounds.top && + newGlobalY + childSize.height <= bounds.bottom; + + if (inBoundsX) { + _offset = Offset(_offset.dx + deltaX, _offset.dy); + _globalOffset = Offset(newGlobalX, newGlobalY); + } + + if (inBoundsY) { + _offset = Offset(_offset.dx, _offset.dy + deltaY); + _globalOffset = Offset(newGlobalX, newGlobalY); + } + + if (oldOffset == _offset) { + return; + } - _alignment = context.findAncestorWidgetOfExactType()?.alignment; + setState(() {}); } } diff --git a/lib/src/positioned/dialog/positioned_dialog.dart b/lib/src/positioned/dialog/positioned_dialog.dart index ff4414b..f8a703c 100644 --- a/lib/src/positioned/dialog/positioned_dialog.dart +++ b/lib/src/positioned/dialog/positioned_dialog.dart @@ -15,6 +15,9 @@ final class PositionedDialog extends EasyDialog { /// The position where the dialog will be shown. final EasyDialogPosition position; + static EasyDialogPosition? maybeOf(BuildContext context) => + _PositionedDialogScope.maybeOf(context)?.position; + /// Creates an instance of [PositionedDialog]. PositionedDialog({ required super.content, @@ -28,9 +31,12 @@ final class PositionedDialog extends EasyDialog { @override EasyOverlayBoxInsertion createInsert(Widget decorated) { return super.createInsert( - Align( - alignment: position.alignment, - child: decorated, + _PositionedDialogScope( + child: Align( + alignment: position.alignment, + child: decorated, + ), + position: position, ), ); } @@ -58,3 +64,20 @@ enum EasyDialogPosition { const EasyDialogPosition(this.alignment); } + +class _PositionedDialogScope extends InheritedWidget { + final EasyDialogPosition position; + const _PositionedDialogScope({ + required super.child, + required this.position, + }); + + static _PositionedDialogScope? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_PositionedDialogScope>(); + } + + @override + bool updateShouldNotify(_PositionedDialogScope oldWidget) { + return oldWidget.position != position; + } +} From 9b95aaca3a4d31b2c5ab750a5521d571bd2f3210 Mon Sep 17 00:00:00 2001 From: Fedor Blagodyr Date: Sat, 1 Nov 2025 00:09:08 +0300 Subject: [PATCH 2/2] issue(40): updates version --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5db23..fb626dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 4.0.5 +* **FEAT:** https://github.com/feduke-nukem/flutter_easy_dialogs/issues/40 + ## 4.0.4 * **FIX:** Fixed https://github.com/feduke-nukem/flutter_easy_dialogs/issues/37 * **FIX:** Fixed https://github.com/feduke-nukem/flutter_easy_dialogs/issues/38 diff --git a/pubspec.yaml b/pubspec.yaml index c6e8140..014abb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_easy_dialogs description: Easy and flexible package for showing dialogs inside your Flutter application without BuildContext. -version: 4.0.4 +version: 4.0.5 homepage: https://github.com/feduke-nukem/flutter_easy_dialogs repository: https://github.com/feduke-nukem/flutter_easy_dialogs/tree/main issue_tracker: https://github.com/feduke-nukem/flutter_easy_dialogs/issues