Last active
December 14, 2025 20:04
-
-
Save hawkkiller/925cbe4d385156366dd92e63d717758b to your computer and use it in GitHub Desktop.
A widget that slightly scales down the pressed widget. Useful for tappable elements, such as buttons to make them feel more alive.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// Notification sent when a [PressScaleTransition] handles a press. | |
| /// Used to prevent ancestor [PressScaleTransition]s from animating. | |
| class _PressScaleNotification extends Notification { | |
| const _PressScaleNotification(); | |
| } | |
| /// A widget that adds a subtle scale down effect when pressed. | |
| /// | |
| /// When nested, only the innermost [PressScaleTransition] will animate, | |
| /// preventing unwanted visual effects on parent buttons. | |
| class PressScaleTransition extends StatefulWidget { | |
| const PressScaleTransition({required this.child, this.enabled = true, super.key}); | |
| final bool enabled; | |
| final Widget child; | |
| @override | |
| State<PressScaleTransition> createState() => _PressScaleTransitionState(); | |
| } | |
| class _PressScaleTransitionState extends State<PressScaleTransition> with SingleTickerProviderStateMixin { | |
| late final AnimationController _controller; | |
| late final Animation<double> _scaleAnimation; | |
| bool _childHandledPress = false; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController( | |
| duration: const Duration(milliseconds: 150), | |
| vsync: this, | |
| ); | |
| _scaleAnimation = Tween<double>(begin: 1.0, end: 0.97).animate( | |
| CurvedAnimation(parent: _controller, curve: Curves.easeInOut), | |
| ); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| void _handleTapDown(TapDownDetails details) { | |
| if (!widget.enabled) return; | |
| // Notify ancestors that this press is being handled | |
| const _PressScaleNotification().dispatch(context); | |
| // Use microtask to check if a child notification arrived first | |
| Future.microtask(() { | |
| if (!_childHandledPress && mounted) { | |
| _controller.forward(); | |
| } | |
| // Reset flag for next interaction | |
| _childHandledPress = false; | |
| }); | |
| } | |
| Future<void> _handleTapUp(TapUpDetails details) async { | |
| if (!widget.enabled) return; | |
| await Future<void>.delayed(const Duration(milliseconds: 50)); | |
| if (!mounted) return; | |
| await _controller.reverse(); | |
| } | |
| Future<void> _handleTapCancel() async { | |
| if (!widget.enabled) return; | |
| await Future<void>.delayed(const Duration(milliseconds: 50)); | |
| if (!mounted) return; | |
| await _controller.reverse(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return NotificationListener<_PressScaleNotification>( | |
| onNotification: (notification) { | |
| // A descendant PressScaleTransition is handling the press | |
| _childHandledPress = true; | |
| return false; // Continue propagating to further ancestors | |
| }, | |
| child: GestureDetector( | |
| behavior: HitTestBehavior.translucent, | |
| onTapDown: _handleTapDown, | |
| onTapUp: _handleTapUp, | |
| onTapCancel: _handleTapCancel, | |
| child: ScaleTransition(scale: _scaleAnimation, child: widget.child), | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment