diff --git a/CHANGELOG.md b/CHANGELOG.md index e415da6..a4852f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.2.2 + +* Fixed exceptions thrown from bouncing widget. [#57](https://github.com/rasitayaz/flutter-pie-menu/issues/57) +* Fixed inconsistency in menu child positioning when the scroll position changes while the menu is open. + ## 3.2.1 * Fixed some stateful widgets are trying to set their state after being disposed. diff --git a/README.md b/README.md index 222bf65..f30b7f4 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ A Flutter package providing a highly customizable circular/radial context menu, - [Customization](#customization) - [Button themes](#button-themes) - [Custom button widgets](#custom-button-widgets) - - [Adjust display angle of menu buttons](#adjust-display-angle-of-menu-buttons) - - [Place the menu at a specific position](#place-the-menu-at-a-specific-position) + - [Display angle of menu buttons](#display-angle-of-menu-buttons) + - [Specific menu position](#specific-menu-position) - [Tap, long press or right click to open the menu](#tap-long-press-or-right-click-to-open-the-menu) - - [Controllers and callbacks](#controllers-and-callbacks) + - [Controllers and callbacks](#controllers-and-callbacks) - [Contributing](#contributing) - [Donation](#donation) @@ -141,7 +141,7 @@ PieAction.builder( ), ``` -### Adjust display angle of menu buttons +### Display angle of menu buttons If you don't want the dynamic angle calculation and have the menu appear at a fixed angle, set `customAngle` and `customAngleAnchor` attributes of `PieTheme`. @@ -154,7 +154,7 @@ PieTheme( You can also use `customAngleDiff` or `spacing` to adjust the angle between buttons, and `angleOffset` to rotate the menu. -### Place the menu at a specific position +### Specific menu position Use `menuAlignment` attribute of `PieTheme` to make the menu appear at a specific position regardless of the pressed point. Combine it with `menuDisplacement` to fine-tune the position. @@ -184,7 +184,7 @@ PieTheme( ), ``` -### Controllers and callbacks +## Controllers and callbacks To open, close or toggle a menu programmatically, assign a `PieMenuController` to it. 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/lib/src/pie_canvas_core.dart b/lib/src/pie_canvas_core.dart index 2e4c91c..bcd04eb 100644 --- a/lib/src/pie_canvas_core.dart +++ b/lib/src/pie_canvas_core.dart @@ -263,220 +263,228 @@ class PieCanvasCoreState extends State _tooltip = _actions[hoveredAction].tooltip; } - return Material( - type: MaterialType.transparency, - child: MouseRegion( - cursor: hoveredAction != null - ? SystemMouseCursors.click - : SystemMouseCursors.basic, - child: Stack( - children: [ - Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: (event) => _pointerDown(event.position), - onPointerMove: (event) => _pointerMove(event.position), - onPointerHover: _state.menuOpen - ? (event) => _pointerMove(event.position) - : null, - onPointerUp: (event) => _pointerUp(event.position), - child: IgnorePointer( - ignoring: _state.menuOpen, - child: widget.child, + return NotificationListener( + onNotification: (notification) { + setState(() {}); + return false; + }, + child: Material( + type: MaterialType.transparency, + child: MouseRegion( + cursor: hoveredAction != null + ? SystemMouseCursors.click + : SystemMouseCursors.basic, + child: Stack( + children: [ + Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (event) => _pointerDown(event.position), + onPointerMove: (event) => _pointerMove(event.position), + onPointerHover: _state.menuOpen + ? (event) => _pointerMove(event.position) + : null, + onPointerUp: (event) => _pointerUp(event.position), + child: IgnorePointer( + ignoring: _state.menuOpen, + child: widget.child, + ), ), - ), - IgnorePointer( - child: AnimatedBuilder( - animation: _fadeAnimation, - builder: (context, child) { - return Opacity( - opacity: _fadeAnimation.value, - child: child, - ); - }, - child: Stack( - children: [ - //* overlay start *// - if (menuRenderBox != null && menuRenderBox.attached) - ...() { - final menuOffset = - menuRenderBox.localToGlobal(Offset.zero); - - switch (_theme.overlayStyle) { - case PieOverlayStyle.around: - return [ - Positioned.fill( - child: CustomPaint( - painter: OverlayPainter( - color: _theme.effectiveOverlayColor, - menuOffset: Offset( - menuOffset.dx - cx, - menuOffset.dy - cy, + IgnorePointer( + child: AnimatedBuilder( + animation: _fadeAnimation, + builder: (context, child) { + return Opacity( + opacity: _fadeAnimation.value, + child: child, + ); + }, + child: Stack( + children: [ + //* overlay start *// + if (menuRenderBox != null && menuRenderBox.attached) + ...() { + final menuOffset = + menuRenderBox.localToGlobal(Offset.zero); + + switch (_theme.overlayStyle) { + case PieOverlayStyle.around: + return [ + Positioned.fill( + child: CustomPaint( + painter: OverlayPainter( + color: _theme.effectiveOverlayColor, + menuOffset: Offset( + menuOffset.dx - cx, + menuOffset.dy - cy, + ), + menuSize: menuRenderBox.size, ), - menuSize: menuRenderBox.size, ), ), - ), - ]; - case PieOverlayStyle.behind: - final bounceAnimation = _childBounceAnimation; - - return [ - Positioned.fill( - child: ColoredBox( - color: _theme.effectiveOverlayColor, + ]; + case PieOverlayStyle.behind: + final bounceAnimation = _childBounceAnimation; + + return [ + Positioned.fill( + child: ColoredBox( + color: _theme.effectiveOverlayColor, + ), ), - ), - Positioned( - left: menuOffset.dx - cx, - top: menuOffset.dy - cy, - child: AnimatedOpacity( - opacity: _state.menuOpen && - _state.hoveredAction != null - ? _theme.childOpacityOnButtonHover - : 1, - duration: _theme.hoverDuration, - curve: Curves.ease, - child: SizedBox.fromSize( - size: menuRenderBox.size, - child: _theme.childBounceEnabled && - bounceAnimation != null - ? BouncingWidget( - theme: _theme, - animation: bounceAnimation, - pressedOffset: menuRenderBox - .globalToLocal(_pointerOffset), - child: - _menuChild ?? const SizedBox(), - ) - : _menuChild, + Positioned( + left: menuOffset.dx - cx, + top: menuOffset.dy - cy, + child: AnimatedOpacity( + opacity: _state.menuOpen && + _state.hoveredAction != null + ? _theme.childOpacityOnButtonHover + : 1, + duration: _theme.hoverDuration, + curve: Curves.ease, + child: SizedBox.fromSize( + size: menuRenderBox.size, + child: _theme.childBounceEnabled && + bounceAnimation != null + ? BouncingWidget( + theme: _theme, + animation: bounceAnimation, + pressedOffset: + menuRenderBox.globalToLocal( + _pointerOffset, + ), + child: _menuChild ?? + const SizedBox(), + ) + : _menuChild, + ), ), ), - ), - ]; - } - }.call(), - //* overlay end *// - - //* tooltip start *// - () { - final tooltipAlignment = _theme.tooltipCanvasAlignment; - - Widget child = AnimatedOpacity( - opacity: hoveredAction != null ? 1 : 0, - duration: _theme.hoverDuration, - curve: Curves.ease, - child: Padding( - padding: _theme.tooltipPadding, - child: DefaultTextStyle.merge( - textAlign: _theme.tooltipTextAlign ?? - (px < cw / 2 - ? TextAlign.right - : TextAlign.left), - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: _theme.brightness == Brightness.light - ? Colors.black - : Colors.white, - ) - .merge(widget.theme.tooltipTextStyle) - .merge(_theme.tooltipTextStyle), - child: _tooltip ?? const SizedBox(), + ]; + } + }.call(), + //* overlay end *// + + //* tooltip start *// + () { + final tooltipAlignment = _theme.tooltipCanvasAlignment; + + Widget child = AnimatedOpacity( + opacity: hoveredAction != null ? 1 : 0, + duration: _theme.hoverDuration, + curve: Curves.ease, + child: Padding( + padding: _theme.tooltipPadding, + child: DefaultTextStyle.merge( + textAlign: _theme.tooltipTextAlign ?? + (px < cw / 2 + ? TextAlign.right + : TextAlign.left), + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: _theme.brightness == Brightness.light + ? Colors.black + : Colors.white, + ) + .merge(widget.theme.tooltipTextStyle) + .merge(_theme.tooltipTextStyle), + child: _tooltip ?? const SizedBox(), + ), ), - ), - ); - - if (_theme.tooltipUseFittedBox) { - child = FittedBox(child: child); - } - - if (tooltipAlignment != null) { - return Align( - alignment: tooltipAlignment, - child: child, ); - } else { - final offsets = [ - _pointerOffset, - for (var i = 0; i < _actions.length; i++) - _getActionOffset(i), - ]; - - double? getTopDistance() { - if (py >= ch / 2) return null; - - final dyMax = offsets - .map((o) => o.dy) - .reduce((dy1, dy2) => max(dy1, dy2)); - return dyMax - cy + _theme.buttonSize / 2; + if (_theme.tooltipUseFittedBox) { + child = FittedBox(child: child); } - double? getBottomDistance() { - if (py < ch / 2) return null; - - final dyMin = offsets - .map((o) => o.dy) - .reduce((dy1, dy2) => min(dy1, dy2)); - - return ch - dyMin + cy + _theme.buttonSize / 2; - } - - return Positioned( - top: getTopDistance(), - bottom: getBottomDistance(), - left: 0, - right: 0, - child: Align( - alignment: px < cw / 2 - ? Alignment.centerRight - : Alignment.centerLeft, + if (tooltipAlignment != null) { + return Align( + alignment: tooltipAlignment, child: child, - ), - ); - } - }.call(), - //* tooltip end *// - - //* action buttons start *// - Flow( - delegate: PieDelegate( - bounceAnimation: _buttonBounceAnimation, - pointerOffset: _pointerOffset, - canvasOffset: _canvasOffset, - baseAngle: _baseAngle, - angleDiff: _angleDiff, - theme: _theme, - ), - children: [ - DecoratedBox( - decoration: _theme.pointerDecoration ?? - BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: _theme.pointerColor ?? - (_theme.brightness == Brightness.light - ? Colors.black.withOpacity(0.35) - : Colors.white.withOpacity(0.5)), - width: 4, - ), - ), + ); + } else { + final offsets = [ + _pointerOffset, + for (var i = 0; i < _actions.length; i++) + _getActionOffset(i), + ]; + + double? getTopDistance() { + if (py >= ch / 2) return null; + + final dyMax = offsets + .map((o) => o.dy) + .reduce((dy1, dy2) => max(dy1, dy2)); + + return dyMax - cy + _theme.buttonSize / 2; + } + + double? getBottomDistance() { + if (py < ch / 2) return null; + + final dyMin = offsets + .map((o) => o.dy) + .reduce((dy1, dy2) => min(dy1, dy2)); + + return ch - dyMin + cy + _theme.buttonSize / 2; + } + + return Positioned( + top: getTopDistance(), + bottom: getBottomDistance(), + left: 0, + right: 0, + child: Align( + alignment: px < cw / 2 + ? Alignment.centerRight + : Alignment.centerLeft, + child: child, + ), + ); + } + }.call(), + //* tooltip end *// + + //* action buttons start *// + Flow( + delegate: PieDelegate( + bounceAnimation: _buttonBounceAnimation, + pointerOffset: _pointerOffset, + canvasOffset: _canvasOffset, + baseAngle: _baseAngle, + angleDiff: _angleDiff, + theme: _theme, ), - for (int i = 0; i < _actions.length; i++) - PieButton( - theme: _theme, - action: _actions[i], - angle: _getActionAngle(i), - hovered: i == hoveredAction, + children: [ + DecoratedBox( + decoration: _theme.pointerDecoration ?? + BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: _theme.pointerColor ?? + (_theme.brightness == Brightness.light + ? Colors.black.withOpacity(0.35) + : Colors.white.withOpacity(0.5)), + width: 4, + ), + ), ), - ], - ), - //* action buttons end *// - ], + for (int i = 0; i < _actions.length; i++) + PieButton( + theme: _theme, + action: _actions[i], + angle: _getActionAngle(i), + hovered: i == hoveredAction, + ), + ], + ), + //* action buttons end *// + ], + ), ), ), - ), - ], + ], + ), ), ), ); @@ -495,7 +503,7 @@ class PieCanvasCoreState extends State required bool rightClicked, required RenderBox renderBox, required Widget child, - required Animation bounceAnimation, + required Animation? bounceAnimation, required Key menuKey, required List actions, required PieTheme theme, diff --git a/lib/src/pie_menu_core.dart b/lib/src/pie_menu_core.dart index ed96f21..47162e6 100644 --- a/lib/src/pie_menu_core.dart +++ b/lib/src/pie_menu_core.dart @@ -78,22 +78,10 @@ class _PieMenuCoreState extends State ); /// Controls [_bounceAnimation]. - late final _bounceController = AnimationController( - duration: _theme.childBounceDuration, - vsync: this, - ); + AnimationController? _bounceController; /// Bounce animation for the child widget. - late final _bounceAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate( - CurvedAnimation( - parent: _bounceController, - curve: _theme.childBounceCurve, - reverseCurve: _theme.childBounceReverseCurve, - ), - ); + Animation? _bounceAnimation; /// Offset of the press event. var _pressedOffset = Offset.zero; @@ -131,6 +119,25 @@ class _PieMenuCoreState extends State void initState() { super.initState(); widget.controller?.addListener(_handleControllerEvent); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_theme.childBounceEnabled) { + final controller = _bounceController = AnimationController( + duration: _theme.childBounceDuration, + vsync: this, + ); + _bounceAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate( + CurvedAnimation( + parent: controller, + curve: _theme.childBounceCurve, + reverseCurve: _theme.childBounceReverseCurve, + ), + ); + } + }); } @override @@ -142,7 +149,7 @@ class _PieMenuCoreState extends State @override void dispose() { _overlayFadeController.dispose(); - _bounceController.dispose(); + _bounceController?.dispose(); _debounceTimer?.cancel(); _bounceStopwatch.stop(); widget.controller?.removeListener(_handleControllerEvent); @@ -152,6 +159,8 @@ class _PieMenuCoreState extends State @override Widget build(BuildContext context) { + final bounceAnimation = _bounceAnimation; + if (_state.menuKey == _uniqueKey) { if (!_previouslyOpen && _state.menuOpen) { _overlayFadeController.forward(from: 0); @@ -203,10 +212,10 @@ class _PieMenuCoreState extends State : 1, duration: _theme.hoverDuration, curve: Curves.ease, - child: _theme.childBounceEnabled + child: bounceAnimation != null && _pressedOffset != Offset.zero ? BouncingWidget( theme: _theme, - animation: _bounceAnimation, + animation: bounceAnimation, pressedOffset: _renderBox?.globalToLocal(_pressedOffset), child: widget.child, @@ -289,7 +298,7 @@ class _PieMenuCoreState extends State _bounceStopwatch.reset(); _bounceStopwatch.start(); - _bounceController.forward(); + _bounceController?.forward(); } void _debounce() { @@ -304,7 +313,7 @@ class _PieMenuCoreState extends State : Duration(milliseconds: minDelayMS); _debounceTimer = Timer(debounceDelay, () { - _bounceController.reverse(); + _bounceController?.reverse(); }); } diff --git a/pubspec.yaml b/pubspec.yaml index ea6be39..319f42f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: pie_menu description: A Flutter package providing a highly customizable circular/radial context menu -version: 3.2.1 +version: 3.2.2 homepage: https://github.com/rasitayaz/flutter-pie-menu repository: https://github.com/rasitayaz/flutter-pie-menu issue_tracker: https://github.com/rasitayaz/flutter-pie-menu/issues @@ -25,5 +25,3 @@ dev_dependencies: flutter_lints: ^4.0.0 flutter_test: sdk: flutter - -flutter: null